| /******************************************************************************* |
| * Copyright (c) 2011-2020 EclipseSource Muenchen GmbH and others. |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Clemens Elflein - initial API and implementation |
| * Johannes Faltermeier - initial API and implementation |
| * Christian W. Damus - bugs 545460, 548592, 559116 |
| ******************************************************************************/ |
| |
| package org.eclipse.emfforms.spi.editor; |
| |
| import java.io.IOException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.EventObject; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| 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.IResourceDeltaVisitor; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.FileLocator; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtensionRegistry; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.emf.common.command.BasicCommandStack; |
| import org.eclipse.emf.common.command.CommandStackListener; |
| import org.eclipse.emf.common.notify.Notifier; |
| import org.eclipse.emf.common.ui.MarkerHelper; |
| import org.eclipse.emf.common.util.Diagnostic; |
| import org.eclipse.emf.common.util.URI; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EStructuralFeature; |
| 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.ecore.xmi.XMLResource; |
| import org.eclipse.emf.ecp.common.spi.ChildrenDescriptorCollector; |
| import org.eclipse.emf.ecp.common.spi.UniqueSetting; |
| import org.eclipse.emf.ecp.view.spi.context.ViewModelContext; |
| import org.eclipse.emf.ecp.view.spi.model.reporting.StatusReport; |
| import org.eclipse.emf.edit.domain.EditingDomain; |
| import org.eclipse.emf.edit.domain.IEditingDomainProvider; |
| import org.eclipse.emf.edit.ui.util.EditUIMarkerHelper; |
| import org.eclipse.emf.edit.ui.util.EditUIUtil; |
| import org.eclipse.emfforms.common.Optional; |
| import org.eclipse.emfforms.internal.editor.Activator; |
| import org.eclipse.emfforms.internal.editor.toolbaractions.LoadEcoreAction; |
| import org.eclipse.emfforms.internal.editor.ui.EditorToolBar; |
| import org.eclipse.emfforms.internal.swt.treemasterdetail.defaultprovider.DefaultDeleteActionBuilder; |
| import org.eclipse.emfforms.spi.editor.helpers.ResourceSetHelpers; |
| import org.eclipse.emfforms.spi.editor.messages.Messages; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.MenuProvider; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.TreeMasterDetailComposite; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.TreeMasterDetailMenuListener; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.TreeMasterDetailSWTBuilder; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.TreeMasterDetailSWTFactory; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.TreeViewerBuilder; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.actions.ActionCollector; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.actions.MasterDetailAction; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.decorator.validation.ecp.ECPValidationLabelDecoratorProvider; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.diagnostic.DiagnosticCache; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.diagnostic.DiagnosticCache.ValidationListener; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.util.CreateElementCallback; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.util.DetailPanelRenderingFinishedCallback; |
| import org.eclipse.emfforms.spi.swt.treemasterdetail.util.RootObject; |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.jface.action.MenuManager; |
| import org.eclipse.jface.dialogs.MessageDialog; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.ISelectionProvider; |
| import org.eclipse.jface.viewers.StructuredSelection; |
| import org.eclipse.jface.viewers.TreeViewer; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.FocusEvent; |
| import org.eclipse.swt.events.FocusListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.layout.FormAttachment; |
| import org.eclipse.swt.layout.FormData; |
| import org.eclipse.swt.layout.FormLayout; |
| 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.IWorkbenchPart; |
| import org.eclipse.ui.PartInitException; |
| import org.eclipse.ui.contexts.IContextActivation; |
| import org.eclipse.ui.contexts.IContextService; |
| import org.eclipse.ui.dialogs.SaveAsDialog; |
| import org.eclipse.ui.ide.IGotoMarker; |
| import org.eclipse.ui.part.EditorPart; |
| |
| /** |
| * The Class GenericEditor it is the generic part for editing any EObject. |
| */ |
| public class GenericEditor extends EditorPart implements IEditingDomainProvider, IGotoMarker { |
| |
| private static final String GENERIC_EDITOR_CONTEXT = "org.eclipse.emfforms.editor.context"; //$NON-NLS-1$ |
| |
| private static final String FRAGMENT_URI = "FRAGMENT_URI"; //$NON-NLS-1$ |
| |
| private static final String RESOURCE_URI = "RESOURCE_URI"; //$NON-NLS-1$ |
| |
| private static final String FEATURE_URI = "FEATURE_URI"; //$NON-NLS-1$ |
| |
| private static final String ITOOLBAR_ACTIONS_ID = "org.eclipse.emfforms.editor.toolbarActions"; //$NON-NLS-1$ |
| |
| /** The Resource loaded from the provided EditorInput. */ |
| private ResourceSet resourceSet; |
| |
| /** The command stack. It is used to mark the editor as dirty as well as undo/redo operations */ |
| private final BasicCommandStack commandStack = new BasicCommandStack(); |
| |
| /** The root view. It is the main Editor panel. */ |
| private TreeMasterDetailComposite rootView; |
| |
| /** |
| * True, if there were changes in the filesystem while the editor was in the background and the changes could not be |
| * applied to current view. |
| */ |
| private boolean filesChangedWithConflict; |
| |
| private final IPartListener partListener = new GenericEditorActivationListener(); |
| |
| private final IResourceChangeListener resourceChangeListener = new GenericEditorResourceChangeListener(); |
| |
| private final MarkerHelper markerHelper = new GenericEditorMarkerHelper(); |
| |
| private final AtomicReference<Job> markerJob = new AtomicReference<>(); |
| |
| private DiagnosticCache cache; |
| |
| private boolean reloading; |
| |
| private boolean closing; |
| |
| /** |
| * @return the {@link DiagnosticCache}. may be <code>null</code> |
| * @since 1.10 |
| */ |
| protected DiagnosticCache getDiagnosticCache() { |
| return cache; |
| } |
| |
| @Override |
| public void doSave(IProgressMonitor monitor) { |
| // Remove the Listener, so that we won't get a changed notification for our own save operation |
| preSave(); |
| if (ResourceSetHelpers.save(resourceSet, getResourceSaveOptions())) { |
| // Tell the CommandStack, that we have saved the file successfully |
| // and inform the Workspace, that the Dirty property has changed. |
| getCommandStack().saveIsDone(); |
| firePropertyChange(PROP_DIRTY); |
| filesChangedWithConflict = false; |
| } |
| // Add the listener again, so that we get notifications for future changes |
| postSave(); |
| } |
| |
| /** |
| * Executes the code which needs to be executed before a save, e.g. removing listeners. |
| * |
| * @since 1.10 |
| */ |
| protected void preSave() { |
| ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceChangeListener); |
| } |
| |
| /** |
| * Executes the code which needs to be executed after a save, e.g. readding listeners. |
| * |
| * @since 1.10 |
| */ |
| protected void postSave() { |
| ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener); |
| } |
| |
| /** |
| * Handles filesystem changes. |
| * |
| * @param changedResources A List of changed Resources |
| * @param removedResources A List of removed Resources |
| */ |
| protected void handleResourceChange(final Collection<Resource> changedResources, |
| final Collection<Resource> removedResources) { |
| if (!isDirty()) { |
| getSite().getShell().getDisplay().asyncExec(() -> { |
| if (resourceSet == null || rootView.isDisposed()) { |
| return; |
| } |
| reloading = true; |
| removeResources(removedResources); |
| |
| // We need to get every changed resource by its URI from the resource set because otherwise proxies will |
| // not be able to resolve after the reload. This is the case because the given resources are not |
| // part of this editor's resource set. |
| final List<Resource> toReload = changedResources.stream() |
| .map(changed -> resourceSet.getResource(changed.getURI(), false)) |
| .filter(Objects::nonNull) |
| .collect(Collectors.toList()); |
| |
| reloadResources(toReload); |
| reloading = false; |
| getCommandStack().flush(); |
| initMarkers(); |
| }); |
| } else { |
| filesChangedWithConflict = true; |
| } |
| } |
| |
| /** |
| * Reloads the given resources and refreshes the tree accordingly. |
| * |
| * @param resources The {@link Resource Resources} to reload |
| * @since 1.22 |
| */ |
| protected void reloadResources(Collection<Resource> resources) { |
| for (final Resource r : resources) { |
| r.unload(); |
| try { |
| r.load(getResourceLoadOptions()); |
| } catch (final IOException e) { |
| } |
| } |
| ResourceSetHelpers.resolveAllProxies(resourceSet); |
| refreshTreeAfterResourceChange(); |
| } |
| |
| /** |
| * Called after a resource change to refresh the tree master detail of the editor. By default only the tree is |
| * refreshed. If the tree's input is not this editor's resource but only derived from it, this method should be |
| * overridden to reset the tree's input. |
| * |
| * @since 1.22 |
| */ |
| protected void refreshTreeAfterResourceChange() { |
| rootView.refresh(); |
| } |
| |
| private boolean discardChanges() { |
| return MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), |
| Messages.GenericEditor_DiscardChangesTitle, |
| Messages.GenericEditor_DiscardChangesDescription); |
| } |
| |
| @Override |
| public void doSaveAs() { |
| final SaveAsDialog saveAsDialog = new SaveAsDialog(getSite().getShell()); |
| final int result = saveAsDialog.open(); |
| if (result == Window.OK) { |
| final IPath path = saveAsDialog.getResult(); |
| setPartName(path.lastSegment()); |
| resourceSet.getResources().get(0) |
| .setURI(URI.createFileURI(path.toOSString())); |
| doSave(null); |
| } |
| } |
| |
| @Override |
| public void init(IEditorSite site, IEditorInput input) |
| throws PartInitException { |
| setSite(site); |
| setInput(input); |
| |
| // Set the Title for this Editor to the Name of the Input (= Filename) |
| setPartName(input.getName()); |
| |
| // As soon as the resource changed, we inform the Workspace, that it is |
| // now dirty |
| getCommandStack().addCommandStackListener(new CommandStackListener() { |
| @Override |
| public void commandStackChanged(EventObject event) { |
| GenericEditor.this.firePropertyChange(PROP_DIRTY); |
| } |
| }); |
| |
| site.getPage().addPartListener(partListener); |
| |
| ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener); |
| // Load the resource from the provided input and display the editor |
| resourceSet = loadResource(getEditorInput()); |
| } |
| |
| /** |
| * Returns the context id set for this editor. |
| * |
| * @return the context id |
| */ |
| protected String getContextId() { |
| return GENERIC_EDITOR_CONTEXT; |
| } |
| |
| @Override |
| public boolean isDirty() { |
| return getCommandStack().isSaveNeeded(); |
| } |
| |
| @Override |
| public boolean isSaveAsAllowed() { |
| return true; |
| } |
| |
| @Override |
| public void createPartControl(Composite parent) { |
| parent.setBackground(new Color(Display.getCurrent(), 255, 255, 255)); |
| parent.setBackgroundMode(SWT.INHERIT_FORCE); |
| |
| final Object editorInput = modifyEditorInput(resourceSet); |
| if (enableValidation()) { |
| setupDiagnosticCache(editorInput); |
| getDiagnosticCache().registerValidationListener(new MarkerValidationListener()); |
| } |
| rootView = createRootView(parent, getEditorTitle(), editorInput, getToolbarActions(), |
| getCreateElementCallback()); |
| |
| initMarkers(); |
| |
| // We need to set the selectionProvider for the editor, so that the EditingDomainActionBarContributor |
| // knows the currently selected object to copy/paste |
| getEditorSite().setSelectionProvider(rootView.getMasterDetailSelectionProvider()); |
| } |
| |
| private synchronized void initMarkers() { |
| if (getDiagnosticCache() == null || reloading || markerJob.get() != null) { |
| return; |
| } |
| |
| final Job job = Job.create(Messages.GenericEditor_ValidationMarkersJobName, monitor -> { |
| try { |
| adjustMarkers(monitor); |
| return Status.OK_STATUS; |
| } catch (final CoreException ex) { |
| return ex.getStatus(); |
| } finally { |
| markerJob.compareAndSet(Job.getJobManager().currentJob(), null); |
| } |
| }); |
| job.setPriority(Job.SHORT); |
| |
| if (markerJob.compareAndSet(null, job)) { |
| job.schedule(); |
| } |
| } |
| |
| private synchronized void adjustMarkers(IProgressMonitor monitor) throws CoreException { |
| if (monitor.isCanceled() || reloading) { |
| return; |
| } |
| deleteMarkers(); |
| for (final Object o : getDiagnosticCache().getObjects()) { |
| try { |
| if (monitor.isCanceled() || reloading) { |
| return; |
| } |
| final Diagnostic value = getDiagnosticCache().getOwnValue(o); |
| if (value.getSeverity() < Diagnostic.WARNING) { |
| continue; |
| } |
| markerHelper.createMarkers(value); |
| } catch (final CoreException ex) { |
| /* silent */ |
| } |
| } |
| } |
| |
| /** |
| * Deletes the problem markers created by this Editor. <b>Please take care that this method should be called from a |
| * {@link Job}</b> to avoid problems with a locked index. |
| * |
| * @throws CoreException if the method fails |
| * @since 1.10 |
| */ |
| protected void deleteMarkers() throws CoreException { |
| final Optional<IFile> file = getFile(); |
| if (!file.isPresent()) { |
| return; |
| } |
| file.get().deleteMarkers("org.eclipse.core.resources.problemmarker", false, //$NON-NLS-1$ |
| IResource.DEPTH_ZERO); |
| } |
| |
| /** |
| * Get the Notifier from the tree input. |
| * |
| * @param editorInput The editor input to transform |
| * @return {@link Notifier} |
| * @throws IllegalStateException if the editor input is not a Notifier |
| */ |
| protected Notifier getNotifierFromEditorInput(Object editorInput) { |
| Object input = editorInput; |
| if (input instanceof RootObject) { |
| input = ((RootObject) input).getRoot(); |
| } |
| if (!Notifier.class.isInstance(input)) { |
| throw new IllegalStateException("The editor input is not a Notifier!"); //$NON-NLS-1$ |
| } |
| return (Notifier) input; |
| } |
| |
| private void setupDiagnosticCache(Object editorInput) { |
| cache = createDiangosticCache(getNotifierFromEditorInput(editorInput)); |
| } |
| |
| /** |
| * Creates the diagnostic cache. |
| * |
| * @param input the input |
| * @return the cache |
| * @since 1.10 |
| */ |
| protected DiagnosticCache createDiangosticCache(final Notifier input) { |
| return new DiagnosticCache(input); |
| } |
| |
| /** |
| * @return whether a diagnostic cache should be managed. |
| * @since 1.10 |
| */ |
| protected boolean enableValidation() { |
| return false; |
| } |
| |
| /** |
| * Creates the top area of the editor. |
| * |
| * @param parent The parent {@link Composite} |
| * @param editorTitle The title of the editor |
| * @param editorInput the editor input |
| * @param toolbarActions The actions shown on the top area |
| * @param createElementCallback a call back if elements are created |
| * @return a {@link TreeMasterDetailComposite} |
| * @since 1.14 |
| */ |
| protected TreeMasterDetailComposite createRootView(Composite parent, String editorTitle, Object editorInput, |
| List<Action> toolbarActions, CreateElementCallback createElementCallback) { |
| final Composite composite = new Composite(parent, SWT.NONE); |
| |
| composite.setLayout(new FormLayout()); |
| |
| final FormData toolbarLayoutData = new FormData(); |
| toolbarLayoutData.left = new FormAttachment(0); |
| toolbarLayoutData.right = new FormAttachment(100); |
| toolbarLayoutData.top = new FormAttachment(0); |
| final EditorToolBar toolbar = new EditorToolBar(composite, SWT.NONE, editorTitle, toolbarActions); |
| toolbar.setLayoutData(toolbarLayoutData); |
| |
| final FormData treeMasterDetailLayoutData = new FormData(); |
| treeMasterDetailLayoutData.top = new FormAttachment(toolbar, 5); |
| treeMasterDetailLayoutData.left = new FormAttachment(0); |
| treeMasterDetailLayoutData.right = new FormAttachment(100); |
| treeMasterDetailLayoutData.bottom = new FormAttachment(100); |
| final TreeMasterDetailComposite treeMasterDetail = createTreeMasterDetail(composite, editorInput, |
| createElementCallback); |
| treeMasterDetail.setLayoutData(treeMasterDetailLayoutData); |
| |
| for (final Action action : toolbarActions) { |
| if (action instanceof IEditingDomainAware) { |
| ((IEditingDomainAware) action).setEditingDomain(getEditingDomain()); |
| } |
| } |
| return treeMasterDetail; |
| } |
| |
| /** |
| * Returns the root composite containing the tree and the detail view. This is null before the editor control is |
| * created. |
| * |
| * @return The root {@link TreeMasterDetailComposite} of this editor |
| * @since 1.20 |
| */ |
| protected TreeMasterDetailComposite getRootView() { |
| return rootView; |
| } |
| |
| /** |
| * This method creates a tree master detail. Override this method if you want to customize the tree. |
| * |
| * @param composite the parent composite |
| * @param editorInput the editor input |
| * @param createElementCallback the create element callback to add |
| * |
| * @return the {@link TreeMasterDetailComposite} |
| */ |
| protected TreeMasterDetailComposite createTreeMasterDetail( |
| final Composite composite, |
| Object editorInput, |
| final CreateElementCallback createElementCallback) { |
| final TreeMasterDetailSWTBuilder builder = TreeMasterDetailSWTFactory |
| .fillDefaults(composite, SWT.NONE, editorInput) |
| .customizeCildCreation(createElementCallback) |
| .customizeMenu(new MenuProvider() { |
| @Override |
| public Menu getMenu(TreeViewer treeViewer, EditingDomain editingDomain) { |
| final MenuManager menuMgr = new MenuManager(); |
| menuMgr.setRemoveAllWhenShown(true); |
| final List<MasterDetailAction> masterDetailActions = ActionCollector.newList() |
| .addCutAction(editingDomain).addCopyAction(editingDomain).addPasteAction(editingDomain) |
| .getList(); |
| menuMgr.addMenuListener(new TreeMasterDetailMenuListener(new ChildrenDescriptorCollector(), menuMgr, |
| treeViewer, editingDomain, masterDetailActions, createElementCallback, |
| new DefaultDeleteActionBuilder())); |
| final Menu menu = menuMgr.createContextMenu(treeViewer.getControl()); |
| return menu; |
| |
| } |
| }) |
| .customizeTree(createTreeViewerBuilder()) |
| .customizeReadOnly(!isEditable(getEditorInput())); |
| |
| if (enableValidation()) { |
| builder.customizeLabelDecorator( |
| new ECPValidationLabelDecoratorProvider(getNotifierFromEditorInput(editorInput), getDiagnosticCache())); |
| } |
| |
| final TreeMasterDetailComposite treeMasterDetail = customizeTree(builder).create(); |
| treeMasterDetail.registerDetailPanelRenderingFinishedCallback( |
| DetailPanelRenderingFinishedCallback.adapt((ctx, __) -> handleDetailActivated(ctx))); |
| return treeMasterDetail; |
| } |
| |
| /** |
| * Create the {@link TreeViewerBuilder} customization which creates the tree for the editor's tree master detail. |
| * <p> |
| * Clients can override this to customize the tree viewer. |
| * |
| * @return the {@link TreeViewerBuilder} which creates the tree viewer for the editor's tree master detail |
| * @since 1.20 |
| */ |
| protected TreeViewerBuilder createTreeViewerBuilder() { |
| return parent -> { |
| final TreeViewer treeViewer = new TreeViewer(parent, SWT.MULTI | SWT.BORDER); |
| treeViewer.setAutoExpandLevel(3); |
| treeViewer.getTree().addFocusListener(new FocusListener() { |
| private IContextActivation activation; |
| |
| @Override |
| public void focusLost(FocusEvent e) { |
| getSite().getService(IContextService.class).deactivateContext(activation); |
| } |
| |
| @Override |
| public void focusGained(FocusEvent e) { |
| activation = getSite().getService(IContextService.class).activateContext(getContextId()); |
| } |
| }); |
| return treeViewer; |
| }; |
| } |
| |
| /** |
| * Allows to modify the input object for the editor. |
| * |
| * @param resourceSet the resourceSet which is the default editor input |
| * @return the object to set as the input |
| */ |
| protected Object modifyEditorInput(ResourceSet resourceSet) { |
| return resourceSet; |
| } |
| |
| /** |
| * Creates a resource set and loads all required resources for the editor input. |
| * |
| * @param editorInput the editor input |
| * @return the resource set |
| * @throws PartInitException if the resource could not be loaded |
| */ |
| protected ResourceSet loadResource(IEditorInput editorInput) throws PartInitException { |
| ResourceSet resourceSet = ResourceSetHelpers.createResourceSet(getCommandStack()); |
| final URI resourceURI = EditUIUtil.getURI(editorInput, resourceSet.getURIConverter()); |
| |
| try { |
| resourceSet = ResourceSetHelpers.loadResourceWithProxies(resourceURI, resourceSet, |
| getResourceLoadOptions()); |
| verifyEditorResource(resourceURI, resourceSet); |
| return resourceSet; |
| // CHECKSTYLE.OFF: IllegalCatch |
| } catch (final Exception e) { |
| throw new PartInitException(e.getLocalizedMessage(), e); |
| } |
| // CHECKSTYLE.ON: IllegalCatch |
| } |
| |
| /** |
| * Returns whether the editor input allows editing of its contents. |
| * |
| * @param editorInput the editor's {@link IEditorInput} |
| * @return <code>true</code> if the input source allows editing, <code>false</code> otherwise |
| * @since 1.22 |
| */ |
| protected boolean isEditable(IEditorInput editorInput) { |
| // Only allow editing data if it can be persisted |
| return editorInput.getPersistable() != null; |
| } |
| |
| /** |
| * Check that the resource was loaded correctly and show any warnings to the user. |
| * |
| * @param resourceSet the resource set |
| * @param resourceURI the URI of the resource |
| * @since 1.19 |
| * |
| */ |
| protected void verifyEditorResource(URI resourceURI, ResourceSet resourceSet) { |
| final Resource resource = resourceSet.getResource(resourceURI, true); |
| if (XMLResource.class.isInstance(resource) |
| && !XMLResource.class.cast(resource).getEObjectToExtensionMap().isEmpty()) { |
| // we are showing a view which wasn't fully loaded |
| MessageDialog |
| .openWarning( |
| getSite().getShell(), |
| Messages.GenericEditor_UnknownFeaturesDialogTitle, |
| Messages.GenericEditor_UnknownFeaturesDialogDescription); |
| } |
| } |
| |
| /** |
| * The options to be used when loading the editor's resource. |
| * |
| * @return the load options |
| * @since 1.19 |
| */ |
| protected Map<Object, Object> getResourceLoadOptions() { |
| final HashMap<Object, Object> options = new HashMap<Object, Object>(); |
| options.put(XMLResource.OPTION_RECORD_UNKNOWN_FEATURE, |
| Boolean.TRUE); |
| options.put(XMLResource.OPTION_DEFER_IDREF_RESOLUTION, Boolean.TRUE); |
| return options; |
| } |
| |
| /** |
| * The options to be used when saving the editor's resource. |
| * |
| * @return the save options |
| * @since 1.19 |
| */ |
| protected Map<Object, Object> getResourceSaveOptions() { |
| final Map<Object, Object> saveOptions = new HashMap<Object, Object>(); |
| saveOptions.put(XMLResource.OPTION_ENCODING, "UTF-8"); //$NON-NLS-1$ |
| return saveOptions; |
| } |
| |
| @Override |
| public void setFocus() { |
| // NOOP |
| } |
| |
| /** |
| * Returns true, if the editor should have shortcuts. |
| * |
| * @return true, if the editor has shortcuts |
| */ |
| protected boolean hasShortcuts() { |
| return false; |
| } |
| |
| /** |
| * Returns the title for the currently displayed editor. |
| * Subclasses should override this function to change the Editor's title |
| * |
| * @return the title |
| */ |
| protected String getEditorTitle() { |
| return Messages.GenericEditor_EditorTitle; |
| } |
| |
| /** |
| * Returns the createElementCallback for this editor. By default, there is none. |
| * |
| * @return the callback |
| */ |
| protected CreateElementCallback getCreateElementCallback() { |
| return null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public EditingDomain getEditingDomain() { |
| if (rootView == null) { |
| return null; |
| } |
| return rootView.getEditingDomain(); |
| } |
| |
| /** |
| * Returns the toolbar actions for this editor. |
| * |
| * @return A list of actions to show in the Editor's Toolbar |
| * @since 1.10 |
| */ |
| protected List<Action> getToolbarActions() { |
| final List<Action> result = new LinkedList<Action>(); |
| if (!isEditable(getEditorInput())) { |
| // If the input isn't editable, toolbar actions are disabled |
| return result; |
| } |
| |
| result.add(new LoadEcoreAction(resourceSet)); |
| |
| result.addAll(readToolbarActions()); |
| return result; |
| } |
| |
| /** |
| * Read toolbar actions from all extensions. |
| * |
| * @return the Actions registered via extension point |
| * @since 1.10 |
| */ |
| protected List<Action> readToolbarActions() { |
| final List<Action> result = new LinkedList<Action>(); |
| |
| final ISelectionProvider selectionProvider = new GenericEditorSelectionProvider(); |
| |
| final IExtensionRegistry registry = Platform.getExtensionRegistry(); |
| if (registry == null) { |
| return result; |
| } |
| |
| final IConfigurationElement[] config = registry.getConfigurationElementsFor(ITOOLBAR_ACTIONS_ID); |
| for (final IConfigurationElement e : config) { |
| try { |
| final Object o = e.createExecutableExtension("toolbarAction"); //$NON-NLS-1$ |
| if (o instanceof IToolbarAction) { |
| final IToolbarAction action = (IToolbarAction) o; |
| if (!action.canExecute(resourceSet)) { |
| continue; |
| } |
| |
| result.add(action.getAction(resourceSet, selectionProvider)); |
| } |
| } catch (final CoreException ex) { |
| Activator.getDefault().getReportService().report( |
| new StatusReport(new Status(IStatus.ERROR, Activator.PLUGIN_ID, ex.getMessage(), ex))); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the ResouceSet of this Editor. |
| * |
| * @return The resource set |
| */ |
| public ResourceSet getResourceSet() { |
| return resourceSet; |
| } |
| |
| @Override |
| public void dispose() { |
| if (getDiagnosticCache() != null) { |
| getDiagnosticCache().dispose(); |
| } |
| ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceChangeListener); |
| getSite().getPage().removePartListener(partListener); |
| super.dispose(); |
| } |
| |
| private Optional<IFile> getFile() { |
| final IEditorInput input = GenericEditor.this.getEditorInput(); |
| if (isEditable(getEditorInput()) && IFileEditorInput.class.isInstance(input)) { |
| return Optional.of(IFileEditorInput.class.cast(input).getFile()); |
| } |
| return Optional.empty(); |
| } |
| |
| /** |
| * |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.ui.ide.IGotoMarker#gotoMarker(org.eclipse.core.resources.IMarker) |
| * @since 1.10 |
| */ |
| @Override |
| public void gotoMarker(IMarker marker) { |
| try { |
| EObject eObject = null; |
| EStructuralFeature feature = null; |
| |
| final String resourceURI = (String) marker.getAttribute(RESOURCE_URI); |
| final String fragmentURI = (String) marker.getAttribute(FRAGMENT_URI); |
| if (resourceURI != null && fragmentURI != null) { |
| final Resource resource = getEditingDomain().getResourceSet().getResource(URI.createURI(resourceURI), |
| true); |
| eObject = resource.getEObject(fragmentURI); |
| |
| final String featureURI = marker.getAttribute(FEATURE_URI, null); |
| if (featureURI != null) { |
| // Don't load on demand because this should be a delegated look-up in the package registry |
| // or else find the Ecore resource already loaded to resolve our model's schema |
| final EObject featureObject = getEditingDomain().getResourceSet().getEObject( |
| URI.createURI(featureURI), |
| false); |
| if (featureObject instanceof EStructuralFeature) { |
| feature = (EStructuralFeature) featureObject; |
| } |
| } |
| } else { |
| // Maybe it's an EMF-standard marker? |
| final List<?> targets = markerHelper.getTargetObjects(getEditingDomain(), marker, false); |
| for (final Object next : targets) { |
| if (next instanceof EObject) { |
| if (eObject == null) { |
| eObject = (EObject) next; |
| } else if (feature == null && next instanceof EStructuralFeature) { |
| feature = (EStructuralFeature) next; |
| } |
| } |
| } |
| } |
| |
| if (eObject == null) { |
| // Nothing to navigate to |
| return; |
| } |
| |
| if (feature == null) { |
| reveal(eObject); |
| } else { |
| reveal(UniqueSetting.createSetting(eObject, feature)); |
| } |
| } catch (final CoreException ex) { |
| // silent |
| } |
| } |
| |
| /** |
| * The given element will be revealed in the tree of the editor. |
| * |
| * @param objectToReveal the object to reveal |
| * @since 1.10 |
| */ |
| public void reveal(EObject objectToReveal) { |
| rootView.refresh(); |
| rootView.selectAndReveal(objectToReveal); |
| } |
| |
| /** |
| * Reveal the control that edits a {@code setting} of some object. |
| * |
| * @param setting the feature setting to reveal of object |
| * @since 1.22 |
| */ |
| public void reveal(UniqueSetting setting) { |
| rootView.refresh(); |
| rootView.selectAndReveal(setting); |
| } |
| |
| /** |
| * @return the commandStack the {@link |
| * import org.eclipse.emf.common.command.CommandStack;} |
| * @since 1.10 |
| */ |
| protected BasicCommandStack getCommandStack() { |
| return commandStack; |
| } |
| |
| /** |
| * Override this method to set additional attributes on the given {@link IMarker}, e.g. location information. |
| * |
| * @param marker the {@link IMarker} to adjust |
| * @param diagnostic the {@link Diagnostic} |
| * @return <code>true</code> if the marker was changed, <code>false</code> otherwise |
| * @throws CoreException in case of an error |
| * @since 1.10 |
| */ |
| protected boolean adjustErrorMarker(IMarker marker, Diagnostic diagnostic) throws CoreException { |
| final List<?> data = diagnostic.getData(); |
| if (data.size() < 1) { |
| return false; |
| } |
| if (!EObject.class.isInstance(data.get(0))) { |
| return false; |
| } |
| final EObject eObject = EObject.class.cast(data.get(0)); |
| if (eObject.eResource() == null) { |
| /* possible when job still running but getting closed */ |
| return false; |
| } |
| final EStructuralFeature feature = data.subList(1, data.size()).stream() |
| .filter(EStructuralFeature.class::isInstance).map(EStructuralFeature.class::cast) |
| .findFirst().orElse(null); |
| final String uri = eObject.eResource().getURI().toString(); |
| final String uriFragment = eObject.eResource().getURIFragment(eObject); |
| marker.setAttribute(RESOURCE_URI, uri); |
| marker.setAttribute(FRAGMENT_URI, uriFragment); |
| if (feature != null) { |
| marker.setAttribute(FEATURE_URI, String.valueOf(EcoreUtil.getURI(feature))); |
| } |
| return true; |
| } |
| |
| /** |
| * Returns whether this editor is currently in the process of shutting down. |
| * |
| * @return <code>true</code> if the editor is currently closing, <code>false</code> otherwise |
| * @since 1.18 |
| */ |
| protected boolean isClosing() { |
| return closing; |
| } |
| |
| /** |
| * Set whether this editor is currently in the process of shutting down. |
| * Set this flag in case you will close the editor. |
| * |
| * @param closing Whether the editor is currently closing (shutting down) |
| * @since 1.18 |
| */ |
| protected void setClosing(boolean closing) { |
| this.closing = closing; |
| } |
| |
| /** |
| * Removes the given {@linkplain Resource Resources} from this editor's {@linkplain ResourceSet}. Thereby the |
| * resources are matched by URI. |
| * |
| * @param resources The {@linkplain Resource Resources} to remove from this editor's {@linkplain ResourceSet}. |
| * @since 1.18 |
| */ |
| protected void removeResources(final Collection<Resource> resources) { |
| for (final Resource removed : resources) { |
| final Resource toRemove = resourceSet.getResource(removed.getURI(), false); |
| if (toRemove != null) { |
| resourceSet.getResources().remove(toRemove); |
| } |
| } |
| } |
| |
| /** |
| * Customize the tree {@code builder}. Subclasses are free to add |
| * customizations or override default customizations installed by this |
| * class, but the latter requires care not to break expected editor |
| * behaviour. |
| * |
| * @param builder the tree builder |
| * @return the {@code builder} for convenience of call chaining, or if |
| * absolutely necessary an entirely new builder |
| * |
| * @since 1.24 |
| */ |
| protected TreeMasterDetailSWTBuilder customizeTree(TreeMasterDetailSWTBuilder builder) { |
| return builder; |
| } |
| |
| /** |
| * React to the rendering or re-activation of a detail context. The default |
| * implementation does nothing; subclasses may just override it. |
| * |
| * @param detailContext the active detail context |
| * |
| * @since 1.24 |
| */ |
| protected void handleDetailActivated(ViewModelContext detailContext) { |
| // Nothing to do |
| } |
| |
| /** |
| * Listens to part events. |
| * |
| */ |
| private final class GenericEditorActivationListener implements IPartListener { |
| @Override |
| public void partOpened(IWorkbenchPart part) { |
| } |
| |
| @Override |
| public void partDeactivated(IWorkbenchPart part) { |
| } |
| |
| @Override |
| public void partClosed(IWorkbenchPart part) { |
| } |
| |
| @Override |
| public void partBroughtToTop(IWorkbenchPart part) { |
| } |
| |
| @Override |
| public void partActivated(IWorkbenchPart part) { |
| if (!isClosing() && part == GenericEditor.this && isDirty() && filesChangedWithConflict |
| && discardChanges()) { |
| reloading = true; |
| reloadResources(resourceSet.getResources()); |
| reloading = false; |
| getCommandStack().flush(); |
| initMarkers(); |
| firePropertyChange(PROP_DIRTY); |
| filesChangedWithConflict = false; |
| } |
| } |
| } |
| |
| /** |
| * Reacts to revalidation changes and creates/removes marker accordingly. |
| * |
| * @author Johannes Faltermeier |
| * |
| */ |
| private final class MarkerValidationListener implements ValidationListener { |
| @Override |
| public void revalidationOccurred(final Collection<EObject> object, boolean potentialStructuralChange) { |
| initMarkers(); |
| } |
| } |
| |
| /** |
| * {@link MarkerHelper} for this editor. |
| * |
| * @author Johannes Faltermeier |
| * |
| */ |
| private final class GenericEditorMarkerHelper extends EditUIMarkerHelper { |
| @Override |
| public IFile getFile(Diagnostic diagnostic) { |
| final Optional<IFile> file = GenericEditor.this.getFile(); |
| if (file.isPresent()) { |
| return file.get(); |
| } |
| return super.getFile(diagnostic); |
| } |
| |
| @Override |
| protected boolean adjustMarker(IMarker marker, Diagnostic diagnostic) throws CoreException { |
| return adjustErrorMarker(marker, diagnostic); |
| } |
| } |
| |
| /** |
| * The GenericEditorResourceChangeListener listens for changes in currently opened Ecore files and reports |
| * them to the EcoreEditor. |
| */ |
| private final class GenericEditorResourceChangeListener implements IResourceChangeListener { |
| |
| @Override |
| public void resourceChanged(IResourceChangeEvent event) { |
| final Collection<Resource> changedResources = new ArrayList<Resource>(); |
| final Collection<Resource> removedResources = new ArrayList<Resource>(); |
| final IResourceDelta delta = event.getDelta(); |
| |
| if (delta == null) { |
| return; |
| } |
| |
| try { |
| delta.accept(new GenericEditorResourceDeltaVisitor(removedResources, changedResources)); |
| } catch (final CoreException ex) { |
| Activator.getDefault().getReportService().report( |
| new StatusReport(new Status(IStatus.ERROR, Activator.PLUGIN_ID, ex.getMessage(), ex))); |
| } |
| if (changedResources.isEmpty() && removedResources.isEmpty()) { |
| return; |
| } |
| handleResourceChange(changedResources, removedResources); |
| } |
| } |
| |
| /** |
| * The delata visitor deciding if changes are relevant for reloading. |
| */ |
| private final class GenericEditorResourceDeltaVisitor implements IResourceDeltaVisitor { |
| private final Collection<Resource> removedResources; |
| private final Collection<Resource> changedResources; |
| |
| GenericEditorResourceDeltaVisitor(Collection<Resource> removedResources, |
| Collection<Resource> changedResources) { |
| this.removedResources = removedResources; |
| this.changedResources = changedResources; |
| } |
| |
| @Override |
| public boolean visit(final IResourceDelta delta) { |
| if ((delta.getFlags() & IResourceDelta.MARKERS) != 0) { |
| return false; |
| } |
| if (delta.getResource().getType() == IResource.FILE |
| && (delta.getKind() == IResourceDelta.REMOVED || |
| delta.getKind() == IResourceDelta.CHANGED)) { |
| final ResourceSet resourceSet = getResourceSet(); |
| if (resourceSet == null) { |
| return false; |
| } |
| Resource resource = null; |
| |
| final URI uri = URI.createPlatformResourceURI(delta.getFullPath().toString(), true); |
| resource = resourceSet.getResource(uri, false); |
| if (resource == null) { |
| try { |
| final URL fileURL = FileLocator.resolve(new URL(uri.toString())); |
| resource = resourceSet.getResource(URI.createFileURI(fileURL.getPath()), false); |
| } catch (final IOException ex) { |
| return false; |
| } |
| } |
| |
| if (resource != null) { |
| if (delta.getKind() == IResourceDelta.REMOVED) { |
| removedResources.add(resource); |
| } else { |
| changedResources.add(resource); |
| } |
| } |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| /** Selection Provider for the GenericEditor. */ |
| private class GenericEditorSelectionProvider implements ISelectionProvider { |
| |
| @Override |
| public void setSelection(ISelection selection) { |
| if (rootView == null) { |
| return; |
| } |
| rootView.getMasterDetailSelectionProvider().setSelection(selection); |
| } |
| |
| @Override |
| public void removeSelectionChangedListener(ISelectionChangedListener listener) { |
| if (rootView == null) { |
| return; |
| } |
| rootView.getMasterDetailSelectionProvider().removeSelectionChangedListener(listener); |
| } |
| |
| @Override |
| public ISelection getSelection() { |
| if (rootView == null) { |
| return StructuredSelection.EMPTY; |
| } |
| return rootView.getMasterDetailSelectionProvider().getSelection(); |
| } |
| |
| @Override |
| public void addSelectionChangedListener(ISelectionChangedListener listener) { |
| if (rootView == null) { |
| return; |
| } |
| rootView.getMasterDetailSelectionProvider().addSelectionChangedListener(listener); |
| } |
| } |
| } |