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