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