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