Refactored trace creation into an undoable operation

The creation of a trace link is now an undoable operation. This means
that all views that want to create a trace link can reuse the operation
from the org.eclipse.capra.ui.operations package and get automatic undo
ability. To facilitate this, a new getTraces() method has also been
implemented in TraceHelper. In addition, the API in this class has been
simplified.
At the same time, creating a trace link with the SelectionView can now
be undone via the Edit menu as well as using the platform-specific keys.

Change-Id: I5042ba23fce8eccceca3ebe8fdfa11080ff0b8c7
diff --git a/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java b/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java
index 03a8068..7b19076 100644
--- a/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java
+++ b/bundles/org.eclipse.capra.core/src/org/eclipse/capra/core/helpers/TraceHelper.java
@@ -17,6 +17,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.eclipse.capra.core.adapters.ArtifactMetaModelAdapter;
 import org.eclipse.capra.core.adapters.Connection;
@@ -56,6 +57,15 @@
 	}
 
 	/**
+	 * Deletes the given traces from the trace model
+	 * 
+	 * @param toDelete the traces to delete from the trace model
+	 */
+	public void deleteTraces(List<Connection> toDelete) {
+		traceAdapter.deleteTrace(toDelete, traceModel);
+	}
+
+	/**
 	 * Annotate artifacts represented by wrappers
 	 *
 	 * @param wrappers
@@ -98,29 +108,36 @@
 			}
 		}
 	}
-	
+
 	public List<EObject> getTracedElements(Connection connection) {
 		List<EObject> tracedElements = new ArrayList<>();
 		tracedElements.add(connection.getOrigin());
 		tracedElements.addAll(connection.getTargets());
 		return tracedElements;
-		
+
 	}
 
 	/**
 	 * Checks if a trace link of a certain type containing a certain selection
-	 * already exists in the trace model.
+	 * already exists in the trace model for the instance of this class.
 	 * 
-	 * @param selection
-	 *            the selected elements
-	 * @param traceType
-	 *            the type of trace link
-	 * @param traceModel
-	 *            the trace model to be checked
+	 * @param selection the selected elements
+	 * @param traceType the type of trace link
 	 * @return true if the link exists
 	 */
+	public boolean traceExists(List<EObject> selection, EClass traceType) {
+		return !getTraces(selection, traceType).isEmpty();
+	}
 
-	public boolean traceExists(List<EObject> selection, EClass traceType, EObject traceModel) {
+	/**
+	 * Returns all trace links of the given type containing the given selection that
+	 * exist in the trace model set in the instance of this class.
+	 * 
+	 * @param selection the selected elements
+	 * @param traceType the type of trace link
+	 * @return a list of trace links that fit the criteria
+	 */
+	public List<Connection> getTraces(List<EObject> selection, EClass traceType) {
 		TraceMetaModelAdapter traceAdapter = ExtensionPointHelper.getTraceMetamodelAdapter().get();
 		// create a connection
 		List<EObject> allElements = new ArrayList<>(selection);
@@ -135,7 +152,8 @@
 
 		Connection connection = new Connection(source, targets, tempTlink);
 
-		return traceAdapter.getAllTraceLinks(traceModel).stream().anyMatch(c -> c.equals(connection));
+		return traceAdapter.getAllTraceLinks(this.traceModel).stream().filter(c -> c.equals(connection))
+				.collect(Collectors.toCollection(ArrayList::new));
 	}
 
 }
diff --git a/bundles/org.eclipse.capra.ui/META-INF/MANIFEST.MF b/bundles/org.eclipse.capra.ui/META-INF/MANIFEST.MF
index 4537245..9ef4cad 100644
--- a/bundles/org.eclipse.capra.ui/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.capra.ui/META-INF/MANIFEST.MF
@@ -21,9 +21,11 @@
  org.eclipse.ui.ide,
  org.eclipse.ui,
  org.eclipse.core.resources,
+ org.eclipse.core.commands,
  org.eclipse.capra.core
 Bundle-ActivationPolicy: lazy
 Export-Package: org.eclipse.capra.ui.handlers,
  org.eclipse.capra.ui.helpers,
