/******************************************************************************* * MontiCore Language Workbench * Copyright (c) 2015, 2016, MontiCore, All rights reserved. * * This project is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this project. If not, see <http://www.gnu.org/licenses/>. *******************************************************************************/ package de.monticore.genericgraphics; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.eclipse.core.resources.IFile; 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.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.draw2d.ColorConstants; import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPartFactory; import org.eclipse.gef.KeyHandler; import org.eclipse.gef.KeyStroke; import org.eclipse.gef.MouseWheelHandler; import org.eclipse.gef.MouseWheelZoomHandler; import org.eclipse.gef.RootEditPart; import org.eclipse.gef.editparts.ScalableRootEditPart; import org.eclipse.gef.editparts.ZoomManager; import org.eclipse.gef.ui.actions.ActionRegistry; import org.eclipse.gef.ui.actions.GEFActionConstants; import org.eclipse.gef.ui.actions.PrintAction; import org.eclipse.gef.ui.actions.ZoomInAction; import org.eclipse.gef.ui.actions.ZoomOutAction; import org.eclipse.gef.ui.parts.ScrollingGraphicalViewer; import org.eclipse.gef.ui.parts.SelectionSynchronizer; import org.eclipse.jface.action.IAction; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.texteditor.AbstractTextEditor; import de.monticore.editorconnector.GraphicalSelectionListener; import de.monticore.genericgraphics.controller.editparts.IMCEditPart; import de.monticore.genericgraphics.controller.editparts.IMCViewElementEditPart; import de.monticore.genericgraphics.controller.editparts.MCEditPartFactory; import de.monticore.genericgraphics.controller.persistence.DefaultGraphicsLoader; import de.monticore.genericgraphics.controller.persistence.ErrorCollector; import de.monticore.genericgraphics.controller.persistence.IGraphicsLoader; import de.monticore.genericgraphics.controller.persistence.util.IPersistenceUtil; import de.monticore.genericgraphics.controller.selection.SelectionSyncException; import de.monticore.genericgraphics.controller.util.ASTNodeProblemReportHandler; import de.monticore.genericgraphics.view.layout.ILayoutAlgorithm; import de.se_rwth.commons.logging.Log; import de.se_rwth.langeditor.injection.DIService; import de.se_rwth.langeditor.modelstates.ModelState; import de.se_rwth.langeditor.modelstates.ObservableModelStates; import de.se_rwth.langeditor.texteditor.TextEditorImpl; /** * <p> * The Generic Graphical Viewer for MontiCore Class Diagrams. * </p> * <p> * The Generic Graphical Viewer makes use of the following utils: * <ul> * <li>{@link IGraphicsLoader}: for loading and saving the model and view data</li> * <li>{@link SelectionSynchronizer}: for synchronizing selection between multiple editors</li> * <li>{@link ILayoutAlgorithm}: for layouting nodes of the diagram.</li> * <li>A ResourceTracker: for reacting on model file changes due to other editors * <ul> * <li>File contents changed and was saved => refresh content of this editor</li> * <li>File was renamed => reload the file. Note: the view file is not moved automatically.</li> * <li>File was deleted => close editor.</li> * </ul> * </li> * </ul> * </p> * <p> * Different functionality is provided by the Generic Graphical Viewer without changing anything, * and just implementing the abstract methods: * <ul> * <li>Loading of model data/view data and combination of both</li> * <li>Redo / Undo functionality for nearly all actions</li> * <li>Observing the underlying model file for changes and updating correctly</li> * <li>Providing a Overview View for the Editor</li> * <li>Showing Problems during parsing in Problems view</li> * <li>Show Problems in Figures</li> * <li>Providing printing functionality</li> * <li>Providing zoom functionality</li> * <li>Providing export to jpg, ico, bmp, gif, png -images</li> * </ul> * </p> * <p> * There are several methods that allow specific configuration, without the need to change any of * the existing methods.<br> * <br> * The following methods need to be implemented to provide functionality needed by the editor: * <ul> * <li>{@link #createEditPartFactory()}</li> * <li>{@link #createPersistenceUtil()}</li> * <li>{@link #createDSLTool(String[])}</li> * <li>{@link #createLayoutAlgorithm()}</li> * <li>{@link #getContents()}</li> * </ul> * Furthermore, the following methods allow to customize the process of loading model data * <ul> * <li>{@link #beforeModelLoad()}</li> * <li>{@link #afterModelLoadBeforeViewLoad()}</li> * <li>{@link #afterViewLoad()}</li> * </ul> * These methods are called as follows: * <ul> * <li>{@link #beforeModelLoad()}</li> * <li>loading of model data</li> * <li>{@link #afterModelLoadBeforeViewLoad()}</li> * <li>loading of view information</li> * <li>{@link #afterViewLoad()}</li> * <li>contents of GraphicalViewer is set with {@link #getContents()}</li> * <li>combination of model and view information with {@link #createLayoutAlgorithm()}</li> * </ul> * </p> * <p> * There are flags to set, for changing default behavior: * <ul> * <li>{link {@link #isAlwaysRefresh()}, {@link #setAlwaysRefresh(boolean)}: If <tt>true</tt> the * editor will reload the model after the model file changed. This means, e.g., that the model file * is reloaded when a single space is inserted. If <tt>false</tt> the editor is reloaded when the * model file changed and was saved.</li> * </ul> * </p> * <b>Note</b>: when overriding (non-abstract) methods (e.g. <code>foo()</code> ), you should always * call <code>super.foo()</code> first in your method.<br> * * @author Tim Enger * @author Philipp Kehrbusch */ public abstract class GenericGraphicsViewer extends ScrollingGraphicalViewer { private IWorkbenchPartSite site; private IFile file; private ActionRegistry actionRegistry; private IGraphicsLoader gLoader; private ILayoutAlgorithm layout; private GraphicalSelectionListener selectionListener; private ResourceTracker resourceListener = new ResourceTracker(); TextEditorImpl txtEditor; /** * Initializes the viewer with an IWorkbenchPartSite and an input file. Must be called before * {@link #createControl(org.eclipse.swt.widgets.Composite)}. * * @param site * @param file * @param txtEditor */ public void init(IWorkbenchPartSite site, IFile file, TextEditorImpl txtEditor) { this.site = site; setInput(file); setEditPartFactory(createEditPartFactory()); if (txtEditor != null) { setTextualEditor(txtEditor); } } /** * Like {@link #init(IWorkbenchPartSite, IFile, AbstractTextEditor)} without assigning a textual * editor. However, the viewer is not fully functional until * {@link #setTextualEditor(AbstractTextEditor)} is called. * * @param site * @param file */ public void init(IWorkbenchPartSite site, IFile file) { init(site, file, null); } public void setTextualEditor(TextEditorImpl txtEditor) { /* Editor can only be set once (graphical viewer is bound to one textual editor). This will also * avoid more than one ASTUpdateListener being created. If several listeners are registered for * one viewer, there will be errors as they are not removed properly. */ if (this.txtEditor != null) { return; } selectionListener = new GraphicalSelectionListener(file, this); site.getWorkbenchWindow().getSelectionService().addPostSelectionListener(selectionListener); this.txtEditor = txtEditor; } public void configure() { // Equivalent to configureGraphicalViewer() from GraphicalEditor getControl().setBackground(ColorConstants.listBackground); gLoader = new DefaultGraphicsLoader(createPersistenceUtil(), file); layout = createLayoutAlgorithm(); setupPrinting(); setupZoom(); // ObservableModelStates observableModelStates = DIService.getInstance(ObservableModelStates.class); observableModelStates.getModelStates().stream() .filter(modelState -> modelState.getStorage().equals(getInputFile())) .forEach(this::acceptModelState); observableModelStates.addStorageObserver(getInputFile(), this::acceptModelState); } private void acceptModelState(ModelState modelState) { if (modelState.isLegal()) { Display.getDefault().asyncExec(() -> { setContents(modelState.getRootNode()); refreshContents(); }); } } public void setInput(IFile file) { // The workspace never changes for an editor. So, removing and re-adding // the resourceListener is not necessary. But it is being done here for the // sake of proper implementation. // Plus, the resourceListener needs to be added to the workspace the first // time around. if (file != null) { this.file = file; file.getWorkspace().removeResourceChangeListener(resourceListener); file.getWorkspace().addResourceChangeListener(resourceListener); } } @SuppressWarnings("unchecked") public void refreshContents() { // Update mapping try { selectionListener.createMappings(); } catch (SelectionSyncException e) { Log.error("0xA1114 Refreshing of content failed due to the following exception: " + e); } beforeModelLoad(); afterModelLoadBeforeViewLoad(); // view data gLoader.loadViewData(); ILayoutAlgorithm layoutAlgo = layout; /* * Don't allow moving etc in the outline view * if (!(getDisplayingPart() instanceof GenericGraphicsEditor)) { for (EditPart ep : new ArrayList<EditPart>(this.getEditPartRegistry().values())) { ep.removeEditPolicy(EditPolicy.DIRECT_EDIT_ROLE); ep.removeEditPolicy(EditPolicy.LAYOUT_ROLE); ep.removeEditPolicy(EditPolicy.CONNECTION_BENDPOINTS_ROLE); } } */ boolean newLayout = gLoader.combineModelViewData( new ArrayList<EditPart>(this.getEditPartRegistry().values()), layoutAlgo); // save automatically generated layout if (newLayout) { this.doSave(new NullProgressMonitor()); } showProblemReports(); } /** * <p> * Sets up a zooming functionality. * </p> * <p> * Provides the following functionality * <ul> * <li>'Numpad +' & 'Numpad -' for zooming in & out</li> * <li>'CTRL + Mousewheel' for zooming in & out</li> * </ul> * </p> */ private void setupZoom() { ActionRegistry aRegistry = getActionRegistry(); // zooming! ScalableRootEditPart rootEditPart = new ScalableRootEditPart(); setRootEditPart(rootEditPart); List<String> zoomContributions = Arrays.asList( new String[] { ZoomManager.FIT_ALL, ZoomManager.FIT_HEIGHT, ZoomManager.FIT_WIDTH }); rootEditPart.getZoomManager().setZoomLevelContributions(zoomContributions); rootEditPart.getZoomManager() .setZoomLevels(new double[] { .25, .5, .75, 1.0, 1.5, 2.0, 2.5, 3, 4 }); IAction zoomIn = new ZoomInAction(rootEditPart.getZoomManager()); IAction zoomOut = new ZoomOutAction(rootEditPart.getZoomManager()); aRegistry.registerAction(zoomIn); aRegistry.registerAction(zoomOut); KeyHandler keyHandler = new KeyHandler(); keyHandler.setParent(getKeyHandler()); keyHandler.put(KeyStroke.getPressed('+', SWT.KEYPAD_ADD, 0), aRegistry.getAction(GEFActionConstants.ZOOM_IN)); keyHandler.put(KeyStroke.getPressed('-', SWT.KEYPAD_SUBTRACT, 0), aRegistry.getAction(GEFActionConstants.ZOOM_OUT)); // Mousewheel Zooming // SWT.MOD1 => CTRL setProperty(MouseWheelHandler.KeyGenerator.getKey(SWT.MOD1), MouseWheelZoomHandler.SINGLETON); setKeyHandler(keyHandler); } /** * Sets up the possibility to print the visualization. */ private void setupPrinting() { if (getDisplayingPart() != null) { IAction printAction = new PrintAction(getDisplayingPart()); getActionRegistry().registerAction(printAction); } } private ActionRegistry getActionRegistry() { if (actionRegistry == null) actionRegistry = new ActionRegistry(); return actionRegistry; } /** * <p> * Specifies the {@link EditPartFactory} which should be used. * </p> * <p> * This {@link EditPartFactory} should be an extension of the {@link MCEditPartFactory} to provide * full support for all features of the framework. * </p> * * @return The {@link EditPartFactory} which should be used. */ public abstract EditPartFactory createEditPartFactory(); /** * <p> * Specifies the {@link IPersistenceUtil} to be used for loading and saving the view files * (graphical information). * </p> * * @return The {@link IPersistenceUtil} to be used for loading and saving the view files * (graphical information). */ public abstract IPersistenceUtil createPersistenceUtil(); /** * <p> * Specifies the {@link ILayoutAlgorithm} to be used for initially layouting the diagram. * </p> * * @return {@link ILayoutAlgorithm} to be used for initially layouting the diagram. */ public abstract ILayoutAlgorithm createLayoutAlgorithm(); /** * Put any functionality in this method, that needs to be executed before the model data is * loaded. <br> * See class description for more details. */ public void beforeModelLoad() { // customization possible } /** * Put any functionality in this method, that needs to be executed after the model data was * loaded, but before the view data is loaded. <br> * See class description for more details. */ public void afterModelLoadBeforeViewLoad() { // customization possible } /** * Put any functionality in this method, that needs to be executed after the model and the view * data were loaded.<br> * See class description for more details. */ public void afterViewLoad() { // customization possible } public void doSave(IProgressMonitor monitor) { Collection<EditPart> epsSet = getEditPartRegistry().values(); List<EditPart> eps = new ArrayList<EditPart>(epsSet); gLoader.saveViewData(eps, monitor); refreshContents(); } public void dispose() { // Save without monitoring doSave(null); // dispose resourcetracker if (resourceListener != null) { file.getWorkspace().removeResourceChangeListener(resourceListener); } // dispose selection listener if (selectionListener != null) { site.getWorkbenchWindow().getSelectionService() .removePostSelectionListener(selectionListener); } } /*******************************************/ /************** FILE CHANGED ***************/ /*******************************************/ /** * <p> * This class is responsible for tracking resource (file) changes. * </p> * The following functionality is provided: * <ul> * <li>File contents changed and was saved => refresh content of this editor</li> * <li>File was renamed => reload the file.<br> * Note: the view file is not moved automatically.</li> * <li>File was deleted => close editor.</li> * </ul> * <b>Note</b>: Set the <code>alwaysRefresh</code> flag: if <tt>true</tt> the editor will reload * the model after the model file changed. This means, e.g., that the model file is reloaded when * a single space is inserted. If <tt>false</tt> the editor is reloaded when the model file * changed and was saved. <br> * <br> * * @author Tim Enger */ class ResourceTracker implements IResourceChangeListener, IResourceDeltaVisitor { @Override public void resourceChanged(IResourceChangeEvent event) { IResourceDelta delta = event.getDelta(); try { if (delta != null) { delta.accept(this); } } catch (CoreException e) { // What should be done here? e.printStackTrace(); } } @Override public boolean visit(IResourceDelta delta) { if (delta == null || !delta.getResource().equals(file)) { return true; } if (delta.getKind() == IResourceDelta.REMOVED) { Display display = site.getShell().getDisplay(); if ((IResourceDelta.MOVED_TO & delta.getFlags()) == 0) { // CASE: the file was deleted => close editor // NOTE: // The case where an open, unsaved file is deleted is // not handled here. // If it should be handled use a PartListener added // to the Workbench in the initialize() method. display.asyncExec(new Runnable() { @Override public void run() { if (getDisplayingPart() instanceof GenericGraphicsEditor && !((GenericGraphicsEditor) getDisplayingPart()).isDirty()) { site.getPage().closeEditor(((GenericGraphicsEditor) getDisplayingPart()), false); } } }); } else { // CASE: the file was moved or renamed => reload and change file final IFile newFile = ResourcesPlugin.getWorkspace().getRoot() .getFile(delta.getMovedToPath()); display.asyncExec(new Runnable() { @Override public void run() { IFile oldViewFile = gLoader.getViewFile(); // superSetInput(new FileEditorInput(newFile)); if (getDisplayingPart() instanceof GenericGraphicsEditor) { ((GenericGraphicsEditor) getDisplayingPart()).setInput(newFile); // will call // #GenericGraphicsViewer.setInput(IFile) } else setInput(newFile); // update files of graphics loader gLoader.setModelFile(newFile); gLoader = new DefaultGraphicsLoader(createPersistenceUtil(), file); // this code means, that the view file, will always be in the same // folder as the model file with the same name but different // extension whenever the model file was moved in eclipse. try { if (oldViewFile != null) { // copy old view file to new location String ext = oldViewFile.getFileExtension(); IPath newPath = (IPath) newFile.getFullPath().clone(); newPath = newPath.removeFileExtension().addFileExtension(ext); oldViewFile.copy(newPath, true, new NullProgressMonitor()); // remove old file oldViewFile.delete(true, new NullProgressMonitor()); } } catch (CoreException e) { e.printStackTrace(); } gLoader.setViewFileAccordingToModelFile(); refreshContents(); } }); } } return false; } } /** * <p> * Show problem reports in: * <ul> * <li>Eclipse Problem View</li> * <li>Graphic Representation</li> * </ul> * </p> * <p> * This method is called during the initialization/refresh of the editor and uses the methods of * the {@link ASTNodeProblemReportHandler} util. * </p> * <p> * <b>Note</b>: This method is intended to be overwritten by subclasses if they want to change the * problem report handling. * </p> * * @param ec The {@link ErrorCollector} used as input for the {@link ProblemReport ProblemReports} */ @SuppressWarnings("unchecked") public void showProblemReports() { ASTNodeProblemReportHandler.showProblemReportsInGraphics(this.getEditPartRegistry().values()); // TODO check for exception always occurring at the start of the editor // when this is active // ASTNodeProblemReportHandler.showProblemReportsInProblemsView(file, // ec); } /*******************************************/ /************ GETTER & SETTERS *************/ /*******************************************/ /** * @return The viewer's input as {@link IFile}. */ public IFile getInputFile() { return file; } /** * Every GEF editor has a {@link RootEditPart}. This {@link RootEditPart} has a single child, * called <i>contents</i> editpart representing the model data of the editor. * * @return The contents {@link IMCEditPart}. */ public IMCEditPart getContentEditPart() { return (IMCEditPart) this.getRootEditPart().getContents(); } public GraphicalSelectionListener getSelectionListener() { return selectionListener; } /** * Applies a layout to the diagram that was generated by the layout algorithm returned in * {@link #createLayoutAlgorithm()}. */ public void applyGeneratedLayout() { if (layout != null) { List<EditPart> eps = new ArrayList<EditPart>(this.getEditPartRegistry().values()); List<IMCViewElementEditPart> veEps = new ArrayList<IMCViewElementEditPart>(); for (EditPart ep : eps) { if (ep instanceof IMCViewElementEditPart) veEps.add((IMCViewElementEditPart) ep); } layout.layout(veEps); doSave(new NullProgressMonitor()); refreshContents(); } } /** * Returns the {@code IWorkbenchPart} this Viewer is attached to. */ public IWorkbenchPart getDisplayingPart() { return site.getPart(); } }