AgeEditor.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ge.gef.ui.editor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.emf.common.CommonPlugin;
import org.eclipse.emf.common.util.URI;
import org.eclipse.gef.fx.nodes.InfiniteCanvas;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.operations.UndoRedoActionGroup;
import org.eclipse.ui.part.EditorPart;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.views.contentoutline.ContentOutline;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.CanonicalBusinessObjectReference;
import org.osate.ge.gef.AgeGefRuntimeException;
import org.osate.ge.gef.DiagramEditorNode;
import org.osate.ge.gef.palette.SimplePaletteItem;
import org.osate.ge.gef.ui.AgeGefUiPlugin;
import org.osate.ge.gef.ui.diagram.GefAgeDiagram;
import org.osate.ge.gef.ui.editor.Interaction.InteractionState;
import org.osate.ge.gef.ui.editor.overlays.Overlays;
import org.osate.ge.gef.ui.preferences.Preferences;
import org.osate.ge.internal.AgeDiagramProvider;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.AgeDiagramUtil;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramModificationAdapter;
import org.osate.ge.internal.diagram.runtime.DiagramModificationListener;
import org.osate.ge.internal.diagram.runtime.DiagramModifier;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DiagramSerialization;
import org.osate.ge.internal.diagram.runtime.ModificationsCompletedEvent;
import org.osate.ge.internal.diagram.runtime.layout.DiagramElementLayoutUtil;
import org.osate.ge.internal.diagram.runtime.layout.LayoutInfoProvider;
import org.osate.ge.internal.diagram.runtime.updating.BusinessObjectNodeFactory;
import org.osate.ge.internal.diagram.runtime.updating.BusinessObjectTreeUpdater;
import org.osate.ge.internal.diagram.runtime.updating.DefaultBusinessObjectTreeUpdater;
import org.osate.ge.internal.diagram.runtime.updating.DefaultDiagramElementGraphicalConfigurationProvider;
import org.osate.ge.internal.diagram.runtime.updating.DiagramUpdater;
import org.osate.ge.internal.services.AadlModificationService;
import org.osate.ge.internal.services.ActionExecutor;
import org.osate.ge.internal.services.ActionExecutor.ExecutionMode;
import org.osate.ge.internal.services.ActionService;
import org.osate.ge.internal.services.ColoringService;
import org.osate.ge.internal.services.ExtensionRegistryService;
import org.osate.ge.internal.services.ExtensionRegistryService.RegisteredImage;
import org.osate.ge.internal.services.ModelChangeNotifier;
import org.osate.ge.internal.services.ModelChangeNotifier.ChangeListener;
import org.osate.ge.internal.services.ProjectProvider;
import org.osate.ge.internal.services.ProjectReferenceService;
import org.osate.ge.internal.services.ReferenceService;
import org.osate.ge.internal.services.SystemInstanceLoadingService;
import org.osate.ge.internal.services.UiService;
import org.osate.ge.internal.services.impl.DefaultActionService;
import org.osate.ge.internal.services.impl.DefaultColoringService;
import org.osate.ge.internal.services.impl.ProjectReferenceServiceProxy;
import org.osate.ge.internal.ui.editor.ActivateAgeEditorAction;
import org.osate.ge.internal.ui.editor.AgeContentOutlinePage;
import org.osate.ge.internal.ui.editor.DiagramContextChecker;
import org.osate.ge.internal.ui.editor.InternalDiagramEditor;
import org.osate.ge.internal.ui.editor.actions.CopyAction;
import org.osate.ge.internal.ui.editor.actions.PasteAction;
import org.osate.ge.internal.ui.editor.actions.SelectAllAction;
import org.osate.ge.internal.ui.handlers.AgeHandlerUtil;
import org.osate.ge.internal.ui.tools.ActivatedEvent;
import org.osate.ge.internal.ui.tools.DeactivatedEvent;
import org.osate.ge.internal.ui.tools.Tool;
import org.osate.ge.services.QueryService;
import org.osate.ge.services.ReferenceResolutionService;
import org.osate.ge.services.impl.DefaultQueryService;
import org.osate.ge.services.impl.DefaultReferenceResolutionService;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;

import com.google.common.collect.ImmutableList;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.embed.swt.FXCanvas;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;

/**
 * JavaFX/GEF based diagram editor implementation
 */
public class AgeEditor extends EditorPart implements InternalDiagramEditor, ITabbedPropertySheetPageContributor {
	/**
	 * List of supported zoom levels. Contains the selectable zoom levels shared between the editor and {@link ZoomSelectorContributionItem}.
	 */
	public static final ImmutableList<Double> ZOOM_LEVELS = ImmutableList.of(.1, .2, .5, .75, 1.0, 1.25, 1.5, 2.0, 2.5,
			3.0, 4.0, 5.0, 7.5, 10.0);
	private static final String CONTRIBUTOR_ID = "org.osate.ge.editor.AgeDiagramEditor";
	private static final String CONTEXT_ID = "org.osate.ge.context";
	private static final String MENU_ID = CONTRIBUTOR_ID;
	private static final double DIAGRAM_PADDING = 16.0; // Padding around the diagram
	private final IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(AgeGefUiPlugin.PLUGIN_ID);
	private final IPreferenceStore preferenceStore = AgeGefUiPlugin.getDefault().getPreferenceStore();

	/**
	 * Handles activation and deactivation of {@link Tool} objects. Only one tools can be activated at a time.
	 *
	 */
	public class ToolHandler {
		private Tool activeTool = null;
		private ImmutableList<BusinessObjectContext> selectedBocs = ImmutableList.of();

		/**
		 * Activates the tool. If a tool is already active, it deactivates it before activating the specified tool. Also deactivates the active palette item.
		 * @param tool the tool to activate.
		 */
		public void activate(final Tool tool) {
			Objects.requireNonNull(tool, "tool must not be null");

			// Deactivate the current tool
			deactivateActiveTool();
			activeTool = tool;
			paletteModel.deactivateNonSelectItem();

			activeTool.activated(new ActivatedEvent(selectedBocs, diagram, aadlModService, getAdapter(UiService.class),
					coloringService));
		}

		/**
		 * Deactivates the active tool. Also deactivates the active palette item.
		 */
		public void deactivateActiveTool() {
			if (activeTool != null) {
				activeTool.deactivated(new DeactivatedEvent());
				activeTool = null;
				paletteModel.deactivateNonSelectItem();
			}
		}