- org.eclipse.capra.ui.views
+ org.eclipse.capra.ui.views,
+ org.eclipse.capra.ui.operations
 
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/handlers/TraceCreationHandler.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/handlers/TraceCreationHandler.java
index 0baa5f2..cf70018 100644
--- a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/handlers/TraceCreationHandler.java
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/handlers/TraceCreationHandler.java
@@ -13,130 +13,35 @@
  *******************************************************************************/
 package org.eclipse.capra.ui.handlers;
 
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.BiFunction;
-import java.util.stream.Collectors;
-
-import org.eclipse.capra.core.adapters.TraceMetaModelAdapter;
-import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
-import org.eclipse.capra.core.handlers.IArtifactHandler;
-import org.eclipse.capra.core.helpers.ArtifactHelper;
-import org.eclipse.capra.core.helpers.EMFHelper;
-import org.eclipse.capra.core.helpers.ExtensionPointHelper;
-import org.eclipse.capra.core.helpers.TraceHelper;
-import org.eclipse.capra.ui.preferences.CapraPreferences;
+import org.eclipse.capra.ui.operations.CreateTraceOperation;
 import org.eclipse.capra.ui.views.SelectionView;
 import org.eclipse.core.commands.AbstractHandler;
 import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.emf.ecore.EClass;
