Handle SelectionOutline interface implementation in SCLScenegraph
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / diagram / participant / ElementPainter.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2018 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  *     Semantum Oy - GitLab issue #66
12  *******************************************************************************/
13 package org.simantics.g2d.diagram.participant;
14
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Composite;
18 import java.awt.Shape;
19 import java.awt.geom.AffineTransform;
20 import java.awt.geom.Point2D;
21 import java.awt.geom.Rectangle2D;
22 import java.nio.CharBuffer;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Random;
32 import java.util.Set;
33 import java.util.UUID;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.ConcurrentMap;
36 import java.util.function.Consumer;
37
38 import org.simantics.g2d.canvas.Hints;
39 import org.simantics.g2d.canvas.ICanvasContext;
40 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
41 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
42 import org.simantics.g2d.canvas.impl.HintReflection.HintListener;
43 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
44 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
45 import org.simantics.g2d.connection.handler.ConnectionHandler;
46 import org.simantics.g2d.diagram.DiagramHints;
47 import org.simantics.g2d.diagram.DiagramUtils;
48 import org.simantics.g2d.diagram.IDiagram;
49 import org.simantics.g2d.diagram.IDiagram.CompositionListener;
50 import org.simantics.g2d.diagram.handler.RelationshipHandler;
51 import org.simantics.g2d.diagram.handler.RelationshipHandler.Relation;
52 import org.simantics.g2d.diagram.handler.TransactionContext;
53 import org.simantics.g2d.diagram.handler.TransactionContext.Transaction;
54 import org.simantics.g2d.diagram.handler.TransactionContext.TransactionListener;
55 import org.simantics.g2d.element.ElementClass;
56 import org.simantics.g2d.element.ElementHints;
57 import org.simantics.g2d.element.ElementUtils;
58 import org.simantics.g2d.element.IElement;
59 import org.simantics.g2d.element.SceneGraphNodeKey;
60 import org.simantics.g2d.element.handler.BendsHandler;
61 import org.simantics.g2d.element.handler.Children;
62 import org.simantics.g2d.element.handler.Children.ChildEvent;
63 import org.simantics.g2d.element.handler.Children.ChildListener;
64 import org.simantics.g2d.element.handler.FillColor;
65 import org.simantics.g2d.element.handler.Outline;
66 import org.simantics.g2d.element.handler.OutlineColorSpec;
67 import org.simantics.g2d.element.handler.Parent;
68 import org.simantics.g2d.element.handler.SceneGraph;
69 import org.simantics.g2d.element.handler.SelectionOutline;
70 import org.simantics.g2d.element.handler.SelectionSpecification;
71 import org.simantics.g2d.element.handler.StrokeSpec;
72 import org.simantics.g2d.element.handler.TerminalTopology;
73 import org.simantics.g2d.element.handler.Transform;
74 import org.simantics.g2d.layers.ILayer;
75 import org.simantics.g2d.layers.ILayersEditor;
76 import org.simantics.g2d.layers.ILayersEditor.ILayersEditorListener;
77 import org.simantics.g2d.participant.TransformUtil;
78 import org.simantics.g2d.scenegraph.SceneGraphConstants;
79 import org.simantics.g2d.utils.ElementNodeBridge;
80 import org.simantics.g2d.utils.TopologicalSelectionExpander;
81 import org.simantics.scenegraph.INode;
82 import org.simantics.scenegraph.Node;
83 import org.simantics.scenegraph.g2d.G2DParentNode;
84 import org.simantics.scenegraph.g2d.G2DSceneGraph;
85 import org.simantics.scenegraph.g2d.IG2DNode;
86 import org.simantics.scenegraph.g2d.nodes.ConnectionNode;
87 import org.simantics.scenegraph.g2d.nodes.DataNode;
88 import org.simantics.scenegraph.g2d.nodes.LinkNode;
89 import org.simantics.scenegraph.g2d.nodes.SelectionNode;
90 import org.simantics.scenegraph.g2d.nodes.ShapeNode;
91 import org.simantics.scenegraph.g2d.nodes.SingleElementNode;
92 import org.simantics.scenegraph.g2d.nodes.UnboundedNode;
93 import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode;
94 import org.simantics.scenegraph.utils.ColorUtil;
95 import org.simantics.scenegraph.utils.GeometryUtils;
96 import org.simantics.scenegraph.utils.NodeUtil;
97 import org.simantics.utils.datastructures.collections.CollectionUtils;
98 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
99 import org.simantics.utils.datastructures.hints.IHintContext.Key;
100 import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;
101 import org.simantics.utils.datastructures.hints.IHintListener;
102 import org.simantics.utils.datastructures.hints.IHintObservable;
103
104 /**
105  * A diagram participant that keeps a diagram and its elements synchronized with
106  * the active canvas scene graph.
107  * 
108  * <p>
109  * Responsibilities include:
110  * <ul>
111  * <li>ensure that the scene graph contains a {@link SingleElementNode} instance
112  * for each diagram element</li>
113  * <li>ensure that the scene graph node order matches the diagram element order</li>
114  * <li>ensure that the scene graph contains a {@link SelectionNode} under the
115  * element instance nodes for each selected node. TODO: maybe try getting
116  * selection out of here into a different participant, but without cloning the
117  * entire listening/element<->scene graph updating infrastructure.</li>
118  * <li></li>
119  * </ul>
120  * 
121  * @author Tuukka Lehtonen
122  * 
123  * @see ElementNodeBridge
124  */
125 public class ElementPainter extends AbstractDiagramParticipant implements CompositionListener, TransactionListener, ChildListener {
126
127     public static final Key      KEY_SELECTION_PROVIDER = new KeyOf(ISelectionProvider.class);
128
129     public static final int SELECTION_PAINT_PRIORITY    = 100;
130
131     public static final Key KEY_SELECTION_FRAME_COLOR   = new KeyOf(Color.class, "SELECTION_FRAME_COLOR");
132     public static final Key KEY_SELECTION_CONTENT_COLOR = new KeyOf(Color.class, "SELECTION_CONTENT_COLOR");
133
134
135     /**
136      * Implement to customize the way a selection is visualized by
137      * ElementPainter.
138      */
139     public static interface ISelectionProvider {
140         public void init(int selectionId, final IElement e, final G2DParentNode parentNode, final String nodeId,
141                 final AffineTransform transform, final Rectangle2D bounds, final Color color);
142     }
143
144     private static final boolean DEBUG                  = false;
145     private static final boolean NODE_TO_ELEMENT_MAPPING = true;
146
147     public static final int      ELEMENT_PAINT_PRIORITY = 10;
148
149     @Reference
150     ZOrderHandler zOrderHandler;
151
152     @Dependency
153     TransformUtil util;
154
155     @Dependency
156     Selection selection;
157
158     SingleElementNode diagramParent;
159     RTreeNode elementParent;
160
161     ElementPainterConfiguration cfg;
162
163     /**
164      * Internally reused to avert constant reallocation.
165      */
166     private transient List<Relation> relations = new ArrayList<Relation>(4);
167     /**
168      * Internally reused to avert constant reallocation.
169      */
170     private transient Set<IElement> relatedElements = new HashSet<IElement>(8);
171
172     public ElementPainter() {
173         this(true);
174     }
175
176     public ElementPainter(boolean paintSelectionFrames) {
177         this(new ElementPainterConfiguration().paintSelectionFrames(paintSelectionFrames));
178     }
179
180     public ElementPainter(ElementPainterConfiguration cfg) {
181         this.cfg = cfg;
182     }
183
184     @Override
185     public void addedToContext(ICanvasContext ctx) {
186         super.addedToContext(ctx);
187         if (zOrderHandler != null) {
188             zOrderHandler.addOrderListener(zOrderListener);
189         }
190     }
191
192     @Override
193     public void removedFromContext(ICanvasContext ctx) {
194         if (zOrderHandler != null) {
195             zOrderHandler.removeOrderListener(zOrderListener);
196         }
197         selections.clear();
198         super.removedFromContext(ctx);
199     }
200
201     @Override
202     protected void onDiagramSet(IDiagram newValue, IDiagram oldValue) {
203         if (oldValue == newValue)
204             return;
205
206         if (oldValue != null) {
207             Map<INode, IElement> nodeToElementMap = oldValue.removeHint(DiagramHints.NODE_TO_ELEMENT_MAP);
208             for (IElement e : oldValue.getElements()) {
209                 removeElement(oldValue, e, nodeToElementMap);
210             }
211
212             oldValue.removeCompositionListener(this);
213             oldValue.removeKeyHintListener(Hints.KEY_DIRTY, diagramHintListener);
214             oldValue.removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, diagramHintListener);
215
216             ILayersEditor layers = oldValue.getHint(DiagramHints.KEY_LAYERS_EDITOR);
217             if (layers != null) {
218                 layers.removeListener(layersListener);
219             }
220
221             for (TransactionContext tc : oldValue.getDiagramClass().getItemsByClass(TransactionContext.class)) {
222                 tc.removeTransactionListener(oldValue, this);
223             }
224         }
225
226         if (newValue != null) {
227             diagram.removeHint(DiagramHints.NODE_TO_ELEMENT_MAP);
228             Map<INode, IElement> nodeElementMap = NODE_TO_ELEMENT_MAPPING ? new HashMap<>() : null;
229             if (nodeElementMap != null)
230                 diagram.setHint(DiagramHints.NODE_TO_ELEMENT_MAP, nodeElementMap);
231
232             for (IElement e : newValue.getElements()) {
233                 addElement(newValue, e, false, nodeElementMap);
234             }
235
236             newValue.addCompositionListener(this);
237             newValue.addKeyHintListener(Hints.KEY_DISABLE_PAINTING, diagramHintListener);
238             newValue.addKeyHintListener(Hints.KEY_DIRTY, diagramHintListener);
239
240             ILayersEditor layers = newValue.getHint(DiagramHints.KEY_LAYERS_EDITOR);
241             if (layers != null) {
242                 layers.addListener(layersListener);
243             }
244
245             for (TransactionContext tc : newValue.getDiagramClass().getItemsByClass(TransactionContext.class)) {
246                 tc.addTransactionListener(newValue, this);
247             }
248         }
249
250         updateAll();
251     }
252
253     @SGInit
254     public void initSG(G2DParentNode parent) {
255         diagramParent = parent.addNode("elements_"+Node.IDCOUNTER, UnboundedNode.class);
256         diagramParent.setZIndex(ELEMENT_PAINT_PRIORITY);
257         elementParent = diagramParent.addNode(SceneGraphConstants.SPATIAL_ROOT_NODE_NAME, RTreeNode.class);
258         elementParent.setLookupId(SceneGraphConstants.SPATIAL_ROOT_NODE_ID);
259         elementParent.setZIndex(0);
260     }
261
262     @SGCleanup
263     public void cleanupSG() {
264         diagramParent.remove();
265         elementParent = null;
266         diagramParent = null;
267     }
268
269     public INode getDiagramElementParentNode() {
270         return elementParent;
271     }
272
273     // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
274     // Element z-order listening and update logic
275     // ------------------------------------------------------------------------
276
277     ZOrderListener zOrderListener = new ZOrderListener() {
278         @Override
279         public void orderChanged(IDiagram diagram) {
280             if (diagram == ElementPainter.this.diagram) {
281                 updateZOrder(diagram, ElementHints.KEY_SG_NODE);
282             }
283         }
284     };
285
286     protected static void updateZOrder(IDiagram diagram, Key elementSgNodeKey) {
287         int zIndex = 0;
288         for (IElement e : diagram.getElements()) {
289             Node node = e.getHint(elementSgNodeKey);
290             if (node instanceof IG2DNode) {
291                 ((IG2DNode) node).setZIndex(++zIndex);
292             }
293         }
294     }
295
296     // ------------------------------------------------------------------------
297     // Element z-order listening and update logic end
298     // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
299
300
301     // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
302     // Layer configuration change listening and reaction logic
303     // ------------------------------------------------------------------------
304
305     ILayersEditorListener layersListener = new ILayersEditorListener() {
306         private void layersChanged() {
307             Object task = BEGIN("EP.layersChanged");
308             // Update visibility/focusability for each node only, do not reinitialize the graphics.
309             updateAllVisibility();
310             END(task);
311         }
312         @Override
313         public void layerRemoved(ILayer layer) {
314             layersChanged();
315         }
316         @Override
317         public void layerDeactivated(ILayer layer) {
318             layersChanged();
319         }
320         @Override
321         public void layerAdded(ILayer layer) {
322             layersChanged();
323         }
324         @Override
325         public void layerActivated(ILayer layer) {
326             layersChanged();
327         }
328         @Override
329         public void ignoreFocusChanged(boolean value) {
330                 ICanvasContext ctx = getContext();
331                 if(ctx == null) return;
332                 G2DSceneGraph sg = ctx.getSceneGraph();
333                 if(sg == null) return;
334                 sg.setGlobalProperty(G2DSceneGraph.IGNORE_FOCUS, value);
335         }
336         @Override
337         public void ignoreVisibilityChanged(boolean value) {
338             layersChanged();
339         }
340     };
341
342     protected void updateAllVisibility() {
343         // TODO: optimize, no node reinitialization
344         updateAll();
345     }
346
347     // ------------------------------------------------------------------------
348     // Layer configuration change listening and reaction logic
349     // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
350
351
352     // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
353     // Diagram/Element hint listeners
354     // ------------------------------------------------------------------------
355
356     class DiagramHintListener extends HintListenerAdapter {
357         @Override
358         public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
359             if (key == Hints.KEY_DISABLE_PAINTING) {
360                 if (diagramParent != null) {
361                     diagramParent.setVisible(!Boolean.TRUE.equals(newValue));
362                 }
363             } else if (key == Hints.KEY_DIRTY) {
364                 if (newValue == Hints.VALUE_Z_ORDER_CHANGED) {
365                     diagram.removeHint(Hints.KEY_DIRTY);
366
367                     if (DEBUG)
368                         System.out.println("Diagram z-order changed: " + diagram);
369
370                     updateZOrder(diagram, ElementHints.KEY_SG_NODE);
371                 }
372             }
373         }
374     };
375
376     private final DiagramHintListener diagramHintListener = new DiagramHintListener();
377
378     /**
379      * This element hint listener tries to ensure that diagram elements and the
380      * normal diagram scene graph stay in sync by listening to any changes
381      * occurring in elements, i.e. in their hints.
382      * 
383      * It does this by listening to {@link Hints#KEY_DIRTY} hint changes.
384      * 
385      * @author Tuukka Lehtonen
386      */
387     class ElementHintListener implements IHintListener {
388         @Override
389         public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
390             if (key == Hints.KEY_DIRTY) {
391                 if (newValue == Hints.VALUE_SG_DIRTY) {
392                     if (sender instanceof IElement) {
393                         assert getContext().getThreadAccess().currentThreadAccess();
394                         Object task = BEGIN("element dirty");
395
396                         IElement e = (IElement) sender;
397                         e.removeHint(Hints.KEY_DIRTY);
398
399                         if (DEBUG)
400                             System.out.println("Element is dirty: " + e);
401
402                         updateSelfAndNeighbors(e, COMPLETE_UPDATE);
403                         END(task);
404                     }
405                 }
406             } else if (key == ElementHints.KEY_FOCUS_LAYERS || key == ElementHints.KEY_VISIBLE_LAYERS) {
407                 if (sender instanceof IElement) {
408                     assert getContext().getThreadAccess().currentThreadAccess();
409                     IElement e = (IElement) sender;
410                     Object task = BEGIN("layers changed: " + e);
411                     update(e);
412                     END(task);
413                 }
414             }
415         }
416
417         @Override
418         public void hintRemoved(IHintObservable sender, Key key, Object oldValue) {
419         }
420     }
421
422     private final ElementHintListener elementHintListener = new ElementHintListener();
423
424     private final Set<Transaction> activeTransactions = new HashSet<Transaction>();
425
426     @Override
427     public void transactionStarted(IDiagram d, Transaction t) {
428         activeTransactions.add(t);
429     }
430
431     Consumer<IElement> COMPLETE_UPDATE = element -> {
432         // Connections may need rerouting
433         if (element.getElementClass().containsClass(ConnectionHandler.class))
434             DiagramUtils.validateAndFix(diagram, Collections.singleton(element));
435
436         //System.out.println("COMPLETE_UPDATE(" + element + ")");
437         update(element);
438         updateSelection(element);
439     };
440
441     Set<IElement> addRelatedElements(Set<IElement> elements) {
442         RelationshipHandler rh = diagram.getDiagramClass().getAtMostOneItemOfClass(RelationshipHandler.class);
443         if (rh != null) {
444             relatedElements.clear();
445             for (IElement el : elements) {
446                 relations.clear();
447                 rh.getRelations(diagram, el, relations);
448                 for (Relation r : relations) {
449                     Object obj = r.getObject();
450                     if (obj instanceof IElement) {
451                         relatedElements.add((IElement) obj);
452                     }
453                 }
454                 relations.clear();
455             }
456             elements.addAll(relatedElements);
457             relatedElements.clear();
458         }
459         return elements;
460     }
461
462     /**
463      * @param e
464      * @param updateCallback
465      */
466     protected void updateSelfAndNeighbors(IElement e, Consumer<IElement> updateCallback) {
467         // Slight optimization for cases that are known to be topologically
468         // non-expandable.
469         if (!isNotSelectionExpandable(e)) {
470             Set<IElement> single = Collections.singleton(e);
471
472             Set<IElement> expanded =
473                 // Also update all elements somehow related to e.
474                 addRelatedElements(
475                         // Get all topological neighbors and element self.
476                         CollectionUtils.join(
477                                 single,
478                                 TopologicalSelectionExpander.expandSelection(diagram, single)
479                         )
480                 );
481             // Perform the updates.
482             for (IElement el : expanded) {
483                 updateCallback.accept(el);
484             }
485         } else {
486             updateCallback.accept(e);
487         }
488     }
489
490     /**
491      * @param e
492      * @return
493      */
494     protected boolean isNotSelectionExpandable(IElement e) {
495         ElementClass ec = e.getElementClass();
496         return !ec.containsClass(ConnectionHandler.class)
497         && !ec.containsClass(BendsHandler.class)
498         && !ec.containsClass(TerminalTopology.class);
499     }
500
501     @Override
502     public void transactionFinished(IDiagram d, Transaction t) {
503         activeTransactions.remove(t);
504     }
505
506     boolean inDiagramTransaction() {
507         return !activeTransactions.isEmpty();
508     }
509
510     @Override
511     public void onElementAdded(IDiagram d, IElement e) {
512         if (DEBUG)
513             System.out.println("EP.onElementAdded(" + d + ", " + e + ")");
514
515         Map<INode, IElement> nodeElementMap = diagram.getHint(DiagramHints.NODE_TO_ELEMENT_MAP);
516         if (inDiagramTransaction()) {
517             addElement(d, e, false, nodeElementMap);
518         } else {
519             addElement(d, e, true, nodeElementMap);
520         }
521     }
522     @Override
523     public void onElementRemoved(IDiagram d, IElement e) {
524         if (DEBUG)
525             System.out.println("EP.onElementRemoved(" + d + ", " + e + ")");
526
527         removeElement(d, e, diagram.getHint(DiagramHints.NODE_TO_ELEMENT_MAP));
528     }
529
530     @Override
531     public void elementChildrenChanged(ChildEvent event) {
532         if (DEBUG)
533             System.out.println("EP.elementChildrenChanged: " + event);
534
535         Map<INode, IElement> nodeElementMap = diagram.getHint(DiagramHints.NODE_TO_ELEMENT_MAP);
536
537         for (IElement removed : event.removed) {
538             removeElement(diagram, removed, nodeElementMap);
539         }
540         for (IElement added : event.added) {
541             addElement(diagram, added, false, nodeElementMap);
542         }
543     }
544
545     private final List<IElement> childrenTemp = new ArrayList<IElement>();
546
547     public void addElement(IDiagram d, IElement e, boolean synchronizeSceneGraphNow, Map<INode, IElement> nodeElementMap) {
548         if (DEBUG)
549             System.out.println("EP.addElement(now=" + synchronizeSceneGraphNow + ", " + e + ")");
550
551         e.addKeyHintListener(Hints.KEY_DIRTY, elementHintListener);
552         e.addKeyHintListener(ElementHints.KEY_VISIBLE_LAYERS, elementHintListener);
553         e.addKeyHintListener(ElementHints.KEY_FOCUS_LAYERS, elementHintListener);
554
555         ElementClass clazz = e.getElementClass();
556         G2DParentNode parentNode = elementParent;
557         Key sgKey = ElementHints.KEY_SG_NODE;
558
559         Parent parent = clazz.getAtMostOneItemOfClass(Parent.class);
560         if (parent != null) {
561             IElement parentElement = parent.getParent(e);
562             if (parentElement != null) {
563                 SingleElementNode parentHolder = parentElement.getHint(sgKey);
564                 if (parentHolder != null) {
565                     parentNode = parentHolder;
566                 }
567             }
568         }
569
570         boolean isConnection = e.getElementClass().containsClass(ConnectionHandler.class);
571
572         if(isConnection) {
573
574             ConnectionNode holder = e.getHint(sgKey);
575             if (holder == null) {
576                 holder = parentNode.addNode(ElementUtils.generateNodeId(e), ConnectionNode.class);
577                 holder.setKey(e.getHint(ElementHints.KEY_OBJECT));
578                 holder.setTypeClass(e.getHint(ElementHints.KEY_TYPE_CLASS));
579                 holder.setTransferableProvider(new ElementTransferableProvider(getContext(), e));
580                 e.setHint(sgKey, holder);
581                 holder.setZIndex(parentNode.getNodeCount() + 1);
582                 if (nodeElementMap != null)
583                     nodeElementMap.put(holder, e);
584             }
585
586         } else {
587
588             SingleElementNode holder = e.getHint(sgKey);
589             if (holder == null) {
590                 holder = parentNode.addNode(ElementUtils.generateNodeId(e), SingleElementNode.class);
591                 holder.setKey(e.getHint(ElementHints.KEY_OBJECT));
592                 holder.setTypeClass(e.getHint(ElementHints.KEY_TYPE_CLASS));
593                 holder.setTransferableProvider(new ElementTransferableProvider(getContext(), e));
594                 e.setHint(sgKey, holder);
595                 holder.setZIndex(parentNode.getNodeCount() + 1);
596                 if (nodeElementMap != null)
597                     nodeElementMap.put(holder, e);
598             }
599
600         }
601
602         Children children = clazz.getAtMostOneItemOfClass(Children.class);
603         if (children != null) {
604             children.addChildListener(e, this);
605
606             childrenTemp.clear();
607             children.getChildren(e, childrenTemp);
608             //System.out.println("children: " + childrenTemp);
609             for (IElement child : childrenTemp) {
610                 addElement(d, child, false, nodeElementMap);
611             }
612             childrenTemp.clear();
613         }
614
615         if (synchronizeSceneGraphNow)
616             updateElement(e, sgKey);
617
618         //setTreeDirty();
619     }
620
621     protected void removeElement(IDiagram d, IElement e, Map<INode, IElement> nodeElementMap) {
622         if (DEBUG)
623             System.out.println("EP.removeElement(" + e + ")");
624
625         e.removeKeyHintListener(Hints.KEY_DIRTY, elementHintListener);
626         e.removeKeyHintListener(ElementHints.KEY_VISIBLE_LAYERS, elementHintListener);
627         e.removeKeyHintListener(ElementHints.KEY_FOCUS_LAYERS, elementHintListener);
628
629         ElementClass clazz = e.getElementClass();
630         if (clazz.containsClass(Children.class)) {
631             Children children = clazz.getSingleItem(Children.class);
632             children.removeChildListener(e, this);
633         }
634
635         List<SceneGraph> nodeHandlers = e.getElementClass().getItemsByClass(SceneGraph.class);
636         for (SceneGraph n : nodeHandlers) {
637             n.cleanup(e);
638         }
639
640         // Remove all hints related to scene graph nodes to prevent leakage of
641         // scene graph resources.
642         Map<SceneGraphNodeKey, Object> sgHints = e.getHintsOfClass(SceneGraphNodeKey.class);
643         for (SceneGraphNodeKey sgKey : sgHints.keySet()) {
644             Node n = e.removeHint(sgKey);
645             if (n != null) {
646                 n.remove();
647                 if (nodeElementMap != null)
648                     nodeElementMap.remove(n);
649             }
650         }
651
652         //setTreeDirty();
653     }
654
655     /**
656      * Invalidate the whole scene graph spatial structure. It will be rebuilt by
657      * RTreeNode when needed the next time.
658      */
659     private void setTreeDirty() {
660         elementParent.setDirty();
661     }
662
663     /**
664      * Mark the specified node invalid with respect to the scene graph spatial
665      * structure.
666      * 
667      * @param node a scene graph node that has somehow changed
668      */
669     private void invalidateNode(INode node) {
670         // TODO: optimize rtree updates instead of killing the whole tree
671         elementParent.setDirty();
672     }
673
674     // ------------------------------------------------------------------------
675     // Diagram/Element hint listeners
676     // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
677
678     public void updateAll() {
679         if (DEBUG)
680             System.out.println("EP.updateAll()");
681
682         Object task = BEGIN("EP.updateAll");
683         paintDiagram(elementParent, diagram, null);
684         updateSelections();
685         setTreeDirty();
686         END(task);
687     }
688
689     public void update(IElement element) {
690         updateElement(element, ElementHints.KEY_SG_NODE);
691     }
692
693     /**
694      *
695      * @param controlGC
696      * @param diagram
697      * @param elementsToPaint
698      *            elements to paint or null for all elements
699      */
700     public void paintDiagram(G2DParentNode parent, IDiagram diagram, Collection<IElement> elementsToPaint) {
701         Object task = BEGIN("EP.paintDiagram");
702         paintDiagram(parent, diagram, elementsToPaint, ElementHints.KEY_SG_NODE);
703         END(task);
704     }
705
706     /**
707      *
708      * @param controlGC
709      * @param diagram
710      * @param elementsToPaint
711      *            elements to paint or null for all elements
712      */
713     public void paintDiagram(G2DParentNode parent, IDiagram diagram, Collection<IElement> elementsToPaint, Key elementSgNodeKey) {
714         if(diagram == null) return;
715         ICanvasContext ctx = getContext();
716         assert (ctx != null);
717
718         Boolean disablePaint = diagram.getHint(Hints.KEY_DISABLE_PAINTING);
719         if (Boolean.TRUE.equals(disablePaint)) {
720             parent.removeNodes();
721             return;
722         }
723
724         // Paint elementsToPaint in correct z-order from diagram.getElements()
725         List<IElement> elements = diagram.getSnapshot();
726
727         Set<SingleElementNode> tmp = new HashSet<SingleElementNode>();
728         int zIndex = 0;
729         for (int pass = 0; pass < 1; ++pass) {
730             for (IElement e : elements) {
731                 if (elements != elementsToPaint && elementsToPaint != null)
732                     if (!elementsToPaint.contains(e))
733                         continue;
734
735                 if (DEBUG)
736                     System.out.println("EP.paintDiagram(" + zIndex + ", " + e + ")");
737
738                 SingleElementNode holder = updateElement(parent, e, elementSgNodeKey, false);
739                 if (holder != null) {
740                     tmp.add(holder);
741                     holder.setZIndex(++zIndex);
742                 }
743             }
744         }
745
746         // Hide unaccessed nodes (but don't remove)
747         for (IG2DNode node : parent.getNodes()) {
748             if (!tmp.contains(node)) {
749                 ((SingleElementNode)node).setVisible(false);
750             }
751         }
752     }
753
754     public void updateElement(IElement e, Key elementSgNodeKey) {
755         updateElement(null, e, elementSgNodeKey, true);
756     }
757
758     /**
759      * @param parent if <code>null</code> the scene graph node structure
760      *        will not be created if it is missing
761      * @param e
762      * @param elementSgNodeKey
763      * @param invalidateNode 
764      */
765     public SingleElementNode updateElement(G2DParentNode parent, IElement e, Key elementSgNodeKey, boolean invalidateNode) {
766         if (DEBUG)
767             System.out.println("EP.updateElement(" + e + ", " + elementSgNodeKey + ")");
768         Object task = BEGIN("EP.updateElement");
769
770         try {
771             SingleElementNode holder = e.getHint(elementSgNodeKey);
772             if (holder == null && parent == null)
773                 return null;
774
775             if (ElementUtils.isHidden(e))
776                 return null;
777
778 //            ElementClass ec = e.getElementClass();
779 //            ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS);
780 //            if (layers != null && !layers.getIgnoreVisibilitySettings()) {
781 //                ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
782 //                if (el != null && !el.isVisible(e, layers)) {
783 //                    return null;
784 //                }
785 //            }
786
787             // Update the node scene graph through SceneGraph handlers.
788             List<SceneGraph> nodeHandlers = e.getElementClass().getItemsByClass(SceneGraph.class);
789             Collection<SceneGraph> decorators = e.getHint(ElementHints.KEY_DECORATORS);
790             if (nodeHandlers.isEmpty() && (decorators == null || decorators.isEmpty()))
791                 return null;
792
793             Composite composite = e.getHint(ElementHints.KEY_COMPOSITE);
794
795             if (holder == null) {
796                 holder = parent.addNode(ElementUtils.generateNodeId(e), SingleElementNode.class);
797                 e.setHint(elementSgNodeKey, holder);
798             }
799             holder.setComposite(composite);
800             holder.setVisible(true);
801
802             for (SceneGraph n : nodeHandlers) {
803                 n.init(e, holder);
804             }
805
806             // Process decorators
807             if (decorators == null || decorators.isEmpty()) {
808                 holder.removeNode("decorators");
809             } else {
810                 G2DParentNode decoratorHolder = holder.getOrCreateNode("decorators", G2DParentNode.class);
811                 decoratorHolder.removeNodes();
812                 for (SceneGraph decorator : decorators) {
813                     decorator.init(e, decoratorHolder);
814                 }
815             }
816
817             if (invalidateNode)
818                 invalidateNode(holder);
819
820             return holder;
821         } finally {
822             END(task);
823         }
824     }
825
826     /**
827      * @param elementsToUpdate to explicitly specify which elements to update
828      *        the selection scene graph for, or <code>null</code> to update
829      *        everything
830      */
831     public void updateSelections() {
832         Object task = BEGIN("EP.updateSelections");
833
834         try {
835             if (!cfg.paintSelectionFrames)
836                 return;
837             if (selection == null)
838                 return;
839
840             boolean selectionsChanged = false;
841
842             // Update and "touch" all selections.
843             Set<Integer> existingSelections = new HashSet<Integer>();
844             Set<INode> selectionNodes = new HashSet<INode>();
845             Set<INode> tmp = new HashSet<INode>();
846             Map<INode, LinkNode> selectionLinks = new HashMap<INode, LinkNode>();
847
848             for (Map.Entry<Integer, Set<IElement>> entry : selection.getSelections().entrySet()) {
849                 Integer selectionId = entry.getKey();
850                 Set<IElement> selectedElements = entry.getValue();
851
852                 existingSelections.add(selectionId);
853
854 //                System.out.println("SELECTION[" + selectionId + "]: " + selectedElements);
855                 ElementNodeBridge bridge = getOrCreateSelectionMap(selectionId);
856                 selectionNodes.clear();
857                 selectionsChanged |= paintSelection(selectedElements, selectionId, selectionNodes, bridge);
858
859                 // Remove selection nodes that were not referenced during the update.
860 //                System.out.println("BRIDGE: " + bridge.toString());
861 //                System.out.println("SELECTED: " + selectionNodes);
862                 tmp.clear();
863                 tmp.addAll(bridge.getRightSet());
864                 tmp.removeAll(selectionNodes);
865 //                System.out.println("REMOVED: " + tmp);
866 //                System.out.println("BRIDGE BEFORE: " + bridge);
867                 selectionsChanged |= bridge.retainAllRight(selectionNodes);
868 //                System.out.println("BRIDGE AFTER: " + bridge);
869
870                 G2DParentNode selectionsNode = getSelectionsNode(selectionId);
871                 selectionLinks.clear();
872                 getSelectedNodeReferences(selectionsNode, selectionLinks);
873
874                 for (INode node : tmp) {
875                     INode linkNode = selectionLinks.get(node.getParent());
876                     if (linkNode != null) {
877                         linkNode.remove();
878                     }
879 //                    System.out.println("REMOVED SELECTION: -> " + node);
880                     node.remove();
881                 }
882             }
883
884             for (Iterator<Map.Entry<Integer, ElementNodeBridge>> iterator = selections.entrySet().iterator(); iterator.hasNext();) {
885                 Map.Entry<Integer, ElementNodeBridge> entry = iterator.next();
886                 Integer selectionId = entry.getKey();
887                 if (!existingSelections.contains(selectionId)) {
888                     // Selection no longer exists.
889                     selectionsChanged = true;
890                     for (INode node : entry.getValue().getRightSet()) {
891 //                        System.out.println("REMOVED SELECTION: " + node);
892                         node.remove();
893                     }
894                     iterator.remove();
895
896                     G2DParentNode selectionsNode = getSelectionsNode(selectionId);
897                     selectionsNode.removeNodes();
898                 }
899             }
900
901             // Make sure the view is refreshed after selection changes.
902             if (selectionsChanged) {
903                 setDirty();
904             }
905         } finally {
906             END(task);
907         }
908     }
909
910     private G2DParentNode getSelectionsNode() {
911         G2DParentNode sels = NodeUtil.lookup(diagramParent, SceneGraphConstants.SELECTIONS_NODE_NAME, G2DParentNode.class);
912         if (sels == null) {
913             DataNode data= NodeUtil.lookup(diagramParent, SceneGraphConstants.DATA_NODE_NAME, DataNode.class);
914             sels = data.addNode(SceneGraphConstants.SELECTIONS_NODE_NAME, G2DParentNode.class);
915             sels.setLookupId(SceneGraphConstants.SELECTIONS_NODE_NAME);
916         }
917         return sels;
918     }
919
920     private G2DParentNode getSelectionsNode(int selectionId) {
921         G2DParentNode selectionsNode = getSelectionsNode();
922         G2DParentNode s = selectionsNode.getOrCreateNode(String.valueOf(selectionId), G2DParentNode.class);
923         return s;
924     }
925
926     private Map<INode, LinkNode> getSelectedNodeReferences(G2DParentNode selectionsNode, Map<INode, LinkNode> result) {
927         for (IG2DNode node : selectionsNode.getSortedNodes()) {
928             if (node instanceof LinkNode) {
929                 INode n = ((LinkNode) node).getDelegate();
930                 if (n != null)
931                     result.put(n, (LinkNode) node);
932             }
933         }
934         return result;
935     }
936
937     public void updateSelection(IElement el) {
938         Object task = BEGIN("EP.updateSelection");
939
940         try {
941             if (!cfg.paintSelectionFrames)
942                 return;
943
944             G2DParentNode elementNode = (G2DParentNode) el.getHint(ElementHints.KEY_SG_NODE);
945             if (elementNode == null)
946                 return;
947
948             boolean nodesUpdated = false;
949
950             for (Map.Entry<Integer, ElementNodeBridge> entry : selections.entrySet()) {
951                 Integer selectionId = entry.getKey();
952                 ElementNodeBridge bridge = entry.getValue();
953                 Color color = getSelectionColor(selectionId);
954
955                 G2DParentNode selectionNode = (G2DParentNode) bridge.getRight(el);
956                 if (selectionNode == null)
957                     continue;
958
959                 if (NodeUtil.needSelectionPaint(elementNode))
960                     paintSelectionFrame(selectionId, elementNode, selectionNode, el, color);
961
962                 nodesUpdated = true;
963             }
964
965             // Make sure the view is refreshed after selection changes.
966             if (nodesUpdated)
967                 setDirty();
968         } finally {
969             END(task);
970         }
971     }
972
973     /**
974      * @param selection
975      * @param selectionId
976      * @param selectionNodes for collecting all the "selection" nodes created or
977      *        referenced by this method
978      * @param bridge
979      * @return
980      */
981     public boolean paintSelection(Set<IElement> selection, int selectionId, Set<INode> selectionNodes, ElementNodeBridge bridge) {
982
983         boolean result = false;
984         Color color = getSelectionColor(selectionId);
985         G2DParentNode selectionsNode = getSelectionsNode(selectionId);
986
987         Class<? extends G2DParentNode> selectionNodeClass = cfg.selectionNodeClass != null ? cfg.selectionNodeClass : G2DParentNode.class;
988
989         for (IElement e : selection) {
990             Node elementNode = e.getHint(ElementHints.KEY_SG_NODE);
991 //            System.out.println("selectionNode: " + elementNode + " " + e);
992             if (elementNode instanceof G2DParentNode) {
993                 G2DParentNode en = (G2DParentNode) elementNode;
994                 G2DParentNode selectionNode = en.getOrCreateNode(NodeUtil.SELECTION_NODE_NAME, selectionNodeClass);
995                 selectionNode.setZIndex(SELECTION_PAINT_PRIORITY);
996                 if (selectionNodes != null)
997                     selectionNodes.add(selectionNode);
998                 if (!bridge.containsLeft(e)) {
999                     //System.out.println("ADDED SELECTION: " + e + " -> " + selectionNode);
1000                     bridge.map(e, selectionNode);
1001                     result = true;
1002                 }
1003
1004                 // Mark this node selected in the scene graph "data area"
1005                 createSelectionReference(selectionsNode, elementNode);
1006
1007                 if (NodeUtil.needSelectionPaint(elementNode))
1008                     paintSelectionFrame(selectionId, en, selectionNode, e, color);
1009
1010             } else {
1011                 if (elementNode != null) {
1012                     // Cannot paint selection for unrecognized non-parenting node
1013                     System.out.println("Cannot add selection child node for non-parent element node: " + elementNode);
1014                 }
1015             }
1016         }
1017
1018 //        if (selection.isEmpty()) {
1019 //            Node pivotNode = (Node) parent.getNode("pivot");
1020 //            if (pivotNode != null)
1021 //                pivotNode.remove();
1022 //        } else {
1023 //            Point2D pivot = ElementUtils.getElementBoundsCenter(selection, pivotPoint);
1024 //            if (pivot != null) {
1025 //                //System.out.println("painting pivot: " + pivot);
1026 //                SelectionPivotNode pivotNode = parent.getOrCreateNode("pivot", SelectionPivotNode.class);
1027 //                pivotNode.setPivot(pivot);
1028 //            } else {
1029 //                parent.removeNode("pivot");
1030 //            }
1031 //        }
1032
1033         return result;
1034     }
1035
1036     
1037     /**
1038      * We need to have separate class for SelectionNode, so that SCLSceneGraph can handle this properly.
1039      * 
1040      */
1041     public static class SelectionShapeNode extends ShapeNode {
1042
1043                 private static final long serialVersionUID = -5393630944240940166L;
1044         
1045     }
1046     
1047     public void paintSelectionFrame(int selectionId, G2DParentNode elementNode, G2DParentNode selectionNode, final IElement e, Color color) {
1048         // The element node already has the correct transform.
1049         AffineTransform selectionTransform = ElementUtils.getTransform(e);// no it doesnt ... new AffineTransform();
1050         Shape shape = ElementUtils.getElementShapeOrBounds(e);
1051         Rectangle2D bounds = shape.getBounds2D();
1052         //System.out.println("selection bounds: "+bounds);
1053         
1054         Point2D scale = GeometryUtils.getScale2D(selectionTransform);
1055         final double marginX = Math.abs(scale.getX()) > 1e-10 ? 1 / scale.getX() : 1;
1056         final double marginY = Math.abs(scale.getY()) > 1e-10 ? 1 / scale.getY() : 1;
1057         
1058         bounds.setFrame(bounds.getMinX() - marginX, bounds.getMinY() - marginY, bounds.getWidth() + 2*marginX, bounds.getHeight() + 2*marginY);
1059
1060         List<SelectionSpecification> ss = e.getElementClass().getItemsByClass(SelectionSpecification.class);
1061         if (!ss.isEmpty()) {
1062             G2DParentNode shapeholder = selectionNode.getOrCreateNode(getNodeId("outlines", e), G2DParentNode.class);
1063
1064             for (SelectionSpecification es : ss) {
1065                 Outline outline = (Outline) es.getAdapter(Outline.class);
1066                 if (outline == null || outline.getElementShape(e) == null)
1067                         continue;
1068                 ShapeNode shapenode = shapeholder.getOrCreateNode(getNodeId("outline", e, es), SelectionShapeNode.class);
1069 //                shapenode.setShape(es.getSelectionShape(e));
1070 //                shapenode.setStroke(SELECTION_STROKE);
1071 //                shapenode.setScaleStroke(true);
1072 //                shapenode.setColor(color);
1073 //                shapenode.setTransform(selectionTransform);
1074 //                shapenode.setFill(false);
1075                 shapenode.setShape(outline.getElementShape(e));
1076                 StrokeSpec strokeSpec = (StrokeSpec) es.getAdapter(StrokeSpec.class);
1077                 if (strokeSpec != null && strokeSpec.getStroke(e) != null)
1078                         shapenode.setStroke(strokeSpec.getStroke(e));
1079                 
1080                 shapenode.setScaleStroke(false);
1081                 //shapenode.setColor(color);
1082                 OutlineColorSpec foregroundColor = (OutlineColorSpec) es.getAdapter(OutlineColorSpec.class);
1083                 if (foregroundColor != null && foregroundColor.getColor(e) != null)
1084                         shapenode.setColor(foregroundColor.getColor(e));
1085                 
1086                 Transform transform = (Transform) es.getAdapter(Transform.class);
1087                 if (transform != null && transform.getTransform(e) != null)
1088                         shapenode.setTransform(transform.getTransform(e));
1089                 
1090                 shapenode.setFill(false);
1091                 FillColor fillColor = (FillColor) es.getAdapter(FillColor.class);
1092                 if (fillColor != null && fillColor.getFillColor(e) != null)
1093                     shapenode.setFill(true);
1094 //                      shapenode.setColor(ColorUtil.withAlpha(backgroundColor.getColor(e), 192));
1095             }
1096             return;
1097         }
1098
1099         List<SelectionOutline> shapeHandlers = e.getElementClass().getItemsByClass(SelectionOutline.class);
1100         if (!shapeHandlers.isEmpty()) {
1101             G2DParentNode shapeholder = selectionNode.getOrCreateNode(getNodeId("outlines", e), G2DParentNode.class);
1102
1103             for (SelectionOutline es : shapeHandlers) {
1104                 ShapeNode shapenode = shapeholder.getOrCreateNode(getNodeId("outline", e, es), SelectionShapeNode.class);
1105 //                shapenode.setShape(es.getSelectionShape(e));
1106 //                shapenode.setStroke(SELECTION_STROKE);
1107 //                shapenode.setScaleStroke(true);
1108 //                shapenode.setColor(color);
1109 //                shapenode.setTransform(selectionTransform);
1110 //                shapenode.setFill(false);
1111                 shapenode.setShape(es.getSelectionShape(e));
1112                 shapenode.setStroke(null);
1113                 shapenode.setScaleStroke(false);
1114                 //shapenode.setColor(color);
1115                 shapenode.setColor(ColorUtil.withAlpha(color, 192));
1116                 shapenode.setTransform(selectionTransform);
1117                 shapenode.setFill(true);
1118             }
1119             return;
1120         }
1121
1122         ISelectionProvider provider = this.getContext().getDefaultHintContext().getHint(KEY_SELECTION_PROVIDER);
1123         if (provider != null) {
1124             provider.init(selectionId, e, selectionNode, getNodeId("shape", e), selectionTransform, bounds, color);
1125         } else {
1126             SelectionNode s = selectionNode.getOrCreateNode(getNodeId("shape", e), SelectionNode.class);
1127             s.init(selectionId, selectionTransform, bounds, color);
1128             Double paddingFactor = diagram.getHint(DiagramHints.SELECTION_PADDING_SCALE_FACTOR);
1129             if (paddingFactor != null)
1130                 s.setPaddingFactor(paddingFactor);
1131         }
1132     }
1133
1134     private void createSelectionReference(G2DParentNode selectionsNode, INode elementNode) {
1135         String id = NodeUtil.lookupId(elementNode);
1136         String uuid = null;
1137         if (id == null)
1138             id = uuid = UUID.randomUUID().toString();
1139         NodeUtil.map(elementNode, id);
1140         LinkNode link = selectionsNode.getOrCreateNode(id, LinkNode.class);
1141         link.setDelegateId(id);
1142         link.setIgnoreDelegate(true);
1143         link.setLookupIdOwner(uuid != null);
1144     }
1145
1146     private transient CharBuffer buf = CharBuffer.allocate(32);
1147
1148     private String getNodeId(String prefix, Object first) {
1149         return getNodeId(prefix, first, null);
1150     }
1151
1152     private String getNodeId(String prefix, Object first, Object second) {
1153         buf.clear();
1154         if (prefix != null)
1155             buf.append(prefix);
1156         if (first != null) {
1157             buf.append('_');
1158             buf.append("" + first.hashCode());
1159         }
1160         if (second != null) {
1161             buf.append('_');
1162             buf.append("" + second.hashCode());
1163         }
1164         buf.limit(buf.position());
1165         buf.rewind();
1166         //System.out.println("node id: " + buf.toString());
1167         return buf.toString();
1168     }
1169
1170     /**
1171      * Get selection color for a selection Id
1172      * @param selectionId selection id
1173      * @return color for the id
1174      */
1175     protected Color getSelectionColor(int selectionId) {
1176         if (selectionId == 0) {
1177             Color c = getHint(KEY_SELECTION_FRAME_COLOR);
1178             if (c != null)
1179                 return c;
1180             return Color.BLACK;
1181         }
1182         Color c = selectionColor.get(selectionId);
1183         if (c == null) {
1184             Random r = new Random(selectionId);
1185             c = new Color(r.nextFloat(), r.nextFloat(), r.nextFloat());
1186             selectionColor.put(selectionId, c);
1187         }
1188         return c;
1189     }
1190
1191     private transient ConcurrentMap<Integer, ElementNodeBridge> selections = new ConcurrentHashMap<Integer, ElementNodeBridge>();
1192
1193     ElementNodeBridge getSelectionMap(int selectionId) {
1194         return selections.get(Integer.valueOf(selectionId));
1195     }
1196
1197     ElementNodeBridge getOrCreateSelectionMap(int selectionId) {
1198         Integer id = Integer.valueOf(selectionId);
1199         synchronized (selections) {
1200             ElementNodeBridge map = selections.get(id);
1201             if (map != null)
1202                 return map;
1203
1204             selections.put(id, map = new ElementNodeBridge(id));
1205             return map;
1206         }
1207     }
1208
1209     private transient Map<Integer, Color>     selectionColor              = new HashMap<Integer, Color>();
1210
1211     private transient BasicStroke             SELECTION_STROKE            = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
1212             BasicStroke.JOIN_BEVEL, 10.0f,
1213             new float[] { 5.0f, 5.0f }, 0.0f);
1214
1215     private transient Point2D                 pivotPoint                  = new Point2D.Double();
1216
1217     @HintListener(Class=Selection.class, Field="SELECTION0")
1218     public void selectionChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
1219         //System.out.println("new selection: " + newValue);
1220         updateSelections();
1221     }
1222
1223     @HintListener(Class=Selection.class, Field="SELECTION0")
1224     public void selectionRemoved(IHintObservable sender, Key key, Object oldValue) {
1225         //System.out.println("selection removed: " + oldValue);
1226         updateSelections();
1227     }
1228
1229     private static Object BEGIN(String name) {
1230         if (DEBUG) {
1231             //return ThreadLog.BEGIN(name);
1232         }
1233         return null;
1234     }
1235
1236     private static void END(Object task) {
1237         if (DEBUG) {
1238             //((Task) task).end();
1239         }
1240     }
1241
1242 }