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