/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.diagram.participant; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.nio.CharBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency; import org.simantics.g2d.canvas.impl.DependencyReflection.Reference; import org.simantics.g2d.canvas.impl.HintReflection.HintListener; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit; import org.simantics.g2d.connection.handler.ConnectionHandler; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.DiagramUtils; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.IDiagram.CompositionListener; import org.simantics.g2d.diagram.handler.RelationshipHandler; import org.simantics.g2d.diagram.handler.RelationshipHandler.Relation; import org.simantics.g2d.diagram.handler.TransactionContext; import org.simantics.g2d.diagram.handler.TransactionContext.Transaction; import org.simantics.g2d.diagram.handler.TransactionContext.TransactionListener; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.SceneGraphNodeKey; import org.simantics.g2d.element.handler.BendsHandler; import org.simantics.g2d.element.handler.Children; import org.simantics.g2d.element.handler.Children.ChildEvent; import org.simantics.g2d.element.handler.Children.ChildListener; import org.simantics.g2d.element.handler.FillColor; import org.simantics.g2d.element.handler.Outline; import org.simantics.g2d.element.handler.OutlineColorSpec; import org.simantics.g2d.element.handler.Parent; import org.simantics.g2d.element.handler.SceneGraph; import org.simantics.g2d.element.handler.SelectionOutline; import org.simantics.g2d.element.handler.SelectionSpecification; import org.simantics.g2d.element.handler.StrokeSpec; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.element.handler.Transform; import org.simantics.g2d.layers.ILayer; import org.simantics.g2d.layers.ILayersEditor; import org.simantics.g2d.layers.ILayersEditor.ILayersEditorListener; import org.simantics.g2d.participant.TransformUtil; import org.simantics.g2d.scenegraph.SceneGraphConstants; import org.simantics.g2d.utils.ElementNodeBridge; import org.simantics.g2d.utils.TopologicalSelectionExpander; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.Node; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.G2DSceneGraph; import org.simantics.scenegraph.g2d.IG2DNode; import org.simantics.scenegraph.g2d.nodes.ConnectionNode; import org.simantics.scenegraph.g2d.nodes.DataNode; import org.simantics.scenegraph.g2d.nodes.LinkNode; import org.simantics.scenegraph.g2d.nodes.SelectionNode; import org.simantics.scenegraph.g2d.nodes.ShapeNode; import org.simantics.scenegraph.g2d.nodes.SingleElementNode; import org.simantics.scenegraph.g2d.nodes.UnboundedNode; import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode; import org.simantics.scenegraph.utils.ColorUtil; import org.simantics.scenegraph.utils.NodeUtil; import org.simantics.utils.datastructures.collections.CollectionUtils; import org.simantics.utils.datastructures.hints.HintListenerAdapter; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; import org.simantics.utils.datastructures.hints.IHintListener; import org.simantics.utils.datastructures.hints.IHintObservable; /** * A diagram participant that keeps a diagram and its elements synchronized with * the active canvas scene graph. * *

* Responsibilities include: *

* * @author Tuukka Lehtonen * * @see ElementNodeBridge */ public class ElementPainter extends AbstractDiagramParticipant implements CompositionListener, TransactionListener, ChildListener { public static final Key KEY_SELECTION_PROVIDER = new KeyOf(ISelectionProvider.class); public static final int SELECTION_PAINT_PRIORITY = 100; public static final Key KEY_SELECTION_FRAME_COLOR = new KeyOf(Color.class, "SELECTION_FRAME_COLOR"); public static final Key KEY_SELECTION_CONTENT_COLOR = new KeyOf(Color.class, "SELECTION_CONTENT_COLOR"); /** * Implement to customize the way a selection is visualized by * ElementPainter. */ public static interface ISelectionProvider { public void init(final IElement e, final G2DParentNode parentNode, final String nodeId, final AffineTransform transform, final Rectangle2D bounds, final Color color); } private static final boolean DEBUG = false; public static final int ELEMENT_PAINT_PRIORITY = 10; @Reference ZOrderHandler zOrderHandler; @Dependency TransformUtil util; @Dependency Selection selection; SingleElementNode diagramParent; RTreeNode elementParent; boolean paintSelectionFrames; /** * Internally reused to avert constant reallocation. */ private transient List relations = new ArrayList(4); /** * Internally reused to avert constant reallocation. */ private transient Set relatedElements = new HashSet(8); public ElementPainter() { this(true); } public ElementPainter(boolean paintSelectionFrames) { this.paintSelectionFrames = paintSelectionFrames; } @Override public void addedToContext(ICanvasContext ctx) { super.addedToContext(ctx); if (zOrderHandler != null) { zOrderHandler.addOrderListener(zOrderListener); } } @Override public void removedFromContext(ICanvasContext ctx) { if (zOrderHandler != null) { zOrderHandler.removeOrderListener(zOrderListener); } selections.clear(); super.removedFromContext(ctx); } @Override protected void onDiagramSet(IDiagram newValue, IDiagram oldValue) { if (oldValue == newValue) return; if (oldValue != null) { for (IElement e : oldValue.getElements()) { removeElement(e); } oldValue.removeCompositionListener(this); oldValue.removeKeyHintListener(Hints.KEY_DIRTY, diagramHintListener); oldValue.removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, diagramHintListener); ILayersEditor layers = oldValue.getHint(DiagramHints.KEY_LAYERS_EDITOR); if (layers != null) { layers.removeListener(layersListener); } for (TransactionContext tc : oldValue.getDiagramClass().getItemsByClass(TransactionContext.class)) { tc.removeTransactionListener(oldValue, this); } } if (newValue != null) { for (IElement e : newValue.getElements()) { addElement(e, false); } newValue.addCompositionListener(this); newValue.addKeyHintListener(Hints.KEY_DISABLE_PAINTING, diagramHintListener); newValue.addKeyHintListener(Hints.KEY_DIRTY, diagramHintListener); ILayersEditor layers = newValue.getHint(DiagramHints.KEY_LAYERS_EDITOR); if (layers != null) { layers.addListener(layersListener); } for (TransactionContext tc : newValue.getDiagramClass().getItemsByClass(TransactionContext.class)) { tc.addTransactionListener(newValue, this); } } updateAll(); } @SGInit public void initSG(G2DParentNode parent) { diagramParent = parent.addNode("elements_"+Node.IDCOUNTER, UnboundedNode.class); diagramParent.setZIndex(ELEMENT_PAINT_PRIORITY); elementParent = diagramParent.addNode("spatialRoot", RTreeNode.class); elementParent.setZIndex(0); } @SGCleanup public void cleanupSG() { diagramParent.remove(); elementParent = null; diagramParent = null; } public INode getDiagramElementParentNode() { return elementParent; } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // Element z-order listening and update logic // ------------------------------------------------------------------------ ZOrderListener zOrderListener = new ZOrderListener() { @Override public void orderChanged(IDiagram diagram) { if (diagram == ElementPainter.this.diagram) { updateZOrder(diagram, ElementHints.KEY_SG_NODE); } } }; protected static void updateZOrder(IDiagram diagram, Key elementSgNodeKey) { int zIndex = 0; for (IElement e : diagram.getElements()) { Node node = e.getHint(elementSgNodeKey); if (node instanceof IG2DNode) { ((IG2DNode) node).setZIndex(++zIndex); } } } // ------------------------------------------------------------------------ // Element z-order listening and update logic end // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // Layer configuration change listening and reaction logic // ------------------------------------------------------------------------ ILayersEditorListener layersListener = new ILayersEditorListener() { private void layersChanged() { Object task = BEGIN("EP.layersChanged"); // Update visibility/focusability for each node only, do not reinitialize the graphics. updateAllVisibility(); END(task); } @Override public void layerRemoved(ILayer layer) { layersChanged(); } @Override public void layerDeactivated(ILayer layer) { layersChanged(); } @Override public void layerAdded(ILayer layer) { layersChanged(); } @Override public void layerActivated(ILayer layer) { layersChanged(); } @Override public void ignoreFocusChanged(boolean value) { ICanvasContext ctx = getContext(); if(ctx == null) return; G2DSceneGraph sg = ctx.getSceneGraph(); if(sg == null) return; sg.setGlobalProperty(G2DSceneGraph.IGNORE_FOCUS, value); } @Override public void ignoreVisibilityChanged(boolean value) { layersChanged(); } }; protected void updateAllVisibility() { // TODO: optimize, no node reinitialization updateAll(); } // ------------------------------------------------------------------------ // Layer configuration change listening and reaction logic // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // Diagram/Element hint listeners // ------------------------------------------------------------------------ class DiagramHintListener extends HintListenerAdapter { @Override public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { if (key == Hints.KEY_DISABLE_PAINTING) { if (diagramParent != null) { diagramParent.setVisible(!Boolean.TRUE.equals(newValue)); } } else if (key == Hints.KEY_DIRTY) { if (newValue == Hints.VALUE_Z_ORDER_CHANGED) { diagram.removeHint(Hints.KEY_DIRTY); if (DEBUG) System.out.println("Diagram z-order changed: " + diagram); updateZOrder(diagram, ElementHints.KEY_SG_NODE); } } } }; private final DiagramHintListener diagramHintListener = new DiagramHintListener(); /** * This element hint listener tries to ensure that diagram elements and the * normal diagram scene graph stay in sync by listening to any changes * occurring in elements, i.e. in their hints. * * It does this by listening to {@link Hints#KEY_DIRTY} hint changes. * * @author Tuukka Lehtonen */ class ElementHintListener implements IHintListener { @Override public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { if (key == Hints.KEY_DIRTY) { if (newValue == Hints.VALUE_SG_DIRTY) { if (sender instanceof IElement) { assert getContext().getThreadAccess().currentThreadAccess(); Object task = BEGIN("element dirty"); IElement e = (IElement) sender; e.removeHint(Hints.KEY_DIRTY); if (DEBUG) System.out.println("Element is dirty: " + e); updateSelfAndNeighbors(e, COMPLETE_UPDATE); END(task); } } } else if (key == ElementHints.KEY_FOCUS_LAYERS || key == ElementHints.KEY_VISIBLE_LAYERS) { if (sender instanceof IElement) { assert getContext().getThreadAccess().currentThreadAccess(); IElement e = (IElement) sender; Object task = BEGIN("layers changed: " + e); update(e); END(task); } } } @Override public void hintRemoved(IHintObservable sender, Key key, Object oldValue) { } } private final ElementHintListener elementHintListener = new ElementHintListener(); private final Set activeTransactions = new HashSet(); @Override public void transactionStarted(IDiagram d, Transaction t) { activeTransactions.add(t); } Consumer COMPLETE_UPDATE = element -> { // Connections may need rerouting if (element.getElementClass().containsClass(ConnectionHandler.class)) DiagramUtils.validateAndFix(diagram, Collections.singleton(element)); //System.out.println("COMPLETE_UPDATE(" + element + ")"); update(element); updateSelection(element); }; Set addRelatedElements(Set elements) { RelationshipHandler rh = diagram.getDiagramClass().getAtMostOneItemOfClass(RelationshipHandler.class); if (rh != null) { relatedElements.clear(); for (IElement el : elements) { relations.clear(); rh.getRelations(diagram, el, relations); for (Relation r : relations) { Object obj = r.getObject(); if (obj instanceof IElement) { relatedElements.add((IElement) obj); } } relations.clear(); } elements.addAll(relatedElements); relatedElements.clear(); } return elements; } /** * @param e * @param updateCallback */ protected void updateSelfAndNeighbors(IElement e, Consumer updateCallback) { // Slight optimization for cases that are known to be topologically // non-expandable. if (!isNotSelectionExpandable(e)) { Set single = Collections.singleton(e); Set expanded = // Also update all elements somehow related to e. addRelatedElements( // Get all topological neighbors and element self. CollectionUtils.join( single, TopologicalSelectionExpander.expandSelection(diagram, single) ) ); // Perform the updates. for (IElement el : expanded) { updateCallback.accept(el); } } else { updateCallback.accept(e); } } /** * @param e * @return */ protected boolean isNotSelectionExpandable(IElement e) { ElementClass ec = e.getElementClass(); return !ec.containsClass(ConnectionHandler.class) && !ec.containsClass(BendsHandler.class) && !ec.containsClass(TerminalTopology.class); } @Override public void transactionFinished(IDiagram d, Transaction t) { activeTransactions.remove(t); } boolean inDiagramTransaction() { return !activeTransactions.isEmpty(); } @Override public void onElementAdded(IDiagram d, IElement e) { if (DEBUG) System.out.println("EP.onElementAdded(" + d + ", " + e + ")"); if (inDiagramTransaction()) { addElement(e, false); } else { addElement(e, true); } } @Override public void onElementRemoved(IDiagram d, IElement e) { if (DEBUG) System.out.println("EP.onElementRemoved(" + d + ", " + e + ")"); removeElement(e); } @Override public void elementChildrenChanged(ChildEvent event) { if (DEBUG) System.out.println("EP.elementChildrenChanged: " + event); for (IElement removed : event.removed) { removeElement(removed); } for (IElement added : event.added) { addElement(added, false); } } private final List childrenTemp = new ArrayList(); public void addElement(IElement e, boolean synchronizeSceneGraphNow) { if (DEBUG) System.out.println("EP.addElement(now=" + synchronizeSceneGraphNow + ", " + e + ")"); e.addKeyHintListener(Hints.KEY_DIRTY, elementHintListener); e.addKeyHintListener(ElementHints.KEY_VISIBLE_LAYERS, elementHintListener); e.addKeyHintListener(ElementHints.KEY_FOCUS_LAYERS, elementHintListener); ElementClass clazz = e.getElementClass(); G2DParentNode parentNode = elementParent; Key sgKey = ElementHints.KEY_SG_NODE; Parent parent = clazz.getAtMostOneItemOfClass(Parent.class); if (parent != null) { IElement parentElement = parent.getParent(e); if (parentElement != null) { SingleElementNode parentHolder = parentElement.getHint(sgKey); if (parentHolder != null) { parentNode = parentHolder; } } } boolean isConnection = e.getElementClass().containsClass(ConnectionHandler.class); if(isConnection) { ConnectionNode holder = e.getHint(sgKey); if (holder == null) { holder = parentNode.addNode(ElementUtils.generateNodeId(e), ConnectionNode.class); holder.setKey(e.getHint(ElementHints.KEY_OBJECT)); holder.setTransferableProvider(new ElementTransferableProvider(getContext(), e)); e.setHint(sgKey, holder); holder.setZIndex(parentNode.getNodeCount() + 1); } } else { SingleElementNode holder = e.getHint(sgKey); if (holder == null) { holder = parentNode.addNode(ElementUtils.generateNodeId(e), SingleElementNode.class); holder.setKey(e.getHint(ElementHints.KEY_OBJECT)); holder.setTransferableProvider(new ElementTransferableProvider(getContext(), e)); e.setHint(sgKey, holder); holder.setZIndex(parentNode.getNodeCount() + 1); } } Children children = clazz.getAtMostOneItemOfClass(Children.class); if (children != null) { children.addChildListener(e, this); childrenTemp.clear(); children.getChildren(e, childrenTemp); //System.out.println("children: " + childrenTemp); for (IElement child : childrenTemp) { addElement(child, false); } childrenTemp.clear(); } if (synchronizeSceneGraphNow) updateElement(e, sgKey); //setTreeDirty(); } protected void removeElement(IElement e) { if (DEBUG) System.out.println("EP.removeElement(" + e + ")"); e.removeKeyHintListener(Hints.KEY_DIRTY, elementHintListener); e.removeKeyHintListener(ElementHints.KEY_VISIBLE_LAYERS, elementHintListener); e.removeKeyHintListener(ElementHints.KEY_FOCUS_LAYERS, elementHintListener); ElementClass clazz = e.getElementClass(); if (clazz.containsClass(Children.class)) { Children children = clazz.getSingleItem(Children.class); children.removeChildListener(e, this); } List nodeHandlers = e.getElementClass().getItemsByClass(SceneGraph.class); for (SceneGraph n : nodeHandlers) { n.cleanup(e); } // Remove all hints related to scene graph nodes to prevent leakage of // scene graph resources. Map sgHints = e.getHintsOfClass(SceneGraphNodeKey.class); for (SceneGraphNodeKey sgKey : sgHints.keySet()) { Node n = e.removeHint(sgKey); if (n != null) { n.remove(); } } //setTreeDirty(); } /** * Invalidate the whole scene graph spatial structure. It will be rebuilt by * RTreeNode when needed the next time. */ private void setTreeDirty() { elementParent.setDirty(); } /** * Mark the specified node invalid with respect to the scene graph spatial * structure. * * @param node a scene graph node that has somehow changed */ private void invalidateNode(INode node) { // TODO: optimize rtree updates instead of killing the whole tree elementParent.setDirty(); } // ------------------------------------------------------------------------ // Diagram/Element hint listeners // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> public void updateAll() { if (DEBUG) System.out.println("EP.updateAll()"); Object task = BEGIN("EP.updateAll"); paintDiagram(elementParent, diagram, null); updateSelections(); setTreeDirty(); END(task); } public void update(IElement element) { updateElement(element, ElementHints.KEY_SG_NODE); } /** * * @param controlGC * @param diagram * @param elementsToPaint * elements to paint or null for all elements */ public void paintDiagram(G2DParentNode parent, IDiagram diagram, Collection elementsToPaint) { Object task = BEGIN("EP.paintDiagram"); paintDiagram(parent, diagram, elementsToPaint, ElementHints.KEY_SG_NODE); END(task); } /** * * @param controlGC * @param diagram * @param elementsToPaint * elements to paint or null for all elements */ public void paintDiagram(G2DParentNode parent, IDiagram diagram, Collection elementsToPaint, Key elementSgNodeKey) { if(diagram == null) return; ICanvasContext ctx = getContext(); assert (ctx != null); Boolean disablePaint = diagram.getHint(Hints.KEY_DISABLE_PAINTING); if (Boolean.TRUE.equals(disablePaint)) { parent.removeNodes(); return; } // Paint elementsToPaint in correct z-order from diagram.getElements() List elements = diagram.getSnapshot(); Set tmp = new HashSet(); int zIndex = 0; for (int pass = 0; pass < 1; ++pass) { for (IElement e : elements) { if (elements != elementsToPaint && elementsToPaint != null) if (!elementsToPaint.contains(e)) continue; if (DEBUG) System.out.println("EP.paintDiagram(" + zIndex + ", " + e + ")"); SingleElementNode holder = updateElement(parent, e, elementSgNodeKey, false); if (holder != null) { tmp.add(holder); holder.setZIndex(++zIndex); } } } // Hide unaccessed nodes (but don't remove) for (IG2DNode node : parent.getNodes()) { if (!tmp.contains(node)) { ((SingleElementNode)node).setVisible(false); } } } public void updateElement(IElement e, Key elementSgNodeKey) { updateElement(null, e, elementSgNodeKey, true); } /** * @param parent if null the scene graph node structure * will not be created if it is missing * @param e * @param elementSgNodeKey * @param invalidateNode */ public SingleElementNode updateElement(G2DParentNode parent, IElement e, Key elementSgNodeKey, boolean invalidateNode) { if (DEBUG) System.out.println("EP.updateElement(" + e + ", " + elementSgNodeKey + ")"); Object task = BEGIN("EP.updateElement"); try { SingleElementNode holder = e.getHint(elementSgNodeKey); if (holder == null && parent == null) return null; if (ElementUtils.isHidden(e)) return null; // ElementClass ec = e.getElementClass(); // ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS); // if (layers != null && !layers.getIgnoreVisibilitySettings()) { // ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class); // if (el != null && !el.isVisible(e, layers)) { // return null; // } // } // Update the node scene graph through SceneGraph handlers. List nodeHandlers = e.getElementClass().getItemsByClass(SceneGraph.class); Collection decorators = e.getHint(ElementHints.KEY_DECORATORS); if (nodeHandlers.isEmpty() && (decorators == null || decorators.isEmpty())) return null; Composite composite = e.getHint(ElementHints.KEY_COMPOSITE); if (holder == null) { holder = parent.addNode(ElementUtils.generateNodeId(e), SingleElementNode.class); e.setHint(elementSgNodeKey, holder); } holder.setComposite(composite); holder.setVisible(true); for (SceneGraph n : nodeHandlers) { n.init(e, holder); } // Process decorators if (decorators == null || decorators.isEmpty()) { holder.removeNode("decorators"); } else { G2DParentNode decoratorHolder = holder.getOrCreateNode("decorators", G2DParentNode.class); decoratorHolder.removeNodes(); for (SceneGraph decorator : decorators) { decorator.init(e, decoratorHolder); } } if (invalidateNode) invalidateNode(holder); return holder; } finally { END(task); } } /** * @param elementsToUpdate to explicitly specify which elements to update * the selection scene graph for, or null to update * everything */ public void updateSelections() { Object task = BEGIN("EP.updateSelections"); try { if (!paintSelectionFrames) return; if (selection == null) return; boolean selectionsChanged = false; // Update and "touch" all selections. Set existingSelections = new HashSet(); Set selectionNodes = new HashSet(); Set tmp = new HashSet(); Map selectionLinks = new HashMap(); for (Map.Entry> entry : selection.getSelections().entrySet()) { Integer selectionId = entry.getKey(); Set selectedElements = entry.getValue(); existingSelections.add(selectionId); // System.out.println("SELECTION[" + selectionId + "]: " + selectedElements); ElementNodeBridge bridge = getOrCreateSelectionMap(selectionId); selectionNodes.clear(); selectionsChanged |= paintSelection(selectedElements, selectionId, selectionNodes, bridge); // Remove selection nodes that were not referenced during the update. // System.out.println("BRIDGE: " + bridge.toString()); // System.out.println("SELECTED: " + selectionNodes); tmp.clear(); tmp.addAll(bridge.getRightSet()); tmp.removeAll(selectionNodes); // System.out.println("REMOVED: " + tmp); // System.out.println("BRIDGE BEFORE: " + bridge); selectionsChanged |= bridge.retainAllRight(selectionNodes); // System.out.println("BRIDGE AFTER: " + bridge); G2DParentNode selectionsNode = getSelectionsNode(selectionId); selectionLinks.clear(); getSelectedNodeReferences(selectionsNode, selectionLinks); for (INode node : tmp) { INode linkNode = selectionLinks.get(node.getParent()); if (linkNode != null) { linkNode.remove(); } // System.out.println("REMOVED SELECTION: -> " + node); node.remove(); } } for (Iterator> iterator = selections.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = iterator.next(); Integer selectionId = entry.getKey(); if (!existingSelections.contains(selectionId)) { // Selection no longer exists. selectionsChanged = true; for (INode node : entry.getValue().getRightSet()) { // System.out.println("REMOVED SELECTION: " + node); node.remove(); } iterator.remove(); G2DParentNode selectionsNode = getSelectionsNode(selectionId); selectionsNode.removeNodes(); } } // Make sure the view is refreshed after selection changes. if (selectionsChanged) { setDirty(); } } finally { END(task); } } private G2DParentNode getSelectionsNode() { G2DParentNode sels = NodeUtil.lookup(diagramParent, SceneGraphConstants.SELECTIONS_NODE_NAME, G2DParentNode.class); if (sels == null) { DataNode data= NodeUtil.lookup(diagramParent, SceneGraphConstants.DATA_NODE_NAME, DataNode.class); sels = data.addNode(SceneGraphConstants.SELECTIONS_NODE_NAME, G2DParentNode.class); sels.setLookupId(SceneGraphConstants.SELECTIONS_NODE_NAME); } return sels; } private G2DParentNode getSelectionsNode(int selectionId) { G2DParentNode selectionsNode = getSelectionsNode(); G2DParentNode s = selectionsNode.getOrCreateNode(String.valueOf(selectionId), G2DParentNode.class); return s; } private Map getSelectedNodeReferences(G2DParentNode selectionsNode, Map result) { for (IG2DNode node : selectionsNode.getSortedNodes()) { if (node instanceof LinkNode) { INode n = ((LinkNode) node).getDelegate(); if (n != null) result.put(n, (LinkNode) node); } } return result; } public void updateSelection(IElement el) { Object task = BEGIN("EP.updateSelection"); try { if (!paintSelectionFrames) return; G2DParentNode elementNode = (G2DParentNode) el.getHint(ElementHints.KEY_SG_NODE); if (elementNode == null) return; boolean nodesUpdated = false; for (Map.Entry entry : selections.entrySet()) { Integer selectionId = entry.getKey(); ElementNodeBridge bridge = entry.getValue(); Color color = getSelectionColor(selectionId); G2DParentNode selectionNode = (G2DParentNode) bridge.getRight(el); if (selectionNode == null) continue; if (NodeUtil.needSelectionPaint(elementNode)) paintSelectionFrame(elementNode, selectionNode, el, color); nodesUpdated = true; } // Make sure the view is refreshed after selection changes. if (nodesUpdated) setDirty(); } finally { END(task); } } /** * @param selection * @param selectionId * @param selectionNodes for collecting all the "selection" nodes created or * referenced by this method * @param bridge * @return */ public boolean paintSelection(Set selection, int selectionId, Set selectionNodes, ElementNodeBridge bridge) { boolean result = false; Color color = getSelectionColor(selectionId); G2DParentNode selectionsNode = getSelectionsNode(selectionId); for (IElement e : selection) { Node elementNode = e.getHint(ElementHints.KEY_SG_NODE); // System.out.println("selectionNode: " + elementNode + " " + e); if (elementNode instanceof G2DParentNode) { G2DParentNode en = (G2DParentNode) elementNode; G2DParentNode selectionNode = en.getOrCreateNode(NodeUtil.SELECTION_NODE_NAME, G2DParentNode.class); selectionNode.setZIndex(SELECTION_PAINT_PRIORITY); if (selectionNodes != null) selectionNodes.add(selectionNode); if (!bridge.containsLeft(e)) { //System.out.println("ADDED SELECTION: " + e + " -> " + selectionNode); bridge.map(e, selectionNode); result = true; } // Mark this node selected in the scene graph "data area" createSelectionReference(selectionsNode, elementNode); if (NodeUtil.needSelectionPaint(elementNode)) paintSelectionFrame(en, selectionNode, e, color); } else { if (elementNode != null) { // Cannot paint selection for unrecognized non-parenting node System.out.println("Cannot add selection child node for non-parent element node: " + elementNode); } } } // if (selection.isEmpty()) { // Node pivotNode = (Node) parent.getNode("pivot"); // if (pivotNode != null) // pivotNode.remove(); // } else { // Point2D pivot = ElementUtils.getElementBoundsCenter(selection, pivotPoint); // if (pivot != null) { // //System.out.println("painting pivot: " + pivot); // SelectionPivotNode pivotNode = parent.getOrCreateNode("pivot", SelectionPivotNode.class); // pivotNode.setPivot(pivot); // } else { // parent.removeNode("pivot"); // } // } return result; } public void paintSelectionFrame(G2DParentNode elementNode, G2DParentNode selectionNode, final IElement e, Color color) { // The element node already has the correct transform. AffineTransform selectionTransform = ElementUtils.getTransform(e);// no it doesnt ... new AffineTransform(); Shape shape = ElementUtils.getElementShapeOrBounds(e); Rectangle2D bounds = shape.getBounds2D(); //System.out.println("selection bounds: "+bounds); final double marginX = 1 / selectionTransform.getScaleX(); final double marginY = 1 / selectionTransform.getScaleY(); bounds.setFrame(bounds.getMinX() - marginX, bounds.getMinY() - marginY, bounds.getWidth() + 2*marginX, bounds.getHeight() + 2*marginY); List ss = e.getElementClass().getItemsByClass(SelectionSpecification.class); if (!ss.isEmpty()) { G2DParentNode shapeholder = selectionNode.getOrCreateNode(getNodeId("outlines", e), G2DParentNode.class); for (SelectionSpecification es : ss) { Outline outline = (Outline) es.getAdapter(Outline.class); if (outline == null || outline.getElementShape(e) == null) continue; ShapeNode shapenode = shapeholder.getOrCreateNode(getNodeId("outline", e, es), ShapeNode.class); // shapenode.setShape(es.getSelectionShape(e)); // shapenode.setStroke(SELECTION_STROKE); // shapenode.setScaleStroke(true); // shapenode.setColor(color); // shapenode.setTransform(selectionTransform); // shapenode.setFill(false); shapenode.setShape(outline.getElementShape(e)); StrokeSpec strokeSpec = (StrokeSpec) es.getAdapter(StrokeSpec.class); if (strokeSpec != null && strokeSpec.getStroke(e) != null) shapenode.setStroke(strokeSpec.getStroke(e)); shapenode.setScaleStroke(false); //shapenode.setColor(color); OutlineColorSpec foregroundColor = (OutlineColorSpec) es.getAdapter(OutlineColorSpec.class); if (foregroundColor != null && foregroundColor.getColor(e) != null) shapenode.setColor(foregroundColor.getColor(e)); Transform transform = (Transform) es.getAdapter(Transform.class); if (transform != null && transform.getTransform(e) != null) shapenode.setTransform(transform.getTransform(e)); shapenode.setFill(false); FillColor fillColor = (FillColor) es.getAdapter(FillColor.class); if (fillColor != null && fillColor.getFillColor(e) != null) shapenode.setFill(true); // shapenode.setColor(ColorUtil.withAlpha(backgroundColor.getColor(e), 192)); } return; } List shapeHandlers = e.getElementClass().getItemsByClass(SelectionOutline.class); if (!shapeHandlers.isEmpty()) { G2DParentNode shapeholder = selectionNode.getOrCreateNode(getNodeId("outlines", e), G2DParentNode.class); for (SelectionOutline es : shapeHandlers) { ShapeNode shapenode = shapeholder.getOrCreateNode(getNodeId("outline", e, es), ShapeNode.class); // shapenode.setShape(es.getSelectionShape(e)); // shapenode.setStroke(SELECTION_STROKE); // shapenode.setScaleStroke(true); // shapenode.setColor(color); // shapenode.setTransform(selectionTransform); // shapenode.setFill(false); shapenode.setShape(es.getSelectionShape(e)); shapenode.setStroke(null); shapenode.setScaleStroke(false); //shapenode.setColor(color); shapenode.setColor(ColorUtil.withAlpha(color, 192)); shapenode.setTransform(selectionTransform); shapenode.setFill(true); } return; } ISelectionProvider provider = this.getContext().getDefaultHintContext().getHint(KEY_SELECTION_PROVIDER); if (provider != null) { provider.init(e, selectionNode, getNodeId("shape", e), selectionTransform, bounds, color); } else { SelectionNode s = selectionNode.getOrCreateNode(getNodeId("shape", e), SelectionNode.class); s.init(selectionTransform, bounds, color); } } private void createSelectionReference(G2DParentNode selectionsNode, INode elementNode) { String id = NodeUtil.lookupId(elementNode); String uuid = null; if (id == null) id = uuid = UUID.randomUUID().toString(); NodeUtil.map(elementNode, id); LinkNode link = selectionsNode.getOrCreateNode(id, LinkNode.class); link.setDelegateId(id); link.setIgnoreDelegate(true); link.setLookupIdOwner(uuid != null); } private transient CharBuffer buf = CharBuffer.allocate(32); private String getNodeId(String prefix, Object first) { return getNodeId(prefix, first, null); } private String getNodeId(String prefix, Object first, Object second) { buf.clear(); if (prefix != null) buf.append(prefix); if (first != null) { buf.append('_'); buf.append("" + first.hashCode()); } if (second != null) { buf.append('_'); buf.append("" + second.hashCode()); } buf.limit(buf.position()); buf.rewind(); //System.out.println("node id: " + buf.toString()); return buf.toString(); } /** * Get selection color for a selection Id * @param selectionId selection id * @return color for the id */ protected Color getSelectionColor(int selectionId) { if (selectionId == 0) { Color c = getHint(KEY_SELECTION_FRAME_COLOR); if (c != null) return c; return Color.BLACK; } Color c = selectionColor.get(selectionId); if (c == null) { Random r = new Random(selectionId); c = new Color(r.nextFloat(), r.nextFloat(), r.nextFloat()); selectionColor.put(selectionId, c); } return c; } private transient ConcurrentMap selections = new ConcurrentHashMap(); ElementNodeBridge getSelectionMap(int selectionId) { return selections.get(Integer.valueOf(selectionId)); } ElementNodeBridge getOrCreateSelectionMap(int selectionId) { Integer id = Integer.valueOf(selectionId); synchronized (selections) { ElementNodeBridge map = selections.get(id); if (map != null) return map; selections.put(id, map = new ElementNodeBridge(id)); return map; } } private transient Map selectionColor = new HashMap(); private transient BasicStroke SELECTION_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[] { 5.0f, 5.0f }, 0.0f); private transient Point2D pivotPoint = new Point2D.Double(); @HintListener(Class=Selection.class, Field="SELECTION0") public void selectionChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { //System.out.println("new selection: " + newValue); updateSelections(); } @HintListener(Class=Selection.class, Field="SELECTION0") public void selectionRemoved(IHintObservable sender, Key key, Object oldValue) { //System.out.println("selection removed: " + oldValue); updateSelections(); } private static Object BEGIN(String name) { if (DEBUG) { //return ThreadLog.BEGIN(name); } return null; } private static void END(Object task) { if (DEBUG) { //((Task) task).end(); } } }