/******************************************************************************* * Copyright (c) 2009 itemis AG (http://www.itemis.eu) and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.xtext.gmf.glue.edit.part; import java.util.List; import java.util.Map; import org.eclipse.core.commands.operations.IOperationHistory; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.expressions.Expression; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart; import org.eclipse.gmf.runtime.diagram.ui.parts.DiagramEditDomain; import org.eclipse.gmf.runtime.diagram.ui.parts.IDiagramEditDomain; import org.eclipse.jface.action.IAction; import org.eclipse.jface.commands.ActionHandler; import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.contentassist.ContentAssistant; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.SourceViewer; import org.eclipse.jface.text.templates.TemplateException; import org.eclipse.jface.viewers.TextCellEditor; import org.eclipse.papyrus.commands.CheckedOperationHistory; import org.eclipse.papyrus.extensionpoints.editors.ui.IPopupEditorHelper; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Shell; import org.eclipse.text.undo.DocumentUndoManagerRegistry; import org.eclipse.text.undo.IDocumentUndoManager; import org.eclipse.ui.ActiveShellExpression; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchCommandConstants; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.console.actions.TextViewerAction; import org.eclipse.ui.handlers.IHandlerActivation; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.texteditor.ITextEditorActionConstants; import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; import org.eclipse.ui.texteditor.IUpdate; import org.eclipse.xtext.gmf.glue.Activator; import org.eclipse.xtext.gmf.glue.partialEditing.CustomXtextSourceViewer; import org.eclipse.xtext.gmf.glue.partialEditing.ISyntheticResourceProvider; import org.eclipse.xtext.gmf.glue.partialEditing.OperationHistoryListener; import org.eclipse.xtext.gmf.glue.partialEditing.PartialModelEditor; import org.eclipse.xtext.gmf.glue.partialEditing.SourceViewerHandle; import org.eclipse.xtext.gmf.glue.partialEditing.SourceViewerHandleFactory; import org.eclipse.xtext.parser.IParseResult; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.ui.editor.model.IXtextDocument; import org.eclipse.xtext.util.concurrent.IUnitOfWork; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Injector; /** * Base class to handle a small in-diagram XtextEditor. * * Override the generated <code>performDirectEdit</code> methods in the EditPart of the label to be directly edited, and * call {@link #showEditor()} instead of opening the default {@link TextCellEditor}. * * @author koehnlein * @author CEA LIST (A. Cuccuru, A. Radermacher): Modifications for the integration into Papyrus * Changes: * - Added "implements" relationship towards IPopupEditorHelper, * related to the DirectEditor extension point of Papyrus * - Signature of the constructor modified * - Method showEditor modified, with the creation of a temporary file, underlying the popup xtext editor * - Method closeEditor modified, with new statements for managing the reconciliation between the original * UML model and the textual specification resulting from the edition in the popup xtext editor. The * temporary text file created by showEditor is also deleted. * - Method createXTextEditor modified. Now uses the Papyrus IEditorSite, and also adds a focus listener for * managing the context eobject and current xtext editor. * - Method setEditorRegin modified (needs some work...) * - Method setEditorBounds modified (needs some work...) * - Method activateServices and deactivateServices, for managing the key binding of the context diagram * editor. */ public class PopupXtextEditorHelper implements IPopupEditorHelper { private static IGraphicalEditPart hostEditPart; private IEditorPart diagramEditor; private int editorOffset; private int initialEditorSize; private int initialDocumentSize; private Composite xtextEditorComposite; private final Injector xtextInjector; private XtextResource xtextResource; private String semanticElementFragment; private EObject semanticElement ; private String textToEdit ; /** * get the hos editpart * @return the editpart */ public static IGraphicalEditPart getHostEditPart() { return hostEditPart; } /** * The file extension used to dynamically select the appropriate xtext editor */ public static String fileExtension ; private IXtextEMFReconciler modelReconciler; private ISyntheticResourceProvider resourceProvider ; private SourceViewerHandle sourceViewerHandle ; /** * @return The source viewer handle for this PopupXtextEditorHelper * */ public SourceViewerHandle getSourceViewerHandle() { return sourceViewerHandle; } private PartialModelEditor partialEditor ; private Shell diagramShell ; private OperationHistoryListener operationHistoryListener; private IXTextSemanticValidator semanticValidator ; /** * The context EObject for this editor. It can be used for content assist, verification, etc. */ public static EObject context ; /** * */ public static boolean ignoreFocusLost = false ; /** * This element was originally undocumented in the XText/GMF integration example * * Modifications performed by CEA LIST * - Signature changed: was public PopupXtextEditorHelper(IGraphicalEditPart editPart, Injector xtextInjector) * @param editPart The editPart on which a direct edit has been performed. * @param xtextInjector The xtextInjector. * @param modelReconciler The IXtextEMFReconciler, to update the context UML model with changes textually specified in the popup xtext editor * @param textToEdit the initialization text, used as the initial textual content for the popup xtext editor * @param fileExtension the extension for the temporary textual file (underlying the editor) * @param semanticValidator the semantic validator used to semantically validate the xtext model before saving */ public PopupXtextEditorHelper(IGraphicalEditPart editPart, Injector xtextInjector, IXtextEMFReconciler modelReconciler, String textToEdit, String fileExtension, IXTextSemanticValidator semanticValidator) { this.hostEditPart = editPart; this.xtextInjector = xtextInjector ; this.textToEdit = "" + textToEdit ; this.modelReconciler = modelReconciler ; PopupXtextEditorHelper.fileExtension = "" + fileExtension ; this.semanticValidator = semanticValidator ; ignoreFocusLost = false ; } /** * This element was originally not documented in the XText / GMF integration example. * */ public void showEditor() { try { semanticElement = hostEditPart.resolveSemanticElement(); if (semanticElement == null) { return; } context = semanticElement ; Resource semanticResource = semanticElement.eResource(); semanticElementFragment = semanticResource.getURIFragment(semanticElement); if (semanticElementFragment == null || "".equals(semanticElementFragment)) { return; } IDiagramEditDomain diagramEditDomain = hostEditPart.getDiagramEditDomain(); diagramEditor = ((DiagramEditDomain) diagramEditDomain).getEditorPart(); createXtextEditor(null) ; } catch (Exception e) { Activator.logError(e); } } /** * This element was originally not documented in the XText/GMF integration example. * * Changes performed by CEA LIST: * - new statements for managing the reconciliation between the original UML model and * the textual specification resulting from the edition in the popup xtext editor. * @param isReconcile Determines whether a reconciliation must be performed or not */ public void closeEditor(boolean isReconcile) { if (sourceViewerHandle != null) { if (isReconcile && this.semanticValidator.validate()) { try { final IXtextDocument xtextDocument = sourceViewerHandle.getDocument(); if (!isDocumentHasErrors(xtextDocument)) { int documentGrowth = xtextDocument.getLength() - initialDocumentSize ; String newText = xtextDocument.get(editorOffset , initialEditorSize + documentGrowth) ; xtextResource = partialEditor.createResource(newText) ; if (xtextResource.getAllContents().hasNext()) modelReconciler.reconcile(semanticElement, xtextResource.getAllContents().next()) ; } } catch (Exception exc) { Activator.logError(exc); } } xtextEditorComposite.setVisible(false); xtextEditorComposite.dispose() ; } SourceViewerHandle.bindPartialModelEditorClass(null) ; } /** * This element was originally not documented in the XText/GMF integration example * * Changes performed by CEA LIST: * - adds a focus listener for managing the context eobject and current xtext editor. * * @param editorInput */ private void createXtextEditor(IEditorInput editorInput) throws Exception { diagramShell = diagramEditor.getSite().getShell(); xtextEditorComposite = new Shell(SWT.RESIZE) ; xtextEditorComposite.setLayout(new FillLayout()); resourceProvider = xtextInjector.getInstance(ISyntheticResourceProvider.class) ; SourceViewerHandleFactory factory = xtextInjector.getInstance(SourceViewerHandleFactory.class) ; sourceViewerHandle = factory.create(xtextEditorComposite, resourceProvider) ; partialEditor = sourceViewerHandle.createPartialEditor("", textToEdit, "", semanticElement, modelReconciler) ; registerKeyListener(); setEditorBounds(); initializeActions(); installUndoRedoSupport(sourceViewerHandle.getViewer()); sourceViewerHandle.getViewer().getTextWidget().addFocusListener(new FocusListener() { public void focusLost(FocusEvent e) { checkedClose(); } public void focusGained(FocusEvent e) { context = semanticElement ; } }) ; xtextEditorComposite.setVisible(true); sourceViewerHandle.getViewer().showAnnotationsOverview(true) ; sourceViewerHandle.getViewer().getTextWidget().setFocus() ; // This last statement is used to trigger initial validation sourceViewerHandle.getViewer().getTextWidget().append("") ; } /** * Perform additional checks before close * (added by CEA LIST) */ private void checkedClose() { context = semanticElement ; if (! keyListener.isContentAssistActive()) { // additional sanity check: on X11 systems, the focus is already lost during resize. // An unwanted closing can be prevented by verifying if the activeShell still points // to the xtextEditorComposite if (xtextEditorComposite.getDisplay().getActiveShell() != xtextEditorComposite) { if (!ignoreFocusLost) { closeEditor(true) ; } else closeEditor(false) ; } } } private PopupXtextEditorKeyListener keyListener ; private void registerKeyListener() { //XtextSourceViewer sourceViewer = (XtextSourceViewer) xtextEditor.getInternalSourceViewer(); final StyledText xtextTextWidget = sourceViewerHandle.getViewer().getTextWidget(); CustomXtextSourceViewer viewer = (CustomXtextSourceViewer)sourceViewerHandle.getViewer() ; keyListener = new PopupXtextEditorKeyListener (this, (ContentAssistant) viewer.getContentAssistant()); //keyListener.installUndoRedoSupport(sourceViewerHandle.getViewer()) ; xtextTextWidget.addVerifyKeyListener(keyListener); xtextTextWidget.addKeyListener(keyListener); } /** * This element was originally not documented in the XText/GMF integration example * * Modifications from CEA LIST: * - modifications to rules for computing the initial location and size of the popup editor. * This still needs some work... */ private void setEditorBounds() { String editString = "" + textToEdit ; int[] numLinesNumColums = StringUtil.getNumLinesNumColumns(editString) ; int numLines = numLinesNumColums[0] ; int numColumns = numLinesNumColums[1]; // ninimal sizes if (numLines < 5) { numLines = 5; } if (numColumns < 60) { numColumns = 60; } IFigure figure = hostEditPart.getFigure() ; Rectangle bounds = figure.getBounds().getCopy(); figure.translateToAbsolute(bounds) ; Point newCoord = diagramShell.getDisplay().map(hostEditPart.getViewer().getControl(), null, new Point(bounds.x, bounds.y)) ; bounds.x = newCoord.x ; bounds.y = newCoord.y ; // not used, delivers wrong results // FontData fontData = figure.getFont().getFontData()[0]; // int fontHeightInPixel = fontData.getHeight(); GC gc = new GC (xtextEditorComposite); FontMetrics fm = gc.getFontMetrics (); int width = numColumns * fm.getAverageCharWidth () + 40; int height = numLines * fm.getHeight(); gc.dispose (); // xtextEditorComposite contains a composite which in turn contains the text widget and an area for markers. // Take difference between client area and size into account. Cannot set size of text widget directly, // since suitable packing is not supported by the layout of the text widget's parent. org.eclipse.swt.graphics.Rectangle clientArea = xtextEditorComposite.getClientArea(); // only correct height, since width is estimated anyway. height += xtextEditorComposite.getSize().y - clientArea.height; xtextEditorComposite.setBounds(bounds.x, bounds.y, width, height); } private boolean isDocumentHasErrors(final IXtextDocument xtextDocument) { return (xtextDocument.readOnly(new IUnitOfWork<Boolean, XtextResource>() { public Boolean exec(XtextResource state) throws Exception { IParseResult parseResult = state.getParseResult(); return !state.getErrors().isEmpty() || parseResult == null || parseResult.hasSyntaxErrors(); } })); } protected Status createErrorStatus(String message, TemplateException e) { return new Status(IStatus.ERROR, "org.eclipse.papyrus.property.editor.xtext",message, e); } protected void installUndoRedoSupport(SourceViewer viewer) { IDocumentUndoManager undoManager = DocumentUndoManagerRegistry.getDocumentUndoManager(viewer.getDocument()); final IUndoContext context = undoManager.getUndoContext(); IOperationHistory operationHistory = CheckedOperationHistory.getInstance(); operationHistoryListener = new OperationHistoryListener(context, new IUpdate() { public void update() { updateAction(ITextEditorActionConstants.REDO); updateAction(ITextEditorActionConstants.UNDO); } }); operationHistory.addOperationHistoryListener(operationHistoryListener); } private Map<String, org.eclipse.ui.console.actions.TextViewerAction> fGlobalActions= Maps.newHashMapWithExpectedSize(10); private List<String> fSelectionActions = Lists.newArrayListWithExpectedSize(3); protected void updateAction(String actionId) { IAction action= fGlobalActions.get(actionId); if (action instanceof IUpdate) ((IUpdate) action).update(); } protected void uninstallUndoRedoSupport() { IOperationHistory operationHistory = PlatformUI.getWorkbench().getOperationSupport().getOperationHistory(); operationHistory.removeOperationHistoryListener(operationHistoryListener); operationHistoryListener = null; } private void initializeActions() { final List<IHandlerActivation> handlerActivations= Lists.newArrayListWithExpectedSize(3); final IHandlerService handlerService= (IHandlerService) PlatformUI.getWorkbench().getAdapter(IHandlerService.class); final Expression expression= new ActiveShellExpression(sourceViewerHandle.getViewer().getControl().getShell()); diagramShell.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { handlerService.deactivateHandlers(handlerActivations); } }); TextViewerAction action= new TextViewerAction(sourceViewerHandle.getViewer(), ITextOperationTarget.UNDO); action.setText("UNDO"); fGlobalActions.put(ITextEditorActionConstants.UNDO, action); action= new TextViewerAction(sourceViewerHandle.getViewer(), ITextOperationTarget.REDO); action.setText("REDO"); fGlobalActions.put(ITextEditorActionConstants.REDO, action); action= new TextViewerAction(sourceViewerHandle.getViewer(), ITextOperationTarget.CUT); action.setText("CUT"); fGlobalActions.put(ITextEditorActionConstants.CUT, action); action= new TextViewerAction(sourceViewerHandle.getViewer(), ITextOperationTarget.COPY); action.setText("COPY"); fGlobalActions.put(ITextEditorActionConstants.COPY, action); action= new TextViewerAction(sourceViewerHandle.getViewer(), ITextOperationTarget.PASTE); action.setText("PASTE"); fGlobalActions.put(ITextEditorActionConstants.PASTE, action); action= new TextViewerAction(sourceViewerHandle.getViewer(), ITextOperationTarget.SELECT_ALL); action.setText("SELECT_ALL"); fGlobalActions.put(ITextEditorActionConstants.SELECT_ALL, action); action= new TextViewerAction(sourceViewerHandle.getViewer(), ISourceViewer.CONTENTASSIST_PROPOSALS); action.setText("CONTENTASSIST_PROPOSALS"); fGlobalActions.put(ITextEditorActionConstants.CONTENT_ASSIST, action); fSelectionActions.add(ITextEditorActionConstants.CUT); fSelectionActions.add(ITextEditorActionConstants.COPY); fSelectionActions.add(ITextEditorActionConstants.PASTE); sourceViewerHandle.getViewer().getTextWidget().addFocusListener(new FocusListener() { public void focusLost(FocusEvent e) { handlerService.deactivateHandlers(handlerActivations); } public void focusGained(FocusEvent e) { IAction action= fGlobalActions.get(ITextEditorActionConstants.REDO); handlerActivations.add(handlerService.activateHandler(IWorkbenchCommandConstants.EDIT_REDO, new ActionHandler(action), expression)); action= fGlobalActions.get(ITextEditorActionConstants.UNDO); handlerActivations.add(handlerService.activateHandler(IWorkbenchCommandConstants.EDIT_UNDO, new ActionHandler(action), expression)); action= fGlobalActions.get(ITextEditorActionConstants.CONTENT_ASSIST); handlerActivations.add(handlerService.activateHandler(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS, new ActionHandler(action), expression)); } }); } }