/******************************************************************************* * Copyright (c) 2007, 2011 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.Container; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.LoaderNode; import org.simantics.scenegraph.ParentNode; import org.simantics.scenegraph.ScenegraphUtils; import org.simantics.scenegraph.g2d.events.Event; import org.simantics.scenegraph.g2d.events.EventTypes; import org.simantics.scenegraph.g2d.events.FocusEvent; import org.simantics.scenegraph.g2d.events.IEventHandler; import org.simantics.scenegraph.g2d.events.KeyEvent; import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent; import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseEnterEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseExitEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent; import org.simantics.scenegraph.g2d.events.NodeEventHandler; import org.simantics.scenegraph.g2d.events.TimeEvent; import org.simantics.scenegraph.g2d.events.command.CommandEvent; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.InitValueSupport; import org.simantics.scenegraph.utils.NodeUtil; import org.simantics.scl.runtime.function.Function1; import org.simantics.scl.runtime.function.Function2; import org.simantics.utils.threads.AWTThread; /** * @author Tuukka Lehtonen */ public class G2DParentNode extends ParentNode implements IG2DNode, InitValueSupport, LoaderNode { private static final long serialVersionUID = 4966823616578337420L; protected static final IG2DNode[] EMPTY_NODE_ARRAY = {}; protected static final String[] EMPTY_STRING_ARRAY = {}; private transient volatile String[] sortedChildrenIds = null; private transient volatile IG2DNode[] sortedChildren = null; protected AffineTransform transform = IdentityAffineTransform.INSTANCE; /** * Z-index of this node. Default value is 0. */ protected int z = 0; public void invalidateChildOrder() { sortedChildrenIds = null; sortedChildren = null; } @Override protected void childrenChanged() { invalidateChildOrder(); } @Override @SyncField("z") public void setZIndex(int z) { if (z != this.z) { G2DParentNode parent = (G2DParentNode) getParent(); if (parent != null) parent.invalidateChildOrder(); this.z = z; } } @Override public int getZIndex() { return z; } @Override public boolean validate() { return true; } @Override public void render(Graphics2D g2d) { AffineTransform ot = null; if (!transform.isIdentity()) { ot = g2d.getTransform(); g2d.transform(transform); } for (IG2DNode node : getSortedNodes()) { if (node.validate()) { node.render(g2d); } } if (ot != null) g2d.setTransform(ot); } @Override public void refresh() { for (IG2DNode node : getSortedNodes()) { if (node.validate()) { node.refresh(); } } } @Override public void accept(IG2DNodeVisitor visitor) { visitor.enter(this); for (IG2DNode node : getSortedNodes()) { if (node.validate()) { node.accept(visitor); } } visitor.leave(this); } /** * Return the IDs of the children of this node in ascending Z order. This * method will always allocate a new result list and sort it. To get the IDs * of the child nodes you need to use this method. Otherwise use * {@link #getSortedNodes()} instead, it is faster more memory efficient. * * @return child node IDs in ascending Z order */ public String[] getSortedNodesById() { if (sortedChildrenIds != null) return sortedChildrenIds; if (children.isEmpty()) return EMPTY_STRING_ARRAY; String[] sorted = null; synchronized (children) { if (sortedChildrenIds != null) return sortedChildrenIds; sorted = children.keySet().toArray(EMPTY_STRING_ARRAY); Arrays.sort(sorted, new Comparator() { @Override public int compare(String a, String b) { int za = getNode(a).getZIndex(); int zb = getNode(b).getZIndex(); return za < zb ? -1 : (za == zb ? 0 : 1); } }); sortedChildrenIds = sorted; } return sorted; } public static final Comparator G2DNODE_Z_COMPARATOR = new Comparator() { @Override public int compare(IG2DNode a, IG2DNode b) { int za = a.getZIndex(); int zb = b.getZIndex(); return za < zb ? -1 : (za == zb ? 0 : 1); } }; /** * @return child nodes in ascending Z order */ public IG2DNode[] getSortedNodes() { if (sortedChildren != null) return sortedChildren; if (children.isEmpty()) return EMPTY_NODE_ARRAY; IG2DNode[] sorted = null; synchronized (children) { if (sortedChildren != null) return sortedChildren; sorted = children.values().toArray(EMPTY_NODE_ARRAY); Arrays.sort(sorted, G2DNODE_Z_COMPARATOR); sortedChildren = sorted; } return sorted; } @Override public void cleanup() { rootNodeCache = DISPOSED; sortedChildren = null; sortedChildrenIds = null; transform = IdentityAffineTransform.INSTANCE; super.cleanup(); } @Override public void repaint() { INode parent = getParent(); while(parent != null && !(parent instanceof G2DSceneGraph)) parent = parent.getParent(); if(parent == null || ((G2DSceneGraph)parent).getRootPane() == null) return; ((G2DSceneGraph)parent).getRootPane().repaint(); } @Override public void asyncRemoveNode(INode node) { ParentNode parent = getParent(); while(parent != null && parent.getParent() != null) parent = parent.getParent(); if(parent != null) { parent.asyncRemoveNode(node); // Pass to root element } else { // This is root, should do something... (Actually G2DSceneGraph does something) } } // Bounds and transformation @Override public AffineTransform getTransform() { return transform; } @Override @PropertySetter("Transform") @SyncField("transform") public void setTransform(AffineTransform transform) { assert(transform != null); if (transform.isIdentity()) this.transform = IdentityAffineTransform.INSTANCE; else this.transform = transform; } /** * Return bounds transformed with local transformation * * @return transformed bounds */ @Override public Rectangle2D getBounds() { Rectangle2D local = getBoundsInLocal(); if (local == null) return null; // Optimize trivial identity transform case. if (transform.isIdentity()) return local; return transform.createTransformedShape(local).getBounds2D(); } // Helper methods for bounds checking @Override public boolean contains(Point2D point) { Rectangle2D bounds = getBounds(); if(bounds == null) return false; return bounds.contains(point); } @Override public boolean intersects(Rectangle2D b) { if (b == null) return true; Rectangle2D a = getBounds(); if (a == null) return true; /* * Compared to Rectangle2D.intersects, this * intersects closed (not open) shapes. */ double ax = a.getX(); double ay = a.getY(); double aw = a.getWidth(); double ah = a.getHeight(); double bx = b.getX(); double by = b.getY(); double bw = b.getWidth(); double bh = b.getHeight(); return (ax + aw >= bx && ay + ah >= by && ax <= bx + bw && ay <= by + bh); } @Override public Point2D localToParent(Point2D point) { return transform.transform(point, null); } @Override public Rectangle2D localToParent(Rectangle2D rect) { return transform.createTransformedShape(rect).getBounds2D(); } @Override public Point2D parentToLocal(Point2D point) { AffineTransform inverse = null; try { inverse = transform.createInverse(); return inverse.transform(point, null); } catch (NoninvertibleTransformException e) { e.printStackTrace(); // FIXME } return point; } @Override public Rectangle2D parentToLocal(Rectangle2D rect) { AffineTransform inverse = null; try { inverse = transform.createInverse(); return inverse.createTransformedShape(rect).getBounds2D(); } catch (NoninvertibleTransformException e) { e.printStackTrace(); // FIXME } return rect; } @Override public Rectangle2D getBoundsInLocal() { return getBoundsInLocal(false); } @Override public Rectangle2D getBoundsInLocal(boolean ignoreNulls) { Iterator it = getNodes().iterator(); if(!it.hasNext()) return null; Rectangle2D bounds = null; while(it.hasNext()) { IG2DNode node = it.next(); Rectangle2D b = node.getBoundsInLocal(ignoreNulls); if(b == null && !ignoreNulls) return null; if(b != null) { if(!GeometryUtils.isUndefinedRectangle(b)) { b = node.localToParent(b); if(bounds == null) { bounds = b.getFrame(); } else { bounds.add(b); } } } } return bounds; } @Override public Point2D localToControl(Point2D point) { IG2DNode node = this; while(node != null) { node.getTransform().transform(point, null); node = (G2DParentNode)node.getParent(); // FIXME: it should be G2DParentNode but you can never be sure } return point; } @Override public Rectangle2D localToControl(Rectangle2D rect) { Shape shape = rect; IG2DNode node = this; while(node != null) { shape = node.getTransform().createTransformedShape(shape); node = (G2DParentNode)node.getParent(); // FIXME: it should be G2DParentNode but you can never be sure } return shape.getBounds2D(); } public Point2D controlToLocal(Point2D point) { AffineTransform at = NodeUtil.getGlobalToLocalTransform(this, null); if (at == null) return point; return at.transform(point, null); } public Rectangle2D controlToLocal(Rectangle2D rect) { AffineTransform at = NodeUtil.getGlobalToLocalTransform(this, null); if (at == null) return rect; return GeometryUtils.transformRectangle(at, rect); } /** * Damn slow method for picking node * * @param point * @return */ public IG2DNode pickNode(Point2D point) { Point2D localpoint = parentToLocal(point); IG2DNode[] nodes = getSortedNodes(); for(int i = nodes.length-1; i >= 0; i--) { IG2DNode n = nodes[i]; // Reverse order.. if(n instanceof G2DParentNode) { IG2DNode node = ((G2DParentNode)n).pickNode(localpoint); if(node != null) return node; } else if(n.contains(localpoint)) { return n; } } return null; } @Override public String toString() { return super.toString() + " [z=" + z + ", transform=" + transform + "]"; } /** * TODO: not sure if this is a good idea at all. * * @see org.simantics.scenegraph.utils.InitValueSupport#initValues() */ @Override public void initValues() { for (IG2DNode node : getSortedNodes()) { if (node instanceof InitValueSupport) { ((InitValueSupport) node).initValues(); } } } /** * @see org.simantics.scenegraph.g2d.IG2DNode#getRootNode() */ public G2DSceneGraph getRootNode2D() { ParentNode root = getRootNode(); return (G2DSceneGraph) root; } @Override public boolean hasFocus() { return getFocusNode() == this; } @Override public IG2DNode getFocusNode() { return getRootNode2D().getFocusNode(); } @Override public void setFocusNode(IG2DNode node) { getRootNode2D().setFocusNode(node); } protected NodeEventHandler getEventHandler() { return NodeUtil.getNodeEventHandler(this); } protected void addEventHandler(IEventHandler handler) { getEventHandler().add(handler); } protected void removeEventHandler(IEventHandler handler) { getEventHandler().remove(handler); } @Override public int getEventMask() { return 0; } @Override public boolean handleEvent(Event e) { int eventType = EventTypes.toType(e); switch (eventType) { case EventTypes.Command: return handleCommand((CommandEvent) e); case EventTypes.FocusGained: case EventTypes.FocusLost: return handleFocusEvent((FocusEvent) e); case EventTypes.KeyPressed: return keyPressed((KeyPressedEvent) e); case EventTypes.KeyReleased: return keyReleased((KeyReleasedEvent) e); case EventTypes.MouseButtonPressed: return mouseButtonPressed((MouseButtonPressedEvent) e); case EventTypes.MouseButtonReleased: return mouseButtonReleased((MouseButtonReleasedEvent) e); case EventTypes.MouseClick: return mouseClicked((MouseClickEvent) e); case EventTypes.MouseDoubleClick: return mouseDoubleClicked((MouseDoubleClickedEvent) e); case EventTypes.MouseMoved: return mouseMoved((MouseMovedEvent) e); case EventTypes.MouseDragBegin: return mouseDragged((MouseDragBegin) e); case EventTypes.MouseEnter: return mouseEntered((MouseEnterEvent) e); case EventTypes.MouseExit: return mouseExited((MouseExitEvent) e); case EventTypes.MouseWheel: return mouseWheelMoved((MouseWheelMovedEvent) e); case EventTypes.Time: return handleTimeEvent((TimeEvent) e); } return false; } protected boolean keyReleased(KeyReleasedEvent e) { return false; } protected boolean keyPressed(KeyPressedEvent e) { return false; } protected boolean handleCommand(CommandEvent e) { return false; } protected boolean handleFocusEvent(FocusEvent e) { return false; } protected boolean handleKeyEvent(KeyEvent e) { return false; } protected boolean mouseButtonPressed(MouseButtonPressedEvent e) { return false; } protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) { return false; } protected boolean mouseClicked(MouseClickEvent e) { return false; } protected boolean mouseDoubleClicked(MouseDoubleClickedEvent e) { return false; } protected boolean mouseMoved(MouseMovedEvent e) { return false; } protected boolean mouseDragged(MouseDragBegin e) { return false; } protected boolean mouseEntered(MouseEnterEvent e) { return false; } protected boolean mouseExited(MouseExitEvent e) { return false; } protected boolean mouseWheelMoved(MouseWheelMovedEvent e) { return false; } protected boolean handleTimeEvent(TimeEvent e) { return false; } protected void setCursor(int cursorType) { Container rootPane = NodeUtil.findRootPane(this); if (rootPane != null) rootPane.setCursor(Cursor.getPredefinedCursor(cursorType)); } protected void setCursor(Cursor cursor) { Container rootPane = NodeUtil.findRootPane(this); if (rootPane != null) rootPane.setCursor(cursor); } @Override public Function1 getPropertyFunction(String propertyName) { return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName); } @Override public T getProperty(String propertyName) { return null; } @Override public void setPropertyCallback(Function2 callback) { } public void synchronizeTransform(double[] data) { this.setTransform(new AffineTransform(data)); } }