-import org.eclipse.emf.ecore.EObject;
-import org.eclipse.emf.ecore.resource.ResourceSet;
-import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
-import org.eclipse.jface.dialogs.MessageDialog;
-import org.eclipse.jface.dialogs.MessageDialogWithToggle;
-import org.eclipse.jface.preference.IPreferenceStore;
-import org.eclipse.jface.viewers.LabelProvider;
-import org.eclipse.jface.window.Window;
-import org.eclipse.swt.SWT;
-import org.eclipse.ui.IWorkbenchWindow;
-import org.eclipse.ui.dialogs.ElementListSelectionDialog;
-import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.core.commands.operations.IOperationHistory;
+import org.eclipse.core.commands.operations.IUndoContext;
+import org.eclipse.core.commands.operations.OperationHistoryFactory;
+import org.eclipse.core.runtime.IAdaptable;
 
 public class TraceCreationHandler extends AbstractHandler {
 
-	private static final String CAPRA_INFORMATION = "Capra Information";
-	private static final String TRACE_LINK_EXISTS = "The trace link you want to create already exists and will therefore not be created";
-	private static final String TRACE_LINK_SUCCESSFULLY_CREATED = "Trace link has been successfully created";
-	private static final String DO_NOT_SHOW_DIALOG_AGAIN = "Do not show this dialog again";
-	private static final String SELECT_TRACE_LINK_TYPE = "Select the trace type you want to create";
-	private static final String SELECTION = "Selection";
+	private IUndoContext undoContext = IOperationHistory.GLOBAL_UNDO_CONTEXT;
 
 	@Override
 	public Object execute(ExecutionEvent event) throws ExecutionException {
-		IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindowChecked(event);
-		createTrace(window, (traceTypes, selection) -> getTraceTypeToCreate(window, traceTypes, selection));
+		IAdaptable adapter = SelectionView.getOpenedView().getSite();
+
+		IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();
+
+		CreateTraceOperation createTraceOperation = new CreateTraceOperation("Create trace link",
+				SelectionView.getOpenedView().getSelection());
+		createTraceOperation.addContext(undoContext);
+		operationHistory.execute(createTraceOperation, null, adapter);
 		return null;
 	}
 
-	public void createTrace(IWorkbenchWindow window,
-			BiFunction<Collection<EClass>, List<EObject>, Optional<EClass>> chooseTraceType) {
-		List<?> artifacts = SelectionView.getOpenedView().getSelection();
-
-		TraceMetaModelAdapter traceAdapter = ExtensionPointHelper.getTraceMetamodelAdapter().get();
-		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
-
-		ResourceSet resourceSet = new ResourceSetImpl();
-		// add trace model to resource set
-		EObject traceModel = persistenceAdapter.getTraceModel(resourceSet);
-		// add artifact model to resource set
-		EObject artifactModel = persistenceAdapter.getArtifactWrappers(resourceSet);
-
-		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
-		TraceHelper traceHelper = new TraceHelper(traceModel);
-
-		// Create the artifact wrappers
-		List<EObject> wrappers = artifactHelper.createWrappers(artifacts);
-
-		// Get the type of trace to be created
-		Collection<EClass> traceTypes = traceAdapter.getAvailableTraceTypes(wrappers);
-		Optional<EClass> chosenType = chooseTraceType.apply(traceTypes, wrappers);
-
-		// Create trace
-		if (chosenType.isPresent()) {
-			// check if the connection already exists
-			if (traceHelper.traceExists(wrappers, chosenType.get(), traceModel)) {
-				MessageDialog.openInformation(window.getShell(), CAPRA_INFORMATION, TRACE_LINK_EXISTS);
-			} else {
-				traceHelper.createTrace(wrappers, chosenType.get());
-				persistenceAdapter.saveTracesAndArtifacts(traceModel, artifactModel);
-				traceHelper.annotateTrace(wrappers);
-
-				// check from preferences if user wants to see the "trace
-				// successfully created dialog"
-				IPreferenceStore store = CapraPreferences.getPreferences();
-				if (store.getBoolean(CapraPreferences.SHOW_TRACE_CREATED_CONFIRMATION_DIALOG)) {
-					MessageDialogWithToggle.open(MessageDialog.INFORMATION, window.getShell(), CAPRA_INFORMATION,
-							TRACE_LINK_SUCCESSFULLY_CREATED, DO_NOT_SHOW_DIALOG_AGAIN, false, store,
-							CapraPreferences.SHOW_TRACE_CREATED_CONFIRMATION_DIALOG, SWT.NONE);
-				}
-			}
-		}
-	}
-
-	private Optional<EClass> getTraceTypeToCreate(IWorkbenchWindow window, Collection<EClass> traceTypes,
-			List<EObject> wrappers) {
-		ElementListSelectionDialog dialog = new ElementListSelectionDialog(window.getShell(), new LabelProvider() {
-			@Override
-			public String getText(Object element) {
-				EClass eclass = (EClass) element;
-				return eclass.getName();
-			}
-		});
-		dialog.setTitle(SELECT_TRACE_LINK_TYPE);
-		dialog.setElements(traceTypes.toArray());
-
-		dialog.setMessage(
-				SELECTION + " : " + wrappers.stream().map(this::getSelectionDisplayName).collect(Collectors.toList()));
-
-		if (dialog.open() == Window.OK) {
-			return Optional.of((EClass) dialog.getFirstResult());
-		}
-
-		return Optional.empty();
-	}
-
-	private String getSelectionDisplayName(EObject element) {
-		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
-		EObject artifactModel = persistenceAdapter.getArtifactWrappers(new ResourceSetImpl());
-		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
-		IArtifactHandler<?> handler = artifactHelper.getHandler(artifactHelper.unwrapWrapper(element)).get();
-
-		return handler.withCastedHandler(artifactHelper.unwrapWrapper(element), (h, o) -> h.getDisplayName(o))
-				.orElse(EMFHelper.getIdentifier(element));
-
+	public void setUndoContext(IUndoContext undoContext) {
+		this.undoContext = undoContext;
 	}
 
 }
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java
new file mode 100644
index 0000000..19f34cf
--- /dev/null
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/operations/CreateTraceOperation.java
@@ -0,0 +1,216 @@
+/*******************************************************************************
+ * Copyright (c) 2016, 2019 Chalmers | University of Gothenburg, rt-labs and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *  
+ * SPDX-License-Identifier: EPL-2.0
+ *  
+ * Contributors:
+ *      Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
+ *      Chalmers | University of Gothenburg - additional features, updated API
+ *******************************************************************************/
+package org.eclipse.capra.ui.operations;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+import org.eclipse.capra.core.adapters.Connection;
+import org.eclipse.capra.core.adapters.TraceMetaModelAdapter;
+import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
+import org.eclipse.capra.core.handlers.IArtifactHandler;
+import org.eclipse.capra.core.helpers.ArtifactHelper;
+import org.eclipse.capra.core.helpers.EMFHelper;
+import org.eclipse.capra.core.helpers.ExtensionPointHelper;
+import org.eclipse.capra.core.helpers.TraceHelper;
+import org.eclipse.capra.ui.preferences.CapraPreferences;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.operations.AbstractOperation;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.MessageDialogWithToggle;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.dialogs.ElementListSelectionDialog;
+
+/**
+ * Operation to create trace links, undo and redo.
+ * 
+ * @author Jan-Philipp Steghöfer
+ *
+ */
+public class CreateTraceOperation extends AbstractOperation {
+
+	private static final String CAPRA_INFORMATION = "Capra Information";
+	private static final String TRACE_LINK_EXISTS = "The trace link you want to create already exists and will therefore not be created";
+	private static final String TRACE_LINK_SUCCESSFULLY_CREATED = "Trace link has been successfully created";
+	private static final String DO_NOT_SHOW_DIALOG_AGAIN = "Do not show this dialog again";
+	private static final String SELECT_TRACE_LINK_TYPE = "Select the trace type you want to create";
+	private static final String SELECTION = "Selection";
+
+	private Optional<EClass> chosenType;
+	private List<EObject> wrappers;
+	private EObject traceModel;
+
+	private List<?> artifacts = null;
+
+	private BiFunction<Collection<EClass>, List<EObject>, Optional<EClass>> chooseTraceType = null;
+
+	private CreateTraceOperation(String label) {
+		super(label);
+	}
+
+	/**
+	 * Creates a new operation to create links.
+	 * 
+	 * @param label     the label used for the operation. Should never be
+	 *                  <code>null</code>.
+	 * @param artifacts the artifacts on which this operation is performed. Should
+	 *                  never be <code>null</code>.
+	 */
+	public CreateTraceOperation(String label, List<?> artifacts) {
+		super(label);
+		Assert.isNotNull(artifacts);
+		this.artifacts = artifacts;
+	}
+
+	@Override
+	public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
+		if (artifacts == null || artifacts.isEmpty()) {
+			return Status.CANCEL_STATUS;
+		}
+		Shell shell = info.getAdapter(Shell.class);
+		if (chooseTraceType == null) {
+			chooseTraceType = (traceTypes, selection) -> getTraceTypeToCreate(shell, traceTypes, selection);
+		}
+		createTrace(shell, chooseTraceType);
+		return Status.OK_STATUS;
+	}
+
+	@Override
+	public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
+		return execute(monitor, info);
+	}
+
+	@Override
+	public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
+		TraceHelper traceHelper = new TraceHelper(traceModel);
+		if (this.wrappers != null && this.chosenType != null && this.traceModel != null) {
+			List<Connection> connections = traceHelper.getTraces(this.wrappers, this.chosenType.get());
+			if (!connections.isEmpty()) {
+				traceHelper.deleteTraces(connections);
+				return Status.OK_STATUS;
+			}
+		}
+		return Status.CANCEL_STATUS;
+	}
+
+	/**
+	 * Create a trace link after eliciting the trace type via the provided
+	 * bi-function.
+	 * 
+	 * @param shell           a shell instance to allow opening message windows
+	 * @param chooseTraceType a bi-function used to select a suitable trace link
+	 *                        type
+	 */
+	public void createTrace(Shell shell,
+			BiFunction<Collection<EClass>, List<EObject>, Optional<EClass>> chooseTraceType) {
+		TraceMetaModelAdapter traceAdapter = ExtensionPointHelper.getTraceMetamodelAdapter().get();
+		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
+
+		ResourceSet resourceSet = new ResourceSetImpl();
+		// add trace model to resource set
+		this.traceModel = persistenceAdapter.getTraceModel(resourceSet);
+		// add artifact model to resource set
+		EObject artifactModel = persistenceAdapter.getArtifactWrappers(resourceSet);
+
+		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
+		TraceHelper traceHelper = new TraceHelper(this.traceModel);
+
+		// Create the artifact wrappers
+		this.wrappers = artifactHelper.createWrappers(this.artifacts);
+
+		// Get the type of trace to be created
+		Collection<EClass> traceTypes = traceAdapter.getAvailableTraceTypes(this.wrappers);
+		this.chosenType = chooseTraceType.apply(traceTypes, this.wrappers);
+
+		// Create trace
+		if (this.chosenType.isPresent()) {
+			// check if the connection already exists
+			if (traceHelper.traceExists(this.wrappers, this.chosenType.get())) {
+				MessageDialog.openInformation(shell, CAPRA_INFORMATION, TRACE_LINK_EXISTS);
+			} else {
+				traceHelper.createTrace(this.wrappers, this.chosenType.get());
+				persistenceAdapter.saveTracesAndArtifacts(traceModel, artifactModel);
+				traceHelper.annotateTrace(this.wrappers);
+
+				// check from preferences if user wants to see the "trace
+				// successfully created dialog"
+				IPreferenceStore store = CapraPreferences.getPreferences();
+				if (store.getBoolean(CapraPreferences.SHOW_TRACE_CREATED_CONFIRMATION_DIALOG)) {
+					MessageDialogWithToggle.open(MessageDialog.INFORMATION, shell, CAPRA_INFORMATION,
+							TRACE_LINK_SUCCESSFULLY_CREATED, DO_NOT_SHOW_DIALOG_AGAIN, false, store,
+							CapraPreferences.SHOW_TRACE_CREATED_CONFIRMATION_DIALOG, SWT.NONE);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Sets the bi-function to select a suitable trace link type.
+	 * 
+	 * @param chooseTraceType the bi-function to use to select a suitable trace link
+	 *                        type
+	 */
+	public void setChooseTraceType(BiFunction<Collection<EClass>, List<EObject>, Optional<EClass>> chooseTraceType) {
+		this.chooseTraceType = chooseTraceType;
+	}
+
+	private Optional<EClass> getTraceTypeToCreate(Shell shell, Collection<EClass> traceTypes, List<EObject> wrappers) {
+		ElementListSelectionDialog dialog = new ElementListSelectionDialog(shell, new LabelProvider() {
+			@Override
+			public String getText(Object element) {
+				EClass eclass = (EClass) element;
+				return eclass.getName();
+			}
+		});
+		dialog.setTitle(SELECT_TRACE_LINK_TYPE);
+		dialog.setElements(traceTypes.toArray());
+
+		dialog.setMessage(
+				SELECTION + " : " + wrappers.stream().map(this::getSelectionDisplayName).collect(Collectors.toList()));
+
+		if (dialog.open() == Window.OK) {
+			return Optional.of((EClass) dialog.getFirstResult());
+		}
+
+		return Optional.empty();
+	}
+
+	private String getSelectionDisplayName(EObject element) {
+		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
+		EObject artifactModel = persistenceAdapter.getArtifactWrappers(new ResourceSetImpl());
+		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
+		IArtifactHandler<?> handler = artifactHelper.getHandler(artifactHelper.unwrapWrapper(element)).get();
+
+		return handler.withCastedHandler(artifactHelper.unwrapWrapper(element), (h, o) -> h.getDisplayName(o))
+				.orElse(EMFHelper.getIdentifier(element));
+
+	}
+
+}
diff --git a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/views/SelectionView.java b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/views/SelectionView.java
index 5228867..45d3aa0 100644
--- a/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/views/SelectionView.java
+++ b/bundles/org.eclipse.capra.ui/src/org/eclipse/capra/ui/views/SelectionView.java
@@ -29,6 +29,8 @@
 import org.eclipse.capra.core.handlers.PriorityHandler;
 import org.eclipse.capra.core.helpers.ArtifactHelper;
 import org.eclipse.capra.core.helpers.ExtensionPointHelper;
+import org.eclipse.core.commands.operations.IOperationHistory;
+import org.eclipse.core.commands.operations.IUndoContext;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
 import org.eclipse.jface.action.IMenuListener;
@@ -51,9 +53,13 @@
 import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Menu;
+import org.eclipse.ui.IActionBars;
 import org.eclipse.ui.ISharedImages;
 import org.eclipse.ui.PartInitException;
 import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.operations.RedoActionHandler;
+import org.eclipse.ui.operations.UndoActionHandler;
 import org.eclipse.ui.part.ViewPart;
 
 public class SelectionView extends ViewPart {
@@ -82,6 +88,22 @@
 	/** The maintained selection of EObjects */
 	private Set<Object> selection = new LinkedHashSet<>();
 
+	/**
+	 * The appropriate undo context. We are using the global context to ensure that
+	 * trace creation can be undone in all viewers and editors.
+	 */
+	private IUndoContext undoContext = IOperationHistory.GLOBAL_UNDO_CONTEXT;
+
+	/**
+	 * Action handler to undo the creation of a trace.
+	 */
+	private UndoActionHandler undoAction;
+
+	/**
+	 * Action handler to redo the creation of a trace.
+	 */
+	private RedoActionHandler redoAction;
+
 	class ViewContentProvider implements IStructuredContentProvider {
 		@Override
 		public void inputChanged(Viewer v, Object oldInput, Object newInput) {
@@ -125,8 +147,8 @@
 	}
 
 	/**
-	 * Leaves the order of objects unchanged by returning 0 for all combinations
-	 * of objects.
+	 * Leaves the order of objects unchanged by returning 0 for all combinations of
+	 * objects.
 	 *
 	 * @see ViewerComparator#compare(Viewer, Object, Object)
 	 */
@@ -181,6 +203,8 @@
 				.map(Transfer.class::cast).collect(Collectors.toList()));
 
 		viewer.addDropSupport(ops, transfers.toArray(DEFAULT_TRANSFERS), new SelectionDropAdapter(viewer));
+
+		createGlobalActionHandlers();
 	}
 
 	private void hookContextMenu() {
@@ -265,4 +289,14 @@
 		selection.removeAll(currentselection);
 		viewer.refresh();
 	}
+
+	private void createGlobalActionHandlers() {
+		// set up action handlers that operate on the current context
+		undoAction = new UndoActionHandler(this.getSite(), undoContext);
+		redoAction = new RedoActionHandler(this.getSite(), undoContext);
+		IActionBars actionBars = getViewSite().getActionBars();
+		actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
+		actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
+		actionBars.updateActionBars();
+	}
 }
diff --git a/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestCreateTraceOperation.java b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestCreateTraceOperation.java
new file mode 100644
index 0000000..9d1800f
--- /dev/null
+++ b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestCreateTraceOperation.java
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * Copyright (c) 2016, 2019 Chalmers | University of Gothenburg, rt-labs and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v20.html
+ *  
+ * SPDX-License-Identifier: EPL-2.0
+ *  
+ * Contributors:
+ *      Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
+ *      Chalmers | University of Gothenburg - additional features, updated API
+ *******************************************************************************/
+package org.eclipse.capra.testsuite;
+
+import static org.eclipse.capra.testsuite.TestHelper.clearWorkspace;
+import static org.eclipse.capra.testsuite.TestHelper.createEClassInEPackage;
+import static org.eclipse.capra.testsuite.TestHelper.createEcoreModel;
+import static org.eclipse.capra.testsuite.TestHelper.createSimpleProject;
+import static org.eclipse.capra.testsuite.TestHelper.getProject;
+import static org.eclipse.capra.testsuite.TestHelper.load;
+import static org.eclipse.capra.testsuite.TestHelper.projectExists;
+import static org.eclipse.capra.testsuite.TestHelper.resetSelectionView;
+import static org.eclipse.capra.testsuite.TestHelper.save;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
+import org.eclipse.capra.core.helpers.ArtifactHelper;
+import org.eclipse.capra.core.helpers.ExtensionPointHelper;
+import org.eclipse.capra.core.helpers.TraceHelper;
+import org.eclipse.capra.generic.tracemodel.TracemodelPackage;
+import org.eclipse.capra.ui.operations.CreateTraceOperation;
+import org.eclipse.capra.ui.views.SelectionView;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.operations.IOperationHistory;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestCreateTraceOperation {
+
+	private static final String CLASS_A_NAME = "A";
+	private static final String CLASS_B_NAME = "B";
+
+	private static final String MODEL_A_FILENAME = "modelA.ecore";
+	private static final String MODEL_B_FILENAME = "modelB.ecore";
+	private static final String MODEL_A_NAME = "modelA";
+	private static final String MODEL_B_NAME = "modelB";
+
+	private static final String TEST_PROJECT_NAME = "TestProject";
+
+	@Before
+	public void init() throws CoreException {
+		clearWorkspace();
+		resetSelectionView();
+	}
+
+	@Test
+	public void testUndoTraceLinkCreation() throws CoreException, IOException {
+		// Create a project
+		createSimpleProject(TEST_PROJECT_NAME);
+		assertTrue(projectExists(TEST_PROJECT_NAME));
+
+		// Create two models and persist them
+		IProject testProject = getProject(TEST_PROJECT_NAME);
+		EPackage a = TestHelper.createEcoreModel(MODEL_A_NAME);
+		createEClassInEPackage(a, CLASS_A_NAME);
+		save(testProject, a);
+
+		EPackage b = createEcoreModel(MODEL_B_NAME);
+		createEClassInEPackage(b, CLASS_B_NAME);
+		save(testProject, b);
+
+		// Load them, choose two elements
+		ResourceSet rs = new ResourceSetImpl();
+
+		EPackage _a = load(testProject, MODEL_A_FILENAME, rs);
+		assertEquals(_a.getName(), MODEL_A_NAME);
+		EClass _A = (EClass) _a.getEClassifier(CLASS_A_NAME);
+
+		EPackage _b = load(testProject, MODEL_B_FILENAME, rs);
+		assertEquals(_b.getName(), MODEL_B_NAME);
+		EClass _B = (EClass) _b.getEClassifier(CLASS_B_NAME);
+
+		// Add them to the selection view
+		SelectionView.getOpenedView().dropToSelection(_A);
+		SelectionView.getOpenedView().dropToSelection(_B);
+
+		EClass traceType = TracemodelPackage.eINSTANCE.getRelatedTo();
+
+		IAdaptable adapter = SelectionView.getOpenedView().getSite();
+
+		IWorkbench workbench = PlatformUI.getWorkbench();
+		IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
+
+		CreateTraceOperation createTraceOperation = new CreateTraceOperation("Create trace link",
+				SelectionView.getOpenedView().getSelection());
+		createTraceOperation.setChooseTraceType((traceTypes, selection) -> {
+			if (traceTypes.contains(traceType)) {
+				return Optional.of(traceType);
+			} else {
+				return Optional.empty();
+			}
+		});
+		try {
+			assertEquals(operationHistory.execute(createTraceOperation, null, adapter), Status.OK_STATUS);
+		} catch (ExecutionException e) {
+			fail("Could not create trace: ExecutionException in operation");
+		}
+
+		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
+		TraceHelper traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(new ResourceSetImpl()));
+		EObject artifactModel = persistenceAdapter.getArtifactWrappers(_A.eResource().getResourceSet());
+		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
+		List<EObject> selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
+
+		// Check that the trace exists
+		selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
+		assertTrue(traceHelper.traceExists(selection, traceType));
+
+		try {
+			assertEquals(operationHistory.undoOperation(createTraceOperation, null, adapter), Status.OK_STATUS);
+		} catch (ExecutionException e) {
+			fail("Could not undo trace creation: ExecutionException in operation");
+		}
+
+		// Check that the trace does not exist
+		traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(new ResourceSetImpl()));
+		selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
+		assertFalse(traceHelper.traceExists(selection, traceType));
+	}
+}
diff --git a/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestDuplicateLinks.java b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestDuplicateLinks.java
index adb5b47..d407a8b 100644
--- a/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestDuplicateLinks.java
+++ b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestDuplicateLinks.java
@@ -186,20 +186,18 @@
 
 		// Test the trace exists method
 		TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
