blob: 83f2e57fea1c4e7259629d67e918be7e0891e822 [file] [log] [blame]
/*******************************************************************************
* <copyright>
*
* Copyright (c) 2005, 2018 SAP AG.
* 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:
* SAP AG - initial API, implementation and documentation
* mwenz - Bug 324859 - Need Undo/Redo support for Non-EMF based domain objects
* mwenz - Bug 340627 - Features should be able to indicate cancellation
* mwenz - Bug 351053 - Remove the need for WorkspaceCommandStackImpl
* mwenz - Bug 486902 - Cancelling model changes done in a CustomFeature logs an error stack trace
*
* </copyright>
*
*******************************************************************************/
package org.eclipse.graphiti.ui.internal.editor;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.common.command.CommandStackListener;
import org.eclipse.emf.transaction.RollbackException;
import org.eclipse.emf.transaction.TransactionalCommandStack;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.emf.workspace.IWorkspaceCommandStack;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.commands.CommandStack;
import org.eclipse.graphiti.features.IFeature;
import org.eclipse.graphiti.features.IFeatureAndContext;
import org.eclipse.graphiti.internal.command.CommandContainer;
import org.eclipse.graphiti.internal.command.DefaultExecutionInfo;
import org.eclipse.graphiti.internal.command.FeatureCommand;
import org.eclipse.graphiti.internal.command.GFPreparableCommand2;
import org.eclipse.graphiti.internal.command.ICommand;
import org.eclipse.graphiti.tb.IToolBehaviorProvider;
import org.eclipse.graphiti.ui.internal.T;
import org.eclipse.graphiti.ui.internal.command.AddModelObjectCommand;
import org.eclipse.graphiti.ui.internal.command.ContextEntryCommand;
import org.eclipse.graphiti.ui.internal.command.CreateConnectionCommand;
import org.eclipse.graphiti.ui.internal.command.GFCommand;
import org.eclipse.graphiti.ui.internal.command.GefCommandWrapper;
import org.eclipse.graphiti.ui.internal.command.ReconnectCommand;
import org.eclipse.graphiti.ui.internal.services.GraphitiUiInternal;
import org.eclipse.graphiti.ui.platform.IConfigurationProvider;
/**
* The Class GFCommandStack.
*
* @noinstantiate This class is not intended to be instantiated by clients.
* @noextend This class is not intended to be subclassed by clients.
*/
public class GFCommandStack extends CommandStack implements CommandStackListener {
static final String OPTION_EXECUTION_INFO = "org.eclipse.graphiti.execution.info";
private TransactionalCommandStack emfCommandStack;
private IConfigurationProvider configurationProvider;
private TransactionalEditingDomain editingDomain;
public GFCommandStack(IConfigurationProvider configurationProvider, TransactionalEditingDomain editingDomain) {
org.eclipse.emf.common.command.CommandStack commandStack = editingDomain.getCommandStack();
if (!(commandStack instanceof TransactionalCommandStack)) {
IllegalArgumentException e = new IllegalArgumentException(
"The command stack of the passed editing domain must be a TransactionalCommandStack");
e.fillInStackTrace();
throw e;
}
emfCommandStack = (TransactionalCommandStack) commandStack;
emfCommandStack.addCommandStackListener(this);
setConfigurationProvider(configurationProvider);
this.editingDomain = editingDomain;
}
@Override
public boolean canRedo() {
return getEmfCommandStack().canRedo();
}
@Override
public boolean canUndo() {
return getEmfCommandStack().canUndo();
}
@Override
public void dispose() {
super.dispose();
emfCommandStack.removeCommandStackListener(this);
emfCommandStack = null;
configurationProvider = null;
editingDomain = null;
}
@Override
public void execute(Command gefCommand) {
if (gefCommand == null) {
return;
}
org.eclipse.emf.common.command.Command emfCommand = GraphitiUiInternal.getCommandService().transformFromGefToEmfCommand(gefCommand);
org.eclipse.emf.common.command.Command gfPreparableCommand = new GFPreparableCommand2(getTransactionalEditingDomain(), emfCommand);
IToolBehaviorProvider tbp = getConfigurationProvider().getDiagramTypeProvider().getCurrentToolBehaviorProvider();
DefaultExecutionInfo executionInfo = new DefaultExecutionInfo();
GraphitiUiInternal.getCommandService().completeExecutionInfo(executionInfo, gefCommand);
// Put the execution info object into the options map for consumption within the execution
Map<String, DefaultExecutionInfo> options = new HashMap<String, DefaultExecutionInfo>(2);
options.put(OPTION_EXECUTION_INFO, executionInfo);
// Store contexts of redo operations for restoring after a feature that
// has done no changes has been executed to re-enable redo. Executing a
// feature will remove the contexts from the operations - see Bug 405710
IUndoContext[][] contexts = new IUndoContext[0][];
if (emfCommandStack instanceof IWorkspaceCommandStack) {
IUndoableOperation[] originalRedoOperations = ((IWorkspaceCommandStack) emfCommandStack)
.getOperationHistory().getRedoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);
contexts = new IUndoContext[originalRedoOperations.length][];
for (int i = 0; i < originalRedoOperations.length; i++) {
contexts[i] = originalRedoOperations[i].getContexts();
}
}
tbp.preExecute(executionInfo);
try {
getEmfCommandStack().execute(gfPreparableCommand, options);
} catch (RollbackException e) {
if (e.getStatus().getSeverity() == IStatus.CANCEL) {
// Just log it as info (operation was cancelled on purpose)
T.racer().log(IStatus.INFO, "Command execution was cancelled: " + e.getMessage()); //$NON-NLS-1$
} else {
T.racer().error("GFCommandStack.execute(Command) " + e, e); //$NON-NLS-1$
}
} catch (Exception e) {
T.racer().error("GFCommandStack.execute(Command) " + e, e); //$NON-NLS-1$
}
tbp.postExecute(executionInfo);
// Check if the executed feature has really done changes (indicated by
// IFeature.hasDoneChanges). If not, remove the operation of the command
// used to execute the feature from the command stack. It will then also
// no longer appear as an entry in the undo stack. This is especially
// necessary for direct editing in a pattern environment when e.g. a new
// object is created and directly offered for direct editing (handled
// internally as two different commands because of EMF restrictions).
// This has been reworked for bug 327756 to enable the call to
// hasDoneChanges also for other types of features (especially
// CustomFeatures and CreateFeatures).
List<IFeature> features = new ArrayList<IFeature>(2);
if (gefCommand instanceof GefCommandWrapper) {
GefCommandWrapper gefCommandWrapper = (GefCommandWrapper) gefCommand;
ICommand command = gefCommandWrapper.getCommand();
extractFeatures(features, command);
} else {
extractFeatures(features, gefCommand);
}
boolean changesDone = false;
// Basic assumption is that no changes were done, only if no features
// were found we cannot judge if any changes happened, in that case we
// need to assume that changes happened.
if (features.size() == 0) {
changesDone = true;
}
// Check if any of the involved features has done changes
for (Iterator<IFeature> iterator = features.iterator(); iterator.hasNext();) {
IFeature feature = iterator.next();
if (feature.hasDoneChanges()) {
// First change is enough
changesDone = true;
break;
}
}
// If no changes were done revert the undo stack entry
if (!changesDone && emfCommandStack instanceof IWorkspaceCommandStack) {
// Retrieve the last operation using the default context
IWorkspaceCommandStack workspaceCommandStackImpl = (IWorkspaceCommandStack) getEmfCommandStack();
IUndoContext context = workspaceCommandStackImpl.getDefaultUndoContext();
IUndoableOperation operation = workspaceCommandStackImpl.getOperationHistory().getUndoOperation(context);
// Replace the found operation with an empty set
workspaceCommandStackImpl.getOperationHistory().replaceOperation(operation, new IUndoableOperation[0]);
// Restore the original contexts of the redo operations to re-enable
// undo as there is no new entry on the undo stack - see Bug 405710
IUndoableOperation[] newRedoOperations = workspaceCommandStackImpl.getOperationHistory().getRedoHistory(
IOperationHistory.GLOBAL_UNDO_CONTEXT);
for (int i = 0; i < newRedoOperations.length; i++) {
for (int j = 0; j < contexts[i].length; j++) {
if (!newRedoOperations[i].hasContext(contexts[i][j])) {
newRedoOperations[i].addContext(contexts[i][j]);
}
}
}
// Update the editor actions bars, especially Edit --> Undo
notifyListeners(gefCommand, CommandStack.POST_MASK);
}
}
/**
* Extracts from the given command (java.lang.Object is used because the
* potentially passed objects do not have another common super class) the
* list of all involved (executed) features. The features are added to the
* passed list.
*
* @param features
* The list to which the features will be added.
* @param command
* Any kind of command object that is executed on this command
* stack.
*/
private void extractFeatures(List<IFeature> features, Object command) {
if (command instanceof FeatureCommand) {
// Used in direct editing
features.add(((FeatureCommand) command).getFeature());
} else if (command instanceof ContextEntryCommand) {
// Used for wrapping any feature in a context button
features.add(((ContextEntryCommand) command).getContextEntry().getFeature());
} else if (command instanceof AddModelObjectCommand) {
// Used for add features
IFeatureAndContext[] innerFeatures = ((AddModelObjectCommand) command).getFeaturesAndContexts();
for (int i = 0; i < innerFeatures.length; i++) {
features.add(innerFeatures[i].getFeature());
}
} else if (command instanceof CreateConnectionCommand) {
// Used for connection creation
IFeatureAndContext[] innerFeatures = ((CreateConnectionCommand) command).getFeaturesAndContexts();
for (int i = 0; i < innerFeatures.length; i++) {
features.add(innerFeatures[i].getFeature());
}
} else if (command instanceof GFCommand) {
// Used for object creation
features.add(((GFCommand) command).getFeature());
} else if (command instanceof ReconnectCommand) {
// Used for reconnection features
features.add(((ReconnectCommand) command).getFeature());
} else if (command instanceof CommandContainer) {
// Used for custom features
ICommand[] innerCommands = ((CommandContainer) command).getCommands();
for (int i = 0; i < innerCommands.length; i++) {
extractFeatures(features, innerCommands[i]);
}
}
}
@Override
public void flush() {
super.flush();
}
@Override
public Object[] getCommands() {
return super.getCommands();
}
@Override
public Command getRedoCommand() {
org.eclipse.emf.common.command.Command emfCommand = getEmfCommandStack().getRedoCommand();
return emfCommand != null ? GraphitiUiInternal.getCommandService().transformFromEmfToGefCommand(emfCommand) : null;
}
@Override
public Command getUndoCommand() {
org.eclipse.emf.common.command.Command emfCommand = getEmfCommandStack().getUndoCommand();
return emfCommand != null ? GraphitiUiInternal.getCommandService().transformFromEmfToGefCommand(emfCommand) : null;
}
@Override
public boolean isDirty() {
return super.isDirty();
}
@Override
public void redo() {
getEmfCommandStack().redo();
}
@Override
public void undo() {
getEmfCommandStack().undo();
}
private TransactionalCommandStack getEmfCommandStack() {
return emfCommandStack;
}
private TransactionalEditingDomain getTransactionalEditingDomain() {
return editingDomain;
}
@SuppressWarnings("deprecation")
public void commandStackChanged(EventObject event) {
notifyListeners();
notifyListeners(null, 0);
}
private IConfigurationProvider getConfigurationProvider() {
return configurationProvider;
}
private void setConfigurationProvider(IConfigurationProvider configurationProvider) {
this.configurationProvider = configurationProvider;
}
}