]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java
Faster bounds calculation for zoom to selection and navigate to target
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / participant / PanZoomRotateHandler.java
index b7321a386e6e919430bd28ac03d533037049774d..6ca4f786247b9e4cc82db6892ad150e0db685a41 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2010 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.g2d.participant;\r
-\r
-import static org.simantics.g2d.canvas.Hints.KEY_CANVAS_TRANSFORM;\r
-\r
-import java.awt.Shape;\r
-import java.awt.geom.AffineTransform;\r
-import java.awt.geom.Point2D;\r
-import java.awt.geom.Rectangle2D;\r
-import java.util.Set;\r
-\r
-import org.simantics.g2d.canvas.Hints;\r
-import org.simantics.g2d.canvas.ICanvasContext;\r
-import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;\r
-import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;\r
-import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;\r
-import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;\r
-import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;\r
-import org.simantics.g2d.diagram.DiagramHints;\r
-import org.simantics.g2d.diagram.DiagramUtils;\r
-import org.simantics.g2d.diagram.IDiagram;\r
-import org.simantics.g2d.diagram.participant.Selection;\r
-import org.simantics.g2d.element.ElementUtils;\r
-import org.simantics.g2d.element.IElement;\r
-import org.simantics.g2d.scenegraph.SceneGraphConstants;\r
-import org.simantics.g2d.utils.GeometryUtils;\r
-import org.simantics.scenegraph.INode;\r
-import org.simantics.scenegraph.g2d.G2DParentNode;\r
-import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;\r
-import org.simantics.scenegraph.g2d.events.command.Command;\r
-import org.simantics.scenegraph.g2d.events.command.CommandEvent;\r
-import org.simantics.scenegraph.g2d.events.command.Commands;\r
-import org.simantics.scenegraph.g2d.nodes.NavigationNode;\r
-import org.simantics.scenegraph.g2d.nodes.TransformNode;\r
-import org.simantics.scenegraph.utils.NodeUtil;\r
-import org.simantics.utils.datastructures.hints.HintListenerAdapter;\r
-import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
-import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;\r
-import org.simantics.utils.datastructures.hints.IHintListener;\r
-import org.simantics.utils.datastructures.hints.IHintObservable;\r
-import org.simantics.utils.page.MarginUtils;\r
-import org.simantics.utils.page.MarginUtils.Margins;\r
-import org.simantics.utils.page.PageDesc;\r
-import org.simantics.utils.threads.ThreadUtils;\r
-\r
-/**\r
- * This participant handles pan, zoom, zoom to fit and rotate commands.\r
- * \r
- * Hints:\r
- *  KEY_TRANSLATE_AMOUNT\r
- *  KEY_ZOOM_AMOUNT\r
- *  KEY_ROTATE_AMOUNT\r
- *  KEY_ZOOM_TO_FIT_MARGINS\r
- *  KEY_ZOOM_OUT_LIMIT\r
- *  KEY_ZOOM_IN_LIMIT\r
- * \r
- * @author Toni Kalajainen\r
- * @author Tuukka Lehtonen\r
- */\r
-public class PanZoomRotateHandler extends AbstractCanvasParticipant {\r
-\r
-    /**\r
-     * Express whether or not the view should attempt to keep the current zoom\r
-     * level when the canvas parenting control is resized. If the viewport is\r
-     * set to be adapted to the resized control, the view transform will be\r
-     * adjusted to accommodate for this. Otherwise the view transform will be\r
-     * left alone when the control is resized.\r
-     * \r
-     * If hint is not specified, the default value is <code>true</code>.\r
-     * \r
-     * See {@link NavigationNode} for the zoom level keep implementation.\r
-     */\r
-    public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL");\r
-\r
-    /**\r
-     * Limit for zooming in expressed as a percentage (100% == 1:1 == identity\r
-     * view transform). If null, there is no limit. Used with an\r
-     * ICanvasContext's hint context.\r
-     */\r
-    public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT");\r
-\r
-    /**\r
-     * Limit for zooming in expressed as a percentage (100% == 1:1 == identity\r
-     * view transform). If null there is no limit. Used with an\r
-     * ICanvasContext's hint context.\r
-     */\r
-    public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT");\r
-\r
-    public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM");\r
-\r
-    public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN");\r
-\r
-\r
-    @Dependency CanvasGrab grab;\r
-    @Dependency TransformUtil util;\r
-    @Dependency KeyUtil keys;\r
-    @Reference  Selection selection;\r
-    @Reference  CanvasBoundsParticipant bounds;\r
-\r
-    // Capture center point\r
-    Point2D centerPointControl;\r
-    Point2D centerPointCanvas;\r
-    Point2D controlSize;\r
-\r
-    final Boolean navigationEnabled;\r
-\r
-    protected NavigationNode node = null;\r
-    protected G2DParentNode oldRoot = null;\r
-\r
-    public PanZoomRotateHandler() {\r
-        this(true);\r
-    }\r
-\r
-    public PanZoomRotateHandler(boolean navigationEnabled) {\r
-        this.navigationEnabled = navigationEnabled;\r
-    }\r
-\r
-    NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() {\r
-        @Override\r
-        public void transformChanged(final AffineTransform transform) {\r
-            ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                    if (isRemoved())\r
-                        return;\r
-                    //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform);\r
-                    setHint(KEY_CANVAS_TRANSFORM, transform);\r
-                }\r
-            });\r
-        }\r
-    };\r
-\r
-    IHintListener hintListener = new HintListenerAdapter() {\r
-        @Override\r
-        public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {\r
-            if (node != null) {\r
-                if (key == Hints.KEY_DISABLE_PAINTING) {\r
-                    boolean visible = !Boolean.TRUE.equals(newValue);\r
-                    if (visible != node.isVisible())\r
-                        node.setVisible(Boolean.valueOf(visible));\r
-                } else if (key == KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL) {\r
-                    boolean noKeepZoom = Boolean.FALSE.equals(newValue);\r
-                    if (noKeepZoom == node.getAdaptViewportToResizedControl())\r
-                        node.setAdaptViewportToResizedControl(Boolean.valueOf(!noKeepZoom));\r
-                } else if (key == KEY_ZOOM_OUT_LIMIT) {\r
-                    node.setZoomOutLimit((Double) newValue);\r
-                } else if (key == KEY_ZOOM_IN_LIMIT) {\r
-                    node.setZoomInLimit((Double) newValue);\r
-                } else if (key == KEY_DISABLE_ZOOM) {\r
-                    node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));\r
-                }\r
-            }\r
-        }\r
-    };\r
-\r
-    @Override\r
-    public void addedToContext(ICanvasContext ctx) {\r
-        super.addedToContext(ctx);\r
-        ctx.getDefaultHintContext().addKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);\r
-        ctx.getDefaultHintContext().addKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);\r
-        ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);\r
-        ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);\r
-        ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_ZOOM, hintListener);\r
-        ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_PAN, hintListener);\r
-    }\r
-\r
-    @Override\r
-    public void removedFromContext(ICanvasContext ctx) {\r
-        ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);\r
-        ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);\r
-        ctx.getDefaultHintContext().removeKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);\r
-        ctx.getDefaultHintContext().removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);\r
-        ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_ZOOM, hintListener);\r
-        ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_PAN, hintListener);\r
-        super.removedFromContext(ctx);\r
-    }\r
-\r
-    @SGInit\r
-    public void initSG(G2DParentNode parent) {\r
-        // Replace old NAVIGATION_NODE with a new one\r
-        INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME);\r
-        if(oldnav != null) {\r
-            node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);\r
-            // FIXME : oldnav seems to be the same node as parent (most of the cases).\r
-            // Deleting it will cause plenty of code to fail, since they refer to the node directly.\r
-            // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now).             \r
-            // oldnav.delete();\r
-        } else {\r
-            node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);\r
-        }\r
-        node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);\r
-        node.setZIndex(0);\r
-        node.setTransformListener(transformListener);\r
-        node.setNavigationEnabled(navigationEnabled);\r
-        node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));\r
-        node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL)));\r
-        Double z = getHint(KEY_ZOOM_AMOUNT);\r
-        if(z != null) {\r
-            util.setTransform(AffineTransform.getScaleInstance(z, z));\r
-            node.setTransform(AffineTransform.getScaleInstance(z, z));\r
-        }\r
-        boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));\r
-        node.setVisible(visible);\r
-        oldRoot = getContext().getCanvasNode();\r
-        getContext().setCanvasNode(node);\r
-    }\r
-\r
-    public void update() {\r
-        if (bounds != null) {\r
-            Rectangle2D vp = bounds.getControlBounds();\r
-            controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY());\r
-            centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY());\r
-            centerPointCanvas = util.controlToCanvas(centerPointControl, null);\r
-        }\r
-    }\r
-\r
-    public TransformNode getNode() {\r
-        return node;\r
-    }\r
-\r
-    /**\r
-     * Ensures that the navigation node handled by this participant contains the\r
-     * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will\r
-     * contain the same value.\r
-     * \r
-     * @param transform\r
-     */\r
-    public void setTransform(AffineTransform transform) {\r
-        getNode().setTransform(transform);\r
-        transformListener.transformChanged(transform);\r
-    }\r
-\r
-    @SGCleanup\r
-    public void cleanupSG() {\r
-        node.remove();\r
-        node = null;\r
-        getContext().setCanvasNode(oldRoot);\r
-    }\r
-\r
-\r
-    /** Arrow key translate */\r
-    public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class);\r
-    public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class);\r
-    public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class);\r
-\r
-    /** Amount of arrow key translate */\r
-    public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30;\r
-    public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2;\r
-    public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1;\r
-    public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2;\r
-    public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2;\r
-\r
-    public final static int ROTATE_GRAB_ID = -666;\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean handleEvent(CommandEvent e) {\r
-        assertDependencies();\r
-        update();\r
-        Command c = e.command;\r
-        boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false;\r
-        boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false;\r
-\r
-        // Arrow key panning\r
-        if (Commands.PAN_LEFT.equals(c) && !panDisabled) {\r
-            util.translateWithControlCoordinates(\r
-                    new Point2D.Double(\r
-                            getTranslateAmount(), 0));\r
-            return true;\r
-        }\r
-        if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {\r
-            util.translateWithControlCoordinates(\r
-                    new Point2D.Double(\r
-                            -getTranslateAmount(), 0));\r
-            return true;\r
-        }\r
-        if (Commands.PAN_UP.equals(c) && !panDisabled) {\r
-            util.translateWithControlCoordinates(\r
-                    new Point2D.Double(\r
-                            0, getTranslateAmount()));\r
-            return true;\r
-        }\r
-        if (Commands.PAN_DOWN.equals(c) && !panDisabled) {\r
-            util.translateWithControlCoordinates(\r
-                    new Point2D.Double(0, -getTranslateAmount()));\r
-            return true;\r
-        }\r
-        if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) {\r
-            if (centerPointControl == null) return false;\r
-            double scaleFactor = getZoomAmount();\r
-            scaleFactor = limitScaleFactor(scaleFactor);\r
-            util.zoomAroundControlPoint(scaleFactor, centerPointControl);\r
-        }\r
-        if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) {\r
-            if (centerPointControl == null) return false;\r
-            double scaleFactor = 1 / getZoomAmount();\r
-            scaleFactor = limitScaleFactor(scaleFactor);\r
-            util.zoomAroundControlPoint(scaleFactor, centerPointControl);\r
-        }\r
-\r
-        if (Commands.ROTATE_CANVAS_CCW.equals(c)) {\r
-            if (centerPointCanvas == null) return false;\r
-            util.rotate(centerPointCanvas, -getRotateAmount());\r
-            setDirty();\r
-            return true;\r
-        }\r
-        if (Commands.ROTATE_CANVAS_CW.equals(c)) {\r
-            if (centerPointCanvas == null) return false;\r
-            util.rotate(centerPointCanvas, getRotateAmount());\r
-            setDirty();\r
-            return true;\r
-        }\r
-        if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) {\r
-            if (centerPointCanvas == null) return false;\r
-            util.rotate(centerPointCanvas, -getRotateAmount());\r
-            grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);\r
-            grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);\r
-            setDirty();\r
-            return true;\r
-        }\r
-        if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) {\r
-            if (centerPointCanvas == null) return false;\r
-            util.rotate(centerPointCanvas, getRotateAmount());\r
-            grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);\r
-            grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);\r
-            setDirty();\r
-            return true;\r
-        }\r
-        if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) {\r
-            if (centerPointCanvas == null) return false;\r
-            grab.releaseCanvas(ROTATE_GRAB_ID);\r
-            grab.releaseCanvas(ROTATE_GRAB_ID - 1);\r
-            setDirty();\r
-            return true;\r
-        }\r
-        if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) {\r
-            if (centerPointCanvas == null) return false;\r
-            grab.releaseCanvas(ROTATE_GRAB_ID);\r
-            grab.releaseCanvas(ROTATE_GRAB_ID - 1);\r
-            setDirty();\r
-            return true;\r
-        }\r
-        if (Commands.ENABLE_PAINTING.equals(c)) {\r
-            Boolean t = getHint(Hints.KEY_DISABLE_PAINTING);\r
-            removeHint(Hints.KEY_DISABLE_PAINTING);\r
-            boolean processed = Boolean.TRUE.equals(t);\r
-            if (processed)\r
-                setDirty();\r
-            return processed;\r
-        }\r
-        if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {\r
-            boolean result = zoomToFit();\r
-            if (!result)\r
-                result = zoomToPage();\r
-            return result;\r
-        }\r
-        if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) {\r
-            if (controlSize==null) return false;\r
-            IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);\r
-            if (d==null) return false;\r
-\r
-            Set<IElement> selections = selection.getAllSelections();\r
-            Shape bounds = ElementUtils.getElementBoundsOnDiagram(selections);\r
-            if (bounds == null) return false;\r
-            Rectangle2D diagramRect = bounds.getBounds2D();\r
-            if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0)\r
-                return false;\r
-\r
-            // HACK: prevents straight connections from being unzoomable.\r
-            if (diagramRect.getWidth() <= 0)\r
-                org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1);\r
-            if (diagramRect.getHeight() <= 0)\r
-                org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0);\r
-\r
-            // Show area\r
-            Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());\r
-            util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));\r
-            return true;\r
-        }\r
-        if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {\r
-            return zoomToPage();\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    private boolean zoomToFit() {\r
-        if (controlSize==null) return false;\r
-        IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);\r
-        if (d==null) return false;\r
-\r
-        Rectangle2D diagramRect = DiagramUtils.getContentRect(d);\r
-        if (diagramRect==null) return false;\r
-        if (diagramRect.isEmpty())\r
-            return false;\r
-\r
-        // Show area\r
-        Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());\r
-        //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")");\r
-        util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));\r
-\r
-        return true;\r
-    }\r
-\r
-    private boolean zoomToPage() {\r
-        if (controlSize==null) return false;\r
-        PageDesc desc = getHint(Hints.KEY_PAGE_DESC);\r
-        if (desc == null)\r
-            return false;\r
-        if (desc.isInfinite())\r
-            return false;\r
-\r
-        // Show page\r
-        Rectangle2D diagramRect = new Rectangle2D.Double();\r
-        desc.getPageRectangle(diagramRect);\r
-        if (diagramRect.isEmpty())\r
-            return false;\r
-\r
-        Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());\r
-        //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")");\r
-        util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));\r
-        return true;\r
-    }\r
-\r
-    public double getTranslateAmount()\r
-    {\r
-        Integer h = getHint(KEY_TRANSLATE_AMOUNT);\r
-        if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;\r
-        return h;\r
-    }\r
-\r
-    public double getZoomAmount()\r
-    {\r
-        Integer h = getHint(KEY_TRANSLATE_AMOUNT);\r
-        if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;\r
-        return h;\r
-    }\r
-\r
-    public double getRotateAmount()\r
-    {\r
-        Integer h = getHint(KEY_ROTATE_AMOUNT);\r
-        if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;\r
-        return h;\r
-    }\r
-\r
-    public double limitScaleFactor(double scaleFactor) {\r
-        Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);\r
-        Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);\r
-\r
-        if (inLimit == null && scaleFactor < 1)\r
-            return scaleFactor;\r
-        if (outLimit == null && scaleFactor > 1)\r
-            return scaleFactor;\r
-\r
-        AffineTransform view = util.getTransform();\r
-        double currentScale = GeometryUtils.getScale(view) * 100.0;\r
-        double newScale = currentScale * scaleFactor;\r
-\r
-        if (inLimit != null && newScale > currentScale && newScale > inLimit) {\r
-            if (currentScale < inLimit)\r
-                scaleFactor = inLimit / currentScale;\r
-            else\r
-                scaleFactor = 1.0;\r
-        } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {\r
-            if (currentScale > outLimit)\r
-                scaleFactor = outLimit / currentScale;\r
-            else\r
-                scaleFactor = 1.0;\r
-        }\r
-        return scaleFactor;\r
-    }\r
-\r
-    public static Margins getZoomToFitMargins(IHintObservable hints) {\r
-        Margins h = hints.getHint(DiagramHints.KEY_MARGINS);\r
-        if (h == null) {\r
-            Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);\r
-            boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);\r
-            if (rulerEnabled) {\r
-                return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;\r
-            } else {\r
-                return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;\r
-            }\r
-        }\r
-        return h;\r
-    }\r
-    \r
+/*******************************************************************************
+ * 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.participant;
+
+import static org.simantics.g2d.canvas.Hints.KEY_CANVAS_TRANSFORM;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.Set;
+
+import org.simantics.g2d.canvas.Hints;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;
+import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
+import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
+import org.simantics.g2d.diagram.DiagramHints;
+import org.simantics.g2d.diagram.DiagramUtils;
+import org.simantics.g2d.diagram.IDiagram;
+import org.simantics.g2d.diagram.participant.Selection;
+import org.simantics.g2d.element.ElementUtils;
+import org.simantics.g2d.element.IElement;
+import org.simantics.g2d.scenegraph.SceneGraphConstants;
+import org.simantics.g2d.utils.GeometryUtils;
+import org.simantics.scenegraph.INode;
+import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
+import org.simantics.scenegraph.g2d.events.command.Command;
+import org.simantics.scenegraph.g2d.events.command.CommandEvent;
+import org.simantics.scenegraph.g2d.events.command.Commands;
+import org.simantics.scenegraph.g2d.nodes.NavigationNode;
+import org.simantics.scenegraph.g2d.nodes.TransformNode;
+import org.simantics.scenegraph.utils.NodeUtil;
+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;
+import org.simantics.utils.page.MarginUtils;
+import org.simantics.utils.page.MarginUtils.Margins;
+import org.simantics.utils.page.PageDesc;
+import org.simantics.utils.threads.ThreadUtils;
+
+/**
+ * This participant handles pan, zoom, zoom to fit and rotate commands.
+ * 
+ * Hints:
+ *  KEY_TRANSLATE_AMOUNT
+ *  KEY_ZOOM_AMOUNT
+ *  KEY_ROTATE_AMOUNT
+ *  KEY_ZOOM_TO_FIT_MARGINS
+ *  KEY_ZOOM_OUT_LIMIT
+ *  KEY_ZOOM_IN_LIMIT
+ * 
+ * @author Toni Kalajainen
+ * @author Tuukka Lehtonen
+ */
+public class PanZoomRotateHandler extends AbstractCanvasParticipant {
+
+    /**
+     * Express whether or not the view should attempt to keep the current zoom
+     * level when the canvas parenting control is resized. If the viewport is
+     * set to be adapted to the resized control, the view transform will be
+     * adjusted to accommodate for this. Otherwise the view transform will be
+     * left alone when the control is resized.
+     * 
+     * If hint is not specified, the default value is <code>true</code>.
+     * 
+     * See {@link NavigationNode} for the zoom level keep implementation.
+     */
+    public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL");
+
+    /**
+     * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
+     * view transform). If null, there is no limit. Used with an
+     * ICanvasContext's hint context.
+     */
+    public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT");
+
+    /**
+     * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
+     * view transform). If null there is no limit. Used with an
+     * ICanvasContext's hint context.
+     */
+    public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT");
+
+    public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM");
+
+    public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN");
+
+
+    @Dependency CanvasGrab grab;
+    @Dependency TransformUtil util;
+    @Dependency KeyUtil keys;
+    @Reference  Selection selection;
+    @Reference  CanvasBoundsParticipant bounds;
+
+    // Capture center point
+    Point2D centerPointControl;
+    Point2D centerPointCanvas;
+    Point2D controlSize;
+
+    final Boolean navigationEnabled;
+
+    protected NavigationNode node = null;
+    protected G2DParentNode oldRoot = null;
+
+    public PanZoomRotateHandler() {
+        this(true);
+    }
+
+    public PanZoomRotateHandler(boolean navigationEnabled) {
+        this.navigationEnabled = navigationEnabled;
+    }
+
+    NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() {
+        @Override
+        public void transformChanged(final AffineTransform transform) {
+            ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() {
+                @Override
+                public void run() {
+                    if (isRemoved())
+                        return;
+                    //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform);
+                    setHint(KEY_CANVAS_TRANSFORM, transform);
+                }
+            });
+        }
+    };
+
+    IHintListener hintListener = new HintListenerAdapter() {
+        @Override
+        public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+            if (node != null) {
+                if (key == Hints.KEY_DISABLE_PAINTING) {
+                    boolean visible = !Boolean.TRUE.equals(newValue);
+                    if (visible != node.isVisible())
+                        node.setVisible(Boolean.valueOf(visible));
+                } else if (key == KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL) {
+                    boolean noKeepZoom = Boolean.FALSE.equals(newValue);
+                    if (noKeepZoom == node.getAdaptViewportToResizedControl())
+                        node.setAdaptViewportToResizedControl(Boolean.valueOf(!noKeepZoom));
+                } else if (key == KEY_ZOOM_OUT_LIMIT) {
+                    node.setZoomOutLimit((Double) newValue);
+                } else if (key == KEY_ZOOM_IN_LIMIT) {
+                    node.setZoomInLimit((Double) newValue);
+                } else if (key == KEY_DISABLE_ZOOM) {
+                    node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
+                }
+            }
+        }
+    };
+
+    @Override
+    public void addedToContext(ICanvasContext ctx) {
+        super.addedToContext(ctx);
+        ctx.getDefaultHintContext().addKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
+        ctx.getDefaultHintContext().addKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
+        ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
+        ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
+        ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
+        ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_PAN, hintListener);
+    }
+
+    @Override
+    public void removedFromContext(ICanvasContext ctx) {
+        ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
+        ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
+        ctx.getDefaultHintContext().removeKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
+        ctx.getDefaultHintContext().removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
+        ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
+        ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_PAN, hintListener);
+        super.removedFromContext(ctx);
+    }
+
+    protected Class<? extends NavigationNode> getNavigationNodeClass() {
+        return NavigationNode.class;
+    }
+
+    @SGInit
+    public void initSG(G2DParentNode parent) {
+        // Replace old NAVIGATION_NODE with a new one
+        INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME);
+        if(oldnav != null) {
+            node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
+            // FIXME : oldnav seems to be the same node as parent (most of the cases).
+            // Deleting it will cause plenty of code to fail, since they refer to the node directly.
+            // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now).             
+            // oldnav.delete();
+        } else {
+            node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
+        }
+        node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
+        node.setZIndex(0);
+        node.setTransformListener(transformListener);
+        node.setNavigationEnabled(navigationEnabled);
+        node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
+        node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL)));
+        Double z = getHint(KEY_ZOOM_AMOUNT);
+        if(z != null) {
+            util.setTransform(AffineTransform.getScaleInstance(z, z));
+            node.setTransform(AffineTransform.getScaleInstance(z, z));
+        }
+        boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
+        node.setVisible(visible);
+        oldRoot = getContext().getCanvasNode();
+        getContext().setCanvasNode(node);
+    }
+
+    public void update() {
+        if (bounds != null) {
+            Rectangle2D vp = bounds.getControlBounds();
+            controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY());
+            centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY());
+            centerPointCanvas = util.controlToCanvas(centerPointControl, null);
+        }
+    }
+
+    public TransformNode getNode() {
+        return node;
+    }
+
+    /**
+     * Ensures that the navigation node handled by this participant contains the
+     * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will
+     * contain the same value.
+     * 
+     * @param transform
+     */
+    public void setTransform(AffineTransform transform) {
+        getNode().setTransform(transform);
+        transformListener.transformChanged(transform);
+    }
+
+    @SGCleanup
+    public void cleanupSG() {
+        node.remove();
+        node = null;
+        getContext().setCanvasNode(oldRoot);
+    }
+
+
+    /** Arrow key translate */
+    public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class);
+    public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class);
+    public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class);
+
+    /** Amount of arrow key translate */
+    public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30;
+    public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2;
+    public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1;
+    public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2;
+    public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2;
+
+    public final static int ROTATE_GRAB_ID = -666;
+
+    @EventHandler(priority = 0)
+    public boolean handleEvent(CommandEvent e) {
+        assertDependencies();
+        update();
+        Command c = e.command;
+        boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false;
+        boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false;
+
+        // Arrow key panning
+        if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
+            util.translateWithControlCoordinates(
+                    new Point2D.Double(
+                            getTranslateAmount(), 0));
+            return true;
+        }
+        if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
+            util.translateWithControlCoordinates(
+                    new Point2D.Double(
+                            -getTranslateAmount(), 0));
+            return true;
+        }
+        if (Commands.PAN_UP.equals(c) && !panDisabled) {
+            util.translateWithControlCoordinates(
+                    new Point2D.Double(
+                            0, getTranslateAmount()));
+            return true;
+        }
+        if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
+            util.translateWithControlCoordinates(
+                    new Point2D.Double(0, -getTranslateAmount()));
+            return true;
+        }
+        if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) {
+            if (centerPointControl == null) return false;
+            double scaleFactor = getZoomAmount();
+            scaleFactor = limitScaleFactor(scaleFactor);
+            util.zoomAroundControlPoint(scaleFactor, centerPointControl);
+        }
+        if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) {
+            if (centerPointControl == null) return false;
+            double scaleFactor = 1 / getZoomAmount();
+            scaleFactor = limitScaleFactor(scaleFactor);
+            util.zoomAroundControlPoint(scaleFactor, centerPointControl);
+        }
+
+        if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
+            if (centerPointCanvas == null) return false;
+            util.rotate(centerPointCanvas, -getRotateAmount());
+            setDirty();
+            return true;
+        }
+        if (Commands.ROTATE_CANVAS_CW.equals(c)) {
+            if (centerPointCanvas == null) return false;
+            util.rotate(centerPointCanvas, getRotateAmount());
+            setDirty();
+            return true;
+        }
+        if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) {
+            if (centerPointCanvas == null) return false;
+            util.rotate(centerPointCanvas, -getRotateAmount());
+            grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
+            grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
+            setDirty();
+            return true;
+        }
+        if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) {
+            if (centerPointCanvas == null) return false;
+            util.rotate(centerPointCanvas, getRotateAmount());
+            grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
+            grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
+            setDirty();
+            return true;
+        }
+        if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) {
+            if (centerPointCanvas == null) return false;
+            grab.releaseCanvas(ROTATE_GRAB_ID);
+            grab.releaseCanvas(ROTATE_GRAB_ID - 1);
+            setDirty();
+            return true;
+        }
+        if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) {
+            if (centerPointCanvas == null) return false;
+            grab.releaseCanvas(ROTATE_GRAB_ID);
+            grab.releaseCanvas(ROTATE_GRAB_ID - 1);
+            setDirty();
+            return true;
+        }
+        if (Commands.ENABLE_PAINTING.equals(c)) {
+            Boolean t = getHint(Hints.KEY_DISABLE_PAINTING);
+            removeHint(Hints.KEY_DISABLE_PAINTING);
+            boolean processed = Boolean.TRUE.equals(t);
+            if (processed)
+                setDirty();
+            return processed;
+        }
+        if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
+            boolean result = zoomToFit();
+            if (!result)
+                result = zoomToPage();
+            return result;
+        }
+        if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) {
+            if (controlSize==null) return false;
+            IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
+            if (d==null) return false;
+
+            Set<IElement> selections = selection.getAllSelections();
+            Rectangle2D diagramRect = ElementUtils.getSurroundingElementBoundsOnDiagram(selections);
+            if (diagramRect == null) return false;
+            if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0)
+                return false;
+
+            // HACK: prevents straight connections from being unzoomable.
+            if (diagramRect.getWidth() <= 0)
+                org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1);
+            if (diagramRect.getHeight() <= 0)
+                org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0);
+
+            // Show area
+            Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
+            util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
+            return true;
+        }
+        if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
+            return zoomToPage();
+        }
+
+        return false;
+    }
+
+    private boolean zoomToFit() {
+        if (controlSize==null) return false;
+        IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
+        if (d==null) return false;
+
+        Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
+        if (diagramRect==null) return false;
+        if (diagramRect.isEmpty())
+            return false;
+
+        // Show area
+        Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
+        //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")");
+        util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
+
+        return true;
+    }
+
+    private boolean zoomToPage() {
+        if (controlSize==null) return false;
+        PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
+        if (desc == null)
+            return false;
+        if (desc.isInfinite())
+            return false;
+
+        // Show page
+        Rectangle2D diagramRect = new Rectangle2D.Double();
+        desc.getPageRectangle(diagramRect);
+        if (diagramRect.isEmpty())
+            return false;
+
+        Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
+        //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")");
+        util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
+        return true;
+    }
+
+    public double getTranslateAmount()
+    {
+        Integer h = getHint(KEY_TRANSLATE_AMOUNT);
+        if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
+        return h;
+    }
+
+    public double getZoomAmount()
+    {
+        Integer h = getHint(KEY_TRANSLATE_AMOUNT);
+        if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
+        return h;
+    }
+
+    public double getRotateAmount()
+    {
+        Integer h = getHint(KEY_ROTATE_AMOUNT);
+        if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
+        return h;
+    }
+
+    public double limitScaleFactor(double scaleFactor) {
+        Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
+        Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
+
+        if (inLimit == null && scaleFactor < 1)
+            return scaleFactor;
+        if (outLimit == null && scaleFactor > 1)
+            return scaleFactor;
+
+        AffineTransform view = util.getTransform();
+        double currentScale = GeometryUtils.getScale(view) * 100.0;
+        double newScale = currentScale * scaleFactor;
+
+        if (inLimit != null && newScale > currentScale && newScale > inLimit) {
+            if (currentScale < inLimit)
+                scaleFactor = inLimit / currentScale;
+            else
+                scaleFactor = 1.0;
+        } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
+            if (currentScale > outLimit)
+                scaleFactor = outLimit / currentScale;
+            else
+                scaleFactor = 1.0;
+        }
+        return scaleFactor;
+    }
+
+    public static Margins getZoomToFitMargins(IHintObservable hints) {
+        Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
+        if (h == null) {
+            Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
+            boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
+            if (rulerEnabled) {
+                return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
+            } else {
+                return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;
+            }
+        }
+        return h;
+    }
+    
 }
\ No newline at end of file