-		EObject traceModel = persistenceAdapter.getTraceModel(_A.eResource().getResourceSet());
-		TraceHelper traceHelper = new TraceHelper(traceModel);
+		TraceHelper traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(_A.eResource().getResourceSet()));
 		EObject artifactModel = persistenceAdapter.getArtifactWrappers(_A.eResource().getResourceSet());
 		ArtifactHelper artifactHelper = new ArtifactHelper(artifactModel);
 		EClass traceType = TracemodelPackage.eINSTANCE.getRelatedTo();
 		List<EObject> selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
 
-		assertFalse(traceHelper.traceExists(selection, traceType,
-				persistenceAdapter.getTraceModel(_A.eResource().getResourceSet())));
+		assertFalse(traceHelper.traceExists(selection, traceType));
 
 		createTraceForCurrentSelectionOfType(TracemodelPackage.eINSTANCE.getRelatedTo());
 
-		assertTrue(traceHelper.traceExists(selection, traceType,
-				persistenceAdapter.getTraceModel(_A.eResource().getResourceSet())));
+		traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(_A.eResource().getResourceSet()));
+		assertTrue(traceHelper.traceExists(selection, traceType));
 
 		// Change the order of the selection in the selection view
 		SelectionView.getOpenedView().clearSelection();
