]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/participant/pointertool/PointerInteractor.java
Sync git svn branch with SVN repository r33324.
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / diagram / participant / pointertool / PointerInteractor.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.g2d.diagram.participant.pointertool;\r
13 \r
14 import java.awt.Shape;\r
15 import java.awt.geom.AffineTransform;\r
16 import java.awt.geom.Path2D;\r
17 import java.awt.geom.Point2D;\r
18 import java.awt.geom.Rectangle2D;\r
19 import java.util.ArrayList;\r
20 import java.util.Collections;\r
21 import java.util.HashSet;\r
22 import java.util.List;\r
23 import java.util.Set;\r
24 \r
25 import org.simantics.g2d.canvas.Hints;\r
26 import org.simantics.g2d.canvas.ICanvasContext;\r
27 import org.simantics.g2d.canvas.ICanvasParticipant;\r
28 import org.simantics.g2d.canvas.IToolMode;\r
29 import org.simantics.g2d.canvas.impl.CanvasContext;\r
30 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;\r
31 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;\r
32 import org.simantics.g2d.connection.IConnectionAdvisor;\r
33 import org.simantics.g2d.connection.handler.ConnectionHandler;\r
34 import org.simantics.g2d.diagram.DiagramHints;\r
35 import org.simantics.g2d.diagram.handler.PickContext;\r
36 import org.simantics.g2d.diagram.handler.PickRequest;\r
37 import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy;\r
38 import org.simantics.g2d.diagram.handler.PickRequest.PickSorter;\r
39 import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;\r
40 import org.simantics.g2d.diagram.participant.Selection;\r
41 import org.simantics.g2d.diagram.participant.TerminalPainter;\r
42 import org.simantics.g2d.diagram.participant.TerminalPainter.ChainedHoverStrategy;\r
43 import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;\r
44 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;\r
45 import org.simantics.g2d.element.ElementClassProviders;\r
46 import org.simantics.g2d.element.IElement;\r
47 import org.simantics.g2d.element.IElementClassProvider;\r
48 import org.simantics.g2d.participant.KeyUtil;\r
49 import org.simantics.g2d.participant.MouseUtil;\r
50 import org.simantics.g2d.participant.TransformUtil;\r
51 import org.simantics.g2d.scenegraph.SceneGraphConstants;\r
52 import org.simantics.g2d.utils.CanvasUtils;\r
53 import org.simantics.g2d.utils.GeometryUtils;\r
54 import org.simantics.scenegraph.g2d.events.Event;\r
55 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;\r
56 import org.simantics.scenegraph.g2d.events.KeyEvent;\r
57 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;\r
58 import org.simantics.scenegraph.g2d.events.MouseEvent;\r
59 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;\r
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;\r
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;\r
62 import org.simantics.scenegraph.g2d.events.command.Commands;\r
63 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;\r
64 import org.simantics.utils.ObjectUtils;\r
65 import org.simantics.utils.datastructures.context.IContext;\r
66 import org.simantics.utils.datastructures.context.IContextListener;\r
67 import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
68 import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;\r
69 import org.simantics.utils.threads.ThreadUtils;\r
70 \r
71 /**\r
72  * Pointer tool does the following operations with mouse:\r
73  * <ul>\r
74  * <li>Selections</li>\r
75  * <li>Scale</li>\r
76  * <li>Rotate</li>\r
77  * <li>Translate</li>\r
78  * <li>Draws connections (requires re-implementing\r
79  * {@link #createConnectTool(TerminalInfo, int, Point2D)})</li>\r
80  * </ul>\r
81  * \r
82  * Pointer tool is active only when {@link Hints#KEY_TOOL} is\r
83  * {@value Hints#POINTERTOOL}.\r
84  * \r
85  * @author Toni Kalajainen\r
86  */\r
87 public class PointerInteractor extends AbstractDiagramParticipant {\r
88 \r
89     /**\r
90      * A hint key for terminal pick distance in control pixels.\r
91      * @see #PICK_DIST\r
92      */\r
93     public static final Key KEY_PICK_DISTANCE = new KeyOf(Double.class, "PICK_DISTANCE");\r
94 \r
95     /**\r
96      * Default terminal pick distance in control pixels.\r
97      * @see #DEFAULT_PICK_DISTANCE\r
98      */\r
99     public static final double PICK_DIST = 10;\r
100 \r
101     /**\r
102      * @see #altToggled(KeyEvent)\r
103      */\r
104     protected int              lastStateMask;\r
105 \r
106     /**\r
107      * @see #altToggled(KeyEvent)\r
108      */\r
109     protected boolean          temporarilyEnabledConnectTool = false;\r
110 \r
111     public class DefaultHoverStrategy extends ChainedHoverStrategy {\r
112         public DefaultHoverStrategy(TerminalHoverStrategy orig) {\r
113             super(orig);\r
114         }\r
115         @Override\r
116         public boolean highlightEnabled() {\r
117             if (Hints.CONNECTTOOL.equals(getToolMode()))\r
118                 return true;\r
119 \r
120             boolean ct = connectToolModifiersPressed(lastStateMask);\r
121             //System.out.println("highlightEnabled: " + String.format("%x", lastStateMask) + " : " + ct);\r
122             return ct;\r
123         }\r
124         @Override\r
125         public boolean canHighlight(TerminalInfo ti) {\r
126             //boolean alt = (lastStateMask & MouseEvent.ALT_MASK) != 0;\r
127             //System.out.println("canHighlight: " + String.format("%x", lastStateMask) + " : " + alt);\r
128             //if (!alt)\r
129             //   return false;\r
130             IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);\r
131             return advisor == null || advisor.canBeginConnection(null, ti.e, ti.t);\r
132         }\r
133     }\r
134 \r
135     @Dependency Selection selection;\r
136     @Dependency KeyUtil keys;\r
137     @Dependency TransformUtil util;\r
138     @Dependency PickContext pickContext;\r
139     @Dependency MouseUtil mice;\r
140     @Reference TerminalPainter terminalPainter;\r
141 \r
142     /**\r
143      * This must be higher than\r
144      * {@link SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY}\r
145      * ({@value SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY}) to allow for\r
146      * better selection handling than is possible by using the plain scene graph\r
147      * event handling facilities which are installed in {@link CanvasContext}.\r
148      */\r
149     public static final int TOOL_PRIORITY = 1 << 21;\r
150 \r
151     /**\r
152      * This must be lower than\r
153      * {@link SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY} (\r
154      * {@value SceneGraphConstants#SCENEGRAPH_EVENT_PRIORITY}) to not start box\r
155      * handling before scene graph nodes have been given a chance to react\r
156      * events.\r
157      */\r
158     public static final int BOX_SELECT_PRIORITY = 1 << 19;\r
159 \r
160     private static final Path2D LINE10;\r
161     private static final Path2D LINE15;\r
162     private static final Path2D LINE20;\r
163 \r
164     boolean clickSelect;\r
165     boolean boxSelect;\r
166     boolean dragElement, dndDragElement;\r
167     boolean connect;\r
168     boolean doubleClickEdit;\r
169     protected IElementClassProvider elementClassProvider;\r
170 \r
171     PickPolicy boxSelectMode = PickPolicy.PICK_CONTAINED_OBJECTS;\r
172 \r
173     DefaultHoverStrategy hoverStrategy;\r
174 \r
175     private PickSorter pickSorter;\r
176     \r
177     public PointerInteractor() {\r
178         this(true, true, true, false, true, false, ElementClassProviders.staticProvider(null), null);\r
179     }\r
180 \r
181     public PointerInteractor(PickSorter pickSorter) {\r
182         this(true, true, true, false, true, false, ElementClassProviders.staticProvider(null), pickSorter);\r
183     }\r
184 \r
185     public PointerInteractor(boolean clickSelect, boolean boxSelect, boolean dragElement, boolean dndDragElement, boolean connect, IElementClassProvider ecp) {\r
186         this(clickSelect, boxSelect, dragElement, dndDragElement, connect, false, ecp, null);\r
187     }\r
188 \r
189     public PointerInteractor(boolean clickSelect, boolean boxSelect, boolean dragElement, boolean dndDragElement, boolean connect, boolean doubleClickEdit, IElementClassProvider ecp, PickSorter pickSorter) {\r
190         super();\r
191         this.clickSelect = clickSelect;\r
192         this.boxSelect = boxSelect;\r
193         this.dragElement = dragElement;\r
194         this.dndDragElement = dndDragElement;\r
195         this.connect = connect;\r
196         this.doubleClickEdit = doubleClickEdit;\r
197         this.elementClassProvider = ecp;\r
198         this.pickSorter = pickSorter;\r
199     }\r
200 \r
201     @Override\r
202     public void addedToContext(ICanvasContext ctx) {\r
203         super.addedToContext(ctx);\r
204         hoverStrategy = new DefaultHoverStrategy((TerminalHoverStrategy) getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY));\r
205         setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, hoverStrategy);\r
206     }\r
207 \r
208     @EventHandler(priority = 0)\r
209     public boolean handleStateMask(MouseEvent me) {\r
210         lastStateMask = me.stateMask;\r
211         if (temporarilyEnabledConnectTool) {\r
212             if (!connectToolModifiersPressed(me.stateMask)) {\r
213                 temporarilyEnabledConnectTool = false;\r
214                 setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);\r
215             }\r
216         } else {\r
217             // It may be that the mouse has come into this control\r
218             // from outside and no key state changes have occurred yet.\r
219             // In this case this code should take care of moving the canvas\r
220             // context into CONNECTTOOL mode.\r
221             if (getToolMode() == Hints.POINTERTOOL\r
222                     && connectToolModifiersPressed(me.stateMask))\r
223             {\r
224                 temporarilyEnabledConnectTool = true;\r
225                 setHint(Hints.KEY_TOOL, Hints.CONNECTTOOL);\r
226             }\r
227         }\r
228         return false;\r
229     }\r
230 \r
231     private static int mask(int mask, int mask2, boolean set) {\r
232         return set ? mask | mask2 : mask & ~mask2;\r
233     }\r
234 \r
235     @EventHandler(priority = -1)\r
236     public boolean altToggled(KeyEvent ke) {\r
237         int mods = ke.stateMask;\r
238         boolean press = ke instanceof KeyPressedEvent;\r
239         boolean modifierPressed = false;\r
240         if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {\r
241             mods = mask(mods, MouseEvent.ALT_MASK, press);\r
242             modifierPressed = true;\r
243         }\r
244         if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT_GRAPH) {\r
245             mods = mask(mods, MouseEvent.ALT_GRAPH_MASK, press);\r
246             modifierPressed = true;\r
247         }\r
248         if (ke.keyCode == java.awt.event.KeyEvent.VK_SHIFT) {\r
249             mods = mask(mods, MouseEvent.SHIFT_MASK, press);\r
250             modifierPressed = true;\r
251         }\r
252         if (ke.keyCode == java.awt.event.KeyEvent.VK_CONTROL) {\r
253             mods = mask(mods, MouseEvent.CTRL_MASK, press);\r
254             modifierPressed = true;\r
255         }\r
256         if (ke.keyCode == java.awt.event.KeyEvent.VK_META) {\r
257             // TODO: NO MASK FOR META!\r
258             modifierPressed = true;\r
259         }\r
260         // Don't deny connecting when CTRL is marked pressed because ALT_GRAPH\r
261         // is actually ALT+CTRL in SWT. There's no way in SWT to tell apart\r
262         // CTRL+ALT and ALT GRAPH.\r
263         boolean otherModifiers = (mods & (MouseEvent.SHIFT_MASK)) != 0;\r
264         boolean altModifier = (mods & (MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) != 0;\r
265         if (modifierPressed) {\r
266             boolean altPressed = !otherModifiers && altModifier;\r
267             lastStateMask = mods;\r
268             if (altPressed) {\r
269                 IToolMode mode = getToolMode();\r
270                 if (mode == Hints.POINTERTOOL) {\r
271                     //System.out.println("TEMP++");\r
272                     temporarilyEnabledConnectTool = true;\r
273                     setHint(Hints.KEY_TOOL, Hints.CONNECTTOOL);\r
274                 }\r
275             } else {\r
276                 if (temporarilyEnabledConnectTool) {\r
277                     //System.out.println("TEMP--");\r
278                     temporarilyEnabledConnectTool = false;\r
279                     setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);\r
280                 }\r
281             }\r
282             // Make sure that TerminalPainter updates its scene graph.\r
283             if (terminalPainter != null) {\r
284                 terminalPainter.update(terminalPainter.highlightEnabled());\r
285             }\r
286         }\r
287         return false;\r
288     }\r
289 \r
290     /**\r
291      * @param controlPos\r
292      * @return <code>null</code> if current canvas transform is not invertible\r
293      */\r
294     public Shape getCanvasPickShape(Point2D controlPos) {\r
295         AffineTransform inverse = util.getInverseTransform();\r
296         if (inverse == null)\r
297             return null;\r
298 \r
299         double      pd              = getPickDistance();\r
300         Rectangle2D controlPickRect = new Rectangle2D.Double(controlPos.getX()-pd, controlPos.getY()-pd, pd*2+1, pd*2+1);\r
301         Shape       canvasShape     = GeometryUtils.transformShape(controlPickRect, inverse);\r
302         return canvasShape;\r
303     }\r
304 \r
305     public List<TerminalInfo> pickTerminals(Point2D controlPos)\r
306     {\r
307         Shape canvasPickRect = getCanvasPickShape(controlPos);\r
308         if (canvasPickRect == null)\r
309             return Collections.emptyList();\r
310         return TerminalUtil.pickTerminals(diagram, canvasPickRect, true, true);\r
311     }\r
312 \r
313     public TerminalInfo pickTerminal(Point2D controlPos)\r
314     {\r
315         Shape canvasPickRect = getCanvasPickShape(controlPos);\r
316         if (canvasPickRect == null)\r
317             return null;\r
318         TerminalInfo ti = TerminalUtil.pickTerminal(diagram, canvasPickRect);\r
319         return ti;\r
320     }\r
321 \r
322     @EventHandler(priority = TOOL_PRIORITY)\r
323     public boolean handlePress(MouseButtonPressedEvent me) {\r
324         if (!connects())\r
325             return false;\r
326         if (me.button != MouseEvent.LEFT_BUTTON)\r
327             return false;\r
328 \r
329         IToolMode mode = getToolMode();\r
330 \r
331         // It may be that the mouse has come into this control\r
332         // from outside without focusing it and without any mouse\r
333         // buttons being pressed. If the user is pressing the\r
334         // connection modifier we need to temporarily enable connect\r
335         // mode here and now.\r
336         if (mode == Hints.POINTERTOOL && connectToolModifiersPressed(me.stateMask)) {\r
337             temporarilyEnabledConnectTool = true;\r
338             mode = Hints.CONNECTTOOL;\r
339             setHint(Hints.KEY_TOOL, Hints.CONNECTTOOL);\r
340         }\r
341 \r
342         if (mode == Hints.CONNECTTOOL) {\r
343             Point2D curCanvasPos = util.controlToCanvas(me.controlPosition, null);\r
344             return checkInitiateConnectTool(me, curCanvasPos);\r
345         }\r
346 \r
347         return false;\r
348     }\r
349 \r
350     protected boolean checkInitiateConnectTool(MouseEvent me, Point2D mouseCanvasPos) {\r
351         // Pick Terminal\r
352         IToolMode mode = getToolMode();\r
353         if (mode == Hints.CONNECTTOOL || connectToolModifiersPressed(me.stateMask)) {\r
354             IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);\r
355             TerminalInfo ti = pickTerminal(me.controlPosition);\r
356 \r
357             ICanvasParticipant bsi = null;\r
358             if (ti != null) {\r
359                 if (advisor == null || advisor.canBeginConnection(null, ti.e, ti.t)) {\r
360                     bsi = createConnectTool(ti, me.mouseId, mouseCanvasPos);\r
361                 }\r
362             } else {\r
363                 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);\r
364                 if (snapAdvisor != null)\r
365                     snapAdvisor.snap(mouseCanvasPos);\r
366 \r
367                 // Start connection out of thin air, without a terminal.\r
368                 bsi = createConnectTool(null, me.mouseId, mouseCanvasPos);\r
369             }\r
370 \r
371             // Did we catch anything?\r
372             if (bsi != null) {\r
373                 startConnectTool(bsi);\r
374                 return true;\r
375             }\r
376         }\r
377 \r
378         return false;\r
379     }\r
380 \r
381     protected void startConnectTool(ICanvasParticipant tool) {\r
382         getContext().add(tool);\r
383         //System.out.println("TEMP: " + temporarilyEnabledConnectTool);\r
384         if (temporarilyEnabledConnectTool) {\r
385             // Resets pointer tool back into use if necessary after\r
386             // connection has been finished or canceled.\r
387             getContext().addContextListener(new ToolModeResetter(tool));\r
388         }\r
389     }\r
390 \r
391     @EventHandler(priority = TOOL_PRIORITY)\r
392     public boolean handleClick(MouseClickEvent me) {\r
393         //System.out.println(getClass().getSimpleName() + ": mouse clicked: @ " + me.time);\r
394 \r
395         if (hasDoubleClickEdit() && me.clickCount == 2) {\r
396             if (handleDoubleClick(me))\r
397                 return true;\r
398         }\r
399 \r
400         if (!hasClickSelect()) return false;\r
401         if (!hasToolMode(Hints.POINTERTOOL)) return false;\r
402 \r
403         // Don't handle any events where click count is more than 1 to prevent\r
404         // current diagram selection from bouncing around unnecessarily.\r
405         if (me.clickCount > 1)\r
406             return false;\r
407 \r
408         boolean isLeft = me.button == MouseEvent.LEFT_BUTTON;\r
409         boolean isRight = me.button == MouseEvent.RIGHT_BUTTON;\r
410         if (!isLeft && !isRight) return false;\r
411 \r
412         boolean popupWasVisible = wasPopupJustClosed(me);\r
413         boolean noModifiers = !anyModifierPressed(me);\r
414         boolean isShiftPressed = me.hasAllModifiers(MouseEvent.SHIFT_MASK);\r
415 \r
416         assertDependencies();\r
417 \r
418         // Pick Terminal\r
419         double pickDist = getPickDistance();\r
420         Rectangle2D controlPickRect = new Rectangle2D.Double(me.controlPosition.getX()-pickDist, me.controlPosition.getY()-pickDist, pickDist*2+1, pickDist*2+1);\r
421         Shape       canvasPickRect  = GeometryUtils.transformShape(controlPickRect, util.getInverseTransform());\r
422         int selectionId = me.mouseId;\r
423 \r
424         PickRequest req = new PickRequest(canvasPickRect);\r
425         req.pickPolicy = PickPolicy.PICK_INTERSECTING_OBJECTS;\r
426         req.pickSorter = pickSorter;\r
427         //req.pickSorter = PickRequest.PickSorter.CONNECTIONS_LAST;\r
428         List<IElement> pickables = new ArrayList<IElement>();\r
429         pickContext.pick(diagram, req, pickables);\r
430 \r
431         Set<IElement> currentSelection = selection.getSelection(selectionId);\r
432 \r
433         // Clear selection\r
434         if (pickables.isEmpty()) {\r
435             if (!popupWasVisible) {\r
436                 // Only clear selection on left button clicks.\r
437                 if (isLeft) {\r
438                     selection.clear(selectionId);\r
439                 }\r
440             }\r
441             if (isRight) {\r
442                 if (/*!currentSelection.isEmpty() &&*/ noModifiers)\r
443                     setHint(DiagramHints.SHOW_POPUP_MENU, me.controlPosition);\r
444             }\r
445             return false;\r
446         }\r
447 \r
448         // Toggle select\r
449         if ((me.stateMask & MouseEvent.CTRL_MASK) != 0) {\r
450             if (isLeft) {\r
451                 /*\r
452                  * - If the mouse points to an object not in the selection, add it to selection.\r
453                  * - If the mouse points to multiple objects, add the first.\r
454                  * - If all objects the mouse points are already in selection, remove one of them from selection.\r
455                  */\r
456                 IElement removable = null;\r
457                 for (int i = pickables.size() - 1; i >= 0; --i) {\r
458                     IElement pickable = pickables.get(i);\r
459                     if (selection.add(selectionId, pickable)) {\r
460                         removable = null;\r
461                         break;\r
462                     } else\r
463                         removable = pickable;\r
464 \r
465                     // Do not perform rotating pick in toggle selection\r
466                     // when only CTRL is pressed. Requires SHIFT+CTRL.\r
467                     if (!isShiftPressed)\r
468                         break;\r
469                 }\r
470                 if (removable != null)\r
471                     selection.remove(selectionId, removable);\r
472             }\r
473             return false;\r
474         }\r
475 \r
476         // Click Select\r
477         {\r
478             if (isLeft && popupWasVisible)\r
479                 // Popup menu is visible, just let it close\r
480                 return false;\r
481 \r
482             // Don't change selection on right clicks if there's more to pick\r
483             // than a single element.\r
484             if (isRight && pickables.size() > 1) {\r
485                 IElement selectElement = singleElementAboveNonselectedConnections(currentSelection, pickables);\r
486                 if (selectElement != null) {\r
487                     selection.setSelection(selectionId, selectElement);\r
488                 }\r
489                 if (!currentSelection.isEmpty() && noModifiers)\r
490                     setHint(DiagramHints.SHOW_POPUP_MENU, me.controlPosition);\r
491                 return false;\r
492             }\r
493 \r
494             /*\r
495              * Select the one object the mouse points to. If multiple object\r
496              * are picked, select the one that is after the earliest by\r
497              * index of the current selection when shift is pressed. Otherwise\r
498              * always pick the topmost element.\r
499              */\r
500             IElement selectedPick = isShiftPressed\r
501                     ? rotatingPick(currentSelection, pickables)\r
502                             : pickables.get(pickables.size() - 1);\r
503 \r
504             // Only select when\r
505             // 1. the selection would actually change\r
506             // AND\r
507             //   2.1. left button was pressed\r
508             //   OR\r
509             //   2.2. right button was pressed and the element to-be-selected\r
510             //        is NOT a part of the current selection\r
511             if (!Collections.singleton(selectedPick).equals(currentSelection)\r
512                     && (isLeft || (isRight && !currentSelection.contains(selectedPick)))) {\r
513                 selection.setSelection(selectionId, selectedPick);\r
514             }\r
515 \r
516             if (isRight && pickables.size() == 1 && noModifiers) {\r
517                 setHint(DiagramHints.SHOW_POPUP_MENU, me.controlPosition);\r
518             }\r
519         }\r
520 \r
521         return false;\r
522     }\r
523 \r
524     /**\r
525      * A heuristic needed for implementing right-click diagram selection in a\r
526      * sensible manner.\r
527      * \r
528      * @param currentSelection\r
529      * @param pickables\r
530      * @return\r
531      */\r
532     private IElement singleElementAboveNonselectedConnections(Set<IElement> currentSelection, List<IElement> pickables) {\r
533         if (pickables.isEmpty())\r
534             return null;\r
535 \r
536         // Check that the pickable-list doesn't contain anything that is in the current selection.\r
537         if (!Collections.disjoint(currentSelection, pickables))\r
538             return null;\r
539 \r
540         IElement top = pickables.get(pickables.size() - 1);\r
541         boolean elementOnTop = !PickRequest.PickFilter.FILTER_CONNECTIONS.accept(top);\r
542         if (!elementOnTop)\r
543             return null;\r
544         for (int i = pickables.size() - 2; i >= 0; --i) {\r
545             IElement e = pickables.get(i);\r
546             if (!PickRequest.PickFilter.FILTER_CONNECTIONS.accept(e))\r
547                 return null;\r
548         }\r
549         return top;\r
550     }\r
551 \r
552     /**\r
553      * Since there's seems to be no better method available for finding out if\r
554      * the SWT popup menu was just recently closed or not, we use the following\r
555      * heuristic:\r
556      * \r
557      * SWT popup was just closed if it was closed < 300ms ago.\r
558      * \r
559      * Note that this is a very bad heuristic and may fail on slower machines or\r
560      * under heavy system load.\r
561      * \r
562      * @return\r
563      */\r
564     private boolean wasPopupJustClosed(Event event) {\r
565         Long popupCloseTime = getHint(DiagramHints.POPUP_MENU_HIDDEN);\r
566         if (popupCloseTime != null) {\r
567             long timeDiff = event.time - popupCloseTime;\r
568             //System.out.println("time diff: " + timeDiff);\r
569             if (timeDiff < 300) {\r
570                 //System.out.println("POPUP WAS JUST CLOSED!");\r
571                 return true;\r
572             }\r
573         }\r
574         //System.out.println("Popup has been closed for a while.");\r
575         return false;\r
576     }\r
577 \r
578     boolean handleDoubleClick(MouseClickEvent me) {\r
579         //System.out.println("mouse double clicked: " + me);\r
580         if (!hasDoubleClickEdit()) return false;\r
581         if (me.button != MouseEvent.LEFT_BUTTON) return false;\r
582         if (getToolMode() != Hints.POINTERTOOL) return false;\r
583         if (me.clickCount < 2) return false;\r
584 \r
585         // Pick Terminal\r
586         double pickDist = getPickDistance();\r
587         Rectangle2D controlPickRect = new Rectangle2D.Double(me.controlPosition.getX()-pickDist, me.controlPosition.getY()-pickDist, pickDist*2+1, pickDist*2+1);\r
588         Shape       canvasPickRect  = GeometryUtils.transformShape(controlPickRect, util.getInverseTransform());\r
589         int         selectionId     = me.mouseId;\r
590 \r
591         PickRequest req             = new PickRequest(canvasPickRect);\r
592         req.pickPolicy = PickPolicy.PICK_INTERSECTING_OBJECTS;\r
593         List<IElement> pick         = new ArrayList<IElement>();\r
594         pickContext.pick(diagram, req, pick);\r
595 \r
596         // Clear selection\r
597         if (pick.isEmpty()) {\r
598             selection.clear(selectionId);\r
599             return false;\r
600         }\r
601 \r
602         IElement selectedPick = rotatingPick(selectionId, pick);\r
603 \r
604         if (!selection.contains(selectionId, selectedPick)) {\r
605             selection.setSelection(selectionId, selectedPick);\r
606         }\r
607 \r
608         CanvasUtils.sendCommand(getContext(), Commands.RENAME);\r
609 \r
610         return false;\r
611     }\r
612 \r
613     // Values shared by #handleDrag and #handleBoxSelect\r
614     private transient Point2D curCanvasDragPos = new Point2D.Double();\r
615     private transient Set<IElement> elementsToDrag = Collections.emptySet();\r
616 \r
617     /**\r
618      * Invoked before scene graph event handling and {@link #handleBoxSelect(MouseDragBegin)}.\r
619      * @param me\r
620      * @return\r
621      * @see #handleBoxSelect(MouseDragBegin)\r
622      */\r
623     @EventHandler(priority = TOOL_PRIORITY)\r
624     public boolean handleDrag(MouseDragBegin me) {\r
625         if (!hasElementDrag() && !hasBoxSelect()) return false;\r
626         if (me.button != MouseEvent.LEFT_BUTTON) return false;\r
627         if (getToolMode() != Hints.POINTERTOOL) return false;\r
628         if (hasToolMode(me.mouseId)) return false;\r
629 \r
630         boolean anyModifierPressed = me.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK);\r
631         boolean nonSelectionModifierPressed = me.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK\r
632                 ^ (MouseEvent.SHIFT_MASK /*| MouseEvent.CTRL_MASK*/));\r
633 \r
634         if (nonSelectionModifierPressed)\r
635             return false;\r
636 \r
637         assertDependencies();\r
638 \r
639         Point2D         curCanvasPos    = util.controlToCanvas(me.controlPosition, curCanvasDragPos);\r
640         PickRequest     req             = new PickRequest(me.startCanvasPos);\r
641         req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;\r
642         List<IElement>  picks           = new ArrayList<IElement>();\r
643         pickContext.pick(diagram, req, picks);\r
644 \r
645         //System.out.println(picks);\r
646         if (picks.isEmpty()) {\r
647             // Widen the area of searching if nothing is found with point picking\r
648                 double pickDist = getPickDistance();\r
649             Rectangle2D     controlPickRect     = new Rectangle2D.Double(me.controlPosition.getX()-pickDist, me.controlPosition.getY()-pickDist, pickDist*2+1, pickDist*2+1);\r
650             Shape           canvasPickRect      = GeometryUtils.transformShape(controlPickRect, util.getInverseTransform());\r
651             req = new PickRequest(canvasPickRect);\r
652             req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;\r
653             pickContext.pick(diagram, req, picks);\r
654             //System.out.println("2nd try: " + picks);\r
655         }\r
656 \r
657         Set<IElement> sel            = selection.getSelection(me.mouseId);\r
658         IElement      topMostPick    = picks.isEmpty() ? null : picks.get(picks.size() - 1);\r
659         Set<IElement> elementsToDrag = new HashSet<IElement>();\r
660         this.elementsToDrag = elementsToDrag;\r
661 \r
662         if (!Collections.disjoint(sel, picks)) {\r
663             elementsToDrag.addAll(sel);\r
664         } else {\r
665             if (topMostPick != null && (sel.isEmpty() || !sel.contains(topMostPick))) {\r
666                 selection.setSelection(me.mouseId, topMostPick);\r
667                 sel = selection.getSelection(me.mouseId);\r
668                 elementsToDrag.addAll(sel);\r
669             }\r
670         }\r
671 \r
672         // Drag Elements\r
673         if (!elementsToDrag.isEmpty() && hasElementDnDDrag()) {\r
674             // To Be Implemented in the next Diagram data model.\r
675         } else {\r
676             if (!anyModifierPressed && !elementsToDrag.isEmpty() && hasElementDrag()) {\r
677                 // Connections are not translatable, re-routing is in RouteGraphNode.\r
678                 boolean onlyConnections = onlyConnections(elementsToDrag);\r
679                 if (!onlyConnections) {\r
680                     ICanvasParticipant tm = createTranslateTool(me.mouseId, me.startCanvasPos, curCanvasPos, elementsToDrag);\r
681                     if (tm != null) {\r
682                         getContext().add(tm);\r
683                         return !onlyConnections;\r
684                     }\r
685                 }\r
686             }\r
687         }\r
688 \r
689         return false;\r
690     }\r
691 \r
692     /**\r
693      * Always invoked after after {@link #handleDrag(MouseDragBegin)} and scene\r
694      * graph event handling to prevent the box selection mode from being\r
695      * initiated before scene graph nodes have a chance to react.\r
696      * \r
697      * <p>\r
698      * Note that this method assumes that <code>elementsToDrag</code> and\r
699      * <code>curCanvasPos</code> are already set by\r
700      * {@link #handleDrag(MouseDragBegin)}.\r
701      * \r
702      * @param me\r
703      * @return\r
704      */\r
705     @EventHandler(priority = BOX_SELECT_PRIORITY)\r
706     public boolean handleBoxSelect(MouseDragBegin me) {\r
707         if (!hasBoxSelect()) return false;\r
708         if (me.button != MouseEvent.LEFT_BUTTON) return false;\r
709         if (getToolMode() != Hints.POINTERTOOL) return false;\r
710         if (hasToolMode(me.mouseId)) return false;\r
711 \r
712         boolean nonSelectionModifierPressed = me.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK\r
713                 ^ (MouseEvent.SHIFT_MASK /*| MouseEvent.CTRL_MASK*/));\r
714         if (nonSelectionModifierPressed)\r
715             return false;\r
716 \r
717         if (!nonSelectionModifierPressed && elementsToDrag.isEmpty()) {\r
718             // Box Select\r
719             ICanvasParticipant bsm = createBoxSelectTool(me.mouseId, me.startCanvasPos, curCanvasDragPos, me.button, boxSelectMode);\r
720             if (bsm != null)\r
721                 getContext().add(bsm);\r
722         }\r
723 \r
724         return false;\r
725     }\r
726 \r
727     private static boolean onlyConnections(Set<IElement> elements) {\r
728         for (IElement e : elements)\r
729             if (!e.getElementClass().containsClass(ConnectionHandler.class))\r
730                 return false;\r
731         return true;\r
732     }\r
733 \r
734     private IElement rotatingPick(int selectionId, List<IElement> pickables) {\r
735         Set<IElement> sel = selection.getSelection(selectionId);\r
736         return rotatingPick(sel, pickables);\r
737     }\r
738 \r
739     private IElement rotatingPick(Set<IElement> sel, List<IElement> pickables) {\r
740         int earliestIndex = pickables.size();\r
741         for (int i = pickables.size() - 1; i >= 0; --i) {\r
742             if (sel.contains(pickables.get(i))) {\r
743                 earliestIndex = i;\r
744                 break;\r
745             }\r
746         }\r
747         if (earliestIndex == 0)\r
748             earliestIndex = pickables.size();\r
749         IElement selectedPick = pickables.get(earliestIndex - 1);\r
750         return selectedPick;\r
751     }\r
752 \r
753     /**\r
754      * Is mouse in some kind of mode?\r
755      * @param mouseId\r
756      * @return\r
757      */\r
758     boolean hasToolMode(int mouseId) {\r
759         for (AbstractMode am : getContext().getItemsByClass(AbstractMode.class))\r
760             if (am.mouseId==mouseId) return true;\r
761         return false;\r
762     }\r
763 \r
764     boolean hasToolMode(IToolMode mode) {\r
765         return ObjectUtils.objectEquals(mode, getToolMode());\r
766     }\r
767 \r
768     boolean hasToolMode(IToolMode... modes) {\r
769         IToolMode current = getToolMode();\r
770         if (current == null)\r
771             return false;\r
772         for (IToolMode mode : modes)\r
773             if (current.equals(mode))\r
774                 return true;\r
775         return false;\r
776     }\r
777 \r
778     protected IToolMode getToolMode() {\r
779         return getHint(Hints.KEY_TOOL);\r
780     }\r
781 \r
782     /// is box select enabled\r
783     protected boolean hasBoxSelect() {\r
784         return boxSelect;\r
785     }\r
786 \r
787     /// is click select enabled\r
788     protected boolean hasClickSelect() {\r
789         return clickSelect;\r
790     }\r
791 \r
792     /// is double click edit enabled\r
793     protected boolean hasDoubleClickEdit() {\r
794         return doubleClickEdit;\r
795     }\r
796 \r
797     // is element drag enabled\r
798     protected boolean hasElementDrag() {\r
799         return dragElement;\r
800     }\r
801 \r
802     // is element drag enabled\r
803     protected boolean hasElementDnDDrag() {\r
804         return dndDragElement;\r
805     }\r
806 \r
807     // is connect enabled\r
808     protected boolean connects() {\r
809         return connect;\r
810     }\r
811 \r
812     public double getPickDistance() {\r
813         Double pickDistance = getHint(KEY_PICK_DISTANCE);\r
814         return pickDistance == null ? PICK_DIST : Math.max(pickDistance, 0);\r
815     }\r
816 \r
817     public PickPolicy getBoxSelectMode() {\r
818         return boxSelectMode;\r
819     }\r
820 \r
821     public void setBoxSelectMode(PickPolicy boxSelectMode) {\r
822         this.boxSelectMode = boxSelectMode;\r
823     }\r
824 \r
825     public void setSelectionEnabled(boolean select) {\r
826         this.clickSelect = select;\r
827         this.boxSelect = select;\r
828         if(select == false) { // Clear all selections if select is disabled\r
829             final int[] ids = selection.getSelectionIds();\r
830             if(ids.length > 0) {\r
831                 ThreadUtils.asyncExec(getContext().getThreadAccess(), new Runnable() {\r
832                     @Override\r
833                     public void run() {\r
834                         for(int id : ids)\r
835                             selection.clear(id);\r
836                         getContext().getContentContext().setDirty();\r
837                     }\r
838                 });\r
839             }\r
840         }\r
841     }\r
842 \r
843     boolean anyModifierPressed(MouseEvent e) {\r
844         return e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK);\r
845     }\r
846 \r
847     boolean connectToolModifiersPressed(int stateMask) {\r
848         return (stateMask & (MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) != 0;\r
849     }\r
850 \r
851     static {\r
852         LINE10 = new Path2D.Double();\r
853         LINE10.moveTo(0, 0);\r
854         LINE10.lineTo(10, 0);\r
855         LINE10.lineTo(7, -3);\r
856         LINE10.moveTo(10, 0);\r
857         LINE10.lineTo(7, 3);\r
858 \r
859         LINE15 = new Path2D.Double();\r
860         LINE15.moveTo(0, 0);\r
861         LINE15.lineTo(15, 0);\r
862         LINE15.lineTo(12, -3);\r
863         LINE15.moveTo(15, 0);\r
864         LINE15.lineTo(12, 3);\r
865 \r
866         LINE20 = new Path2D.Double();\r
867         LINE20.moveTo(0, 0);\r
868         LINE20.lineTo(20, 0);\r
869         LINE20.lineTo(17, -3);\r
870         LINE20.moveTo(20, 0);\r
871         LINE20.lineTo(17, 3);\r
872 \r
873     }\r
874 \r
875     // CUSTOMIZE\r
876 \r
877     protected ICanvasParticipant createConnectTool(TerminalInfo ti, int mouseId, Point2D startCanvasPos) {\r
878         return null;\r
879     }\r
880 \r
881     protected ICanvasParticipant createConnectToolWithTerminals(List<TerminalInfo> tis, int mouseId, Point2D startCanvasPos) {\r
882         return null;\r
883     }\r
884 \r
885     protected ICanvasParticipant createTranslateTool(int mouseId, Point2D startCanvasPos, Point2D curCanvasPos, Set<IElement> elementsToDrag) {\r
886         return new TranslateMode(startCanvasPos, curCanvasPos, mouseId, elementsToDrag);\r
887     }\r
888 \r
889     protected ICanvasParticipant createBoxSelectTool(int mouseId, Point2D startCanvasPos, Point2D curCanvasPos, int button, PickPolicy boxSelectMode) {\r
890         return new BoxSelectionMode(startCanvasPos, curCanvasPos, mouseId, button, boxSelectMode);\r
891     }\r
892 \r
893     /**\r
894      * A context listener for resetting tool mode back to pointer mode after the\r
895      * tracked participant has been removed.\r
896      */\r
897     protected class ToolModeResetter implements IContextListener<ICanvasParticipant> {\r
898         private ICanvasParticipant tracked;\r
899         public ToolModeResetter(ICanvasParticipant trackedParticipant) {\r
900             this.tracked = trackedParticipant;\r
901         }\r
902         @Override\r
903         public void itemAdded(IContext<ICanvasParticipant> sender, ICanvasParticipant item) {\r
904         }\r
905         @Override\r
906         public void itemRemoved(IContext<ICanvasParticipant> sender, ICanvasParticipant item) {\r
907             if (item == tracked) {\r
908                 sender.removeContextListener(this);\r
909                 if (!isRemoved() && !connectToolModifiersPressed(lastStateMask)) {\r
910                     temporarilyEnabledConnectTool = false;\r
911                     setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);\r
912                 }\r
913             }\r
914         }\r
915     }\r
916 \r
917 }\r