		private void setSelectedElements(final ImmutableList<BusinessObjectContext> newSelectedBocs) {
			// Ignore the selection if nothing has changed
			if (Objects.equals(this.selectedBocs, newSelectedBocs)) {
				return;
			}

			this.selectedBocs = newSelectedBocs;

			if (selectedBocs.isEmpty()) {
				return;
			}

			// Notify the active tool
			if (activeTool != null) {
				activeTool.selectionChanged(new org.osate.ge.internal.ui.tools.SelectionChangedEvent(selectedBocs));
			}
		}
	}

	/**
	 * Class for managing the editor's selection
	 */
	private class SelectionProvider implements ISelectionProvider {
		private final ListenerList<ISelectionChangedListener> listeners = new ListenerList<>();
		private IStructuredSelection currentSelection;

		@Override
		public void setSelection(final ISelection newSelection) {
			// Set our selection to the diagram nodes contained in the specified selection.
			if (newSelection instanceof IStructuredSelection) {
				final IStructuredSelection ss = (IStructuredSelection) newSelection;
				final List<?> selectedObjects = ss.toList();
				final IStructuredSelection newStructuredSelection = new StructuredSelection(selectedObjects.stream()
						.filter(DiagramNode.class::isInstance)
						.map(DiagramNode.class::cast)
						.distinct()
						.toArray());

				// Update the current selection and notify listeners
				if (!Objects.equals(currentSelection, newStructuredSelection)) {
					currentSelection = newStructuredSelection;

					// Notify listeners
					final SelectionChangedEvent e = new SelectionChangedEvent(this, currentSelection);
					for (final ISelectionChangedListener listener : listeners) {
						listener.selectionChanged(e);
					}
				}
			}
		}

		@Override
		public void addSelectionChangedListener(final ISelectionChangedListener listener) {
			listeners.add(listener);
		}

		@Override
		public void removeSelectionChangedListener(final ISelectionChangedListener listener) {
			listeners.remove(listener);
		}

		@Override
		public IStructuredSelection getSelection() {
			return this.currentSelection;
		}
	}

	// This wrapper adds padding around the diagram.
	private static class DiagramNodeWrapper extends Group {
		private final Rectangle invisibleBoundsRect = new Rectangle();

		public DiagramNodeWrapper(final Node diagramSceneNode) {
			// The content bounds of the canvas will be set by the geometry bounds.
			// Create padding by creating a rectangle filled with a transparent color.
			invisibleBoundsRect.setFill(Color.TRANSPARENT);
			getChildren().setAll(invisibleBoundsRect, diagramSceneNode);
		}

		@Override
		protected void layoutChildren() {
			super.layoutChildren();

			if (getChildren().size() >= 2) {
				// Adjust size of rectangle. Right and bottom sides have more padding to allow for scroll bars.
				final Bounds diagramBounds = getChildren().get(1).getBoundsInParent();
				invisibleBoundsRect.setX(diagramBounds.getMinX() - DIAGRAM_PADDING);
				invisibleBoundsRect.setY(diagramBounds.getMinY() - DIAGRAM_PADDING);
				invisibleBoundsRect.setWidth(diagramBounds.getWidth() + DIAGRAM_PADDING * 3);
				invisibleBoundsRect.setHeight(diagramBounds.getHeight() + DIAGRAM_PADDING * 3);
			}
		}
	}

	// Global Services
	private final ModelChangeNotifier modelChangeNotifier;
	private final ReferenceService referenceService;
	private final ExtensionRegistryService extRegistry;
	private final ActionService actionService;
	private final SystemInstanceLoadingService systemInstanceLoader;
	private final AadlModificationService aadlModService;
	private IEclipseContext eclipseContext;

	private boolean disposed = false;
	private AgeDiagram diagram;
	private GefAgeDiagram gefDiagram;
	private Overlays overlays;
	private IFile diagramFile;
	private IProject project;

	// Diagram-specific Services
	private final QueryService queryService;
	private final ColoringService coloringService;
	private final BusinessObjectTreeUpdater boTreeUpdater;
	private final DefaultDiagramElementGraphicalConfigurationProvider deInfoProvider;
	private final DiagramUpdater diagramUpdater;
	private ProjectProvider projectProvider = () -> project;
	private AgeDiagramProvider diagramProvider = () -> diagram;
	private final ProjectReferenceServiceProxy projectReferenceService;
	private ActionExecutor actionExecutor;
	private final SelectionProvider selectionProvider = new SelectionProvider();
	private final Map<Class<?>, Object> adapterMap = new HashMap<>();

	private MenuManager contextMenuManager;
	private AgeContentOutlinePage outlinePage;
	private TabbedPropertySheetPage propertySheetPage;
	private FXCanvas fxCanvas;
	private InfiniteCanvas canvas;
	private int cleanDiagramChangeNumber = -1; // The diagram change number of the "clean" diagram.
	private AgeEditorPaletteModel paletteModel;
	private final ToolHandler toolHandler = new ToolHandler();
	private TooltipManager tooltipManager;
	private final List<InputEventHandler> inputEventHandlers = new ArrayList<>();
	private Interaction activeInteraction;
	private final ISelectionListener toolPostSelectionListener = (part, selection) -> toolHandler
			.setSelectedElements(AgeHandlerUtil.getSelectedBusinessObjectContexts());