@@ -210,18 +208,16 @@
 
 		// Check that the trace exists
 		selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
-		assertTrue(traceHelper.traceExists(selection, traceType,
-				persistenceAdapter.getTraceModel(_A.eResource().getResourceSet())));
+		traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(_A.eResource().getResourceSet()));
+		assertTrue(traceHelper.traceExists(selection, traceType));
 
 		// Add another class to the selection view
 		SelectionView.getOpenedView().dropToSelection(_C);
 
 		// Check that the trace does not exist
 		selection = artifactHelper.createWrappers(SelectionView.getOpenedView().getSelection());
-		assertFalse(traceHelper.traceExists(selection, traceType,
-				persistenceAdapter.getTraceModel(_A.eResource().getResourceSet())));
-
-
+		traceHelper = new TraceHelper(persistenceAdapter.getTraceModel(_A.eResource().getResourceSet()));
+		assertFalse(traceHelper.traceExists(selection, traceType));
 	}
 
 }
diff --git a/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestHelper.java b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestHelper.java
index c68ca18..d875914 100644
--- a/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestHelper.java
+++ b/tests/org.eclipse.capra.testsuite/src/org/eclipse/capra/testsuite/TestHelper.java
@@ -25,7 +25,7 @@
 import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
 import org.eclipse.capra.core.helpers.ArtifactHelper;
 import org.eclipse.capra.core.helpers.ExtensionPointHelper;
