blob: 60405ce9db28bec83664546611057c4bf52bf544 [file] [log] [blame]
/*********************************************************************
* Copyright (c) 2005, 2019 SAP SE
*
* 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/
*
* Contributors:
* SAP SE - initial API, implementation and documentation
* mwenz - Bug 329523 - Add notification of DiagramTypeProvider after saving a diagram
* mwenz - Bug 347152 - Do not log diagnostics errors as errors in the Eclipse error log
* mwenz - Bug 359928 - DiagramEditorBehavior does not initialize adapterActive field
* Bug 336488 - DiagramEditor API - Rename from DiagramEditorBehavior to DefaultUpdateBehavior
* mwenz - Bug 389426 - Add factory method for creating EMF workspace synchronizer delegate
* pjpaulin - Bug 352120 - Eliminated assumption that diagram is in an IEditorPart
* pjpaulin - Bug 352120 - Now uses IDiagramContainerUI interface
* mwenz - Bug 427444 - Issues with drill-down diagram editor when diagram file is deleted
* mwenz - Bug 430687 - UpdateBehaviour createEditingDomain should be able to access diagram input (sphinx compatibility)
* jsivadier - Bug 467502 - Improve DiagramComposite implementation without IWorkbenchPart
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipse.graphiti.ui.editor;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.emf.workspace.IWorkspaceCommandStack;
import org.eclipse.emf.workspace.util.WorkspaceSynchronizer;
import org.eclipse.emf.workspace.util.WorkspaceSynchronizer.Delegate;
import org.eclipse.graphiti.mm.pictograms.Diagram;
import org.eclipse.graphiti.platform.IDiagramContainer;
import org.eclipse.graphiti.ui.internal.Messages;
import org.eclipse.graphiti.ui.internal.editor.DomainModelWorkspaceSynchronizerDelegate;
import org.eclipse.graphiti.ui.internal.services.GraphitiUiInternal;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.PlatformUI;
/**
* The default implementation for the {@link IDiagramContainerUI} behavior extension
* that controls update behavior of the editor and defines the EMF adapters that
* watch over model object modifications. Clients may subclass to change the
* behavior; use {@link IDiagramContainerUI#createUpdateBehavior()} to return the
* instance that shall be used.<br>
* Note that there is always a 1:1 relation with a {@link IDiagramContainerUI}.
*
* @since 0.9
*/
public class DefaultUpdateBehavior extends PlatformObject implements IEditingDomainProvider, IOperationHistoryListener {
/**
* @since 0.10
*/
protected final DiagramBehavior diagramBehavior;
/**
* The editing domain that is used throughout the {@link DiagramBehavior} is
* kept here and only here.
*/
private TransactionalEditingDomain editingDomain;
/**
* Closes editor if model object is deleted.
*/
private ElementDeleteListener elementDeleteListener = null;
/**
* This resource set update adapter is added to the {@link ResourceSet} of
* the {@link TransactionalEditingDomain}. When notified, the resource set
* adapter adds the passed adapter (the
* {@link DefaultUpdateBehavior#updateAdapter} instance held in
* {@link DefaultUpdateBehavior}) to the adapters of a newly added Resource
* in the ResourceSet or removes the adapter from a removed resource.
*
* @see DefaultUpdateBehavior#initializeEditingDomain(TransactionalEditingDomain)
*/
private ResourceSetUpdateAdapter resourceSetUpdateAdapter;
/**
* Is toggled by {@link DefaultUpdateBehavior#updateAdapter}.
*/
private boolean resourceDeleted = false;
/**
* Is toggled by {@link DefaultUpdateBehavior#updateAdapter}.
*/
private boolean resourceChanged = false;
// Note: there are some concerns out there that the Synchronizer does not
// work for inter-EditingDomain-eventing, @see
// http://www.eclipse.org/forums/index.php?t=msg&th=140242&start=0&
private WorkspaceSynchronizer workspaceSynchronizer;
/**
* Flag that indicates if the {@link #updateAdapter} shall be active or not.
* It may be deactivated when mass operations (e.g. saving the diagram
* editor with all its resources) take place. Use the methods
* {@link #isAdapterActive()} and {@link #setAdapterActive(boolean)} to
* access this field.
*/
private boolean adapterActive = true;
/**
* Stores the update adapter that cares about refreshing the diagram editor
* in case of resource changes. May be disabled by overriding
* {@link #isAdapterActive()} and returning false. The instance will be
* created in the
* {@link #initializeEditingDomain(TransactionalEditingDomain)} method and
* can be exchanged with an own implementation by overriding
* {@link #createUpdateAdapter()}.
*
* @see #init()
* @see #initializeEditingDomain(TransactionalEditingDomain)
*/
private Adapter updateAdapter = null;
/**
* Creates a new {@link DefaultUpdateBehavior} instance associated with the
* given {@link DiagramBehavior}.
*
* @param diagramEditor
* the part this model editor works on
* @since 0.10
*/
public DefaultUpdateBehavior(DiagramBehavior diagramBehavior) {
super();
this.diagramBehavior = diagramBehavior;
}
/**
* Returns the flag that indicates if the underlying resource of the
* {@link Diagram} has been deleted. Note that this flag will only be
* updated in case the {@link #updateAdapter} is enabled, see
* {@link #adapterActive}, {@link #isAdapterActive()} and
* {@link #setAdapterActive(boolean)}. If this flag is set the editor will
* close on receiving the next event.
*
* @return <code>true</code> in case the resource has been deleted,
* <code>false</code> otherwise
*/
protected boolean isResourceDeleted() {
return resourceDeleted;
}
/**
* Sets the flag that indicates if the underlying resource of the
* {@link Diagram} has been deleted. Note that this flag should only be
* updated by the {@link #updateAdapter}, see {@link #adapterActive},
* {@link #isAdapterActive()} and {@link #setAdapterActive(boolean)}.
* <p>
* Should not be called by external clients.
*
* @param resourceDeleted
* the value to set the flag to, <code>true</code> indicates that
* the resource has been deleted.
* @noreference This method is not intended to be referenced by clients.
*/
public void setResourceDeleted(boolean resourceDeleted) {
this.resourceDeleted = resourceDeleted;
}
/**
* Returns the flag that indicates if the underlying resource of the
* {@link Diagram} has been changed. Note that this flag will only be
* updated in case the {@link #updateAdapter} is enabled, see
* {@link #adapterActive}, {@link #isAdapterActive()} and
* {@link #setAdapterActive(boolean)}.
*
* @return <code>true</code> in case the resource has been changed,
* <code>false</code> otherwise
*/
protected boolean isResourceChanged() {
return resourceChanged;
}
/**
* Sets the flag that indicates if the underlying resource of the
* {@link Diagram} has been changed. Note that this flag should only be
* updated by the {@link #updateAdapter}, see {@link #adapterActive},
* {@link #isAdapterActive()} and {@link #setAdapterActive(boolean)}.
* <p>
* Should not be called by external clients.
*
* @param resourceChanged
* the value to set the flag to, <code>true</code> indicates that
* the resource has been changed.
* @noreference This method is not intended to be referenced by clients.
*/
public void setResourceChanged(boolean resourceChanged) {
this.resourceChanged = resourceChanged;
}
/**
* Handles activation of the editor. In case of the underlying diagram
* resource being deleted ({@link #resourceDeleted} is <code>true</code>)
* the editor will be closed after a call to {@link #handleDirtyConflict()}
* that returns <code>true</code>. Also it will call
* {@link #handleChangedResources()} in case the underlying diagram resource
* has changed ({@link #resourceChanged} is <code>true</code>).
*/
public void handleActivate() {
if (isResourceDeleted()) {
if (handleDirtyConflict()) {
closeContainer();
} else {
setResourceDeleted(false);
setResourceChanged(false);
}
} else if (isResourceChanged()) {
handleChangedResources();
setResourceChanged(false);
}
}
/**
* Returns the flag that indicates if the {@link #updateAdapter} shall be
* active of not ({@link #adapterActive}). In case this method returns
* <code>false</code>, the {@link #updateAdapter} will do nothing on being
* called.
*
* @return <code>true</code> in case the adapter shall run,
* <code>false</code> otherwise.
*/
protected boolean isAdapterActive() {
return adapterActive;
}
/**
* Sets the flag that indicates if the {@link #updateAdapter} shall be
* active of not ({@link #adapterActive}).
*
* @param active
* the new value for the flag
*/
public void setAdapterActive(boolean active) {
adapterActive = active;
}
/**
* Handles what to do with changed resources on editor activation.
*/
protected void handleChangedResources() {
if (!diagramBehavior.getDiagramContainer().isDirty() || handleDirtyConflict()) {
IUndoContext undoContext = ((IWorkspaceCommandStack) getEditingDomain().getCommandStack())
.getDefaultUndoContext();
IOperationHistory operationHistory = getOperationHistory();
if (operationHistory != null) {
operationHistory.dispose(undoContext, true, true, true);
}
// Disable adapters temporarily.
diagramBehavior.disableAdapters();
try {
// We unload our resources such that refreshEditorContent does a
// complete diagram refresh.
EList<Resource> resources = getEditingDomain().getResourceSet().getResources();
for (Resource resource : resources) {
resource.unload();
}
diagramBehavior.refreshContent();
} finally {
// Re-enable adapters again
diagramBehavior.enableAdapters();
}
}
}
/**
* Shows a dialog that asks if conflicting changes should be discarded or
* not. See {@link #handleActivate()}.
*
* @return <code>true</code> in case the editor shall be closed,
* <code>false</code> otherwise
*/
protected boolean handleDirtyConflict() {
return MessageDialog.openQuestion(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
Messages.DiscardChangesDialog_0_xmsg, Messages.DiscardChangesDialog_1_xmsg);
}
/**
* This returns the editing domain as required by the
* {@link IEditingDomainProvider} interface.
*
* @return The {@link TransactionalEditingDomain} that is used within this
* editor
*/
public TransactionalEditingDomain getEditingDomain() {
return editingDomain;
}
/**
* Initializes listeners and adapters.
*/
public void init() {
// Retrieve the object from the editor input
final EObject object = (EObject) diagramBehavior.getAdapter(Diagram.class);
// Register for object deletion
if (object != null) {
elementDeleteListener = new ElementDeleteListener(diagramBehavior);
object.eAdapters().add(elementDeleteListener);
}
IOperationHistory operationHistory = getOperationHistory();
if (operationHistory != null) {
operationHistory.addOperationHistoryListener(this);
}
}
/**
* Hook to create an {@link DefaultUpdateBehavior#updateAdapter} that cares
* about updating the diagram editor. The default implementation simply
* creates a {@link DefaultUpdateAdapter}.
*
* @return The newly created adapter
* @since 0.11
*/
protected Adapter createUpdateAdapter() {
return new DefaultUpdateAdapter();
}
/**
* Returns the {@link #updateAdapter} of this instance or <code>null</code>
* in case the adapter has not yet been initialized.
*
* @return
* @since 0.11
*/
protected Adapter getUpdateAdapter() {
return updateAdapter;
}
/**
* Created the {@link TransactionalEditingDomain} that shall be used within
* the diagram editor and initializes it by delegating to
* {@link #initializeEditingDomain(TransactionalEditingDomain)}.
*
* @param input
* The {@link IDiagramEditorInput} instance that was used to open
* the {@link IDiagramContainer}.
*
* @since 0.11
*/
protected void createEditingDomain(IDiagramEditorInput input) {
TransactionalEditingDomain editingDomain = GraphitiUiInternal.getEmfService()
.createResourceSetAndEditingDomain();
initializeEditingDomain(editingDomain);
}
/**
* This sets up the editing domain for this model editor.
*
* @param domain
* The {@link TransactionalEditingDomain} that is used within
* this model editor
*/
protected void initializeEditingDomain(TransactionalEditingDomain domain) {
editingDomain = domain;
final ResourceSet resourceSet = domain.getResourceSet();
updateAdapter = createUpdateAdapter();
resourceSetUpdateAdapter = new ResourceSetUpdateAdapter(updateAdapter);
resourceSet.eAdapters().add(resourceSetUpdateAdapter);
// Install synchronizer for editor-external changes to the files
// underlying the resources of the ED
Delegate synchronizerDelegate = createWorkspaceSynchronizerDelegate();
if (synchronizerDelegate != null) {
workspaceSynchronizer = new WorkspaceSynchronizer(getEditingDomain(), synchronizerDelegate);
}
// Problem analysis
diagramBehavior.editingDomainInitialized();
}
/**
* Can be overridden to return a client specific implementation of a
* {@link WorkspaceSynchronizer} {@link Delegate} object. Graphiti uses the
* returned instance to manage and react on changes done to the resources
* tied to the diagram outside of the diagram editor's
* TransactionalEditingDomain.
*
* @return The delegate to use. The default implementation return's
* Graphiti's own implementation of such a delegate that should be
* sufficient for most client editors. Might return
* <code>null</code>; in this case no {@link WorkspaceSynchronizer}
* will be installed.
*
* @since 0.10
*/
protected WorkspaceSynchronizer.Delegate createWorkspaceSynchronizerDelegate() {
return new DomainModelWorkspaceSynchronizerDelegate(diagramBehavior);
}
/**
* Disposes this {@link DefaultUpdateBehavior} and free all resources it
* holds. In case you only want to omit or influence the disposal of the
* {@link TransactionalEditingDomain}, you can also override
* {@link #disposeEditingDomain()}.
*/
public void dispose() {
editingDomain.getResourceSet().eAdapters().remove(resourceSetUpdateAdapter);
resourceSetUpdateAdapter = null;
IOperationHistory operationHistory = getOperationHistory();
if (operationHistory != null) {
operationHistory.removeOperationHistoryListener(this);
}
for (Resource r : editingDomain.getResourceSet().getResources()) {
r.eAdapters().remove(updateAdapter);
}
EObject object = (EObject) diagramBehavior.getAdapter(Diagram.class);
if (object != null) {
object.eAdapters().remove(elementDeleteListener);
}
if (workspaceSynchronizer != null) {
workspaceSynchronizer.dispose();
}
// Remove reference
disposeEditingDomain();
editingDomain = null;
}
/**
* Cares about disposing the {@link TransactionalEditingDomain} held in this
* instance. Is called during the {@link #dispose()} method.
*/
protected void disposeEditingDomain() {
editingDomain.dispose();
}
/**
* Is called by the operation history of the
* {@link TransactionalEditingDomain} in case the history changes. Reacts on
* undo and redo events and updates the dirty state of the editor.
*
* @param event
* the {@link OperationHistoryEvent} to react upon
*/
public void historyNotification(OperationHistoryEvent event) {
switch (event.getEventType()) {
case OperationHistoryEvent.REDONE:
case OperationHistoryEvent.UNDONE:
diagramBehavior.getDiagramContainer().updateDirtyState();
break;
}
}
private Shell getShell() {
return diagramBehavior.getDiagramContainer().getSite().getShell();
}
/**
* Closes the {@link IDiagramContainerUI} (usually a diagram Editor) behind
* this {@link DefaultUpdateBehavior} instance. Called e.g. when the diagram
* resource underneath the editor has been deleted.
*
* @since 0.11
*/
protected void closeContainer() {
IDiagramContainerUI diagramContainer = diagramBehavior.getDiagramContainer();
if (diagramContainer == null) {
return;
}
IWorkbenchPartSite site = diagramContainer.getSite();
// Since we run asynchronously we have to check if our UI is still
// there.
if (site == null) {
return;
}
IWorkbenchPage page = site.getPage();
if (page == null) {
return;
}
diagramContainer.close();
}
/**
* Returns the {@link IOperationHistory} for the command stack. The default
* implementation retrieves the command stack of the current
* {@link TransactionalEditingDomain} and returns its operation history.
*
* @return The operation history
* @since 0.11
*/
protected IOperationHistory getOperationHistory() {
IOperationHistory history = null;
TransactionalEditingDomain domain = getEditingDomain();
if (domain != null) {
IWorkspaceCommandStack commandStack = (IWorkspaceCommandStack) domain.getCommandStack();
if (commandStack != null) {
history = commandStack.getOperationHistory();
}
}
return history;
}
/**
* @since 0.10
*/
public void setEditingDomain(TransactionalEditingDomain editingDomain) {
this.editingDomain = editingDomain;
initializeEditingDomain(editingDomain);
}
/**
* The default implementation of the update adapter that cares about
* refreshing the diagram editor in case of resource changes. The default
* implementation will trigger the update of the diagram editor in case the
* EMF resource has been changed externally; in case the resource was
* deleted the default implementation will close the editor if there are no
* unsaved changes or ask the user what to do in case of unsaved changes.
*
* @since 0.11
*/
protected class DefaultUpdateAdapter extends AdapterImpl {
@Override
public void notifyChanged(Notification msg) {
if (!isAdapterActive()) {
return;
}
if (msg.getFeatureID(Resource.class) == Resource.RESOURCE__IS_LOADED) {
if (msg.getNewBooleanValue() == Boolean.FALSE) {
final Resource resource = (Resource) msg.getNotifier();
final URI uri = resource.getURI();
IDiagramContainerUI diagramContainer = diagramBehavior.getDiagramContainer();
if (editingDomain.getResourceSet().getURIConverter().exists(uri, null)) {
// file content has changes
setResourceChanged(true);
IWorkbenchPart activePart = null;
if (diagramContainer.getSite() != null) {
activePart = diagramContainer.getSite().getPage().getActivePart();
}
if (activePart == diagramContainer) {
getShell().getDisplay().asyncExec(new Runnable() {
public void run() {
handleActivate();
}
});
}
} else {
// File has been deleted
if (!diagramContainer.isDirty()) {
// Close diagram editor in case the diagram is
// stored in the deleted resource
Diagram diagram = diagramContainer.getDiagramTypeProvider().getDiagram();
if (diagram != null) {
URI diagramUri = EcoreUtil.getURI(diagram);
if (diagramUri != null) {
URI uriOfDiagramFile = diagramUri.trimFragment();
// Bug 427444: To find out if the diagram
// resource has been deleted check if the
// base URI of the diagram object is the
// same as the URI of the resource
if (uriOfDiagramFile.equals(uri)) {
closeEditorAsynchronously();
}
}
}
} else {
// Ask user what to da with unsaved changes in the
// editor
setResourceDeleted(true);
IWorkbenchPart activePart = null;
if (diagramContainer.getSite() != null) {
activePart = diagramContainer.getSite().getPage().getActivePart();
}
if (activePart == diagramContainer) {
getShell().getDisplay().asyncExec(new Runnable() {
public void run() {
handleActivate();
}
});
}
}
}
}
}
super.notifyChanged(msg);
}
/**
* Triggers closing of the editor by scheduling an asynchronous call to
* {@link DefaultUpdateBehavior#closeContainer()}.
*/
protected void closeEditorAsynchronously() {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
closeContainer();
}
});
}
}
}