	private final IPartListener partListener = new IPartListener() {
		@Override
		public void partDeactivated(IWorkbenchPart part) {
			if (part == AgeEditor.this) {
				tooltipManager.hideTooltip();
			}
		}

		@Override
		public void partClosed(IWorkbenchPart part) {
			if (part == AgeEditor.this) {
				tooltipManager.hideTooltip();
				deactivateActiveTool();
				final IWorkbenchSite site = getSite();
				if (site != null && site.getWorkbenchWindow() != null
						&& site.getWorkbenchWindow().getPartService() != null) {
					site.getWorkbenchWindow().getPartService().removePartListener(this);
				}
			}
		}

		@Override
		public void partOpened(IWorkbenchPart part) {
			// Ignore
		}

		@Override
		public void partActivated(IWorkbenchPart part) {
			if (part != AgeEditor.this && !(part instanceof ContentOutline)) {
				toolHandler.deactivateActiveTool();
			}
		}

		@Override
		public void partBroughtToTop(IWorkbenchPart part) {
			// Ignore
		}
	};
	private final IOperationHistoryListener operationHistoryListener = event -> {
		if (event.getOperation().hasContext(DefaultActionService.CONTEXT)) {
			fireDirtyPropertyChangeEvent();
		}
	};
	private final DoubleProperty zoom = new SimpleDoubleProperty(1.0) {
		@Override
		protected void invalidated() {
			final Bounds canvasBounds = canvas.getLayoutBounds();

			// Get the center point in diagram coordinates
			final double viewCenterX = (canvasBounds.getWidth() / 2 - canvas.getHorizontalScrollOffset())
					/ canvas.getContentTransform().getMxx();
			final double viewCenterY = (canvasBounds.getHeight() / 2 - canvas.getVerticalScrollOffset())
					/ canvas.getContentTransform().getMyy();

			// Adjust the scaling
			final double scaling = zoom.get();
			canvas.setContentTransform(new Affine(Transform.scale(scaling, scaling)));

			// Set a new horizontal and vertical scroll offset based on thew new scaling
			canvas.setHorizontalScrollOffset((canvasBounds.getWidth() / 2) - viewCenterX * scaling);
			canvas.setVerticalScrollOffset((canvasBounds.getHeight() / 2) - viewCenterY * scaling);
		}
	};

	// Resource change listener which updates the editor's input when the resource for the existing input is renamed and closed the editor
	// when the resource is deleted.
	private IResourceChangeListener resourceChangeListener = event -> {
		if (event.getType() != IResourceChangeEvent.POST_CHANGE) {
			return;
		}

		// Determine whether the diagram input has changed and store the new values. This is done here to avoid storing resource deltas
		// which may expire. Editor changes will be performed in the UI thread.
		final IResourceDelta rootDelta = event.getDelta();
		final IResourceDelta diagramDelta = rootDelta.findMember(getInput().getFile().getFullPath());
		final boolean removed;
		final IFile newDiagramFile;
		if (diagramDelta != null && diagramDelta.getKind() == IResourceDelta.REMOVED) {
			removed = (diagramDelta.getFlags() & IResourceDelta.MOVED_TO) == 0;

			// Find the resource for the new file
			final IPath newPath = diagramDelta.getMovedToPath();
			final IResource newResource = ResourcesPlugin.getWorkspace().getRoot().findMember(newPath);
			newDiagramFile = newResource instanceof IFile ? (IFile) newResource : null;
		} else {
			removed = false;
			newDiagramFile = null;
		}

		Display.getDefault().asyncExec(() -> {
			//
			// Handle Image Updates
			//
			if (gefDiagram != null) {
				gefDiagram.refreshImages();
			}

			//
			// Handle the diagram input update
			//
			if (removed) {
				// Close editor
				closeEditor();
			} else if (newDiagramFile != null) {
				setInput(new FileEditorInput(newDiagramFile));
			}
		});
	};

	// Fields related to handling model and diagram updates
	private boolean updateWhenVisible = false;
	private volatile boolean dirtyModel = false;
	private volatile boolean forceUpdateOnNextModelChange = false;
	private boolean diagramContextWasValid = true;
	private boolean diagramContextIsValid = true;

	private ChangeListener modelChangeListener = new ChangeListener() {
		@Override
		public void afterModelChangeNotification() {
			final boolean requireVisible = !forceUpdateOnNextModelChange;
			forceUpdateOnNextModelChange = false; // Reset flag
			dirtyModel = true;
			updateDiagram(requireVisible);

			// Ensure the property sheet page is refreshed after the diagram update.
			refreshPropertySheetPage();
		}
	};

	private PaintListener paintListener = e -> {
		if (updateWhenVisible) {
			updateDiagram(true);
			updateWhenVisible = false;
		}
	};

	private final DiagramModificationListener diagramModificationListener = new DiagramModificationAdapter() {
		@Override
		public void modificationsCompleted(final ModificationsCompletedEvent e) {
			@SuppressWarnings("unchecked")
			final List<? extends DiagramNode> diagramNodes = selectionProvider.getSelection().toList();
			// Check if selected elements are visible
			if (diagramNodes.stream().map(gefDiagram::getSceneNode).anyMatch(Objects::isNull)) {
				// Clear selection if element is no longer visible
				clearSelection();
			}

			// Refresh overlays in case the nodes representing the selected diagram elements have changed.
			if (overlays != null) {
				overlays.refresh(selectionProvider.getSelection());
			}

			refreshPropertySheetPage();
		}
	};