-import org.eclipse.capra.ui.handlers.TraceCreationHandler;
+import org.eclipse.capra.ui.operations.CreateTraceOperation;
 import org.eclipse.capra.ui.plantuml.ToggleTransitivityHandler;
 import org.eclipse.capra.ui.views.SelectionView;
 import org.eclipse.cdt.core.CCorePlugin;
@@ -64,7 +64,7 @@
 import org.eclipse.jdt.core.IPackageFragmentRoot;
 import org.eclipse.jdt.core.IType;
 import org.eclipse.jdt.core.JavaCore;
-import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.swt.widgets.Shell;
 import org.eclipse.ui.PlatformUI;
 
 /**
@@ -85,8 +85,7 @@
 	/**
 	 * Creates an empty project
 	 *
-	 * @param projectName
-	 *            the name of the project
+	 * @param projectName the name of the project
 	 * @throws CoreException
 	 */
 	public static IProject createSimpleProject(String projectName) throws CoreException {
@@ -101,8 +100,7 @@
 	/**
 	 * Creates a Java project and a Java class declaration inside it.
 	 *
-	 * @param projectName
-	 *            the name of the project
+	 * @param projectName the name of the project
 	 * @return the created Java class
 	 * @throws CoreException
 	 */
@@ -165,10 +163,8 @@
 	/**
 	 * Checks if the project with the provided name exists.
 	 *
-	 * @param projectName
-	 *            the name of the project
-	 * @return true if the project exists in the active workspace, false
-	 *         otherwise
+	 * @param projectName the name of the project
+	 * @return true if the project exists in the active workspace, false otherwise
 	 */
 	public static boolean projectExists(String projectName) {
 		return getProject(projectName).exists();
@@ -177,8 +173,7 @@
 	/**
 	 * Returns a handle to the project resource with the given name.
 	 *
-	 * @param projectName
-	 *            the name of the project
+	 * @param projectName the name of the project
 	 * @return a handle to the project resource
 	 */
 	public static IProject getProject(String projectName) {
@@ -189,8 +184,7 @@
 	/**
 	 * Creates an empty Ecore model.
 	 *
-	 * @param name
-	 *            the name of the model
+	 * @param name the name of the model
 	 * @return
 	 */
 	public static EPackage createEcoreModel(String name) {
@@ -202,10 +196,8 @@
 	/**
 	 * Creates an EClass entity in the provided model.
 	 *
-	 * @param p
-	 *            an Ecore model
-	 * @param name
-	 *            the name of the created EClass entity
+	 * @param p    an Ecore model
+	 * @param name the name of the created EClass entity
 	 */
 	public static void createEClassInEPackage(EPackage p, String name) {
 		EClass c = EcoreFactory.eINSTANCE.createEClass();
@@ -216,10 +208,8 @@
 	/**
 	 * Nests a new EPackage inside the provided EPackage.
 	 *
-	 * @param p
-	 *            a new EPackage
-	 * @param name
-	 *            the name of the created EPackage
+	 * @param p    a new EPackage
+	 * @param name the name of the created EPackage
 	 */
 	public static void createEPackageInEPackage(EPackage p, String name) {
 		EPackage pkg = EcoreFactory.eINSTANCE.createEPackage();
@@ -230,10 +220,8 @@
 	/**
 	 * Persists (saves) the provided Ecore model in the specified project.
 	 *
-	 * @param project
-	 *            a handle to the project in which the model is to be persisted
-	 * @param pack
-	 *            the Ecore model to be persisted
+	 * @param project a handle to the project in which the model is to be persisted
+	 * @param pack    the Ecore model to be persisted
 	 * @throws IOException
 	 */
 	public static void save(IProject project, EPackage pack) throws IOException {
@@ -247,12 +235,9 @@
 	/**
 	 * Returns an Ecore model entity from the specified project.
 	 *
-	 * @param project
-	 *            the project containing the model
-	 * @param p
-	 *            the name of the model
-	 * @param rs
-	 *            the provided ResourceSet instance
+	 * @param project the project containing the model
+	 * @param p       the name of the model
+	 * @param rs      the provided ResourceSet instance
 	 * @return an Ecore model entity
 	 * @throws IOException
 	 */
@@ -264,13 +249,13 @@
 	/**
 	 * Creates a trace between the objects that are in the Selection view.
 	 *
-	 * @param traceType
-	 *            the type of the trace that is to connect the objects
+	 * @param traceType the type of the trace that is to connect the objects
 	 */
 	public static void createTraceForCurrentSelectionOfType(EClass traceType) {
-		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
-		TraceCreationHandler handler = new TraceCreationHandler();
-		handler.createTrace(window, (traceTypes, selection) -> {
+		Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
+		CreateTraceOperation operation = new CreateTraceOperation("Create trace link",
+				SelectionView.getOpenedView().getSelection());
+		operation.createTrace(shell, (traceTypes, selection) -> {
 			if (traceTypes.contains(traceType)) {
 				return Optional.of(traceType);
 			} else {
@@ -282,10 +267,8 @@
 	/**
 	 * Checks if there is a trace between the provided Objects.
 	 *
-	 * @param a
-	 *            first EObject
-	 * @param b
-	 *            second EObject
+	 * @param a first EObject
+	 * @param b second EObject
 	 * @return true if a trace exists between the two objects, false otherwise
 	 */
 	public static boolean thereIsATraceBetween(Object firstObject, Object secondObject) {
@@ -331,8 +314,7 @@
 	/**
 	 * Creates an empty C or C++ project.
 	 *
-	 * @param projectName
-	 *            the name of the project to be created
+	 * @param projectName the name of the project to be created
 	 * @return a handle to the created project
 	 * @throws CoreException
 	 * @throws BuildException
@@ -360,10 +342,8 @@
 	/**
 	 * Creates a C source file in the provided C project.
 	 *
-	 * @param fileName
-	 *            the name of the C source file to be created in the project
-	 * @param cProject
-	 *            the project in which the file is to be created
+	 * @param fileName the name of the C source file to be created in the project
+	 * @param cProject the project in which the file is to be created
 	 * @return the created TranslationUnit
 	 * @throws CoreException
 	 */
@@ -386,10 +366,8 @@
 	/**
 	 * Creates an empty file in the project with the provided name.
 	 *
-	 * @param fileName
-	 *            the name of the created file
-	 * @param projectName
-	 *            the name of the project in which the file is to be created
+	 * @param fileName    the name of the created file
+	 * @param projectName the name of the project in which the file is to be created
 	 * @return a handle to the created file
 	 * @throws CoreException
 	 */