blob: edb660130253caa1e7c1b8d63ff2797bd789e77e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Andrey Loskutov <loskutov@gmx.de> - generified interface, bug 462760
*******************************************************************************/
package org.eclipse.ui.actions;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.IShellProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.internal.ide.StatusUtil;
import org.eclipse.ui.internal.progress.ProgressMonitorJobsDialog;
import org.eclipse.ui.progress.IProgressConstants2;
/**
* The abstract superclass for actions which invoke commands implemented in
* org.eclipse.core.* on a set of selected resources.
*
* It iterates over all selected resources; errors are collected and displayed
* to the user via a problems dialog at the end of the operation. User requests
* to cancel the operation are passed along to the core.
* <p>
* Subclasses must implement the following methods:
* <ul>
* <li><code>invokeOperation</code> - to perform the operation on one of the
* selected resources</li>
* <li><code>getOperationMessage</code> - to furnish a title for the progress
* dialog</li>
* </ul>
* </p>
* <p>
* Subclasses may override the following methods:
* <ul>
* <li><code>shouldPerformResourcePruning</code> - reimplement to turn off</li>
* <li><code>updateSelection</code> - extend to refine enablement criteria</li>
* <li><code>getProblemsTitle</code> - reimplement to furnish a title for the
* problems dialog</li>
* <li><code>getProblemsMessage</code> - reimplement to furnish a message for
* the problems dialog</li>
* <li><code>run</code> - extend to </li>
* </ul>
* </p>
*/
public abstract class WorkspaceAction extends SelectionListenerAction {
/**
* The shell in which to show the progress and problems dialog.
*/
private final IShellProvider shellProvider;
/**
* Creates a new action with the given text.
*
* @param shell
* the shell (for the modal progress dialog and error messages)
* @param text
* the string used as the text for the action, or
* <code>null</code> if there is no text
* @deprecated See {@link #WorkspaceAction(IShellProvider, String)}
*/
@Deprecated
protected WorkspaceAction(final Shell shell, String text) {
super(text);
Assert.isNotNull(shell);
shellProvider = () -> shell;
}
/**
* Creates a new action with the given text.
*
* @param provider
* the shell provider (for the modal progress dialog and error
* messages)
* @param text
* the string used as the text for the action, or
* <code>null</code> if there is no text
* @since 3.4
*/
protected WorkspaceAction(IShellProvider provider, String text) {
super(text);
Assert.isNotNull(provider);
shellProvider = provider;
}
/**
* Opens an error dialog to display the given message.
* <p>
* Note that this method must be called from UI thread.
* </p>
*
* @param message
* the message
*/
void displayError(String message) {
if (message == null) {
message = IDEWorkbenchMessages.WorkbenchAction_internalError;
}
MessageDialog.openError(shellProvider.getShell(), getProblemsTitle(), message);
}
/**
* Runs <code>invokeOperation</code> on each of the selected resources,
* reporting progress and fielding cancel requests from the given progress
* monitor.
* <p>
* Note that if an action is running in the background, the same action
* instance can be executed multiple times concurrently. This method must
* not access or modify any mutable state on action class.
*
* @param monitor
* a progress monitor
* @return The result of the execution
*/
final IStatus execute(List<? extends IResource> resources, IProgressMonitor mon) {
MultiStatus errors = null;
// 1FTIMQN: ITPCORE:WIN - clients required to do too much iteration work
if (shouldPerformResourcePruning()) {
resources = pruneResources(resources);
}
SubMonitor subMonitor = SubMonitor.convert(mon, resources.size());
// Fix for bug 31768 - Don't provide a task name in beginTask
// as it will be appended to each subTask message. Need to
// call setTaskName as its the only was to assure the task name is
// set in the monitor (see bug 31824)
subMonitor.setTaskName(getOperationMessage());
for (IResource resource : resources) {
try {
invokeOperation(resource, subMonitor.split(1));
} catch (CoreException e) {
errors = recordError(errors, e);
}
}
return errors == null ? Status.OK_STATUS : errors;
}
/**
* Returns the string to display for this action's operation.
* <p>
* Note that this hook method is invoked in a non-UI thread.
* </p>
* <p>
* Subclasses must implement this method.
* </p>
*
* @return the message
*
* @since 3.1
*/
protected abstract String getOperationMessage();
/**
* Returns the string to display for this action's problems dialog.
* <p>
* The <code>WorkspaceAction</code> implementation of this method returns
* a vague message (localized counterpart of something like "The following
* problems occurred."). Subclasses may reimplement to provide something
* more suited to the particular action.
* </p>
*
* @return the problems message
*
* @since 3.1
*/
protected String getProblemsMessage() {
return IDEWorkbenchMessages.WorkbenchAction_problemsMessage;
}
/**
* Returns the title for this action's problems dialog.
* <p>
* The <code>WorkspaceAction</code> implementation of this method returns
* a generic title (localized counterpart of "Problems"). Subclasses may
* reimplement to provide something more suited to the particular action.
* </p>
*
* @return the problems dialog title
*
* @since 3.1
*/
protected String getProblemsTitle() {
return IDEWorkbenchMessages.WorkspaceAction_problemsTitle;
}
/**
* Returns the shell for this action. This shell is used for the modal
* progress and error dialogs.
*
* @return the shell
*/
Shell getShell() {
return shellProvider.getShell();
}
/**
* Performs this action's operation on each of the selected resources,
* reporting progress to, and fielding cancel requests from, the given
* progress monitor.
* <p>
* Note that this method is invoked in a non-UI thread.
* </p>
* <p>
* Subclasses must implement this method.
* <p>
* @deprecated Since 3.3, subclasses should instead implement the method
* {@link #createOperation(IStatus[])} and provide an empty implementation
* for this method.
* </p>
*
* @param resource
* one of the selected resources
* @param monitor
* a progress monitor
* @exception CoreException
* if the operation fails
*
* @since 3.1
*/
@Deprecated
protected void invokeOperation(IResource resource, IProgressMonitor monitor) throws CoreException {
}
/**
* Returns whether the given resource is a descendent of any of the
* resources in the given list.
*
* @param resources
* the list of resources (element type: <code>IResource</code>)
* @param child
* the resource to check
* @return <code>true</code> if <code>child</code> is a descendent of
* any of the elements of <code>resources</code>
*/
boolean isDescendent(List<IResource> resources, IResource child) {
IResource parent = child.getParent();
return parent != null && (resources.contains(parent) || isDescendent(resources, parent));
}
/**
* Performs pruning on the given list of resources, as described in
* <code>shouldPerformResourcePruning</code>.
*
* @param resourceCollection
* the list of resources (element type: <code>IResource</code>)
* @return the list of resources (element type: <code>IResource</code>)
* after pruning.
* @see #shouldPerformResourcePruning
*/
List<IResource> pruneResources(List<? extends IResource> resourceCollection) {
List<IResource> prunedList = new ArrayList<>(resourceCollection);
Iterator<IResource> elementsEnum = prunedList.iterator();
while (elementsEnum.hasNext()) {
IResource currentResource = elementsEnum.next();
if (isDescendent(prunedList, currentResource)) {
elementsEnum.remove(); // Removes currentResource
}
}
return prunedList;
}
/**
* Records the core exception to be displayed to the user once the action is
* finished.
*
* @param error
* a <code>CoreException</code>
*/
MultiStatus recordError(MultiStatus errors, CoreException error) {
if (errors == null) {
errors = new MultiStatus(IDEWorkbenchPlugin.IDE_WORKBENCH, IStatus.ERROR, getProblemsMessage(), null);
}
errors.merge(error.getStatus());
return errors;
}
/**
* The <code>CoreWrapperAction</code> implementation of this
* <code>IAction</code> method uses a <code>ProgressMonitorDialog</code>
* to run the operation. The operation calls <code>execute</code> (which,
* in turn, calls <code>invokeOperation</code>). Afterwards, any
* <code>CoreException</code>s encountered while running the operation
* are reported to the user via a problems dialog.
* <p>
* Subclasses may extend this method.
* </p>
*/
@Override
public void run() {
IStatus[] errorStatus = new IStatus[1];
try {
new ProgressMonitorJobsDialog(shellProvider.getShell()).run(true, true, createOperation(errorStatus));
} catch (InterruptedException e) {
return;
} catch (InvocationTargetException e) {
// we catch ExecutionException in the created operation, but unexpected runtime
// exceptions or errors may still occur
String msg = NLS.bind(
IDEWorkbenchMessages.WorkspaceAction_logTitle, getClass()
.getName(), e.getTargetException());
IDEWorkbenchPlugin.log(msg, StatusUtil.newStatus(IStatus.ERROR, msg, e.getTargetException()));
displayError(e.getTargetException().getMessage());
}
// If errors occurred, open an Error dialog & build a multi status error
// for it
if (errorStatus[0] != null && !errorStatus[0].isOK()) {
// no special message
ErrorDialog.openError(shellProvider.getShell(), getProblemsTitle(), null, errorStatus[0]);
}
}
/**
* Returns whether this action should attempt to optimize the resources
* being operated on. This kind of pruning makes sense when the operation
* has depth infinity semantics (when the operation is applied explicitly to
* a resource then it is also applied implicitly to all the resource's
* descendents).
* <p>
* The <code>WorkspaceAction</code> implementation of this method returns
* <code>true</code>. Subclasses should reimplement to return
* <code>false</code> if pruning is not required.
* </p>
*
* @return <code>true</code> if pruning should be performed, and
* <code>false</code> if pruning is not desired
*
* @since 3.1
*/
protected boolean shouldPerformResourcePruning() {
return true;
}
/**
* The <code>WorkspaceAction</code> implementation of this
* <code>SelectionListenerAction</code> method ensures that this action is
* disabled if any of the selected resources are inaccessible. Subclasses
* may extend to react to selection changes; however, if the super method
* returns <code>false</code>, the overriding method should also return
* <code>false</code>.
*/
@Override
protected boolean updateSelection(IStructuredSelection selection) {
if (!super.updateSelection(selection) || selection.isEmpty()) {
return false;
}
for (IResource r : getSelectedResources()) {
if (!r.isAccessible()) {
return false;
}
}
return true;
}
/**
* Returns the elements that the action is to be performed on. By default
* return the selected resources.
* <p>
* Subclasses may override this method.
*
* @return list of resource elements (element type: <code>IResource</code>)
*/
protected List<? extends IResource> getActionResources() {
return getSelectedResources();
}
/**
* Run the action in the background rather than with the progress dialog.
*
* @param rule
* The rule to apply to the background job or <code>null</code>
* if there isn't one.
*/
public void runInBackground(ISchedulingRule rule) {
runInBackground(rule, (Object[]) null);
}
/**
* Run the action in the background rather than with the progress dialog.
*
* @param rule
* The rule to apply to the background job or <code>null</code>
* if there isn't one.
* @param jobFamily
* a single family that the job should belong to or
* <code>null</code> if none.
*
* @since 3.1
*/
public void runInBackground(ISchedulingRule rule, Object jobFamily) {
if (jobFamily == null) {
runInBackground(rule, (Object[]) null);
} else {
runInBackground(rule, new Object[] { jobFamily });
}
}
/**
* Run the action in the background rather than with the progress dialog.
*
* @param rule
* The rule to apply to the background job or <code>null</code>
* if there isn't one.
* @param jobFamilies
* the families the job should belong to or <code>null</code>
* if none.
*
* @since 3.1
*/
public void runInBackground(ISchedulingRule rule, final Object[] jobFamilies) {
// obtain a copy of the selected resources before the job is forked
final List<IResource> resources = new ArrayList<>(getActionResources());
Job job = new WorkspaceJob(removeMnemonics(getText())) {
@Override
public boolean belongsTo(Object family) {
if (jobFamilies == null || family == null) {
return false;
}
for (Object jobFamily : jobFamilies) {
if (family.equals(jobFamily)) {
return true;
}
}
return false;
}
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) {
return WorkspaceAction.this.execute(resources, monitor);
}
};
if (rule != null) {
job.setRule(rule);
}
if (job.belongsTo(ResourcesPlugin.FAMILY_MANUAL_BUILD)) {
job.setProperty(IProgressConstants2.SHOW_IN_TASKBAR_ICON_PROPERTY, Boolean.TRUE);
}
job.setUser(true);
job.schedule();
}
/**
* Returns the operation to perform when this action runs. The returned
* operation must be an {@link IRunnableWithProgress} that will perform the
* action's work. The default implementation returns an operation that will
* iterate over the selected resources and call
* {@link #invokeOperation(IResource, IProgressMonitor)} for each resource.
* Subclasses must either implement
* {@link #invokeOperation(IResource, IProgressMonitor)} or override this
* method to provide a different operation. Subclasses typically override
* this method when an undoable operation is to be provided.
*
* @param errorStatus
* an array of error status objects to which the result of
* running the operation should be added.
*
* @return the operation to perform when this action runs.
* @since 3.3
*/
protected IRunnableWithProgress createOperation(final IStatus[] errorStatus) {
return new WorkspaceModifyOperation() {
@Override
public void execute(IProgressMonitor monitor) {
errorStatus[0] = WorkspaceAction.this.execute(getActionResources(), monitor);
}
};
}
}