/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * 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 * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.modeling.actions; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.jface.window.Window; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.simantics.Simantics; import org.simantics.databoard.Bindings; import org.simantics.databoard.util.URIStringUtils; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.common.NamedResource; import org.simantics.db.common.procedure.adapter.ProcedureAdapter; import org.simantics.db.common.request.ReadRequest; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.common.utils.OrderedSetUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.variable.RVI; import org.simantics.db.layer0.variable.Variable; import org.simantics.db.layer0.variable.Variables; import org.simantics.diagram.content.ConnectionUtil; import org.simantics.diagram.flag.FlagUtil; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.DataElementMap; import org.simantics.g2d.diagram.participant.Selection; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.elementclass.FlagClass; import org.simantics.g2d.participant.CanvasBoundsParticipant; import org.simantics.g2d.participant.TransformUtil; import org.simantics.g2d.utils.GeometryUtils; import org.simantics.layer0.Layer0; import org.simantics.layer0.utils.operations.Operation; import org.simantics.modeling.ModelingOperationConstants; import org.simantics.modeling.ModelingResources; import org.simantics.modeling.utils.Monitors; import org.simantics.structural.stubs.StructuralResource2; import org.simantics.ui.workbench.ResourceEditorInput; import org.simantics.ui.workbench.ResourceEditorInput2; import org.simantics.utils.datastructures.persistent.IContextMap; import org.simantics.utils.page.MarginUtils; import org.simantics.utils.strings.AlphanumComparator; import org.simantics.utils.threads.ThreadUtils; import org.simantics.utils.ui.ErrorLogger; import org.simantics.utils.ui.workbench.WorkbenchUtils; public class NavigateToTarget extends Operation { public NavigateToTarget() { super("Navigate to Target"); } @Override public void exec(Session session, IContextMap parameters) { final Resource r = (Resource) parameters.get(SUBJECT); final IWorkbenchWindow window = (IWorkbenchWindow) parameters.get(ModelingOperationConstants.WORKBENCH_WINDOW); final IWorkbenchPart part = (IWorkbenchPart) parameters.get(ModelingOperationConstants.WORKBENCH_PART); session.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph g) throws DatabaseException { final Resource thisDiagram = getOwnerList(g, r); if (thisDiagram == null) return; Set counterparts = FlagUtil.getCounterparts(g, r); if (counterparts.size() > 1) { // Ask user which target to navigate to. asyncQueryTarget(window.getShell(), g, thisDiagram, counterparts, target -> navigateToTarget( window, part, thisDiagram, target)); } else if (counterparts.size() == 1) { // Target is defined. final String error = navigateToTarget( g, window, part, thisDiagram, counterparts.iterator().next() ); if (error != null && !error.isEmpty()) { window.getShell().getDisplay().asyncExec(new Runnable() { @Override public void run() { IStatusLineManager status = WorkbenchUtils.getStatusLine(part); status.setErrorMessage(error); } }); } } else { // Try other methods of getting a navigation target besides flags. Resource target = Monitors.getMonitoredElement(g, r); if (target != null) navigateToTarget( g, window, part, thisDiagram, target ); } } }, new ProcedureAdapter() { @Override public void exception(Throwable t) { ErrorLogger.defaultLogError(t); } }); } protected void navigateToTarget(final IWorkbenchWindow window, final IWorkbenchPart sourceEditor, final Resource sourceDiagram, final Resource target) { Simantics.getSession().asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { navigateToTarget(graph, window, sourceEditor, sourceDiagram, target); } }, new ProcedureAdapter() { public void exception(Throwable t) { ErrorLogger.defaultLogError(t); } }); } /** * @param graph * @param window * @param sourceEditor * @param sourceDiagram * @param target * @return null if everything's OK * @throws DatabaseException */ protected String navigateToTarget(ReadGraph graph, IWorkbenchWindow window, IWorkbenchPart sourceEditor, Resource sourceDiagram, final Resource target) throws DatabaseException { ModelingResources MOD = ModelingResources.getInstance(graph); final Resource otherDiagram = getOwnerList(graph, target); if (otherDiagram == null) return ""; if (!sourceDiagram.equals(otherDiagram)) { // Find the structural path Resource otherComposite = graph.getPossibleObject(otherDiagram, MOD.DiagramToComposite); if (otherComposite == null) return ""; Variable compositeVariable = Variables.getVariable(graph, otherComposite); final Resource model = Variables.getPossibleModel(graph, compositeVariable); final RVI rvi = model == null ? null : compositeVariable.getPossibleRVI(graph); if (model == null || rvi == null) return "Navigating via flags only possible under model configuration"; window.getShell().getDisplay().asyncExec( editorActivator(sourceEditor, otherDiagram, model, rvi, part -> { final ICanvasContext openedCanvas = (ICanvasContext) part.getAdapter(ICanvasContext.class); assert openedCanvas != null; // CanvasContext-wide denial of initial zoom-to-fit on diagram open. openedCanvas.getDefaultHintContext().setHint(DiagramHints.KEY_INITIAL_ZOOM_TO_FIT, Boolean.FALSE); ThreadUtils.asyncExec( openedCanvas.getThreadAccess(), elementSelectorZoomer( openedCanvas, Collections.singleton( target ), false ) ); })); } else { ICanvasContext canvas = (ICanvasContext) sourceEditor.getAdapter( ICanvasContext.class ); assert canvas != null; ThreadUtils.asyncExec( canvas.getThreadAccess(), elementSelectorZoomer( canvas, Collections.singleton( target ), true )); } return null; } protected void asyncQueryTarget(final Shell parentShell, ReadGraph graph, Resource sourceDiagram, Set counterparts, final Consumer navigationCallback) throws DatabaseException { ModelingResources MOD = ModelingResources.getInstance(graph); StructuralResource2 STR = StructuralResource2.getInstance(graph); ConnectionUtil cu = new ConnectionUtil(graph); final List outputs = new ArrayList(counterparts.size()); final List inputs = new ArrayList(counterparts.size()); for (Resource counterpart : counterparts) { final Resource diagram = getOwnerList(graph, counterpart); if (diagram == null) continue; Resource composite = graph.getPossibleObject(diagram, MOD.DiagramToComposite); if (composite == null) continue; Variable v = Variables.getPossibleVariable(graph, composite); if (v == null) continue; String rvi = Variables.getRVI(graph, v); rvi = URIStringUtils.unescape(rvi); Resource connectedComponent = null; for (Resource connector : graph.getObjects(counterpart, STR.IsConnectedTo)) { Resource diagramConnection = ConnectionUtil.tryGetConnection(graph, connector); if (diagramConnection != null) { Collection connectors = cu.getConnectors(diagramConnection, new HashSet()); connectors.remove(connector); if (connectors.isEmpty()) { continue; } else { connectedComponent = graph.getPossibleObject(diagramConnection, MOD.ElementToComponent); if (connectedComponent == null) { for (Resource conn : connectors) { Resource element = cu.getConnectedComponent(diagramConnection, conn); if (element == null) continue; connectedComponent = graph.getPossibleObject(element, MOD.ElementToComponent); if (connectedComponent != null) break; } } } } } if (connectedComponent != null) { rvi += Variables.Role.CHILD.getIdentifier(); rvi += NameUtils.getSafeName(graph, connectedComponent); } boolean localFlag = sourceDiagram.equals(diagram); if (localFlag) { Layer0 L0 = Layer0.getInstance(graph); rvi += " (local"; String label = graph.getPossibleRelatedValue2(counterpart, L0.HasLabel, Bindings.STRING); if (label != null && !label.isEmpty()) rvi += " " + label; rvi += ")"; } FlagClass.Type type = FlagUtil.getFlagType(graph, counterpart, FlagClass.Type.In); switch (type) { case In: inputs.add( new NamedResource(rvi, counterpart) ); break; case Out: outputs.add( new NamedResource(rvi, counterpart) ); break; } } // To make result ordering stable and logical Collections.sort(outputs, COMPARATOR); Collections.sort(inputs, COMPARATOR); outputs.addAll(inputs); if (outputs.isEmpty()) return; parentShell.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (parentShell.isDisposed()) return; NavigationTargetChooserDialog dialog = new NavigationTargetChooserDialog( parentShell, outputs.toArray(new NamedResource[0]), "Choose Navigation Target", "Select single navigation target from list"); if (dialog.open() != Window.OK) return; if (dialog.getSelection() != null) navigationCallback.accept( dialog.getSelection().getResource() ); } }); } private static final Comparator COMPARATOR = new Comparator() { @Override public int compare(NamedResource o1, NamedResource o2) { return AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(o1.getName(), o2.getName()); } }; public static Runnable editorActivator(final IWorkbenchPart part, final Resource diagram, final Resource model, final RVI rvi, final Consumer successCallback) { String sourcePartId = part.getSite().getId(); return editorActivator(sourcePartId, diagram, model, rvi, successCallback); } public static Runnable editorActivator(final String editorPartId, final Resource diagram, final Resource model, final RVI rvi, final Consumer successCallback) { return () -> { try { // open and activate new editor IEditorPart newPart = WorkbenchUtils.openEditor(editorPartId, createEditorInput(editorPartId, diagram, model, rvi)); newPart.getSite().getPage().activate(newPart); successCallback.accept(newPart); } catch (PartInitException e) { ErrorLogger.defaultLogError(e); } }; } private static IEditorInput createEditorInput(String editorPartId, Resource diagram, Resource model, RVI rvi) { if (model != null) return new ResourceEditorInput2(editorPartId, diagram, model, rvi); else return new ResourceEditorInput(editorPartId, diagram); } /** * @param editorPartId * @param diagram * @param model * @param rvi * @param successCallback * @return * @deprecated use {@link #editorActivator(String, Resource, Resource, RVI, Callback)} instead */ @Deprecated public static Runnable editorActivator(final String editorPartId, final Resource diagram, final Resource model, final String rvi, final Consumer successCallback) { return () -> { try { // open and activate new editor IEditorPart newPart = WorkbenchUtils.openEditor(editorPartId, new ResourceEditorInput2(editorPartId, diagram, model, rvi)); newPart.getSite().getPage().activate(newPart); successCallback.accept(newPart); } catch (PartInitException e) { ErrorLogger.defaultLogError(e); } }; } public static Runnable elementSelectorZoomer(final ICanvasContext canvas, final Collection elementObjects, final boolean keepZoom) { return new Runnable() { int tries = 0; @Override public void run() { //System.out.println(tries + ": elementSelectorZoomer: " + canvas.isDisposed() + ", " + elementObjects); if (canvas.isDisposed()) return; // This will prevent eternal looping in unexpected situations. if (++tries > 10) { ErrorLogger.defaultLog(new Status(IStatus.INFO, "", "NavigateToTarget.elementSelectorZoomer failed to find any of the requested elements " + elementObjects + ". Giving up.")); return; } IDiagram diagram = canvas.getHintStack().getHint(DiagramHints.KEY_DIAGRAM); if (diagram == null || !zoomToSelection(canvas, diagram, selectElement(canvas, diagram, elementObjects), keepZoom)) { // Reschedule for later in hopes that initialization is complete. ThreadUtils.getNonBlockingWorkExecutor().schedule(this, 200, TimeUnit.MILLISECONDS); } } }; } public static Set selectElement(final ICanvasContext canvas, final IDiagram diagram, final Collection elementObjects) { // Select element Set selection = new HashSet(elementObjects.size()); DataElementMap dataMap = diagram.getDiagramClass().getSingleItem(DataElementMap.class); for (Object obj : elementObjects) { IElement element = dataMap.getElement(diagram, obj); if (element != null) { selection.add(element); } } if (!selection.isEmpty()) { for (Selection s : canvas.getItemsByClass(Selection.class)) { s.setSelection(0, selection); } } return selection; } public static boolean zoomToSelection(final ICanvasContext canvas, final IDiagram diagram, Set selection, final boolean keepZoom) { final TransformUtil util = canvas.getSingleItem(TransformUtil.class); CanvasBoundsParticipant boundsParticipant = canvas.getAtMostOneItemOfClass(CanvasBoundsParticipant.class); if (boundsParticipant == null) return false; final Rectangle2D controlBounds = boundsParticipant.getControlBounds().getFrame(); if (controlBounds == null || controlBounds.isEmpty()) return false; final Shape shp = ElementUtils.getElementBoundsOnDiagram(selection); if (shp == null) return false; ThreadUtils.asyncExec(canvas.getThreadAccess(), new Runnable() { @Override public void run() { if (canvas.isDisposed()) return; Rectangle2D diagramRect = shp.getBounds2D(); // Make sure that even empty bounds can be zoomed into. org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1); if (keepZoom) { double scaleFactor = GeometryUtils.getScale(util.getTransform()); double cwh = controlBounds.getWidth() / (scaleFactor*2); double chh = controlBounds.getHeight() / (scaleFactor*2); AffineTransform view = new AffineTransform(); view.scale(scaleFactor, scaleFactor); view.translate(-diagramRect.getCenterX()+cwh, -diagramRect.getCenterY()+chh); util.setTransform(view); } else { MarginUtils.Margin margin = MarginUtils.marginOf(40, 0, 0); MarginUtils.Margins margins = new MarginUtils.Margins(margin, margin, margin, margin); util.fitArea(controlBounds, diagramRect, margins); } } }); return true; } public static Resource getOwnerList(ReadGraph g, Resource listElement) throws DatabaseException { return OrderedSetUtils.getSingleOwnerList(g, listElement, DiagramResource.getInstance(g).Composite); } }