687731f4c2f9d8528ab40cc0c6189151672828f4
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / connection / RouteGraphNode.java
1 /*******************************************************************************
2  * Copyright (c) 2011 Association for Decentralized Information Management in
3  * 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.scenegraph.g2d.nodes.connection;
13
14 import java.awt.BasicStroke;
15 import java.awt.Color;
16 import java.awt.Graphics2D;
17 import java.awt.RenderingHints;
18 import java.awt.Shape;
19 import java.awt.Stroke;
20 import java.awt.event.KeyEvent;
21 import java.awt.geom.AffineTransform;
22 import java.awt.geom.Path2D;
23 import java.awt.geom.Point2D;
24 import java.awt.geom.Rectangle2D;
25 import java.lang.reflect.Constructor;
26 import java.util.Collection;
27 import java.util.Map;
28
29 import org.simantics.diagram.connection.RouteGraph;
30 import org.simantics.diagram.connection.RouteLine;
31 import org.simantics.diagram.connection.RouteLink;
32 import org.simantics.diagram.connection.RouteTerminal;
33 import org.simantics.diagram.connection.actions.IAction;
34 import org.simantics.diagram.connection.actions.IReconnectAction;
35 import org.simantics.diagram.connection.actions.MoveAction;
36 import org.simantics.diagram.connection.actions.ReconnectLineAction;
37 import org.simantics.diagram.connection.delta.RouteGraphDelta;
38 import org.simantics.diagram.connection.rendering.BasicConnectionStyle;
39 import org.simantics.diagram.connection.rendering.ConnectionStyle;
40 import org.simantics.diagram.connection.rendering.IRouteGraphRenderer;
41 import org.simantics.diagram.connection.rendering.StyledRouteGraphRenderer;
42 import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;
43 import org.simantics.diagram.connection.splitting.SplittedRouteGraph;
44 import org.simantics.scenegraph.INode;
45 import org.simantics.scenegraph.ISelectionPainterNode;
46 import org.simantics.scenegraph.g2d.G2DNode;
47 import org.simantics.scenegraph.g2d.G2DParentNode;
48 import org.simantics.scenegraph.g2d.IG2DNode;
49 import org.simantics.scenegraph.g2d.events.EventTypes;
50 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
51 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent;
52 import org.simantics.scenegraph.g2d.events.MouseEvent;
53 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
54 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
55 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
56 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
57 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
58 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
59 import org.simantics.scenegraph.g2d.events.command.Commands;
60 import org.simantics.scenegraph.g2d.nodes.GridNode;
61 import org.simantics.scenegraph.g2d.nodes.LinkNode;
62 import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Action;
63 import org.simantics.scenegraph.g2d.nodes.connection.HighlightActionPointsAction.Pick;
64 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
65 import org.simantics.scenegraph.utils.GeometryUtils;
66 import org.simantics.scenegraph.utils.InitValueSupport;
67 import org.simantics.scenegraph.utils.NodeUtil;
68
69 import gnu.trove.map.hash.THashMap;
70
71 /**
72  * @author Tuukka Lehtonen
73  */
74 public class RouteGraphNode extends G2DNode implements ISelectionPainterNode, InitValueSupport  {
75
76     private static final long       serialVersionUID = -917194130412280965L;
77
78     private static final double     TOLERANCE        = IAction.TOLERANCE;
79     private static final Stroke     SELECTION_STROKE = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
80     private static final Color      SELECTION_COLOR  = new Color(255, 0, 255, 96);
81
82     private static final HighlightActionPointsAction highlightActions = new HighlightActionPointsAction(null);
83
84     protected RouteGraph            rg;
85     protected IRouteGraphRenderer   baseRenderer;
86     protected IRouteGraphRenderer   renderer;
87     protected double                pickTolerance    = TOLERANCE;
88     protected boolean               editable         = true;
89     private boolean                 branchable       = true;
90
91     protected IRouteGraphListener   rgListener;
92     protected RouteGraphDelta       rgDelta;
93
94     protected transient double      mouseX;
95     protected transient double      mouseY;
96     protected transient Point2D     pt               = new Point2D.Double();
97
98     protected transient MoveAction  dragAction;
99     protected transient IAction     currentAction;
100     protected transient Rectangle2D bounds;
101
102     /**
103      * Dynamic color for connection rendering.
104      */
105     protected transient Color       dynamicColor;
106
107     /**
108      * Dynamic stroke for connection rendering.
109      */
110     protected transient Stroke      dynamicStroke;
111
112     protected transient Path2D      selectionPath    = new Path2D.Double();
113     protected transient Stroke      selectionStroke  = null;
114
115     protected transient boolean     highlightActionsEnabled = false;
116     protected transient AffineTransform lastViewTransform = null;
117
118     /**
119      * x = NaN is used to indicate that possible branch point should not be
120      * rendered but interaction has not ended yet.
121      */
122     protected transient Point2D     newBranchPointPosition = null;
123
124
125     protected transient Map<Object,ILineEndStyle> dynamicStyles = null;
126     
127     private transient boolean ignoreSelection = false;
128     
129     @Override
130     public void initValues() {
131         dynamicColor = null;
132         wrapRenderer();
133     }
134
135     public void setIgnoreSelection(boolean value) {
136         ignoreSelection = value;
137     }
138     
139     public boolean getIgnoreSelection() {
140         return ignoreSelection;
141     }  
142   
143     @PropertySetter("color")
144     @SyncField(value = {"dynamicColor"})
145     public void setDynamicColor(Color color) {
146         this.dynamicColor = color;
147         wrapRenderer();
148     }
149
150     @PropertySetter("width")
151     @SyncField("dynamicStroke")
152     public void setDynamicStroke(Stroke stroke) {
153         this.dynamicStroke = stroke;
154         wrapRenderer();
155         createSelectionStroke();
156     }
157     
158     @SyncField(value = {"dynamicStyles"})
159     public void setDynamicLineEnd(RouteTerminal terminal, ILineEndStyle style) {
160         if (dynamicStyles == null)
161                 dynamicStyles = new THashMap<Object, ILineEndStyle>();
162         terminal.setDynamicStyle(style);
163         if (terminal.getData() != null) {
164                 if (style != null)
165                         dynamicStyles.put(terminal.getData(),style);
166                 else
167                         dynamicStyles.remove(terminal.getData());
168         }
169     }
170     
171     private void updateLineEnds() {
172         if (dynamicStyles == null)
173                 return;
174         for (RouteTerminal t : rg.getTerminals()) {
175                 if (t.getData() == null)
176                         continue;
177                 ILineEndStyle dynamicStyle = dynamicStyles.get(t.getData());
178                 if (dynamicStyle != null)
179                         t.setDynamicStyle(dynamicStyle);
180         }
181     }
182
183     @SyncField(value = {"rg"})
184     public void setRouteGraph(RouteGraph graph) {
185         this.rg = graph;
186         updateLineEnds();
187         updateBounds();
188     }
189
190     @SyncField(value = {"rgDelta"})
191     public void setRouteGraphDelta(RouteGraphDelta delta) {
192         this.rgDelta = delta;
193     }
194
195     @SyncField(value = {"renderer"})
196     public void setRenderer(IRouteGraphRenderer renderer) {
197
198         this.baseRenderer = renderer;
199         wrapRenderer();
200
201         createSelectionStroke();      
202     }
203     
204     private void createSelectionStroke() {
205          BasicConnectionStyle style = tryGetStyle();
206          selectionStroke = null;
207          if (style != null) {
208              BasicStroke stroke = (BasicStroke) style.getLineStroke();
209              if (stroke != null) {
210                  float width = Math.max(stroke.getLineWidth() + 0.75f, stroke.getLineWidth()*1.3f);
211                  selectionStroke = new BasicStroke(width, BasicStroke.CAP_BUTT, stroke.getLineJoin());
212              }
213          } else {
214              selectionStroke = SELECTION_STROKE;
215          }
216     }
217     
218     private void wrapRenderer() {
219         
220         if(baseRenderer == null) {
221             renderer = null;
222             return;
223         }
224         
225         if(dynamicColor != null || dynamicStroke != null) {
226             BasicConnectionStyle baseStyle = (BasicConnectionStyle)tryGetStyle(baseRenderer);
227             try {
228                 Constructor<? extends BasicConnectionStyle> c = baseStyle.getClass().getConstructor(Color.class, Color.class, double.class, Stroke.class, Stroke.class, double.class, double.class);
229                 renderer = new StyledRouteGraphRenderer(c.newInstance(
230                         dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),
231                                 baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),
232                                     dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(), 
233                                             dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),
234                                                     baseStyle.getDegeneratedLineLength(), baseStyle.getRounding()));
235             } catch (Exception e) {
236                 renderer = new StyledRouteGraphRenderer(new BasicConnectionStyle(
237                         dynamicColor != null ? dynamicColor : baseStyle.getLineColor(),
238                                 baseStyle.getBranchPointColor(), baseStyle.getBranchPointRadius(),
239                                     dynamicStroke != null ? dynamicStroke : baseStyle.getLineStroke(), 
240                                             dynamicStroke != null ? dynamicStroke : baseStyle.getRouteLineStroke(),
241                                                     baseStyle.getDegeneratedLineLength(), baseStyle.getRounding()));
242             }
243             
244             
245         } else {
246             renderer = baseRenderer;
247         }
248         
249     }
250
251     @SyncField(value = {"pickTolerance"})
252     public void setPickTolerance(double tolerance) {
253         this.pickTolerance = tolerance;
254     }
255
256     @SyncField(value = {"editable"})
257     public void setEditable(boolean editable) {
258         this.editable = editable;
259     }
260
261     @SyncField(value = {"branchable"})
262     public void setBranchable(boolean branchable) {
263         this.branchable = branchable;
264     }
265
266     public RouteGraph getRouteGraph() {
267         return rg;
268     }
269
270     public RouteGraphDelta getRouteGraphDelta() {
271         return rgDelta;
272     }
273
274     public IRouteGraphRenderer getRenderer() {
275         return renderer;
276     }
277
278     public boolean isEditable() {
279         return editable;
280     }
281
282     public boolean isBranchable() {
283         return branchable;
284     }
285     
286     public double getPickTolerance() {
287                 return pickTolerance;
288         }
289
290     /**
291      * When in client-server mode, listener is only set on the server side and
292      * fireRouteGraphChanged will tell it when rg has changed.
293      * 
294      * @param listener
295      */
296     public void setRouteGraphListener(IRouteGraphListener listener) {
297         this.rgListener = listener;
298     }
299
300     /**
301      * @param before
302      * @param after
303      * @return <code>true</code> if changes were fired
304      */
305     private boolean setRouteGraphAndFireChanges(RouteGraph before, RouteGraph after) {
306         RouteGraphDelta delta = new RouteGraphDelta(before, after);
307         if (!delta.isEmpty()) {
308             setRouteGraph(after);
309             setRouteGraphDelta(delta);
310             fireRouteGraphChanged(before, after, delta);
311             return true;
312         }
313         return false;
314     }
315
316     @ServerSide
317     protected void fireRouteGraphChanged(RouteGraph before, RouteGraph after, RouteGraphDelta delta) {
318         if (rgListener != null) {
319             RouteGraphChangeEvent event = new RouteGraphChangeEvent(this, before, after, delta);
320             rgListener.routeGraphChanged(event);
321         }
322     }
323
324     public void showBranchPoint(Point2D p) {
325         newBranchPointPosition = p;
326     }
327
328     @Override
329     public void init() {
330         super.init();
331         addEventHandler(this);
332     }
333
334     @Override
335     public void cleanup() {
336         rgListener = null;
337         removeEventHandler(this);
338         super.cleanup();
339     }
340
341     protected boolean isSelected() {
342         return NodeUtil.isSelected(this, 1);
343     }
344
345     @Override
346     public void render(Graphics2D g) {
347         if (renderer == null)
348             return;
349
350         AffineTransform ot = null;
351         AffineTransform t = getTransform();
352         if (t != null && !t.isIdentity()) {
353             ot = g.getTransform();
354             g.transform(getTransform());
355         }
356
357         Object aaHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
358         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
359
360         boolean selected = ignoreSelection ? false : NodeUtil.isSelected(this, 1);
361
362         rg.updateTerminals();
363
364         if (currentAction != null) {
365             currentAction.render(g, renderer, mouseX, mouseY);
366         } else {
367             if (selected && selectionStroke != null) {
368                 selectionPath.reset();
369                 rg.getPath2D(selectionPath);
370                 Shape selectionShape = selectionStroke.createStrokedShape(selectionPath);
371                 g.setColor(SELECTION_COLOR);
372                 g.fill(selectionShape);
373             }
374
375             renderer.render(g, rg);
376             if(selected)
377                 renderer.renderGuides(g, rg);
378
379             if (selected && highlightActionsEnabled) {
380                 // Needed for performing actions in #mouseClicked
381                 this.lastViewTransform = g.getTransform();
382                 highlightActions.setRouteGraph(rg);
383                 highlightActions.render(g, renderer, mouseX, mouseY);
384                 highlightActions.setRouteGraph(null);
385             }
386         }
387
388         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, aaHint);
389
390         if (editable && branchable && newBranchPointPosition != null && !Double.isNaN(newBranchPointPosition.getX())) {
391             ConnectionStyle style = tryGetStyle();
392             if(style != null)
393                 style.drawBranchPoint(g, newBranchPointPosition.getX(), newBranchPointPosition.getY());
394         }
395
396         if (ot != null)
397             g.setTransform(ot);
398     }
399     
400     private BasicConnectionStyle tryGetStyle() {
401         return tryGetStyle(renderer);
402     }
403
404     private BasicConnectionStyle tryGetStyle(IRouteGraphRenderer renderer) {
405         if (renderer instanceof StyledRouteGraphRenderer) {
406             ConnectionStyle cs = ((StyledRouteGraphRenderer) renderer).getStyle();
407             if (cs instanceof BasicConnectionStyle)
408                 return (BasicConnectionStyle) cs;
409         }
410         return null;
411     }
412
413     private double getSelectionStrokeWidth() {
414         if (selectionStroke instanceof BasicStroke) {
415             BasicStroke bs = (BasicStroke) selectionStroke;
416             return bs.getLineWidth();
417         }
418         return 1.0;
419     }
420
421     @Override
422     public Rectangle2D getBoundsInLocal() {
423         return bounds;
424     }
425
426     protected void updateBounds() {
427         Rectangle2D r = this.bounds;
428         if (r == null)
429             r = new Rectangle2D.Double();
430         this.bounds = calculateBounds(r);
431
432         // Need to expand to take stroke width into account.
433         double sw = getSelectionStrokeWidth() / 2;
434         GeometryUtils.expandRectangle(this.bounds, sw, sw);
435     }
436
437     protected Rectangle2D calculateBounds(Rectangle2D rect) {
438         RouteGraph rg = this.rg;
439         if (currentAction instanceof MoveAction)
440             rg = ((MoveAction) currentAction).getRouteGraph();
441         rg.getBounds(rect);
442         return rect;
443     }
444
445     protected void getMouseLocalPos(MouseEvent e) {
446         //System.out.println("m: " + e.controlPosition);
447         pt.setLocation(e.controlPosition);
448         //System.out.println("parent: " + pt);
449         pt = NodeUtil.worldToLocal(this, pt, pt);
450         //System.out.println("local: " + pt);
451         mouseX = pt.getX();
452         mouseY = pt.getY();
453     }
454
455     @Override
456     protected boolean mouseDragged(MouseDragBegin e) {
457         if (dragAction != null && !e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.button == MouseEvent.LEFT_BUTTON) {
458             currentAction = dragAction;
459             dragAction = null;
460         }
461         return updateCurrentAction(e, true);
462     }
463
464     @Override
465     protected boolean mouseMoved(MouseMovedEvent e) {
466         //System.out.println("mouse moved: " + e);
467
468         // Handle connection branching visualization.
469         getMouseLocalPos(e);
470         if (newBranchPointPosition == null && e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK)) {
471             if (bounds.contains(mouseX, mouseY)) {
472                 newBranchPointPosition = new Point2D.Double(Double.NaN, Double.NaN);
473             }
474         }
475         if (newBranchPointPosition != null) {
476             RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);
477             if (line != null) {
478                 newBranchPointPosition.setLocation(mouseX, mouseY);
479                 SplittedRouteGraph.snapToLine(newBranchPointPosition, line);
480                 repaint();
481             } else if (!Double.isNaN(newBranchPointPosition.getX())) {
482                 newBranchPointPosition.setLocation(Double.NaN, Double.NaN);
483                 repaint();
484             }
485         }
486
487         // Make sure that highlight action rendering is according to mouse hover.
488         if (highlightActionsEnabled) {
489             if (NodeUtil.isSelected(this, 1)) {
490                 repaint();
491             }
492         }
493
494         return updateCurrentAction(e, false);
495     }
496
497     protected boolean updateCurrentAction(MouseEvent e, boolean updateMousePos) {
498         boolean oldHighlight = highlightActionsEnabled;
499         highlightActionsEnabled = e.hasAllModifiers(MouseEvent.CTRL_MASK);
500         if (oldHighlight != highlightActionsEnabled)
501             repaint();
502
503         if (currentAction != null) {
504             if (updateMousePos)
505                 getMouseLocalPos(e);
506             updateBounds();
507
508             // Repaint, but only if absolutely necessary.
509             if (currentAction instanceof MoveAction || bounds.contains(mouseX, mouseY))
510                 repaint();
511
512             return true;
513         }
514         return false;
515     }
516
517     @Override
518     protected boolean mouseClicked(MouseClickEvent e) {
519         if (!editable)
520             return false;
521
522         if (e.button == MouseEvent.LEFT_BUTTON) {
523             if (isSelected() && highlightActionsEnabled) {
524                 // Reconnection / segment deletion only available for branched connections. 
525                 if (rg.getTerminals().size() > 2) {
526                     Pick pick = highlightActions.pickAction(rg, lastViewTransform, mouseX, mouseY);
527                     if (pick.hasAction(Action.REMOVE)) {
528                         RemoveLineAction remove = RemoveLineAction.perform(rg, pick.line.getLine(), mouseX, mouseY);
529                         if (remove != null) {
530                             setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());
531                             repaint();
532                             return true;
533                         }
534                     }
535                     if (pick.hasAction(Action.RECONNECT)) {
536                         currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);
537                         if (currentAction != null) {
538                             repaint();
539                         }
540                     }
541                 }
542             }
543         }
544
545         return false;
546     }
547
548     @Override
549     protected boolean mouseButtonPressed(MouseButtonPressedEvent e) {
550         if (!editable)
551             return false;
552
553         if (e.button == MouseEvent.LEFT_BUTTON) {
554             // Visualize new branch point no longer.
555             newBranchPointPosition = null;
556
557             getMouseLocalPos(e);
558             dragAction = null;
559 //          if(currentAction instanceof HighlightActionPointsAction) {
560 //              RemoveLineAction remove = RemoveLineAction.perform(rg, mouseX, mouseY);
561 //              if (remove != null) {
562 //                  setRouteGraphAndFireChanges(remove.getOriginalRouteGraph(), remove.getRouteGraph());
563 //                  repaint();
564 //              } else {
565 //                  currentAction = ReconnectLineAction.create(rg, mouseX, mouseY);
566 //                  if (currentAction != null)
567 //                      repaint();
568 //              }
569 //          }
570 //          else
571             if(currentAction instanceof IReconnectAction) {
572                 RouteGraph originalRg = rg.copy();
573                 ((IReconnectAction)currentAction).finish(mouseX, mouseY);
574                 currentAction = null;
575
576                 setRouteGraphAndFireChanges(originalRg, rg);
577
578                 currentAction = null;
579                 repaint();
580                 return true;
581             }
582             else {
583                 if (!allowConnectionRerouting()) {
584                     return false;
585                 }
586                 //System.out.println("move action");
587                 dragAction = SnappingMoveAction.create(rg, mouseX, mouseY, pickTolerance, moveFilter, getSnapAdvisor());
588                 //System.out.println("DRAG ACTION: " + dragAction);
589             }
590
591             //System.out.println(this + " NEW action: " + currentAction);
592             if (currentAction != null)
593                 return true;
594         }
595         return false;
596     }
597
598     /**
599      * Checks the selections data node in the scene graph for any links 
600      * @return
601      */
602     private boolean allowConnectionRerouting() {
603         final int maxOtherNodesSelected = 1;
604
605         INode selections = NodeUtil.tryLookup(this, "selections");
606         if (!(selections instanceof G2DParentNode))
607             return true;
608         G2DParentNode p = (G2DParentNode) selections;
609         for (IG2DNode selection : p.getNodes()) {
610             if (!(selection instanceof G2DParentNode))
611                 continue;
612
613             G2DParentNode sp = (G2DParentNode) selection;
614             Collection<IG2DNode> links = sp.getNodes();
615             if (links.isEmpty())
616                 return true;
617             int othersSelected = 0;
618             for (IG2DNode link : links) {
619                 if (link instanceof LinkNode) {
620                     INode node = ((LinkNode) link).getDelegate();
621                     if (!NodeUtil.isParentOf(node, this)) {
622                         othersSelected++;
623                         if (othersSelected > maxOtherNodesSelected)
624                             return false;
625                     }
626                 }
627             }
628             if (othersSelected > maxOtherNodesSelected)
629                 return false;
630         }
631         return true;
632     }
633
634     protected ISnapAdvisor getSnapAdvisor() {
635         GridNode grid = lookupNode(GridNode.GRID_NODE_ID, GridNode.class);
636         return grid != null ? grid.getSnapAdvisor() : null;
637     }
638
639     MoveAction.TargetFilter moveFilter = new MoveAction.TargetFilter() {
640         @Override
641         public boolean accept(Object target) {
642             return (target instanceof RouteLine) || (target instanceof RouteLink);
643         }
644     };
645
646     @Override
647     protected boolean handleCommand(CommandEvent e) {
648         /*if (Commands.DELETE.equals(e.command)) {
649             Object target = rg.pick(mouseX, mouseY, pickTolerance);
650             return deleteTarget(target);
651         } else if (Commands.SPLIT_CONNECTION.equals(e.command)) {
652             Object target = rg.pick(mouseX, mouseY, pickTolerance);
653             return splitTarget(target);
654         } else */
655         if (Commands.CANCEL.equals(e.command)) {
656             return cancelCurrentAction();
657         }
658         return false;
659     }
660
661     protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) {
662         if (currentAction instanceof MoveAction) {
663             MoveAction move = (MoveAction) currentAction;
664             RouteGraph originalRg = rg.copy();
665             move.finish(mouseX, mouseY);
666
667             setRouteGraphAndFireChanges(originalRg, rg);
668
669             currentAction = null;
670             repaint();
671             return true;
672         }
673         return false;
674     }
675
676     @Override
677     protected boolean keyPressed(KeyPressedEvent e) {
678         if (!editable)
679             return false;
680
681         if (!e.hasAnyModifier(MouseEvent.ALL_MODIFIERS_MASK) && e.keyCode == KeyEvent.VK_S) {
682             Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES | RouteGraph.PICK_TRANSIENT_LINES);
683             return splitTarget(target);
684         }
685         else if (!e.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK | MouseEvent.CTRL_MASK) && (e.keyCode == KeyEvent.VK_R || e.keyCode == KeyEvent.VK_D)) {
686             Object target = rg.pick(mouseX, mouseY, pickTolerance, RouteGraph.PICK_PERSISTENT_LINES);
687             return deleteTarget(target);
688         }
689         else if (e.keyCode == KeyEvent.VK_ESCAPE) {
690             return cancelCurrentAction();
691         }
692 //        else if (e.keyCode == KeyEvent.VK_D) {
693 //            if (target instanceof RouteTerminal) {
694 //                RouteTerminal terminal = (RouteTerminal) target;
695 //                RouteGraph before = rg.copy();
696 //                rg.toggleDirectLines(terminal);
697 //                setRouteGraphAndFireChanges(before, rg);
698 //                repaint();
699 //            }
700 //        }
701 //        else if (target != null && e.getKeyCode() == KeyEvent.VK_P) {
702 //            rg.print();
703 //        }
704         else if (e.keyCode == KeyEvent.VK_CONTROL) {
705             highlightActionsEnabled = true;
706             repaint();
707         }
708         else if (e.keyCode == KeyEvent.VK_ALT) {
709             // Begin connection branching visualization.
710             RouteLine line = rg.pickLine(mouseX, mouseY, pickTolerance);
711             if (branchable && line != null) {
712                 newBranchPointPosition = new Point2D.Double(mouseX, mouseY);
713                 SplittedRouteGraph.snapToLine(newBranchPointPosition, line);
714                 repaint();
715             }
716         }
717
718         return false;
719     }
720
721     @Override
722     protected boolean keyReleased(KeyReleasedEvent e) {
723         if (e.keyCode == KeyEvent.VK_ALT) {
724             // End connection branching visualization.
725             if (newBranchPointPosition != null) {
726                 newBranchPointPosition = null;
727                 repaint();
728             }
729         }
730         if (e.keyCode == KeyEvent.VK_CONTROL) {
731             highlightActionsEnabled = false;
732             repaint();
733         }
734         return false;
735     }
736
737
738     private boolean cancelCurrentAction() {
739         if (currentAction != null) {
740             currentAction = null;
741             repaint();
742             return true;
743         }
744         return false;
745     }
746
747     private boolean splitTarget(Object target) {
748         if (target instanceof RouteLine) {
749             RouteLine rLine = (RouteLine)target;
750             RouteGraph before = rg.copy();
751             rg.split(rLine, rLine.isHorizontal() ? mouseX : mouseY);
752             setRouteGraphAndFireChanges(before, rg);
753             repaint();
754             return true;
755         }
756         return false;
757     }
758
759     private boolean deleteTarget(Object target) {
760         boolean changed = false;
761         if (target instanceof RouteLine) {
762             RouteLine line = (RouteLine) target;
763             RouteGraph before = rg.copy();
764             rg.merge(line);
765             changed = setRouteGraphAndFireChanges(before, rg);
766         }
767         else if (target instanceof RouteLink) {
768             RouteGraph before = rg.copy();
769             rg.deleteCorner((RouteLink) target);
770             changed = setRouteGraphAndFireChanges(before, rg);
771         }
772 //        else if (target instanceof RouteTerminal) {
773 //            RouteGraph before = rg.copy();
774 //            rg.remove((RouteTerminal) target);
775 //            changed = setRouteGraphAndFireChanges(before, rg);
776 //        }
777         if (changed)
778             repaint();
779         return changed;
780     }
781
782     /**
783      * A version of MoveAction that snaps movements using the specified
784      * ISnapAdvisor.
785      */
786     static class SnappingMoveAction extends MoveAction {
787
788         private ISnapAdvisor snapAdvisor;
789         private Point2D      point = new Point2D.Double();
790
791         public SnappingMoveAction(RouteGraph rg, Object target, ISnapAdvisor snapAdvisor) {
792             super(rg, target);
793             this.snapAdvisor = snapAdvisor;
794         }
795
796         protected void move(RouteGraph rg, Object target, double x, double y) {
797             point.setLocation(x, y);
798             snapAdvisor.snap(point);
799             super.move(rg, target, point.getX(), point.getY());
800         }
801
802         public static MoveAction create(RouteGraph rg, double x, double y, double tolerance, TargetFilter filter, ISnapAdvisor snapAdvisor) {
803             Object target = rg.pick(x, y, tolerance, RouteGraph.PICK_LINES | RouteGraph.PICK_INTERIOR_POINTS);
804             if (target != null && (filter == null || filter.accept(target))) {
805                 if (snapAdvisor != null)
806                     return new SnappingMoveAction(rg, target, snapAdvisor);
807                 return new MoveAction(rg, target);
808             }
809             return null;
810         }
811
812     }
813
814     @Override
815     public int getEventMask() {
816         return EventTypes.CommandMask | EventTypes.KeyMask | EventTypes.MouseMask;
817     }
818
819 }