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