blob: 6653f89d3c2c0fb965e424864a7e815ec2faa2f6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.internal.ui.actions;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.ui.TeamUIMessages;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.ui.IActionDelegate2;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.ISources;
import org.eclipse.ui.IViewActionDelegate;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.ide.ResourceUtil;
/**
* The abstract superclass of all Team actions. This class contains some convenience
* methods for getting selected objects and mapping selected objects to their
* providers.
*
* Team providers may subclass this class when creating their actions.
* Team providers may also instantiate or subclass any of the
* subclasses of TeamAction provided in this package.
*/
public abstract class TeamAction extends AbstractHandler implements IObjectActionDelegate, IViewActionDelegate, IWorkbenchWindowActionDelegate, IActionDelegate2 {
// The current selection
private IStructuredSelection selection;
// The shell, required for the progress dialog
private Shell shell;
// Constants for determining the type of progress. Subclasses may
// pass one of these values to the run method.
public final static int PROGRESS_DIALOG = 1;
public final static int PROGRESS_BUSYCURSOR = 2;
private IWorkbenchPart targetPart;
private IWorkbenchWindow window;
private IPartListener2 targetPartListener = new IPartListener2() {
@Override
public void partActivated(IWorkbenchPartReference partRef) {
}
@Override
public void partBroughtToTop(IWorkbenchPartReference partRef) {
}
@Override
public void partClosed(IWorkbenchPartReference partRef) {
if(targetPart == null) {
return;
}
IWorkbenchPart part = partRef.getPart(false);
if (targetPart == part) {
targetPart = null;
}
}
@Override
public void partDeactivated(IWorkbenchPartReference partRef) {
}
@Override
public void partHidden(IWorkbenchPartReference partRef) {
}
@Override
public void partInputChanged(IWorkbenchPartReference partRef) {
}
@Override
public void partOpened(IWorkbenchPartReference partRef) {
}
@Override
public void partVisible(IWorkbenchPartReference partRef) {
}
};
private ISelectionListener selectionListener = (part, selection) -> {
if(selection instanceof IStructuredSelection)
TeamAction.this.selection = (IStructuredSelection)selection;
};
/**
* Creates an array of the given class type containing all the
* objects in the selection that adapt to the given class.
*
* @param selection
* @param c
* @return the selected adaptables
*/
@SuppressWarnings("unchecked")
public static <T> T[] getSelectedAdaptables(ISelection selection, Class<T> c) {
ArrayList<T> result = null;
if (selection != null && !selection.isEmpty()) {
result = new ArrayList<>();
Iterator elements = ((IStructuredSelection) selection).iterator();
while (elements.hasNext()) {
T adapter = getAdapter(elements.next(), c);
if (c.isInstance(adapter)) {
result.add(adapter);
}
}
}
if (result != null && !result.isEmpty()) {
return result.toArray((T[]) Array.newInstance(c, result.size()));
}
return (T[]) Array.newInstance(c, 0);
}
/**
* Find the object associated with the given object when it is adapted to
* the provided class. Null is returned if the given object does not adapt
* to the given class
*
* @param adaptable
* @param c
* @return Object
*/
@SuppressWarnings("unchecked")
public static <T> T getAdapter(Object adaptable, Class<T> c) {
if (c.isInstance(adaptable)) {
return (T) adaptable;
}
if (adaptable instanceof IAdaptable) {
IAdaptable a = (IAdaptable) adaptable;
T adapter = a.getAdapter(c);
if (c.isInstance(adapter)) {
return adapter;
}
}
return null;
}
/**
* Returns the selected projects.
*
* @return the selected projects
*/
protected IProject[] getSelectedProjects() {
IResource[] selectedResources = getSelectedResources();
if (selectedResources.length == 0) return new IProject[0];
ArrayList<IProject> projects = new ArrayList<>();
for (IResource resource : selectedResources) {
if (resource.getType() == IResource.PROJECT) {
projects.add((IProject) resource);
}
}
return projects.toArray(new IProject[projects.size()]);
}
/**
* Returns an array of the given class type c that contains all
* instances of c that are either contained in the selection or
* are adapted from objects contained in the selection.
*
* @param c
* @return the selection adapted to the given class
*/
protected <T> T[] getAdaptedSelection(Class<T> c) {
return getSelectedAdaptables(selection, c);
}
/**
* Returns the selected resources.
*
* @return the selected resources
*/
protected IResource[] getSelectedResources() {
return Utils.getContributedResources(getSelection().toArray());
}
protected IStructuredSelection getSelection() {
if (selection == null)
selection = StructuredSelection.EMPTY;
return selection;
}
/**
* Return the selected resource mappins that contain resources in
* projects that are associated with a repository of the given id.
* @param providerId the repository provider id
* @return the resource mappings that contain resources associated with the given provider
*/
protected ResourceMapping[] getSelectedResourceMappings(String providerId) {
Object[] elements = getSelection().toArray();
ArrayList<ResourceMapping> providerMappings = new ArrayList<>();
for (Object object : elements) {
Object adapted = getResourceMapping(object);
if (adapted instanceof ResourceMapping) {
ResourceMapping mapping = (ResourceMapping) adapted;
if (providerId == null || isMappedToProvider(mapping, providerId)) {
providerMappings.add(mapping);
}
}
}
return providerMappings.toArray(new ResourceMapping[providerMappings.size()]);
}
private Object getResourceMapping(Object object) {
if (object instanceof ResourceMapping)
return object;
return Utils.getResourceMapping(object);
}
private boolean isMappedToProvider(ResourceMapping element, String providerId) {
IProject[] projects = element.getProjects();
for (IProject project : projects) {
RepositoryProvider provider = RepositoryProvider.getProvider(project);
if (provider != null && provider.getID().equals(providerId)) {
return true;
}
}
return false;
}
/**
* Convenience method for getting the current shell.
*
* @return the shell
*/
protected Shell getShell() {
if (shell != null) {
return shell;
} else if (targetPart != null) {
return targetPart.getSite().getShell();
} else if (window != null) {
return window.getShell();
} else {
IWorkbench workbench = TeamUIPlugin.getPlugin().getWorkbench();
if (workbench == null) return null;
IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
if (window == null) return null;
return window.getShell();
}
}
/**
* Convenience method for running an operation with progress and
* error feedback.
*
* @param runnable the runnable which executes the operation
* @param problemMessage the message to display in the case of errors
* @param progressKind one of PROGRESS_BUSYCURSOR or PROGRESS_DIALOG
*/
final protected void run(final IRunnableWithProgress runnable, final String problemMessage, int progressKind) {
final Exception[] exceptions = new Exception[] {null};
switch (progressKind) {
case PROGRESS_BUSYCURSOR :
BusyIndicator.showWhile(Display.getCurrent(), () -> {
try {
runnable.run(new NullProgressMonitor());
} catch (InvocationTargetException e1) {
exceptions[0] = e1;
} catch (InterruptedException e2) {
exceptions[0] = null;
}
});
break;
default :
case PROGRESS_DIALOG :
try {
new ProgressMonitorDialog(getShell()).run(true, true, runnable);
} catch (InvocationTargetException e) {
exceptions[0] = e;
} catch (InterruptedException e) {
exceptions[0] = null;
}
break;
}
if (exceptions[0] != null) {
handle(exceptions[0], null, problemMessage);
}
}
/*
* Method declared on IActionDelegate.
*/
@Override
public void selectionChanged(IAction action, ISelection selection) {
if (selection instanceof IStructuredSelection) {
this.selection = (IStructuredSelection) selection;
if (action != null) {
setActionEnablement(action);
}
}
}
/**
* Method invoked from <code>selectionChanged(IAction, ISelection)</code>
* to set the enablement status of the action. The instance variable
* <code>selection</code> will contain the latest selection so the methods
* <code>getSelectedResources()</code> and <code>getSelectedProjects()</code>
* will provide the proper objects.
*
* This method can be overridden by subclasses but should not be invoked by them.
*/
protected void setActionEnablement(IAction action) {
action.setEnabled(isEnabled());
}
/**
* If an exception occurs during enablement testing, this method is invoked
* to determine if the action should be enabled or not.
* @param exception the exception
* @return whether the action should be enabled or not
*/
protected boolean isEnabledForException(TeamException exception) {
if (exception.getStatus().getCode() == IResourceStatus.OUT_OF_SYNC_LOCAL) {
// Enable the action to allow the user to discover the problem
return true;
}
// We should not open a dialog when determining menu enablement so log it instead
TeamPlugin.log(exception);
return false;
}
/*
* Method declared on IObjectActionDelegate.
*/
@Override
public void setActivePart(IAction action, IWorkbenchPart targetPart) {
if(targetPart != null) {
this.shell = targetPart.getSite().getShell();
this.targetPart = targetPart;
}
}
/**
* Shows the given errors to the user.
*
* @param exception the status containing the error
* @param title the title of the error dialog
* @param message the message for the error dialog
*/
protected void handle(Exception exception, String title, String message) {
Utils.handleError(getShell(), exception, title, message);
}
/**
* Convenience method that maps the given resources to their providers.
* The returned Hashtable has keys which are ITeamProviders, and values
* which are Lists of IResources that are shared with that provider.
*
* @return a hashtable mapping providers to their resources
*/
protected Hashtable<RepositoryProvider, List<IResource>> getProviderMapping(IResource[] resources) {
Hashtable<RepositoryProvider, List<IResource>> result = new Hashtable<>();
for (IResource resource : resources) {
RepositoryProvider provider = RepositoryProvider.getProvider(resource.getProject());
List<IResource> list = result.get(provider);
if (list == null) {
list = new ArrayList<>();
result.put(provider, list);
}
list.add(resource);
}
return result;
}
/**
* @return IWorkbenchPart
*/
protected IWorkbenchPart getTargetPart() {
if(targetPart == null) {
IWorkbenchPage page = TeamUIPlugin.getActivePage();
if (page != null) {
targetPart = page.getActivePart();
}
}
return targetPart;
}
/**
* Return the path that was active when the menu item was selected.
* @return IWorkbenchPage
*/
protected IWorkbenchPage getTargetPage() {
IWorkbenchPart target = getTargetPart();
if (target == null) {
return TeamUIPlugin.getActivePage();
}
IWorkbenchPage page = target.getSite().getPage();
if(page == null) {
// part was disposed => null targetPart to avoid memory leak
targetPart = null;
return TeamUIPlugin.getActivePage();
}
return page;
}
/**
* Show the view with the given ID in the perspective from which the action
* was executed. Returns null if the view is not registered.
*
* @param viewId
* @return IViewPart
*/
protected IViewPart showView(String viewId) {
try {
return getTargetPage().showView(viewId);
} catch (PartInitException pe) {
return null;
}
}
@Override
public void init(IViewPart view) {
if(view != null) {
this.shell = view.getSite().getShell();
this.targetPart = view;
}
}
@Override
public void init(IWorkbenchWindow window) {
this.window = window;
this.shell = window.getShell();
window.getSelectionService().addPostSelectionListener(selectionListener);
window.getActivePage().addPartListener(targetPartListener);
}
public IWorkbenchWindow getWindow() {
return window;
}
@Override
public void dispose() {
if(window != null) {
window.getSelectionService().removePostSelectionListener(selectionListener);
if (window.getActivePage() != null) {
window.getActivePage().removePartListener(targetPartListener);
}
targetPartListener = null;
}
// Don't hold on to anything when we are disposed to prevent memory leaks (see bug 195521)
selection = null;
window = null;
targetPart = null;
shell = null;
super.dispose();
}
/**
* Actions must override to do their work.
*/
protected abstract void execute(IAction action)
throws InvocationTargetException, InterruptedException;
/**
* This method is called by the platform UI framework when a command is run for
* which this action is the handler. The handler doesn't have an explicit context, for
* example unlike a view, editor, or workbench window actions, they are not initialized
* with a part. As a result when the action is run it will use the selection service
* to determine to elements on which to perform the action.
* <p>
* CVS actions should ensure that they can run without a proxy action. Meaning that
* <code>selectionChanged</code> and <code>run</code> should support passing
* <code>null</code> as the IAction parameter.
* </p>
* @throws ExecutionException
*/
@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
IWorkbenchWindow activeWorkbenchWindow = HandlerUtil.getActiveWorkbenchWindow(event);
if (activeWorkbenchWindow != null) {
ISelection selection = HandlerUtil.getCurrentSelection(event);
if (selection != null) {
IWorkbenchPart part = HandlerUtil.getActivePart(event);
try {
execute(activeWorkbenchWindow, part, selection);
} catch (InvocationTargetException e) {
throw new ExecutionException(TeamUIMessages.TeamAction_errorTitle, e);
} catch (InterruptedException e) {
// Operation was canceled. Ignore
}
}
}
return null;
}
private void execute(IWorkbenchWindow activeWorkbenchWindow,
IWorkbenchPart part, ISelection selection)
throws InvocationTargetException, InterruptedException {
// If the action is run from within an editor, try and find the
// file for the given editor.
if (part != null && part instanceof IEditorPart) {
IEditorInput input = ((IEditorPart) part).getEditorInput();
IFile file = ResourceUtil.getFile(input);
if (file != null) {
selectionChanged((IAction) null, new StructuredSelection(file));
}
} else {
// Fallback is to prime the action with the selection
selectionChanged((IAction) null, selection);
}
// Safe guard to ensure that the action is only run when enabled.
if (isEnabled()) {
execute((IAction) null);
} else {
MessageDialog.openInformation(activeWorkbenchWindow.getShell(),
TeamUIMessages.TeamAction_handlerNotEnabledTitle,
TeamUIMessages.TeamAction_handlerNotEnabledMessage);
}
}
/**
* Common run method for all Team actions.
*/
@Override
public void run(IAction action) {
try {
execute(action);
} catch (InvocationTargetException e) {
// Handle the exception and any accumulated errors
handle(e);
} catch (InterruptedException e) {
// Operation was canceled. Ignore.
}
}
/**
* This method can be overridden by subclasses but should not be invoked by
* them.
*
* @param e
* Exception to handle
*/
protected void handle(Exception e) {
handle(e, TeamUIMessages.TeamAction_errorTitle, null);
}
/**
* The <code>TeamAction</code> implementation of this
* <code>IActionDelegate2</code> method does nothing. Subclasses may
* reimplement.
*/
@Override
public void init(IAction action) {
}
/**
* The <code>TeamAction</code> implementation of this
* <code>IActionDelegate2</code> method redirects to the <code>run</code>
* method. Subclasses may reimplement.
*/
@Override
final public void runWithEvent(IAction action, Event event) {
run(action);
}
@Override
public void setEnabled(Object evaluationContext) {
IWorkbenchWindow activeWorkbenchWindow = (IWorkbenchWindow) HandlerUtil
.getVariable(evaluationContext,
ISources.ACTIVE_WORKBENCH_WINDOW_NAME);
if (activeWorkbenchWindow != null) {
ISelection selection = (ISelection) HandlerUtil.getVariable(
evaluationContext, ISources.ACTIVE_CURRENT_SELECTION_NAME);
if (selection == null) {
selection = StructuredSelection.EMPTY;
}
IWorkbenchPart part = (IWorkbenchPart) HandlerUtil.getVariable(
evaluationContext, ISources.ACTIVE_PART_NAME);
updateSelection(part, selection);
}
}
private void updateSelection(IWorkbenchPart part, ISelection selection) {
// If the action is run from within an editor, try and find the
// file for the given editor.
setActivePart(null, part);
if (part != null && part instanceof IEditorPart) {
IEditorInput input = ((IEditorPart) part).getEditorInput();
IFile file = ResourceUtil.getFile(input);
if (file != null) {
selectionChanged((IAction) null, new StructuredSelection(file));
}
} else {
// Fallback is to prime the action with the selection
selectionChanged((IAction) null, selection);
}
}
}