	/**
	 * Create a new instance
	 */
	public AgeEditor() {
		final Bundle bundle = FrameworkUtil.getBundle(getClass());
		this.eclipseContext = EclipseContextFactory.getServiceContext(bundle.getBundleContext());

		// Retrieve global services
		this.modelChangeNotifier = Objects.requireNonNull(eclipseContext.get(ModelChangeNotifier.class),
				"unable to retrieve model change notifier");
		this.referenceService = Objects.requireNonNull(eclipseContext.get(ReferenceService.class),
				"unable to retrieve reference service");
		this.extRegistry = Objects.requireNonNull(eclipseContext.get(ExtensionRegistryService.class),
				"Unable to retrieve extension registry");
		this.queryService = new DefaultQueryService(referenceService);
		this.aadlModService = Objects.requireNonNull(eclipseContext.get(AadlModificationService.class),
				"unable to retrieve AADL modification service");
		this.actionService = Objects.requireNonNull(eclipseContext.get(ActionService.class),
				"unable to retrieve action service");
		systemInstanceLoader = Objects.requireNonNull(eclipseContext.get(SystemInstanceLoadingService.class),
				"unable to retrieve system instance loading service");

		// Create diagram-specific services
		this.projectReferenceService = new ProjectReferenceServiceProxy(referenceService, projectProvider);
		final BusinessObjectNodeFactory nodeFactory = new BusinessObjectNodeFactory(
				projectReferenceService);
		boTreeUpdater = new DefaultBusinessObjectTreeUpdater(projectProvider, extRegistry, projectReferenceService,
				queryService, nodeFactory);
		deInfoProvider = new DefaultDiagramElementGraphicalConfigurationProvider(queryService, diagramProvider,
				extRegistry);
		diagramUpdater = new DiagramUpdater(boTreeUpdater, deInfoProvider, actionService, projectReferenceService,
				projectReferenceService);
		this.coloringService = new DefaultColoringService(
				new org.osate.ge.internal.services.impl.DefaultColoringService.StyleRefresher() {
					@Override
					public void refreshDiagramColoring() {
						if (Display.getCurrent() != Display.getDefault()) {
							throw new AgeGefRuntimeException("Invalid thread");
						}

						gefDiagram.refreshDiagramStyles();
					}

					@Override
					public void refreshColoring(final Collection<DiagramElement> diagramElements) {
						if (Display.getCurrent() != Display.getDefault()) {
							throw new AgeGefRuntimeException("Invalid thread");
						}

						gefDiagram.refreshStyle(diagramElements);
					}
				});

		// Initialize the tooltip manager
		this.tooltipManager = new TooltipManager(extRegistry);

		// Initialize the outline and property sheet page
		outlinePage = new AgeContentOutlinePage(this, projectProvider, extRegistry, projectReferenceService);
		propertySheetPage = new TabbedPropertySheetPage(this);

		// Store editor specific adapters in the adapter map. Global services will be retrieved from the eclipse context.
		adapterMap.put(ColoringService.class, coloringService);
		adapterMap.put(QueryService.class, queryService);
		adapterMap.put(AgeDiagramProvider.class, diagramProvider);
		adapterMap.put(ProjectProvider.class, projectProvider);
		adapterMap.put(UiService.class, this);
		adapterMap.put(ProjectReferenceService.class, projectReferenceService);
		adapterMap.put(ReferenceResolutionService.class,
				new DefaultReferenceResolutionService(projectReferenceService));
		adapterMap.put(IContentOutlinePage.class, outlinePage);
		adapterMap.put(IPropertySheetPage.class, propertySheetPage);

		// Add preference listener
		preferences.addPreferenceChangeListener(preferenceChangeListener);
	}

	private final IPreferenceChangeListener preferenceChangeListener = event -> {
		if (Objects.equals(event.getKey(), Preferences.SHOW_GRID)) {
			canvas.setShowGrid(preferenceStore.getBoolean(Preferences.SHOW_GRID));
		}
	};

