| /******************************************************************************* |
| * Copyright (c) 2000, 2018 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.ccvs.ui.actions; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.*; |
| |
| import org.eclipse.core.commands.IHandlerListener; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.mapping.ResourceMapping; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.jface.action.IAction; |
| import org.eclipse.jface.dialogs.MessageDialogWithToggle; |
| import org.eclipse.jface.dialogs.ProgressMonitorDialog; |
| import org.eclipse.jface.operation.IRunnableWithProgress; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.text.TextSelection; |
| import org.eclipse.jface.viewers.*; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.custom.BusyIndicator; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.team.core.TeamException; |
| import org.eclipse.team.internal.ccvs.core.*; |
| import org.eclipse.team.internal.ccvs.core.filehistory.CVSFileRevision; |
| import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot; |
| import org.eclipse.team.internal.ccvs.core.util.Util; |
| import org.eclipse.team.internal.ccvs.ui.*; |
| import org.eclipse.team.internal.ccvs.ui.repo.RepositoryManager; |
| import org.eclipse.team.internal.ccvs.ui.tags.TagSource; |
| import org.eclipse.team.internal.ccvs.ui.tags.TagSourceWorkbenchAdapter; |
| import org.eclipse.team.internal.ui.Utils; |
| import org.eclipse.team.internal.ui.actions.TeamAction; |
| import org.eclipse.team.internal.ui.dialogs.IPromptCondition; |
| import org.eclipse.ui.*; |
| import org.eclipse.ui.actions.RetargetAction; |
| import org.eclipse.ui.ide.IDE; |
| |
| /** |
| * CVSAction is the common superclass for all CVS actions. It provides |
| * facilities for enablement handling, standard error handling, selection |
| * retrieval and prompting. |
| */ |
| abstract public class CVSAction extends TeamAction implements IEditorActionDelegate { |
| |
| private List<IStatus> accumulatedStatus = new ArrayList<>(); |
| private RetargetAction retargetAction; |
| private IAction action; |
| |
| public CVSAction() { |
| super(); |
| } |
| |
| /** |
| * Initializes a retarget action that will listen to part changes and allow parts to |
| * override this action's behavior. The retarget action is used if this |
| * action is shown in a top-level menu or toolbar. |
| * @param window the workbench window showing this action |
| * @since 3.1 |
| */ |
| private void initializeRetargetAction(IWorkbenchWindow window) { |
| // Don't need to specify a the title because it will use this actions |
| // title instead. |
| retargetAction = new RetargetAction(getId(), ""); //$NON-NLS-1$ |
| retargetAction.addPropertyChangeListener(event -> { |
| if (event.getProperty().equals(IAction.ENABLED)) { |
| Object val1 = event.getNewValue(); |
| if (val1 instanceof Boolean && action != null) { |
| action.setEnabled(((Boolean) val1).booleanValue()); |
| } |
| } else if (event.getProperty().equals(IAction.CHECKED)) { |
| Object val2 = event.getNewValue(); |
| if (val2 instanceof Boolean && action != null) { |
| action.setChecked(((Boolean) val2).booleanValue()); |
| } |
| } else if (event.getProperty().equals(IAction.TEXT)) { |
| Object val3 = event.getNewValue(); |
| if (val3 instanceof String && action != null) { |
| action.setText((String) val3); |
| } |
| } else if (event.getProperty().equals(IAction.TOOL_TIP_TEXT)) { |
| Object val4 = event.getNewValue(); |
| if (val4 instanceof String && action != null) { |
| action.setToolTipText((String) val4); |
| } |
| } else if (event.getProperty().equals(SubActionBars.P_ACTION_HANDLERS)) { |
| if (action != null && retargetAction != null) { |
| action.setEnabled(retargetAction.isEnabled()); |
| } |
| } |
| }); |
| window.getPartService().addPartListener(retargetAction); |
| IWorkbenchPart activePart = window.getPartService().getActivePart(); |
| if (activePart != null) |
| retargetAction.partActivated(activePart); |
| } |
| |
| /** |
| * Common run method for all CVS actions. |
| */ |
| @Override |
| final public void run(IAction action) { |
| try { |
| if (!beginExecution(action)) return; |
| // If the action has been replaced by another handler, then |
| // call that one instead. |
| if(retargetAction != null && retargetAction.getActionHandler() != null) { |
| retargetAction.run(); |
| } else { |
| execute(action); |
| } |
| endExecution(); |
| } catch (InvocationTargetException e) { |
| // Handle the exception and any accumulated errors |
| handle(e); |
| } catch (InterruptedException e) { |
| // Show any problems that have occurred so far |
| handle(null); |
| } catch (TeamException e) { |
| // Handle the exception and any accumulated errors |
| handle(e); |
| } |
| } |
| |
| /** |
| * Return the command and retarget action id for this action. This is used to |
| *match retargetable actions and allow key bindings. |
| * |
| * @return the id for this action |
| * @since 3.1 |
| */ |
| public String getId() { |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Called when this action is added to a top-level menu or toolbar (e.g. IWorkbenchWindowDelegate) |
| * @since 3.1 |
| */ |
| @Override |
| public void init(IWorkbenchWindow window) { |
| super.init(window); |
| initializeRetargetAction(window); |
| } |
| |
| @Override |
| public boolean isEnabled() { |
| if(retargetAction != null && retargetAction.getActionHandler() != null) { |
| return retargetAction.isEnabled(); |
| } |
| // don't know so let subclasses decide |
| return false; |
| } |
| |
| @Override |
| public void dispose() { |
| super.dispose(); |
| IWorkbenchWindow window = getWindow(); |
| if (window != null) { |
| IPartService partService = window.getPartService(); |
| if (partService != null) |
| partService.removePartListener(retargetAction); |
| } |
| |
| if(retargetAction != null) { |
| retargetAction.dispose(); |
| retargetAction = null; |
| } |
| } |
| |
| @Override |
| public void selectionChanged(final IAction action, ISelection selection) { |
| if (selection instanceof TextSelection) { |
| // Since we have a text selection, we will assume that the target is the active editor. |
| // Look for the active editor and see it adapts to ResourceMapping or IResource. |
| // See bug 132176 |
| IWorkbenchPage activePage= PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); |
| IEditorPart part= activePage != null ? activePage.getActiveEditor() : null; |
| if (part != null) { |
| IEditorInput input = part.getEditorInput(); |
| ResourceMapping mapping = Utils.getResourceMapping(input); |
| if (mapping != null) { |
| selection = new StructuredSelection(mapping); |
| } else { |
| IResource resource = Utils.getResource(input); |
| if (resource != null) { |
| selection = new StructuredSelection(resource); |
| } |
| } |
| } |
| } |
| super.selectionChanged(action, selection); |
| this.action = action; |
| } |
| |
| @Override |
| protected void setActionEnablement(IAction action) { |
| if(retargetAction != null && retargetAction.getActionHandler() != null) { |
| action.setEnabled(retargetAction.isEnabled()); |
| } else { |
| super.setActionEnablement(action); |
| } |
| } |
| |
| /** |
| * This method gets invoked before the <code>CVSAction#execute(IAction)</code> |
| * method. It can perform any prechecking and initialization required before |
| * the action is executed. Subclasses may override but must invoke this |
| * inherited method to ensure proper initialization of this superclass is performed. |
| * These included preparation to accumulate IStatus and checking for dirty editors. |
| */ |
| protected boolean beginExecution(IAction action) throws TeamException { |
| accumulatedStatus.clear(); |
| if(needsToSaveDirtyEditors()) { |
| if(!saveAllEditors()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * This method gets invoked after <code>CVSAction#execute(IAction)</code> |
| * if no exception occurred. Subclasses may override but should invoke this |
| * inherited method to ensure proper handling of any accumulated IStatus. |
| */ |
| protected void endExecution() throws TeamException { |
| if ( ! accumulatedStatus.isEmpty()) { |
| handle(null); |
| } |
| } |
| |
| /** |
| * Add a status to the list of accumulated status. |
| * These will be provided to method handle(Exception, IStatus[]) |
| * when the action completes. |
| */ |
| protected void addStatus(IStatus status) { |
| accumulatedStatus.add(status); |
| } |
| |
| /** |
| * Return the list of status accumulated so far by the action. This |
| * will include any OK status that were added using addStatus(IStatus) |
| */ |
| protected IStatus[] getAccumulatedStatus() { |
| return accumulatedStatus.toArray(new IStatus[accumulatedStatus.size()]); |
| } |
| |
| /** |
| * Return the title to be displayed on error dialogs. |
| * Subclasses should override to present a custom message. |
| */ |
| protected String getErrorTitle() { |
| return CVSUIMessages.CVSAction_errorTitle; |
| } |
| |
| /** |
| * Return the title to be displayed on error dialogs when warnings occur. |
| * Subclasses should override to present a custom message. |
| */ |
| protected String getWarningTitle() { |
| return CVSUIMessages.CVSAction_warningTitle; |
| } |
| |
| /** |
| * Return the message to be used for the parent MultiStatus when |
| * multiple errors occur during an action. |
| * Subclasses should override to present a custom message. |
| */ |
| protected String getMultiStatusMessage() { |
| return CVSUIMessages.CVSAction_multipleProblemsMessage; |
| } |
| |
| /** |
| * Return the status to be displayed in an error dialog for the given list |
| * of non-OK status. |
| * |
| * This method can be overridden but subclasses. Returning an OK status will |
| * prevent the error dialog from being shown. |
| */ |
| protected IStatus getStatusToDisplay(IStatus[] problems) { |
| if (problems.length == 1) { |
| return problems[0]; |
| } |
| MultiStatus combinedStatus = new MultiStatus(CVSUIPlugin.ID, 0, getMultiStatusMessage(), null); |
| for (IStatus problem : problems) { |
| combinedStatus.merge(problem); |
| } |
| return combinedStatus; |
| } |
| |
| /** |
| * Method that implements generic handling of an exception. |
| * |
| * This method will also use any accumulated status when determining what |
| * information (if any) to show the user. |
| * |
| * @param exception the exception that occurred (or null if none occurred) |
| * @param status any status accumulated by the action before the end of |
| * the action or the exception occurred. |
| */ |
| @Override |
| protected void handle(Exception exception) { |
| // Get the non-OK status |
| List<IStatus> problems = new ArrayList<>(); |
| IStatus[] status = getAccumulatedStatus(); |
| if (status != null) { |
| for (IStatus iStatus : status) { |
| if ( ! iStatus.isOK() || iStatus.getCode() == CVSStatus.SERVER_ERROR) { |
| problems.add(iStatus); |
| } |
| } |
| } |
| // Handle the case where there are no problem status |
| if (problems.isEmpty()) { |
| if (exception == null) return; |
| handle(exception, getErrorTitle(), null); |
| return; |
| } |
| |
| // For now, display both the exception and the problem status |
| // Later, we can determine how to display both together |
| if (exception != null) { |
| handle(exception, getErrorTitle(), null); |
| } |
| |
| String message = null; |
| IStatus statusToDisplay = getStatusToDisplay(problems.toArray(new IStatus[problems.size()])); |
| if (statusToDisplay.isOK()) return; |
| if (statusToDisplay.isMultiStatus() && statusToDisplay.getChildren().length == 1) { |
| message = statusToDisplay.getMessage(); |
| statusToDisplay = statusToDisplay.getChildren()[0]; |
| } |
| String title; |
| if (statusToDisplay.getSeverity() == IStatus.ERROR) { |
| title = getErrorTitle(); |
| } else { |
| title = getWarningTitle(); |
| } |
| CVSUIPlugin.openError(getShell(), title, message, new CVSException(statusToDisplay)); |
| } |
| |
| /** |
| * Convenience method for running an operation with the appropriate progress. |
| * Any exceptions are propagated so they can be handled by the |
| * <code>CVSAction#run(IAction)</code> error handling code. |
| * |
| * @param runnable the runnable which executes the operation |
| * @param cancelable indicate if a progress monitor should be cancelable |
| * @param progressKind one of PROGRESS_BUSYCURSOR or PROGRESS_DIALOG |
| */ |
| final protected void run(final IRunnableWithProgress runnable, boolean cancelable, int progressKind) throws InvocationTargetException, InterruptedException { |
| final Exception[] exceptions = new Exception[] {null}; |
| |
| // Ensure that no repository view refresh happens until after the action |
| final IRunnableWithProgress innerRunnable = monitor -> getRepositoryManager().run(runnable, monitor); |
| |
| switch (progressKind) { |
| case PROGRESS_BUSYCURSOR : |
| BusyIndicator.showWhile(Display.getCurrent(), () -> { |
| try { |
| innerRunnable.run(new NullProgressMonitor()); |
| } catch (InvocationTargetException e1) { |
| exceptions[0] = e1; |
| } catch (InterruptedException e2) { |
| exceptions[0] = e2; |
| } |
| }); |
| break; |
| case PROGRESS_DIALOG : |
| default : |
| new ProgressMonitorDialog(getShell()).run(cancelable, cancelable, innerRunnable); |
| break; |
| } |
| if (exceptions[0] != null) { |
| if (exceptions[0] instanceof InvocationTargetException) |
| throw (InvocationTargetException)exceptions[0]; |
| else |
| throw (InterruptedException)exceptions[0]; |
| } |
| } |
| |
| /** |
| * Answers if the action would like dirty editors to saved |
| * based on the CVS preference before running the action. By |
| * default, CVSActions do not save dirty editors. |
| */ |
| protected boolean needsToSaveDirtyEditors() { |
| return false; |
| } |
| |
| /** |
| * Returns the selected CVS resources |
| */ |
| protected ICVSResource[] getSelectedCVSResources() { |
| ArrayList<Object> resources = null; |
| IStructuredSelection selection = getSelection(); |
| if (!selection.isEmpty()) { |
| resources = new ArrayList<>(); |
| Iterator elements = selection.iterator(); |
| while (elements.hasNext()) { |
| Object next = elements.next(); |
| if (next instanceof ICVSResource) { |
| resources.add(next); |
| continue; |
| } |
| if (next instanceof IAdaptable) { |
| IAdaptable a = (IAdaptable) next; |
| Object adapter = a.getAdapter(ICVSResource.class); |
| if (adapter instanceof ICVSResource) { |
| resources.add(adapter); |
| continue; |
| } |
| } |
| } |
| } |
| if (resources != null && !resources.isEmpty()) { |
| return resources.toArray(new ICVSResource[resources.size()]); |
| } |
| return new ICVSResource[0]; |
| } |
| |
| /** |
| * Get selected CVS remote folders |
| */ |
| protected ICVSRemoteFolder[] getSelectedRemoteFolders() { |
| ArrayList<Object> resources = null; |
| IStructuredSelection selection = getSelection(); |
| if (!selection.isEmpty()) { |
| resources = new ArrayList<>(); |
| Iterator elements = selection.iterator(); |
| while (elements.hasNext()) { |
| Object next = elements.next(); |
| if (next instanceof ICVSRemoteFolder) { |
| resources.add(next); |
| continue; |
| } |
| if (next instanceof IAdaptable) { |
| IAdaptable a = (IAdaptable) next; |
| Object adapter = a.getAdapter(ICVSRemoteFolder.class); |
| if (adapter instanceof ICVSRemoteFolder) { |
| resources.add(adapter); |
| continue; |
| } |
| } |
| } |
| } |
| if (resources != null && !resources.isEmpty()) { |
| return resources.toArray(new ICVSRemoteFolder[resources.size()]); |
| } |
| return new ICVSRemoteFolder[0]; |
| } |
| |
| /** |
| * Returns the selected remote resources |
| */ |
| protected ICVSRemoteResource[] getSelectedRemoteResources() { |
| ArrayList<Object> resources = null; |
| IStructuredSelection selection = getSelection(); |
| if (!selection.isEmpty()) { |
| resources = new ArrayList<>(); |
| Iterator elements = selection.iterator(); |
| while (elements.hasNext()) { |
| Object next = elements.next(); |
| if (next instanceof ICVSRemoteResource) { |
| resources.add(next); |
| continue; |
| } |
| if (next instanceof CVSFileRevision) { |
| resources.add(((CVSFileRevision)next).getCVSRemoteFile()); |
| continue; |
| } |
| if (next instanceof ILogEntry) { |
| resources.add(((ILogEntry)next).getRemoteFile()); |
| continue; |
| } |
| if (next instanceof IAdaptable) { |
| IAdaptable a = (IAdaptable) next; |
| Object adapter = a.getAdapter(ICVSRemoteResource.class); |
| if (adapter instanceof ICVSRemoteResource) { |
| resources.add(adapter); |
| continue; |
| } |
| } |
| } |
| } |
| if (resources != null && !resources.isEmpty()) { |
| ICVSRemoteResource[] result = new ICVSRemoteResource[resources.size()]; |
| resources.toArray(result); |
| return result; |
| } |
| return new ICVSRemoteResource[0]; |
| } |
| |
| /** |
| * A helper prompt condition for prompting for CVS dirty state. |
| */ |
| public static IPromptCondition getOverwriteLocalChangesPrompt(final IResource[] dirtyResources) { |
| return new IPromptCondition() { |
| List resources = Arrays.asList(dirtyResources); |
| @Override |
| public boolean needsPrompt(IResource resource) { |
| return resources.contains(resource); |
| } |
| @Override |
| public String promptMessage(IResource resource) { |
| return NLS.bind(CVSUIMessages.ReplaceWithAction_localChanges, new String[] { resource.getName() }); |
| } |
| }; |
| } |
| |
| /** |
| * Checks if a the resources' parent's tags are different then the given tag. |
| * Prompts the user that they are adding mixed tags and returns <code>true</code> if |
| * the user wants to continue or <code>false</code> otherwise. |
| */ |
| public static boolean checkForMixingTags(final Shell shell, IResource[] resources, final CVSTag tag) throws CVSException { |
| final IPreferenceStore store = CVSUIPlugin.getPlugin().getPreferenceStore(); |
| if(!store.getBoolean(ICVSUIConstants.PREF_PROMPT_ON_MIXED_TAGS)) { |
| return true; |
| }; |
| |
| final boolean[] result = new boolean[] { true }; |
| |
| for (IResource resource : resources) { |
| if (resource.getType() != IResource.PROJECT) { |
| ICVSResource cvsResource = CVSWorkspaceRoot.getCVSResourceFor(resource); |
| CVSTag parentTag = cvsResource.getParent().getFolderSyncInfo().getTag(); |
| // prompt if the tags are not equal |
| // consider BASE to be equal the parent tag since we don't make BASE sticky on replace |
| if (!CVSTag.equalTags(tag, parentTag) && !CVSTag.equalTags(tag, CVSTag.BASE)) { |
| shell.getDisplay().syncExec(() -> { |
| MessageDialogWithToggle dialog = MessageDialogWithToggle.openOkCancelConfirm(shell, |
| CVSUIMessages.CVSAction_mixingTagsTitle, |
| NLS.bind(CVSUIMessages.CVSAction_mixingTags, new String[] { tag.getName() }), |
| CVSUIMessages.CVSAction_doNotShowThisAgain, false, store, |
| ICVSUIConstants.PREF_PROMPT_ON_MIXED_TAGS); |
| result[0] = dialog.getReturnCode() == 0; |
| }); |
| // only prompt once |
| break; |
| } |
| } |
| } |
| return result[0]; |
| } |
| |
| /** |
| * Based on the CVS preference for saving dirty editors this method will either |
| * ignore dirty editors, save them automatically, or prompt the user to save them. |
| * |
| * @return <code>true</code> if the command succeeded, and <code>false</code> |
| * if at least one editor with unsaved changes was not saved |
| */ |
| private boolean saveAllEditors() { |
| final int option = CVSUIPlugin.getPlugin().getPreferenceStore().getInt(ICVSUIConstants.PREF_SAVE_DIRTY_EDITORS); |
| final boolean[] okToContinue = new boolean[] {true}; |
| if (option != ICVSUIConstants.OPTION_NEVER) { |
| Display.getDefault().syncExec(() -> { |
| boolean confirm = option == ICVSUIConstants.OPTION_PROMPT; |
| IResource[] selectedResources = getSelectedResources(); |
| if (selectedResources != null) { |
| okToContinue[0] = IDE.saveAllEditors(selectedResources, confirm); |
| } |
| }); |
| } |
| return okToContinue[0]; |
| } |
| |
| @Override |
| protected void handle(Exception exception, String title, String message) { |
| CVSUIPlugin.openError(getShell(), title, message, exception, CVSUIPlugin.LOG_NONTEAM_EXCEPTIONS); |
| } |
| |
| protected RepositoryManager getRepositoryManager() { |
| return CVSUIPlugin.getPlugin().getRepositoryManager(); |
| } |
| |
| /* |
| * @see org.eclipse.team.internal.ui.actions.TeamAction#getSelectedResources() |
| */ |
| protected final IResource[] getSelectedResourcesWithOverlap() { |
| IStructuredSelection selection = getSelection(); |
| CVSActionSelectionProperties props = CVSActionSelectionProperties.getProperties(getSelection()); |
| if (props == null) { |
| return Utils.getContributedResources(selection.toArray()); |
| } |
| return props.getAllSelectedResources(); |
| } |
| |
| @Override |
| protected final IResource[] getSelectedResources() { |
| IStructuredSelection selection = getSelection(); |
| CVSActionSelectionProperties props = CVSActionSelectionProperties.getProperties(getSelection()); |
| if (props == null) { |
| return CVSActionSelectionProperties.getNonOverlapping(Utils.getContributedResources(selection.toArray())); |
| } |
| return props.getNonoverlappingSelectedResources(); |
| } |
| |
| @Override |
| public void setActiveEditor(IAction action, IEditorPart targetEditor) { |
| } |
| |
| /** |
| * These handlers won't have any interesting property changes. There is |
| * no need to notify listeners. |
| * @param handlerListener |
| * @since 3.1 |
| */ |
| @Override |
| public void removeHandlerListener(IHandlerListener handlerListener) { |
| } |
| @Override |
| public void addHandlerListener(IHandlerListener handlerListener) { |
| } |
| |
| @Override |
| public boolean isHandled() { |
| return true; |
| } |
| |
| protected final ICVSResource getCVSResourceFor(IResource resource) { |
| CVSActionSelectionProperties props = CVSActionSelectionProperties.getProperties(getSelection()); |
| if (props == null) { |
| return CVSWorkspaceRoot.getCVSResourceFor(resource); |
| } |
| return props.getCVSResourceFor(resource); |
| } |
| |
| public static CVSTag getAccurateFileTag(ICVSResource cvsResource) throws CVSException { |
| CVSTag tag = null; |
| if (cvsResource != null) { |
| return Util.getAccurateFileTag(cvsResource, getTags(cvsResource)); |
| } |
| return tag; |
| } |
| |
| public static CVSTag[] getTags(ICVSResource cvsResource) { |
| TagSource tagSource= TagSource.create(new ICVSResource[] { cvsResource }); |
| return tagSource.getTags(TagSource.convertIncludeFlaqsToTagTypes(TagSourceWorkbenchAdapter.INCLUDE_ALL_TAGS)); |
| |
| } |
| } |