--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
+ * in Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ * VTT Technical Research Centre of Finland - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.g2d.diagram.participant.pointertool;\r
+\r
+import java.awt.Shape;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.Path2D;\r
+import java.awt.geom.Point2D;\r
+import java.awt.geom.Rectangle2D;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.HashSet;\r
+import java.util.List;\r
+import java.util.Set;\r
+\r
+import org.simantics.g2d.canvas.Hints;\r
+import org.simantics.g2d.canvas.ICanvasContext;\r
+import org.simantics.g2d.canvas.ICanvasParticipant;\r
+import org.simantics.g2d.canvas.IToolMode;\r
+import org.simantics.g2d.canvas.impl.CanvasContext;\r
+import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;\r
+import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;\r
+import org.simantics.g2d.connection.IConnectionAdvisor;\r
+import org.simantics.g2d.connection.handler.ConnectionHandler;\r
+import org.simantics.g2d.diagram.DiagramHints;\r
+import org.simantics.g2d.diagram.handler.PickContext;\r
+import org.simantics.g2d.diagram.handler.PickRequest;\r
+import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy;\r
+import org.simantics.g2d.diagram.handler.PickRequest.PickSorter;\r
+import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;\r
+import org.simantics.g2d.diagram.participant.Selection;\r
+import org.simantics.g2d.diagram.participant.TerminalPainter;\r
+import org.simantics.g2d.diagram.participant.TerminalPainter.ChainedHoverStrategy;\r
+import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;\r
+import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;\r
+import org.simantics.g2d.element.ElementClassProviders;\r
+import org.simantics.g2d.element.IElement;\r
+import org.simantics.g2d.element.IElementClassProvider;\r
+import org.simantics.g2d.participant.KeyUtil;\r
+import org.simantics.g2d.participant.MouseUtil;\r
+import org.simantics.g2d.participant.TransformUtil;\r
+import org.simantics.g2d.scenegraph.SceneGraphConstants;\r
+import org.simantics.g2d.utils.CanvasUtils;\r
+import org.simantics.g2d.utils.GeometryUtils;\r
+import org.simantics.scenegraph.g2d.events.Event;\r
+import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;\r
+import org.simantics.scenegraph.g2d.events.KeyEvent;\r
+import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;\r
+import org.simantics.scenegraph.g2d.events.command.Commands;\r
+import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;\r
+import org.simantics.utils.ObjectUtils;\r
+import org.simantics.utils.datastructures.context.IContext;\r
+import org.simantics.utils.datastructures.context.IContextListener;\r
+import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
+import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;\r
+import org.simantics.utils.threads.ThreadUtils;\r
+\r
+/**\r
+ * Pointer tool does the following operations with mouse:\r
+ * <ul>\r
+ * <li>Selections</li>\r
+ * <li>Scale</li>\r
+ * <li>Rotate</li>\r
+ * <li>Translate</li>\r
+ * <li>Draws connections (requires re-implementing\r
+ * {@link #createConnectTool(TerminalInfo, int, Point2D)})</li>\r
+ * </ul>\r
+ * \r
+ * Pointer tool is active only when {@link Hints#KEY_TOOL} is\r
+ * {@value Hints#POINTERTOOL}.\r
+ * \r
+ * @author Toni Kalajainen\r
+ */\r
+public class PointerInteractor extends AbstractDiagramParticipant {\r
+\r
+ /**\r
+ * A hint key for terminal pick distance in control pixels.\r
+ * @see #PICK_DIST\r
+ */\r
+ public static final Key KEY_PICK_DISTANCE = new KeyOf(Double.class, "PICK_DISTANCE");\r
+\r
+ /**\r
+ * Default terminal pick distance in control pixels.\r
+ * @see #DEFAULT_PICK_DISTANCE\r
+ */\r
+ public static final double PICK_DIST = 10;\r
+\r
+ /**\r
+ * @see #altToggled(KeyEvent)\r
+ */\r
+ protected int lastStateMask;\r
+\r
+ /**\r
+ * @see #altToggled(KeyEvent)\r
+ */\r
+ protected boolean temporarilyEnabledConnectTool = false;\r
+\r
+ public class DefaultHoverStrategy extends ChainedHoverStrategy {\r
+ public DefaultHoverStrategy(TerminalHoverStrategy orig) {\r
+ super(orig);\r
+ }\r
+ @Override\r
+ public boolean highlightEnabled() {\r
+ if (Hints.CONNECTTOOL.equals(getToolMode()))\r
+ return true;\r
+\r
+ boolean ct = connectToolModifiersPressed(lastStateMask);\r
+ //System.out.println("highlightEnabled: " + String.format("%x", lastStateMask) + " : " + ct);\r
+ return ct;\r
+ }\r
+ @Override\r
+ public boolean canHighlight(TerminalInfo ti) {\r
+ //boolean alt = (lastStateMask & MouseEvent.ALT_MASK) != 0;\r
+ //System.out.println("canHighlight: " + String.format("%x", lastStateMask) + " : " + alt);\r
+ //if (!alt)\r
+ // return false;\r
+ IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);\r
+ return advisor == null || advisor.canBeginConnection(null, ti.e, ti.t);\r
+ }\r
+ }\r
+\r
+ @Dependency Selection selection;\r
+ @Dependency KeyUtil keys;\r
+ @Dependency TransformUtil util;\r
+ @Dependency PickContext pickContext;\r
+ @Dependency MouseUtil mice;\r
+ @Reference TerminalPainter terminalPainter;\r
+\r
+ /**\r
+ * This must be higher than\r
+ * {@link SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY}\r
+ * ({@value SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY}) to allow for\r
+ * better selection handling than is possible by using the plain scene graph\r
+ * event handling facilities which are installed in {@link CanvasContext}.\r
+ */\r
+ public static final int TOOL_PRIORITY = 1 << 21;\r
+\r
+ /**\r
+ * This must be lower than\r
+ * {@link SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY} (\r
+ * {@value SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY}) to not start box\r
+ * handling before scene graph nodes have been given a chance to react\r
+ * events.\r
+ */\r
+ public static final int BOX_SELECT_PRIORITY = 1 << 19;\r
+\r
+ private static final Path2D LINE10;\r
+ private static final Path2D LINE15;\r
+ private static final Path2D LINE20;\r
+\r
+ boolean clickSelect;\r
+ boolean boxSelect;\r
+ boolean dragElement, dndDragElement;\r
+ boolean connect;\r
+ boolean doubleClickEdit;\r
+ protected IElementClassProvider elementClassProvider;\r
+\r
+ PickPolicy boxSelectMode = PickPolicy.PICK_CONTAINED_OBJECTS;\r
+\r
+ DefaultHoverStrategy hoverStrategy;\r
+\r
+ private PickSorter pickSorter;\r
+ \r
+ public PointerInteractor() {\r
+ this(true, true, true, false, true, false, ElementClassProviders.staticProvider(null), null);\r
+ }\r
+\r
+ public PointerInteractor(PickSorter pickSorter) {\r
+ this(true, true, true, false, true, false, ElementClassProviders.staticProvider(null), pickSorter);\r
+ }\r
+\r
+ public PointerInteractor(boolean clickSelect, boolean boxSelect, boolean dragElement, boolean dndDragElement, boolean connect, IElementClassProvider ecp) {\r
+ this(clickSelect, boxSelect, dragElement, dndDragElement, connect, false, ecp, null);\r
+ }\r
+\r
+ public PointerInteractor(boolean clickSelect, boolean boxSelect, boolean dragElement, boolean dndDragElement, boolean connect, boolean doubleClickEdit, IElementClassProvider ecp, PickSorter pickSorter) {\r
+ super();\r
+ this.clickSelect = clickSelect;\r
+ this.boxSelect = boxSelect;\r
+ this.dragElement = dragElement;\r
+ this.dndDragElement = dndDragElement;\r
+ this.connect = connect;\r
+ this.doubleClickEdit = doubleClickEdit;\r
+ this.elementClassProvider = ecp;\r
+ this.pickSorter = pickSorter;\r
+ }\r
+\r
+ @Override\r
+ public void addedToContext(ICanvasContext ctx) {\r
+ super.addedToContext(ctx);\r
+ hoverStrategy = new DefaultHoverStrategy((TerminalHoverStrategy) getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY));\r
+ setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, hoverStrategy);\r
+ }\r
+\r
+ @EventHandler(priority = 0)\r
+ public boolean handleStateMask(MouseEvent me) {\r
+ lastStateMask = me.stateMask;\r
+ if (temporarilyEnabledConnectTool) {\r
+ if (!connectToolModifiersPressed(me.stateMask)) {\r
+ temporarilyEnabledConnectTool = false;\r
+ setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);\r
+ }\r
+ } else {\r
+ // It may be that the mouse has come into this control\r
+ // from outside and no key state changes have occurred yet.\r
+ // In this case this code should take care of moving the canvas\r
+ // context into CONNECTTOOL mode.\r
+ if (getToolMode() == Hints.POINTERTOOL\r
+ && connectToolModifiersPressed(me.stateMask))\r
+ {\r
+ temporarilyEnabledConnectTool = true;\r
+ setHint(Hints.KEY_TOOL, Hints.CONNECTTOOL);\r
+ }\r
+ }\r
+ return false;\r
+ }\r
+\r
+ private static int mask(int mask, int mask2, boolean set) {\r
+ return set ? mask | mask2 : mask & ~mask2;\r
+ }\r
+\r
+ @EventHandler(priority = -1)\r
+ public boolean altToggled(KeyEvent ke) {\r
+ int mods = ke.stateMask;\r
+ boolean press = ke instanceof KeyPressedEvent;\r
+ boolean modifierPressed = false;\r
+ if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {\r
+ mods = mask(mods, MouseEvent.ALT_MASK, press);\r
+ modifierPressed = true;\r
+ }\r
+ if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT_GRAPH) {\r
+ mods = mask(mods, MouseEvent.ALT_GRAPH_MASK, press);\r
+ modifierPressed = true;\r
+ }\r
+ if (ke.keyCode == java.awt.event.KeyEvent.VK_SHIFT) {\r
+ mods = mask(mods, MouseEvent.SHIFT_MASK, press);\r
+ modifierPressed = true;\r
+ }\r
+ if (ke.keyCode == java.awt.event.KeyEvent.VK_CONTROL) {\r
+ mods = mask(mods, MouseEvent.CTRL_MASK, press);\r
+ modifierPressed = true;\r
+ }\r
+ if (ke.keyCode == java.awt.event.KeyEvent.VK_META) {\r
+ // TODO: NO MASK FOR META!\r
+ modifierPressed = true;\r
+ }\r
+ // Don't deny connecting when CTRL is marked pressed because ALT_GRAPH\r
+ // is actually ALT+CTRL in SWT. There's no way in SWT to tell apart\r
+ // CTRL+ALT and ALT GRAPH.\r
+ boolean otherModifiers = (mods & (MouseEvent.SHIFT_MASK)) != 0;\r
+ boolean altModifier = (mods & (MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) != 0;\r
+ if (modifierPressed) {\r
+ boolean altPressed = !otherModifiers && altModifier;\r
+ lastStateMask = mods;\r
+ if (altPressed) {\r
+ IToolMode mode = getToolMode();\r
+ if (mode == Hints.POINTERTOOL) {\r
+ //System.out.println("TEMP++");\r
+ temporarilyEnabledConnectTool = true;\r
+ setHint(Hints.KEY_TOOL, Hints.CONNECTTOOL);\r
+ }\r
+ } else {\r
+ if (temporarilyEnabledConnectTool) {\r
+ //System.out.println("TEMP--");\r
+ temporarilyEnabledConnectTool = false;\r
+ setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);\r
+ }\r
+ }\r
+ // Make sure that TerminalPainter updates its scene graph.\r
+ if (terminalPainter != null) {\r
+ terminalPainter.update(terminalPainter.highlightEnabled());\r
+ }\r
+ }\r
+ return false;\r
+ }\r
+\r
+ /**\r
+ * @param controlPos\r
+ * @return <code>null</code> if current canvas transform is not invertible\r
+ */\r
+ public Shape getCanvasPickShape(Point2D controlPos) {\r
+ AffineTransform inverse = util.getInverseTransform();\r
+ if (inverse == null)\r
+ return null;\r
+\r
+ double pd = getPickDistance();\r
+ Rectangle2D controlPickRect = new Rectangle2D.Double(controlPos.getX()-pd, controlPos.getY()-pd, pd*2+1, pd*2+1);\r
+ Shape canvasShape = GeometryUtils.transformShape(controlPickRect, inverse);\r
+ return canvasShape;\r
+ }\r
+\r
+ public List<TerminalInfo> pickTerminals(Point2D controlPos)\r
+ {\r
+ Shape canvasPickRect = getCanvasPickShape(controlPos);\r
+ if (canvasPickRect == null)\r
+ return Collections.emptyList();\r
+ return TerminalUtil.pickTerminals(diagram, canvasPickRect, true, true);\r
+ }\r
+\r
+ public TerminalInfo pickTerminal(Point2D controlPos)\r
+ {\r
+ Shape canvasPickRect = getCanvasPickShape(controlPos);\r
+ if (canvasPickRect == null)\r
+ return null;\r
+ TerminalInfo ti = TerminalUtil.pickTerminal(diagram, canvasPickRect);\r
+ return ti;\r
+ }\r
+\r
+ @EventHandler(priority = TOOL_PRIORITY)\r
+ public boolean handlePress(MouseButtonPressedEvent me) {\r
+ if (!connects())\r
+ return false;\r
+ if (me.button != MouseEvent.LEFT_BUTTON)\r
+ return false;\r
+\r
+ IToolMode mode = getToolMode();\r
+\r
+ // It may be that the mouse has come into this control\r
+ // from outside without focusing it and without any mouse\r
+ // buttons being pressed. If the user is pressing the\r
+ // connection modifier we need to temporarily enable connect\r
+ // mode here and now.\r
+ if (mode == Hints.POINTERTOOL && connectToolModifiersPressed(me.stateMask)) {\r
+ temporarilyEnabledConnectTool = true;\r
+ mode = Hints.CONNECTTOOL;\r
+ setHint(Hints.KEY_TOOL, Hints.CONNECTTOOL);\r
+ }\r
+\r
+ if (mode == Hints.CONNECTTOOL) {\r
+ Point2D curCanvasPos = util.controlToCanvas(me.controlPosition, null);\r
+ return checkInitiateConnectTool(me, curCanvasPos);\r
+ }\r
+\r
+ return false;\r
+ }\r
+\r
+ protected boolean checkInitiateConnectTool(MouseEvent me, Point2D mouseCanvasPos) {\r
+ // Pick Terminal\r
+ IToolMode mode = getToolMode();\r
+ if (mode == Hints.CONNECTTOOL || connectToolModifiersPressed(me.stateMask)) {\r
+ IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);\r
+ TerminalInfo ti = pickTerminal(me.controlPosition);\r
+\r
+ ICanvasParticipant bsi = null;\r
+ if (ti != null) {\r
+ if (advisor == null || advisor.canBeginConnection(null, ti.e, ti.t)) {\r
+ bsi = createConnectTool(ti, me.mouseId, mouseCanvasPos);\r
+ }\r
+ } else {\r
+ ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);\r
+ if (snapAdvisor != null)\r
+ snapAdvisor.snap(mouseCanvasPos);\r
+\r
+ // Start connection out of thin air, without a terminal.\r
+ bsi = createConnectTool(null, me.mouseId, mouseCanvasPos);\r
+ }\r
+\r
+ // Did we catch anything?\r
+ if (bsi != null) {\r
+ startConnectTool(bsi);\r
+ return true;\r
+ }\r
+ }\r
+\r
+ return false;\r
+ }\r
+\r
+ protected void startConnectTool(ICanvasParticipant tool) {\r
+ getContext().add(tool);\r
+ //System.out.println("TEMP: " + temporarilyEnabledConnectTool);\r
+ if (temporarilyEnabledConnectTool) {\r
+ // Resets pointer tool back into use if necessary after\r
+ // connection has been finished or canceled.\r
+ getContext().addContextListener(new ToolModeResetter(tool));\r
+ }\r
+ }\r
+\r
+ @EventHandler(priority = TOOL_PRIORITY)\r
+ public boolean handleClick(MouseClickEvent me) {\r
+ //System.out.println(getClass().getSimpleName() + ": mouse clicked: @ " + me.time);\r
+\r
+ if (hasDoubleClickEdit() && me.clickCount == 2) {\r
+ if (handleDoubleClick(me))\r
+ return true;\r
+ }\r
+\r
+ if (!hasClickSelect()) return false;\r
+ if (!hasToolMode(Hints.POINTERTOOL)) return false;\r
+\r
+ // Don't handle any events where click count is more than 1 to prevent\r
+ // current diagram selection from bouncing around unnecessarily.\r
+ if (me.clickCount > 1)\r
+ return false;\r
+\r
+ boolean isLeft = me.button == MouseEvent.LEFT_BUTTON;\r
+ boolean isRight = me.button == MouseEvent.RIGHT_BUTTON;\r
+ if (!isLeft && !isRight) return false;\r
+\r
+ boolean popupWasVisible = wasPopupJustClosed(me);\r
+ boolean noModifiers = !anyModifierPressed(me);\r
+ boolean isShiftPressed = me.hasAllModifiers(MouseEvent.SHIFT_MASK);\r
+\r
+ assertDependencies();\r
+\r
+ // Pick Terminal\r
+ double pickDist = getPickDistance();\r
+ Rectangle2D controlPickRect = new Rectangle2D.Double(me.controlPosition.getX()-pickDist, me.controlPosition.getY()-pickDist, pickDist*2+1, pickDist*2+1);\r
+ Shape canvasPickRect = GeometryUtils.transformShape(controlPickRect, util.getInverseTransform());\r
+ int selectionId = me.mouseId;\r
+\r
+ PickRequest req = new PickRequest(canvasPickRect);\r
+ req.pickPolicy = PickPolicy.PICK_INTERSECTING_OBJECTS;\r
+ req.pickSorter = pickSorter;\r
+ //req.pickSorter = PickRequest.PickSorter.CONNECTIONS_LAST;\r
+ List<IElement> pickables = new ArrayList<IElement>();\r
+ pickContext.pick(diagram, req, pickables);\r
+\r
+ Set<IElement> currentSelection = selection.getSelection(selectionId);\r
+\r
+ // Clear selection\r
+ if (pickables.isEmpty()) {\r
+ if (!popupWasVisible) {\r
+ // Only clear selection on left button clicks.\r
+ if (isLeft) {\r
+ selection.clear(selectionId);\r
+ }\r
+ }\r
+ if (isRight) {\r
+ if (/*!currentSelection.isEmpty() &&*/ noModifiers)\r
+ setHint(DiagramHints.SHOW_POPUP_MENU, me.controlPosition);\r
+ }\r
+ return false;\r
+ }\r
+\r
+ // Toggle select\r
+ if ((me.stateMask & MouseEvent.CTRL_MASK) != 0) {\r
+ if (isLeft) {\r
+ /*\r
+ * - If the mouse points to an object not in the selection, add it to selection.\r
+ * - If the mouse points to multiple objects, add the first.\r
+ * - If all objects the mouse points are already in selection, remove one of them from selection.\r
+ */\r
+ IElement removable = null;\r
+ for (int i = pickables.size() - 1; i >= 0; --i) {\r
+ IElement pickable = pickables.get(i);\r
+ if (selection.add(selectionId, pickable)) {\r
+ removable = null;\r
+ break;\r
+ } else\r
+ removable = pickable;\r
+\r
+ // Do not perform rotating pick in toggle selection\r
+ // when only CTRL is pressed. Requires SHIFT+CTRL.\r
+ if (!isShiftPressed)\r
+ break;\r
+ }\r
+ if (removable != null)\r
+ selection.remove(selectionId, removable);\r
+ }\r
+ return false;\r
+ }\r
+\r
+ // Click Select\r
+ {\r
+ if (isLeft && popupWasVisible)\r
+ // Popup menu is visible, just let it close\r
+ return false;\r
+\r
+ // Don't change selection on right clicks if there's more to pick\r
+ // than a single element.\r
+ if (isRight && pickables.size() > 1) {\r
+ IElement selectElement = singleElementAboveNonselectedConnections(currentSelection, pickables);\r
+ if (selectElement != null) {\r
+ selection.setSelection(selectionId, selectElement);\r
+ }\r
+ if (!currentSelection.isEmpty() && noModifiers)\r
+ setHint(DiagramHints.SHOW_POPUP_MENU, me.controlPosition);\r
+ return false;\r
+ }\r
+\r
+ /*\r
+ * Select the one object the mouse points to. If multiple object\r
+ * are picked, select the one that is after the earliest by\r
+ * index of the current selection when shift is pressed. Otherwise\r
+ * always pick the topmost element.\r
+ */\r
+ IElement selectedPick = isShiftPressed\r
+ ? rotatingPick(currentSelection, pickables)\r
+ : pickables.get(pickables.size() - 1);\r
+\r
+ // Only select when\r
+ // 1. the selection would actually change\r
+ // AND\r
+ // 2.1. left button was pressed\r
+ // OR\r
+ // 2.2. right button was pressed and the element to-be-selected\r
+ // is NOT a part of the current selection\r
+ if (!Collections.singleton(selectedPick).equals(currentSelection)\r
+ && (isLeft || (isRight && !currentSelection.contains(selectedPick)))) {\r
+ selection.setSelection(selectionId, selectedPick);\r
+ }\r
+\r
+ if (isRight && pickables.size() == 1 && noModifiers) {\r
+ setHint(DiagramHints.SHOW_POPUP_MENU, me.controlPosition);\r
+ }\r
+ }\r
+\r
+ return false;\r
+ }\r
+\r
+ /**\r
+ * A heuristic needed for implementing right-click diagram selection in a\r
+ * sensible manner.\r
+ * \r
+ * @param currentSelection\r
+ * @param pickables\r
+ * @return\r
+ */\r
+ private IElement singleElementAboveNonselectedConnections(Set<IElement> currentSelection, List<IElement> pickables) {\r
+ if (pickables.isEmpty())\r
+ return null;\r
+\r
+ // Check that the pickable-list doesn't contain anything that is in the current selection.\r
+ if (!Collections.disjoint(currentSelection, pickables))\r
+ return null;\r
+\r
+ IElement top = pickables.get(pickables.size() - 1);\r
+ boolean elementOnTop = !PickRequest.PickFilter.FILTER_CONNECTIONS.accept(top);\r
+ if (!elementOnTop)\r
+ return null;\r
+ for (int i = pickables.size() - 2; i >= 0; --i) {\r
+ IElement e = pickables.get(i);\r
+ if (!PickRequest.PickFilter.FILTER_CONNECTIONS.accept(e))\r
+ return null;\r
+ }\r
+ return top;\r
+ }\r
+\r
+ /**\r
+ * Since there's seems to be no better method available for finding out if\r
+ * the SWT popup menu was just recently closed or not, we use the following\r
+ * heuristic:\r
+ * \r
+ * SWT popup was just closed if it was closed < 300ms ago.\r
+ * \r
+ * Note that this is a very bad heuristic and may fail on slower machines or\r
+ * under heavy system load.\r
+ * \r
+ * @return\r
+ */\r
+ private boolean wasPopupJustClosed(Event event) {\r
+ Long popupCloseTime = getHint(DiagramHints.POPUP_MENU_HIDDEN);\r
+ if (popupCloseTime != null) {\r
+ long timeDiff = event.time - popupCloseTime;\r
+ //System.out.println("time diff: " + timeDiff);\r
+ if (timeDiff < 300) {\r
+ //System.out.println("POPUP WAS JUST CLOSED!");\r
+ return true;\r
+ }\r
+ }\r
+ //System.out.println("Popup has been closed for a while.");\r
+ return false;\r
+ }\r
+\r
+ boolean handleDoubleClick(MouseClickEvent me) {\r
+ //System.out.println("mouse double clicked: " + me);\r
+ if (!hasDoubleClickEdit()) return false;\r
+ if (me.button != MouseEvent.LEFT_BUTTON) return false;\r
+ if (getToolMode() != Hints.POINTERTOOL) return false;\r
+ if (me.clickCount < 2) return false;\r
+\r
+ // Pick Terminal\r
+ double pickDist = getPickDistance();\r
+ Rectangle2D controlPickRect = new Rectangle2D.Double(me.controlPosition.getX()-pickDist, me.controlPosition.getY()-pickDist, pickDist*2+1, pickDist*2+1);\r
+ Shape canvasPickRect = GeometryUtils.transformShape(controlPickRect, util.getInverseTransform());\r
+ int selectionId = me.mouseId;\r
+\r
+ PickRequest req = new PickRequest(canvasPickRect);\r
+ req.pickPolicy = PickPolicy.PICK_INTERSECTING_OBJECTS;\r
+ List<IElement> pick = new ArrayList<IElement>();\r
+ pickContext.pick(diagram, req, pick);\r
+\r
+ // Clear selection\r
+ if (pick.isEmpty()) {\r
+ selection.clear(selectionId);\r
+ return false;\r
+ }\r
+\r
+ IElement selectedPick = rotatingPick(selectionId, pick);\r
+\r
+ if (!selection.contains(selectionId, selectedPick)) {\r
+ selection.setSelection(selectionId, selectedPick);\r
+ }\r
+\r
+ CanvasUtils.sendCommand(getContext(), Commands.RENAME);\r
+\r
+ return false;\r
+ }\r
+\r
+ // Values shared by #handleDrag and #handleBoxSelect\r
+ private transient Point2D curCanvasDragPos = new Point2D.Double();\r
+ private transient Set<IElement> elementsToDrag = Collections.emptySet();\r
+\r
+ /**\r
+ * Invoked before scene graph event handling and {@link #handleBoxSelect(MouseDragBegin)}.\r
+ * @param me\r
+ * @return\r
+ * @see #handleBoxSelect(MouseDragBegin)\r
+ */\r
+ @EventHandler(priority = TOOL_PRIORITY)\r
+ public boolean handleDrag(MouseDragBegin me) {\r
+ if (!hasElementDrag() && !hasBoxSelect()) return false;\r
+ if (me.button != MouseEvent.LEFT_BUTTON) return false;\r
+ if (getToolMode() != Hints.POINTERTOOL) return false;\r
+ if (hasToolMode(me.mouseId)) return false;\r
+\r
+ boolean anyModifierPressed = me.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK);\r
+ boolean nonSelectionModifierPressed = me.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK\r
+ ^ (MouseEvent.SHIFT_MASK /*| MouseEvent.CTRL_MASK*/));\r
+\r
+ if (nonSelectionModifierPressed)\r
+ return false;\r
+\r
+ assertDependencies();\r
+\r
+ Point2D curCanvasPos = util.controlToCanvas(me.controlPosition, curCanvasDragPos);\r
+ PickRequest req = new PickRequest(me.startCanvasPos);\r
+ req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;\r
+ List<IElement> picks = new ArrayList<IElement>();\r
+ pickContext.pick(diagram, req, picks);\r
+\r
+ //System.out.println(picks);\r
+ if (picks.isEmpty()) {\r
+ // Widen the area of searching if nothing is found with point picking\r
+ double pickDist = getPickDistance();\r
+ Rectangle2D controlPickRect = new Rectangle2D.Double(me.controlPosition.getX()-pickDist, me.controlPosition.getY()-pickDist, pickDist*2+1, pickDist*2+1);\r
+ Shape canvasPickRect = GeometryUtils.transformShape(controlPickRect, util.getInverseTransform());\r
+ req = new PickRequest(canvasPickRect);\r
+ req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;\r
+ pickContext.pick(diagram, req, picks);\r
+ //System.out.println("2nd try: " + picks);\r
+ }\r
+\r
+ Set<IElement> sel = selection.getSelection(me.mouseId);\r
+ IElement topMostPick = picks.isEmpty() ? null : picks.get(picks.size() - 1);\r
+ Set<IElement> elementsToDrag = new HashSet<IElement>();\r
+ this.elementsToDrag = elementsToDrag;\r
+\r
+ if (!Collections.disjoint(sel, picks)) {\r
+ elementsToDrag.addAll(sel);\r
+ } else {\r
+ if (topMostPick != null && (sel.isEmpty() || !sel.contains(topMostPick))) {\r
+ selection.setSelection(me.mouseId, topMostPick);\r
+ sel = selection.getSelection(me.mouseId);\r
+ elementsToDrag.addAll(sel);\r
+ }\r
+ }\r
+\r
+ // Drag Elements\r
+ if (!elementsToDrag.isEmpty() && hasElementDnDDrag()) {\r
+ // To Be Implemented in the next Diagram data model.\r
+ } else {\r
+ if (!anyModifierPressed && !elementsToDrag.isEmpty() && hasElementDrag()) {\r
+ // Connections are not translatable, re-routing is in RouteGraphNode.\r
+ boolean onlyConnections = onlyConnections(elementsToDrag);\r
+ if (!onlyConnections) {\r
+ ICanvasParticipant tm = createTranslateTool(me.mouseId, me.startCanvasPos, curCanvasPos, elementsToDrag);\r
+ if (tm != null) {\r
+ getContext().add(tm);\r
+ return !onlyConnections;\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ return false;\r
+ }\r
+\r
+ /**\r
+ * Always invoked after after {@link #handleDrag(MouseDragBegin)} and scene\r
+ * graph event handling to prevent the box selection mode from being\r
+ * initiated before scene graph nodes have a chance to react.\r
+ * \r
+ * <p>\r
+ * Note that this method assumes that <code>elementsToDrag</code> and\r
+ * <code>curCanvasPos</code> are already set by\r
+ * {@link #handleDrag(MouseDragBegin)}.\r
+ * \r
+ * @param me\r
+ * @return\r
+ */\r
+ @EventHandler(priority = BOX_SELECT_PRIORITY)\r
+ public boolean handleBoxSelect(MouseDragBegin me) {\r
+ if (!hasBoxSelect()) return false;\r
+ if (me.button != MouseEvent.LEFT_BUTTON) return false;\r
+ if (getToolMode() != Hints.POINTERTOOL) return false;\r
+ if (hasToolMode(me.mouseId)) return false;\r
+\r
+ boolean nonSelectionModifierPressed = me.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK\r
+ ^ (MouseEvent.SHIFT_MASK /*| MouseEvent.CTRL_MASK*/));\r
+ if (nonSelectionModifierPressed)\r
+ return false;\r
+\r
+ if (!nonSelectionModifierPressed && elementsToDrag.isEmpty()) {\r
+ // Box Select\r
+ ICanvasParticipant bsm = createBoxSelectTool(me.mouseId, me.startCanvasPos, curCanvasDragPos, me.button, boxSelectMode);\r
+ if (bsm != null)\r
+ getContext().add(bsm);\r
+ }\r
+\r
+ return false;\r
+ }\r
+\r
+ private static boolean onlyConnections(Set<IElement> elements) {\r
+ for (IElement e : elements)\r
+ if (!e.getElementClass().containsClass(ConnectionHandler.class))\r
+ return false;\r
+ return true;\r
+ }\r
+\r
+ private IElement rotatingPick(int selectionId, List<IElement> pickables) {\r
+ Set<IElement> sel = selection.getSelection(selectionId);\r
+ return rotatingPick(sel, pickables);\r
+ }\r
+\r
+ private IElement rotatingPick(Set<IElement> sel, List<IElement> pickables) {\r
+ int earliestIndex = pickables.size();\r
+ for (int i = pickables.size() - 1; i >= 0; --i) {\r
+ if (sel.contains(pickables.get(i))) {\r
+ earliestIndex = i;\r
+ break;\r
+ }\r
+ }\r
+ if (earliestIndex == 0)\r
+ earliestIndex = pickables.size();\r
+ IElement selectedPick = pickables.get(earliestIndex - 1);\r
+ return selectedPick;\r
+ }\r
+\r
+ /**\r
+ * Is mouse in some kind of mode?\r
+ * @param mouseId\r
+ * @return\r
+ */\r
+ boolean hasToolMode(int mouseId) {\r
+ for (AbstractMode am : getContext().getItemsByClass(AbstractMode.class))\r
+ if (am.mouseId==mouseId) return true;\r
+ return false;\r
+ }\r
+\r
+ boolean hasToolMode(IToolMode mode) {\r
+ return ObjectUtils.objectEquals(mode, getToolMode());\r
+ }\r
+\r
+ boolean hasToolMode(IToolMode... modes) {\r
+ IToolMode current = getToolMode();\r
+ if (current == null)\r
+ return false;\r
+ for (IToolMode mode : modes)\r
+ if (current.equals(mode))\r
+ return true;\r
+ return false;\r
+ }\r
+\r
+ protected IToolMode getToolMode() {\r
+ return getHint(Hints.KEY_TOOL);\r
+ }\r
+\r
+ /// is box select enabled\r
+ protected boolean hasBoxSelect() {\r
+ return boxSelect;\r
+ }\r
+\r
+ /// is click select enabled\r
+ protected boolean hasClickSelect() {\r
+ return clickSelect;\r
+ }\r
+\r
+ /// is double click edit enabled\r
+ protected boolean hasDoubleClickEdit() {\r
+ return doubleClickEdit;\r
+ }\r
+\r
+ // is element drag enabled\r
+ protected boolean hasElementDrag() {\r
+ return dragElement;\r
+ }\r
+\r
+ // is element drag enabled\r
+ protected boolean hasElementDnDDrag() {\r
+ return dndDragElement;\r
+ }\r
+\r
+ // is connect enabled\r
+ protected boolean connects() {\r
+ return connect;\r
+ }\r
+\r
+ public double getPickDistance() {\r
+ Double pickDistance = getHint(KEY_PICK_DISTANCE);\r
+ return pickDistance == null ? PICK_DIST : Math.max(pickDistance, 0);\r
+ }\r
+\r
+ public PickPolicy getBoxSelectMode() {\r
+ return boxSelectMode;\r
+ }\r
+\r
+ public void setBoxSelectMode(PickPolicy boxSelectMode) {\r
+ this.boxSelectMode = boxSelectMode;\r
+ }\r
+\r
+ public void setSelectionEnabled(boolean select) {\r
+ this.clickSelect = select;\r
+ this.boxSelect = select;\r
+ if(select == false) { // Clear all selections if select is disabled\r
+ final int[] ids = selection.getSelectionIds();\r
+ if(ids.length > 0) {\r
+ ThreadUtils.asyncExec(getContext().getThreadAccess(), new Runnable() {\r
+ @Override\r
+ public void run() {\r
+ for(int id : ids)\r
+ selection.clear(id);\r
+ getContext().getContentContext().setDirty();\r
+ }\r
+ });\r
+ }\r
+ }\r
+ }\r
+\r
+ boolean anyModifierPressed(MouseEvent e) {\r
+ return e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK);\r
+ }\r
+\r
+ boolean connectToolModifiersPressed(int stateMask) {\r
+ return (stateMask & (MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) != 0;\r
+ }\r
+\r
+ static {\r
+ LINE10 = new Path2D.Double();\r
+ LINE10.moveTo(0, 0);\r
+ LINE10.lineTo(10, 0);\r
+ LINE10.lineTo(7, -3);\r
+ LINE10.moveTo(10, 0);\r
+ LINE10.lineTo(7, 3);\r
+\r
+ LINE15 = new Path2D.Double();\r
+ LINE15.moveTo(0, 0);\r
+ LINE15.lineTo(15, 0);\r
+ LINE15.lineTo(12, -3);\r
+ LINE15.moveTo(15, 0);\r
+ LINE15.lineTo(12, 3);\r
+\r
+ LINE20 = new Path2D.Double();\r
+ LINE20.moveTo(0, 0);\r
+ LINE20.lineTo(20, 0);\r
+ LINE20.lineTo(17, -3);\r
+ LINE20.moveTo(20, 0);\r
+ LINE20.lineTo(17, 3);\r
+\r
+ }\r
+\r
+ // CUSTOMIZE\r
+\r
+ protected ICanvasParticipant createConnectTool(TerminalInfo ti, int mouseId, Point2D startCanvasPos) {\r
+ return null;\r
+ }\r
+\r
+ protected ICanvasParticipant createConnectToolWithTerminals(List<TerminalInfo> tis, int mouseId, Point2D startCanvasPos) {\r
+ return null;\r
+ }\r
+\r
+ protected ICanvasParticipant createTranslateTool(int mouseId, Point2D startCanvasPos, Point2D curCanvasPos, Set<IElement> elementsToDrag) {\r
+ return new TranslateMode(startCanvasPos, curCanvasPos, mouseId, elementsToDrag);\r
+ }\r
+\r
+ protected ICanvasParticipant createBoxSelectTool(int mouseId, Point2D startCanvasPos, Point2D curCanvasPos, int button, PickPolicy boxSelectMode) {\r
+ return new BoxSelectionMode(startCanvasPos, curCanvasPos, mouseId, button, boxSelectMode);\r
+ }\r
+\r
+ /**\r
+ * A context listener for resetting tool mode back to pointer mode after the\r
+ * tracked participant has been removed.\r
+ */\r
+ protected class ToolModeResetter implements IContextListener<ICanvasParticipant> {\r
+ private ICanvasParticipant tracked;\r
+ public ToolModeResetter(ICanvasParticipant trackedParticipant) {\r
+ this.tracked = trackedParticipant;\r
+ }\r
+ @Override\r
+ public void itemAdded(IContext<ICanvasParticipant> sender, ICanvasParticipant item) {\r
+ }\r
+ @Override\r
+ public void itemRemoved(IContext<ICanvasParticipant> sender, ICanvasParticipant item) {\r
+ if (item == tracked) {\r
+ sender.removeContextListener(this);\r
+ if (!isRemoved() && !connectToolModifiersPressed(lastStateMask)) {\r
+ temporarilyEnabledConnectTool = false;\r
+ setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+}\r