	@Override
	public void dispose() {
		try {
			// Remove listeners
			ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceChangeListener);
			PlatformUI.getWorkbench()
					.getOperationSupport()
					.getOperationHistory()
					.removeOperationHistoryListener(operationHistoryListener);
			preferences.removePreferenceChangeListener(preferenceChangeListener);
			getSite().setSelectionProvider(null);
			getSite().getWorkbenchWindow().getSelectionService().removePostSelectionListener(toolPostSelectionListener);
			this.modelChangeNotifier.removeChangeListener(modelChangeListener);

			if (overlays != null) {
				selectionProvider.removeSelectionChangedListener(overlays);
			}

			if (diagram != null) {
				diagram.removeModificationListener(diagramModificationListener);
				diagram.resetActionExecutor();
			}

			// Dispose of other objects
			if (gefDiagram != null) {
				gefDiagram.close();
				gefDiagram = null;
			}

			if (contextMenuManager != null) {
				contextMenuManager.removeAll();
				contextMenuManager.dispose();
			}

			projectReferenceService.dispose();
			outlinePage.dispose();

			if (propertySheetPage != null) {
				propertySheetPage.dispose();
				propertySheetPage = null;
			}

			adapterMap.clear();

			if (fxCanvas != null) {
				fxCanvas = null;
			}

			super.dispose();
		} finally {
			disposed = true;

			// Remove invalidated actions from the action service.
			actionService.invalidateInvalidActions();
		}
	}

	@Override
	public boolean isDisposed() {
		return disposed;
	}

	@Override
	public void init(final IEditorSite site, final IEditorInput input) throws PartInitException {
		setInput(input);
		setSite(site);

		// Register a resource change listener to handle moving and deleting the resource.
		ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener);

		site.getWorkbenchWindow().getSelectionService().addPostSelectionListener(toolPostSelectionListener);
		site.setSelectionProvider(selectionProvider);

		// Activate Context
		final IContextService contextService = site.getService(IContextService.class);
		if (contextService != null) {
			contextService.activateContext(CONTEXT_ID);
		}

		// Register actions for retargatable actions
		new UndoRedoActionGroup(site, DefaultActionService.CONTEXT, true).fillActionBars(site.getActionBars());
		registerAction(new CopyAction());
		registerAction(new PasteAction(this));
		registerAction(new SelectAllAction(this));

		// Load the diagram
		final org.osate.ge.diagram.Diagram mmDiagram = DiagramSerialization
				.readMetaModelDiagram(URI.createPlatformResourceURI(diagramFile.getFullPath().toString(), true));
		diagram = DiagramSerialization.createAgeDiagram(project, mmDiagram, extRegistry);

		// Display warning if the diagram is stored with a newer version of the diagram file format.
		if (mmDiagram.getFormatVersion() > DiagramSerialization.FORMAT_VERSION) {
			MessageDialog.openWarning(Display.getCurrent().getActiveShell(),
					"Diagram Created with Newer Version of OSATE", "The diagram '" + diagramFile.getName()
							+ "' was created with a newer version of the OSATE. The diagram may not be correctly displayed. Saving the diagram with this version of OSATE may result in the loss of diagram information.");
		}

		// Ensure the project is built. This prevents being unable to find the context due to the Xtext index not having completed.
		try {
			project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, new NullProgressMonitor());
		} catch (CoreException e) {
			throw new AgeGefRuntimeException(e);
		}

		// Update the diagram to finish initializing the diagram's fields
		actionService.execute("Update on Load", ExecutionMode.HIDE, () -> {
			diagram.modify("Update Diagram", m -> {
				// Check the diagram's context
				final DiagramContextChecker contextChecker = new DiagramContextChecker(project, projectReferenceService,
						systemInstanceLoader);
				final boolean workbenchIsVisible = isWorkbenchVisible();
				final DiagramContextChecker.Result result = contextChecker.checkContextFullBuild(diagram,
						workbenchIsVisible);

				if (!result.isContextValid()) {
					// If the workbench is not visible, then close the diagram to avoid an error which could have been avoided by relinking since
					// we only prompts to relink if the workbench is visible.
					if (!workbenchIsVisible) {
						closeEditor();
					}

					final String refContextLabel = projectReferenceService
							.getLabel(diagram.getConfiguration().getContextBoReference());

					throw new AgeGefRuntimeException("Unable to resolve context: " + refContextLabel);
				}

				diagramUpdater.updateDiagram(diagram);
			});
			return null;
		});

		this.modelChangeNotifier.addChangeListener(modelChangeListener);

		// Set the initial selection to the diagram
		selectionProvider.setSelection(new StructuredSelection(diagram));

		site.getWorkbenchWindow().getPartService().addPartListener(partListener);
		diagram.addModificationListener(diagramModificationListener);
	}

	private void registerAction(final IAction action) {
		getEditorSite().getActionBars().setGlobalActionHandler(action.getId(), action);

	}

	@Override
	protected void setInput(final IEditorInput input) {
		if (!(input instanceof IFileEditorInput)) {
			throw new AgeGefRuntimeException("Input must implement " + IFileEditorInput.class.getName());
		}

		super.setInput(input);

		final IFileEditorInput fileInput = (IFileEditorInput) input;
		this.diagramFile = fileInput.getFile();
		this.setPartName(this.diagramFile.getName());
		this.project = this.diagramFile.getProject();
	}

	private IFileEditorInput getInput() {
		final IEditorInput input = getEditorInput();
		if (!(input instanceof IFileEditorInput)) {
			throw new AgeGefRuntimeException("Unexpected editor input: " + input);
		}

		return (IFileEditorInput) input;
	}

	@Override
	public void createPartControl(final Composite parent) {
		//
		// Create the FX canvas which is an SWT widget for embedding JavaFX content.
		//
		fxCanvas = new FXCanvas(parent, SWT.NONE);
		fxCanvas.addDisposeListener(e -> {
			fxCanvas.getScene().setRoot(new Group());
			fxCanvas.setScene(null);
		});
		fxCanvas.addPaintListener(paintListener);

		// Suppress SWT key press handling when interaction is active
		fxCanvas.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(final org.eclipse.swt.events.KeyEvent e) {
				if (activeInteraction != null) {
					e.doit = false;
				}
			}
		});

		fxCanvas.addFocusListener(new FocusAdapter() {
			@Override
			public void focusLost(FocusEvent e) {
				deactivateInteraction();
			}
		});

		// Create the context menu
		contextMenuManager = new MenuManager(MENU_ID, MENU_ID);
		contextMenuManager.setRemoveAllWhenShown(true);
		final Menu contextMenu = contextMenuManager.createContextMenu(fxCanvas);
		fxCanvas.setMenu(contextMenu);
		getEditorSite().registerContextMenu(MENU_ID, contextMenuManager, selectionProvider, true);

		// Create the action executor. It will append an action to activate the editor when undoing and redoing actions.
		actionExecutor = (label, mode, action) -> {
			final boolean reverseActionWasSpecified = actionService.execute(label, mode, action);

			// If an action isn't running and the action is executing as normal, then activate the editor if the action is undoable.
			// This will ensure that when the action is undone, the editor will be switched to the one in which the action was performed.
			if (isEditorActive() && reverseActionWasSpecified && !actionService.isActionExecuting()
					&& mode == ExecutionMode.NORMAL) {
				actionService.execute("Activate Editor", ExecutionMode.APPEND_ELSE_HIDE,
						new ActivateAgeEditorAction(AgeEditor.this));
			}

			fireDirtyPropertyChangeEvent();

			return reverseActionWasSpecified;
		};

		// Initialize the palette model
		final AgeEditorPaletteModel.ImageProvider imageProvider = id -> {
			final RegisteredImage img = extRegistry.getImageMap().get(id);
			if (img == null) {
				return Optional.empty();
			}

			final URI imageUri = URI.createPlatformPluginURI("/" + img.plugin + "/" + img.path, true);
			if (CommonPlugin.asLocalURI(imageUri).isFile()) {
				return Optional.of(new Image(imageUri.toString()));
			} else {
				return Optional.empty();
			}
		};

		Object diagramBo = AgeDiagramUtil.getConfigurationContextBusinessObject(diagram, projectReferenceService);
		if (diagramBo == null) {
			diagramBo = project;
		}

		this.paletteModel = new AgeEditorPaletteModel(extRegistry.getPaletteContributors(), diagramBo, imageProvider);

		// If the palette item changes while an interaction is active, deactivate the interaction.
		this.paletteModel.activeItemProperty()
				.addListener((javafx.beans.value.ChangeListener<SimplePaletteItem>) (observable, oldValue,
						newValue) -> deactivateInteraction());

		// Initialize the JavaFX nodes based on the diagram
		canvas = new InfiniteCanvas();
		// Set show grid based on preferences
		canvas.setShowGrid(preferenceStore.getBoolean(Preferences.SHOW_GRID));
		final Scene scene = new Scene(new DiagramEditorNode(paletteModel, canvas));
		fxCanvas.setScene(scene);
		gefDiagram = new GefAgeDiagram(diagram, coloringService);

		// Create a wrapper around the diagram's scene node.
		final Group wrapper = new DiagramNodeWrapper(gefDiagram.getSceneNode());

		// Add the wrapper to the canvas
		canvas.getContentGroup().getChildren().add(wrapper);

		gefDiagram.updateDiagramFromSceneGraph(false);

		// Treat the current state of the diagram as clean.
		cleanDiagramChangeNumber = diagram.getCurrentChangeNumber();

		adapterMap.put(LayoutInfoProvider.class, gefDiagram);

		// Create overlays
		overlays = new Overlays(gefDiagram);
		selectionProvider.addSelectionChangedListener(overlays);
		canvas.getScrolledOverlayGroup().getChildren().add(overlays);

		// Perform the initial incremental layout
		diagram.modify("Incremental Layout", m -> DiagramElementLayoutUtil.layoutIncrementally(diagram, m, gefDiagram));

		// Set action executor after initial load. This occurs after the incremental layout to prevent the loading and initial layout from being undoable
		diagram.setActionExecutor(actionExecutor);

		// Refresh the dirty state whenever an operation occurs
		final IOperationHistory history = PlatformUI.getWorkbench().getOperationSupport().getOperationHistory();
		history.addOperationHistoryListener(operationHistoryListener);

		canvas.setOnScroll(e -> {
			if (e.isControlDown()) {
				// Adjust zoom
				if (e.getDeltaY() < 0.0) {
					zoomOut();
				} else {
					zoomIn();
				}
			} else {
				if (e.isShiftDown()) {
					// Scroll in X direction
					canvas.setHorizontalScrollOffset(canvas.getHorizontalScrollOffset() - e.getDeltaY());
				} else { // Scroll
					canvas.setHorizontalScrollOffset(canvas.getHorizontalScrollOffset() - e.getDeltaX());
					canvas.setVerticalScrollOffset(canvas.getVerticalScrollOffset() + e.getDeltaY());
				}
			}
		});

		//
		// Listeners to handle tooltips
		//
		canvas.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, e -> {
			if (e.getTarget() instanceof Node && activeInteraction == null && tooltipManager != null
					&& gefDiagram != null) {
				final DiagramElement de = gefDiagram.getDiagramElement((Node) e.getTarget());
				if (de != null) {
					tooltipManager.mouseEnter(de);
				}
			}
		});

		canvas.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, e -> {
			if (e.getTarget() instanceof Node && activeInteraction == null && tooltipManager != null
					&& gefDiagram != null) {
				final DiagramElement de = gefDiagram.getDiagramElement((Node) e.getTarget());
				if (de != null) {
					tooltipManager.mouseExit(de);
				}
			}
		});

		//
		// General input handlers
		//
		// Event handler. Delegates to input event handlers or the active interaction as appropriate
		final EventHandler<? super InputEvent> handleInput = e -> {
			if (activeInteraction == null) {
				// Delegate processing of the event to the input event handlers
				for (final InputEventHandler inputEventHandler : inputEventHandlers) {
					final InputEventHandler.HandledEvent r = inputEventHandler.handleEvent(e);
					if (r != null) {
						activeInteraction = r.newInteraction;
						if (activeInteraction != null) {
							canvas.setCursor(activeInteraction.getCursor());

							if (tooltipManager != null) {
								tooltipManager.hideTooltip();
							}
						}

						break;
					}
				}
			} else {
				if (activeInteraction.handleEvent(e) == InteractionState.COMPLETE) {
					deactivateInteraction();
				}

				canvas.setCursor(activeInteraction == null ? null : activeInteraction.getCursor());
			}
		};

		// Handle mouse button presses
		canvas.addEventFilter(MouseEvent.MOUSE_PRESSED, handleInput);
		canvas.addEventFilter(MouseEvent.MOUSE_DRAGGED, handleInput);
		canvas.addEventFilter(MouseEvent.MOUSE_RELEASED, handleInput);
		scene.addEventFilter(KeyEvent.KEY_PRESSED, handleInput);
		canvas.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
			if (activeInteraction == null) {
				Cursor cursor = Cursor.DEFAULT;
				for (final InputEventHandler inputEventHandler : inputEventHandlers) {
					final Cursor overrideCursor = inputEventHandler.getCursor(e);
					if (overrideCursor != null) {
						cursor = overrideCursor;
						break;
					}
				}
				canvas.setCursor(cursor);
			}

			handleInput.handle(e);
		});

		// Create input event handlers
		inputEventHandlers.add(new OpenPropertiesViewInputEventHandler(this));
		inputEventHandlers.add(new ResizeInputEventHandler(this));
		inputEventHandlers.add(new MarqueeSelectInputEventHandler(this));
		inputEventHandlers.add(new MoveConnectionPointTool(this));
		inputEventHandlers.add(new RenameInputEventHandler(this));
		inputEventHandlers.add(new SelectInputEventHandler(this));
		inputEventHandlers.add(new MoveInputEventHandler(this));
		inputEventHandlers.add(new PaletteCommandInputEventHandler(this));
	}

	/**
	 * Deactivate the current interaction. If an interaction is not active then this function is a no-op.
	 */
	private void deactivateInteraction() {
		if (activeInteraction != null) {
			// Set the active interaction field to null to avoid events potentially causing this block to be executed
			// multiple times for the same interaction.
			final Interaction tmp = activeInteraction;
			activeInteraction = null;
			tmp.close();
			canvas.setCursor(Cursor.DEFAULT);
		}
	}

	/**
	 * Returns the {@link FXCanvas} which contains the JavaFX scene. Intended to be used only by tests.
	 * @return the {@link FXCanvas}.
	 */
	public final FXCanvas getFxCanvas() {
		return fxCanvas;
	}

	/**
	 * Returns the palette model.
	 * @return the palette model
	 */
	public final AgeEditorPaletteModel getPaletteModel() {
		return paletteModel;
	}

	/**
	 * The overlays scene graph node
	 * @return the overlays
	 */
	public final Overlays getOverlays() {
		return overlays;
	}

	/**
	 * Gets the scene node for a diagram node.
	 * @param dn the diagram node for which to get the scene node
	 * @return the scene node
	 */
	public final Node getSceneNode(final DiagramNode dn) {
		return gefDiagram.getSceneNode(dn);
	}

	/**
	 * Ensures the top left corner of the node is visible for tests.  Must be called from the UI thread.
	 * @param sceneNode the scene node to scroll to.
	 */
	public final void scrollToTopLeft(final Node sceneNode) {
		final Bounds bounds = canvas.sceneToLocal(sceneNode.localToScene(sceneNode.getBoundsInLocal()));
		canvas.setVerticalScrollOffset(canvas.getVerticalScrollOffset() - bounds.getMinY());
		canvas.setHorizontalScrollOffset(canvas.getHorizontalScrollOffset() - bounds.getMinX());
	}

	/**
	 * The current user-specified zoom level. The zoom level is the amount of scaling to apply.
	 * @return the zoom level.
	 */
	public final DoubleProperty zoomProperty() {
		return zoom;
	}

	/**
	 * Gets the value of {@link #zoomProperty()}
	 * @return the zoom level
	 */
	public final double getZoom() {
		return zoom.get();
	}

	/**
	 * Sets the zoom level
	 * @param value the zoom level. In order for incremental zooming to work, the zoom level must be a value contained in {@link #ZOOM_LEVELS}
	 */
	public final void setZoom(final double value) {
		zoom.set(value);
	}

	/**
	 * Finds the current zoom level in the {@link #ZOOM_LEVELS} list and sets the zoom level to the next value. If the current zoom level is not
	 * in the list or if it is the last value, then the zoom level is not changed
	 */
	public void zoomIn() {
		final int newZoomLevelIndex = ZOOM_LEVELS.indexOf(getZoom()) + 1;
		if (newZoomLevelIndex > 0 && newZoomLevelIndex < ZOOM_LEVELS.size()) {
			setZoom(ZOOM_LEVELS.get(newZoomLevelIndex));
		}
	}

	/**
	 * Finds the current zoom level in the {@link #ZOOM_LEVELS} list and sets the zoom level to the previous value. If the current zoom level is not
	 * in the list or if it is the first value, then the zoom level is not changed
	 */
	public void zoomOut() {
		final int newZoomLevelIndex = ZOOM_LEVELS.indexOf(getZoom()) - 1;
		if (newZoomLevelIndex >= 0) {
			setZoom(ZOOM_LEVELS.get(newZoomLevelIndex));
		}
	}

	/**
	 * The width of the individual grid cells.
	 * This value is available regardless of whether the grid is show.
	 * @return the width of the grid cells
	 */
	public final double getGridCellWidth() {
		return canvas.getGridCellWidth();
	}

	/**
	 * The height of the individual grid cells.
	 * This value is available regardless of whether the grid is show.
	 * @return the height of the grid cells
	 */
	public final double getGridCellHeight() {
		return canvas.getGridCellWidth();
	}

	@Override
	public void doSave(final IProgressMonitor monitor) {
		try {
			if (diagramFile == null) {
				throw new AgeGefRuntimeException("diagram file is null");
			}

			// Handle the diagram being read-only
			if (diagramFile.isReadOnly()) {
				final IStatus status = ResourcesPlugin.getWorkspace()
						.validateEdit(new IFile[] { diagramFile }, getSite().getShell());

				if (status.matches(IStatus.CANCEL) || !status.isOK() || diagramFile.isReadOnly()) {
					Display.getDefault().syncExec(() -> monitor.setCanceled(true));

					// Display error message in a subset of cases
					if (!status.isOK()) {
						StatusManager.getManager().handle(status, StatusManager.SHOW);
					} else if (diagramFile.isReadOnly()) {
						StatusManager.getManager()
								.handle(new Status(IStatus.ERROR, AgeGefUiPlugin.PLUGIN_ID, "Diagram is read-only"),
										StatusManager.SHOW);
					}

					return;
				}
			}

			// Save the file
			DiagramSerialization.write(getProject(), diagram,
					URI.createPlatformResourceURI(diagramFile.getFullPath().toString(), true));

			// Clear Ghosts
			diagramUpdater.clearGhosts();

			// Store current change number
			cleanDiagramChangeNumber = diagram.getCurrentChangeNumber();
			fireDirtyPropertyChangeEvent();
		} catch (final Exception e) {
			Status errorStatus = new Status(IStatus.ERROR, AgeGefUiPlugin.PLUGIN_ID, 0, e.getMessage(), e);
			Display.getDefault()
					.asyncExec(() -> new ErrorDialog(Display.getDefault().getActiveShell(), "Error Saving Diagram",
							"Unable to save diagram.", errorStatus, IStatus.ERROR).open());
			throw e;
		}
	}

	private boolean isEditorActive() {
		final IWorkbenchSite site = getSite();
		if (site == null) {
			return false;
		}

		final IWorkbenchPage page = site.getPage();
		if (page == null) {
			return false;
		}

		return page.getActiveEditor() == this;
	}

	// Updates the diagram in the current thread if it is the display thread or asynchronously if it isn't
	private void updateDiagram(final boolean requireVisible) {
		if (Display.getDefault().getThread() != Thread.currentThread()) {
			Display.getDefault().asyncExec(() -> updateDiagram(requireVisible));
			return;
		}

		// Don't update unless the diagram is visible
		final boolean isVisible = fxCanvas != null && fxCanvas.isVisible();
		if (!requireVisible || isVisible) {
			// Update the entire diagram
			updateWhenVisible = false;
			dirtyModel = false;
			updateDiagram();
		} else {
			// Queue the update for when the control becomes visible
			updateWhenVisible = true;
		}
	}

	@Override
	public void updateDiagram() {
		// Check the context
		Display.getCurrent().syncExec(() -> {
			final boolean promptToRelink = fxCanvas.isVisible() && diagramContextWasValid;
			final DiagramContextChecker contextChecker = new DiagramContextChecker(project, projectReferenceService,
					systemInstanceLoader);
			final DiagramContextChecker.Result result = contextChecker.checkContextIncrementalBuild(diagram,
					promptToRelink);
			diagramContextWasValid = result.isContextValid(); // Store for next execution
			setDiagramContextIsValid(result.isContextValid()); // Update editor with new state
		});

		// Updating the diagram should not be part of the undo stack. The layout portion is not included in the action because
		// layout needs to be undoable.
		actionService.execute("Update Diagram", ExecutionMode.APPEND_ELSE_HIDE, () -> {
			// Update the diagram
			diagramUpdater.updateDiagram(diagram);

			// Perform the layout as a separate operation because the sizes for the shapes will not be set until the JavaFX nodes are updated.
			diagram.modify("Layout Incrementally",
					m -> DiagramElementLayoutUtil.layoutIncrementally(diagram, m, gefDiagram));

			return null;
		});
	}

	private void setDiagramContextIsValid(final boolean value) {
		final boolean prevContextWasValid = diagramContextIsValid;
		diagramContextIsValid = value;
		if (diagramContextIsValid != prevContextWasValid) {
			Display.getDefault().asyncExec(() -> {
				// Close the editor if the context isn't valid.
				if (!diagramContextIsValid) {
					closeEditor();
				}
			});
		}
	}

	private final void fireDirtyPropertyChangeEvent() {
		firePropertyChange(PROP_DIRTY);
	}

	@Override
	public void doSaveAs() {
		throw new UnsupportedOperationException("Save as not supported");
	}

	@Override
	public boolean isSaveAsAllowed() {
		return false;
	}

	@Override
	public boolean isDirty() {
		return cleanDiagramChangeNumber != diagram.getCurrentChangeNumber();
	}

	@Override
	public void setFocus() {
		fxCanvas.setFocus();
	}

	@Override
	public <T> T getAdapter(final Class<T> required) {
		Object adapter = adapterMap.get(required);
		if (adapter == null) {
			adapter = eclipseContext.get(required);
		}

		if (adapter != null) {
			return required.cast(adapter);
		}

		return super.getAdapter(required);
	}

	/**
	 * Provide a contributor ID that matches the original graphical editor. If the editor ID is modified to match the original ID, this will
	 * no longer be necessary.
	 */
	@Override
	public String getContributorId() {
		return CONTRIBUTOR_ID;
	}

	@Override
	public AgeDiagram getDiagram() {
		return diagram;
	}

	@Override
	public IProject getProject() {
		return project;
	}

	@Override
	public IFile getDiagramFile() {
		return diagramFile;
	}

	@Override
	public void forceDiagramUpdateOnNextModelChange() {
		this.forceUpdateOnNextModelChange = true;
	}

	private final Runnable updateNowIfModelHasChangedRunnable = () -> updateDiagram(false);

	@Override
	public void updateNowIfModelHasChanged() {
		if (dirtyModel) {
			final Display display = Display.getDefault();
			if (display == null || display.getThread() == Thread.currentThread()) {
				updateNowIfModelHasChangedRunnable.run();
			} else {
				display.syncExec(updateNowIfModelHasChangedRunnable);
			}
		}
	}

	@Override
	public void modifyDiagram(final String label, final DiagramModifier modifier) {
		diagram.modify(label, modifier);
	}

	@Override
	public DiagramUpdater getDiagramUpdater() {
		return diagramUpdater;
	}

	@Override
	public BusinessObjectTreeUpdater getBoTreeUpdater() {
		return boTreeUpdater;
	}

	@Override
	public void closeEditor() {
		getSite().getPage().closeEditor(this, false);
	}

	@Override
	public ActionExecutor getActionExecutor() {
		return actionExecutor;
	}

	@Override
	public void activateTool(final Tool tool) {
		toolHandler.activate(tool);
	}

	@Override
	public void deactivateActiveTool() {
		toolHandler.deactivateActiveTool();
	}

	@Override
	public IAction getGlobalActionHandler(final String actionId) {
		final IEditorSite site = getEditorSite();
		if (site == null) {
			return null;
		}

		return site.getActionBars().getGlobalActionHandler(actionId);
	}

	/**
	 * Returns a new mutable list containing the diagram elements contained in the selection.
	 * @return the selected diagram elements.
	 */
	@Override
	public List<DiagramElement> getSelectedDiagramElements() {
		final IStructuredSelection selection = selectionProvider.getSelection();
		if (selection == null) {
			return Collections.emptyList();
		}

		final List<DiagramElement> selectedElements = new ArrayList<>(selection.size());
		for (final Object s : selection) {
			if (s instanceof DiagramElement) {
				selectedElements.add((DiagramElement) s);
			}
		}

		return selectedElements;
	}

	@Override
	public void selectDiagramNodes(final Collection<? extends DiagramNode> diagramNodes) {
		selectionProvider.setSelection(new StructuredSelection(diagramNodes.toArray()));
	}

	@Override
	public void selectDiagramElementsForBusinessObject(final Object bo) {
		final CanonicalBusinessObjectReference searchRef = referenceService.getCanonicalReference(bo);
		final List<DiagramElement> elementsForBo = diagram.getAllDiagramNodes()
				.filter(DiagramElement.class::isInstance)
				.map(DiagramElement.class::cast)
				.filter(de -> Objects.equals(searchRef, referenceService.getCanonicalReference(de.getBusinessObject())))
				.collect(Collectors.toList());
		selectDiagramNodes(elementsForBo);
	}

	@Override
	public void clearSelection() {
		selectDiagramNodes(Collections.emptyList());

		if (outlinePage != null) {
			// Clear outline selection
			outlinePage.setSelection(null);
		}

		if (propertySheetPage != null && propertySheetPage.getCurrentTab() != null) {
			propertySheetPage.selectionChanged(AgeEditor.this, StructuredSelection.EMPTY);
		}
	}

	/**
	 * Intended for use by tests..
	 * @return the {@link GefAgeDiagram} instance.
	 */
	public GefAgeDiagram getGefDiagram() {
		return gefDiagram;
	}

	/**
	 * Returns the reference service
	 * @return the reference service
	 */
	ReferenceService getReferenceService() {
		return referenceService;
	}

	/**
	 * Returns the AADL modification service
	 * @return the AADL modification service
	 */
	AadlModificationService getAadlModificationService() {
		return aadlModService;
	}

	/**
	 * Returns the query service
	 * @return the query service
	 */
	QueryService getQueryService() {
		return queryService;
	}

	/**
	 * Returns the model change notification service
	 * @return the model change notification service
	 */
	ModelChangeNotifier getModelChangeNotifier() {
		return modelChangeNotifier;
	}

	@Override
	public void addSelectionChangedListener(final ISelectionChangedListener listener) {
		selectionProvider.addSelectionChangedListener(listener);
	}

	@Override
	public boolean isEditable() {
		return diagramContextIsValid;
	}

	private static boolean isWorkbenchVisible() {
		return PlatformUI.getWorkbench() != null && PlatformUI.getWorkbench().getActiveWorkbenchWindow() != null
				&& PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell() != null
				&& PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell().isVisible();
	}

	/**
	 * Ensures a diagram node is visible in the diagram view. Must be called from the UI thread.
	 * @param dn the diagram node to reveal.
	 */
	@Override
	public void reveal(final DiagramNode dn) {
		reveal(getSceneNode(dn));
	}

	/**
	 * Ensures a scene node which is inside the infinite canvas is visible. Must be called from the UI thread.
	 * @param sceneNode the scene node to reveal.
	 */
	public final void reveal(final Node sceneNode) {
		canvas.reveal(sceneNode);
	}

	/**
	 * For the property section to refresh. Calling {@link TabbedPropertySheetPage#refresh()} does not update visibility of property sections.
	 */
	private void refreshPropertySheetPage() {
		if (propertySheetPage != null && propertySheetPage.getCurrentTab() != null) {
			final IStructuredSelection selection = selectionProvider.getSelection();
			// Set the selection to empty because the property sheet page is not update if the selections are "equal". Although the same diagram
			// elements may be selected, the diagram elements or referenced business objects may have changed.
			propertySheetPage.selectionChanged(AgeEditor.this, StructuredSelection.EMPTY);
			propertySheetPage.selectionChanged(AgeEditor.this, selection);
		}
	}
}