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