/******************************************************************************* * 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.scenegraph.g2d; import java.awt.Component; import java.awt.Container; import java.awt.Graphics2D; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JComponent; import javax.swing.RepaintManager; import org.simantics.scenegraph.ILookupService; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.LookupService; import org.simantics.scenegraph.ParentNode; import org.simantics.scenegraph.g2d.events.EventDelegator; import org.simantics.scenegraph.g2d.events.INodeEventHandlerProvider; import org.simantics.scenegraph.g2d.events.NodeEventHandler; /** * The root node of a 2D scene graph. * * Implements {@link ILookupService} according to the reference implementation * {@link LookupService}. * * @author J-P Laine */ @SuppressWarnings("deprecation") public class G2DSceneGraph extends G2DParentNode implements ILookupService, INodeEventHandlerProvider { private static final long serialVersionUID = -7066146333849901429L; public static final String IGNORE_FOCUS = "ignoreFocus"; protected transient Container rootPane = null; // TODO: swing dependency in here might not be a good idea // This variable is actually used in remote use, when rendering is not performed locally private transient final Object treeLock = new Object(); private HashMap pending = new HashMap(); private HashMap globalProperties = new HashMap(); /** * For preventing duplicates on the nodesToRemove queue. */ protected transient Set nodesToRemoveSet = new HashSet(); protected Deque nodesToRemove = new ArrayDeque(); private transient EventDelegator eventDelegator = new EventDelegator(this); private transient NodeEventHandler eventHandler = new NodeEventHandler(this); /** * The node that has input focus in the scene graph. The input node will * receive key and command events. */ private transient IG2DNode focusNode; /** * The custom repaint manager of this scene graph. */ private transient G2DRepaintManager repaintManager; /** * Returns the event delegator, that is responsible for delegating events to nodes in the sg tree * * @return EventDelegator instance, always not null */ public EventDelegator getEventDelegator() { return eventDelegator; } /** * Returns the node event handler, that is responsible for delegating events * to nodes in the sg tree. * * @return NodeEventHandler instance for this scene graph, always non-null */ public NodeEventHandler getEventHandler() { return eventHandler; } public void setFocusNode(IG2DNode focusNode) { this.focusNode = focusNode; } public IG2DNode getFocusNode() { return focusNode; } @Override public void render(Graphics2D g2d) { refresh(); Component rootPane = getRootPane(); if (rootPane != null) g2d.setRenderingHint(G2DRenderingHints.KEY_COMPONENT, rootPane); synchronized(treeLock) { super.render(g2d); } } @Override public void refresh() { performCleanup(); super.refresh(); } /** * Util method for executing updates to scenegraph tree * NOTE: You should really consider performance issues when using this * * @param r Runnable to be executed while rendering is not performed */ public void syncExec(Runnable r) { synchronized(treeLock) { r.run(); } } /** * Set rootpane for swing components. This is used as parent for the components created by ComponentNode. * * @param rootPane Component that is used as a parent for the swing component (This shouldn't be visible) */ public void setRootPane(JComponent rootPane) { synchronized (RepaintManager.class) { RepaintManager old = RepaintManager.currentManager(rootPane); old = findProperRepaintManager(old); this.repaintManager = new G2DRepaintManager(rootPane.getClass(), old); RepaintManager.setCurrentManager(repaintManager); } this.rootPane = rootPane; } /** * Set rootpane for swing components. This is used as parent for the components created by ComponentNode. * Supports separate component that is responsible for repainting the scenegraph. * * @param rootPane Component that is used as a parent for the swing component (This shouldn't be visible) * @param paintContext Component that is responsible for repainting the scenegraph */ public void setRootPane(Container rootPane, Component paintContext) { synchronized (RepaintManager.class) { RepaintManager old = RepaintManager.currentManager(paintContext); old = findProperRepaintManager(old); this.repaintManager = new G2DRepaintManager(paintContext.getClass(), old); RepaintManager.setCurrentManager(repaintManager); } this.rootPane = rootPane; } private RepaintManager findProperRepaintManager(RepaintManager old) { while (old instanceof G2DRepaintManager) { G2DRepaintManager g2drm = (G2DRepaintManager) old; old = g2drm.getDelegate(); } return old; } public G2DRepaintManager getRepaintManager() { return repaintManager; } /** * Put the node to the remove queue */ @Override public void asyncRemoveNode(INode node) { synchronized(nodesToRemove) { // Prevent nodes from winding up twice on the nodesToRemove queue if (nodesToRemoveSet.add(node)) { nodesToRemove.add(node); // This is performed when called inside the render } } } /** * Perform the actual removal of the nodes in the nodesToRemove list */ public void performCleanup() { synchronized(nodesToRemove) { while(nodesToRemove.size() > 0) { INode node = nodesToRemove.removeFirst(); ParentNode parent = node.getParent(); // This works around issue #2071 if (parent != null) parent.removeNode(node); } if (!nodesToRemoveSet.isEmpty()) nodesToRemoveSet.clear(); } } @Override public void cleanup() { super.cleanup(); nodesToRemove.clear(); nodesToRemove = null; nodesToRemoveSet.clear(); nodesToRemoveSet = null; eventHandler.dispose(); eventHandler = null; eventDelegator.dispose(); eventDelegator = null; } public Container getRootPane() { return (Container) this.rootPane; } @Override public String toString() { return super.toString() + " [root pane=" + rootPane + "]"; } @Override public ParentNode getRootNode() { // This is a root node! return this; } // ILookupService implementation private final Object lookupLock = new Object(); private final Map toNode = new HashMap(); private final Map toId = new HashMap(); transient Logger logger = Logger.getLogger(getClass().getName()); @Override public INode map(String id, INode node) { if (id == null) throw new NullPointerException("null id"); if (node == null) throw new NullPointerException("null node"); INode oldNode; String oldId; synchronized (lookupLock) { oldNode = toNode.put(id, node); oldId = toId.put(node, id); // Keep the mapping a consistent bijection: // If ID => INode mapping is removed, the INode => ID mappings must // removed also. if (oldNode != null && !oldNode.equals(node)) { String removedId = toId.remove(oldNode); if (!id.equals(removedId)) toNode.remove(removedId); } if (oldId != null && !oldId.equals(id)) { INode removedNode = toNode.remove(oldId); if (removedNode != node) toId.remove(removedNode); } } if (logger.isLoggable(Level.FINER)) logger.fine("map(" + id + ", " + node + ")"); if (oldNode != null || oldId != null) { if (logger.isLoggable(Level.FINE)) { logger.info("replaced mappings for ID " + oldId + " and node " + oldNode); } } return oldNode; } @Override public INode unmap(String id) { INode node; String mappedId; synchronized (lookupLock) { node = toNode.remove(id); if (node == null) return null; mappedId = toId.remove(node); } if (logger.isLoggable(Level.FINER)) logger.fine("unmap(" + id + "): " + node); if (mappedId != null && !mappedId.equals(id)) { if (logger.isLoggable(Level.WARNING)) logger.log(Level.WARNING, "mapping was out-of-sync: " + id + " => " + node + " & " + mappedId + " => " + node, new Exception("trace")); } return node; } @Override public String unmap(INode node) { String id; INode mappedNode; synchronized (lookupLock) { id = toId.remove(node); if (node == null) return null; mappedNode = toNode.remove(id); } if (logger.isLoggable(Level.FINER)) logger.fine("unmap(" + node + "): " + id); if (mappedNode != null && node != mappedNode) { if (logger.isLoggable(Level.WARNING)) logger.log(Level.WARNING, "mapping was out-of-sync: " + node + " => " + id + " & " + id + " => " + mappedNode, new Exception("trace")); } return id; } @Override public INode lookupNode(String id) { synchronized (lookupLock) { return toNode.get(id); } } @Override public String lookupId(INode node) { synchronized (lookupLock) { return toId.get(node); } } public boolean isPending() { return !pending.isEmpty(); } synchronized public void increasePending(Object object) { Integer ref = pending.get(object); if (ref == null) pending.put(object, 1); else pending.put(object, ref+1); } synchronized public void setPending(Object object) { pending.put(object, 1); } synchronized public void clearPending(Object object) { pending.remove(object); } synchronized public void decreasePending(Object object) { Integer ref = pending.get(object); if (ref == null) { return; //throw new IllegalStateException("Ref count in unregister was 0 for " + object); } if (ref > 1) pending.put(object, ref-1); else if (ref==1) pending.remove(object); else { return; //throw new IllegalStateException("Ref count in unregister was 0 for " + object); } } synchronized public void setGlobalProperty(String key, Object value) { globalProperties.put(key, value); } @SuppressWarnings("unchecked") synchronized public T getGlobalProperty(String key, T defaultValue) { T t = (T)globalProperties.get(key); if(t == null) return defaultValue; return t; } }