]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/NavigationNode.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / NavigationNode.java
diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/NavigationNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/NavigationNode.java
new file mode 100644 (file)
index 0000000..e51ff35
--- /dev/null
@@ -0,0 +1,394 @@
+/*******************************************************************************\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.scenegraph.g2d.nodes;
+\r
+import java.awt.Graphics2D;\r
+import java.awt.Rectangle;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.Point2D;\r
+import java.awt.geom.Rectangle2D;\r
+import java.beans.PropertyChangeEvent;\r
+import java.beans.PropertyChangeListener;\r
+import java.util.concurrent.ScheduledFuture;\r
+\r
+import org.simantics.scenegraph.g2d.events.EventTypes;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent;\r
+import org.simantics.scenegraph.utils.GeometryUtils;\r
+import org.simantics.scenegraph.utils.Quality;\r
+import org.simantics.scenegraph.utils.QualityHints;\r
+import org.simantics.utils.threads.AWTThread;\r
+import org.simantics.utils.threads.Executable;\r
+import org.simantics.utils.threads.ExecutorWorker;\r
+
+/**\r
+ * @author Tuukka Lehtonen\r
+ */\r
+public class NavigationNode extends TransformNode implements PropertyChangeListener {
+\r
+    public interface TransformListener {
+        void transformChanged(AffineTransform transform);
+    }
+
+    private static final long serialVersionUID = -2561419753994187972L;
+
+    protected Rectangle2D bounds = null;
+
+    protected Boolean visible = Boolean.TRUE;
+
+    protected Boolean adaptViewportToResizedControl = Boolean.TRUE;
+
+    protected Double zoomInLimit = null;
+
+    protected Double zoomOutLimit = null;
+
+    protected Boolean navigationEnabled = Boolean.TRUE;
+
+    protected Boolean zoomEnabled = Boolean.TRUE;\r
+\r
+    protected Quality lowQualityMode = Quality.LOW;\r
+    protected Quality highQualityMode = Quality.HIGH;\r
+\r
+    /**\r
+     * The rendering quality used when {@link #dynamicQuality} is false.\r
+     */\r
+    protected Quality staticQualityMode = Quality.LOW;\r
+\r
+    protected Boolean dynamicQuality = Boolean.TRUE;\r
+\r
+    private TransformListener transformListener = null;\r
+\r
+    private static final int REPAINT_DELAY = 250;\r
+    private transient boolean qualityPaint = true;\r
+    private transient ScheduledFuture<Object> pendingTask;\r
+\r
+    protected transient Point2D dragDelta = null;\r
+    transient Rectangle r = new Rectangle();\r
+    protected transient Rectangle2D performZoomTo = null;\r
+
+    @Override\r
+    public void init() {\r
+        super.init();\r
+        addEventHandler(this);\r
+    }\r
+\r
+    @Override\r
+    public void cleanup() {\r
+        removeEventHandler(this);\r
+        super.cleanup();\r
+    }\r
+
+    @SyncField("bounds")
+    protected void setBounds(Rectangle2D bounds) {
+        this.bounds = (Rectangle2D)bounds.clone();
+    }
+
+    @Override
+    public Rectangle2D getBoundsInLocal() {
+        // In order to render everything under NavigationNode.
+        return null;
+    }
+
+    @SyncField("visible")
+    public void setVisible(Boolean visible) {
+        this.visible = visible;
+    }
+
+    @SyncField("navigationEnabled")
+    public void setNavigationEnabled(Boolean navigationEnabled) {
+        this.navigationEnabled = navigationEnabled;
+    }\r
+
+    @SyncField("zoomEnabled")\r
+    public void setZoomEnabled(Boolean zoomEnabled) {\r
+        this.zoomEnabled = zoomEnabled;\r
+    }\r
+\r
+    @SyncField("lowQualityMode")\r
+    public void setLowQualityMode(Quality mode) {\r
+        this.lowQualityMode = mode;\r
+    }\r
+\r
+    @SyncField("highQualityMode")\r
+    public void setHighQualityMode(Quality mode) {\r
+        this.highQualityMode = mode;\r
+    }\r
+\r
+    /**\r
+     * @param mode a quality to define a static quality mode that is used when\r
+     *        {@link #dynamicQuality} is false or <code>null</code> to undefine\r
+     *        static quality mode\r
+     */\r
+    @SyncField("staticQualityMode")\r
+    public void setStaticQualityMode(Quality mode) {\r
+        this.staticQualityMode = mode;\r
+    }\r
+\r
+    public Quality getStaticQualityMode() {\r
+        return staticQualityMode;\r
+    }\r
+\r
+    /**\r
+     * With dynamic quality rendering will proceed with low quality settings\r
+     * during interaction or when instructed to do so through Graphics2D\r
+     * rendering hints. Without dynamic quality rendering will always proceed in\r
+     * the mode set with {@link #setStaticQualityMode(Quality)}. If swtatic\r
+     * quality mode is not set (i.e. <code>null</code>), rendering will proceed\r
+     * with whatever settings are in the Graphics2D instance at that time.\r
+     * \r
+     * @param dynamicQuality\r
+     */\r
+    @SyncField("dynamicQuality")\r
+    public void setDynamicQuality(Boolean dynamicQuality) {\r
+        this.dynamicQuality = dynamicQuality;\r
+    }\r
+
+    public boolean isVisible() {
+        return visible;
+    }
+
+    /**
+     * Set whether the node should try its best to keep the viewport the same
+     * when the control is resized or not.
+     * 
+     * @param adapt <code>true</code> to attempt to keep the viewport, i.e.
+     *        adjust the view transform or <code>false</code> to leave the view
+     *        transform as is and let the viewport change.
+     */
+    @SyncField("adaptViewportToResizedControl")
+    public void setAdaptViewportToResizedControl(Boolean adapt) {
+        this.adaptViewportToResizedControl = adapt;
+    }
+
+    public boolean getAdaptViewportToResizedControl() {
+        return adaptViewportToResizedControl;
+    }
+
+    @SyncField("zoomOutLimit")
+    public void setZoomOutLimit(Double zoomOutLimit) {
+        this.zoomOutLimit = zoomOutLimit;
+    }
+
+    @SyncField("zoomInLimit")
+    public void setZoomInLimit(Double zoomInLimit) {
+        this.zoomInLimit = zoomInLimit;
+    }
+
+    public Double getZoomInLimit() {
+        return zoomInLimit;
+    }
+
+    public Double getZoomOutLimit() {
+        return zoomOutLimit;
+    }
+
+    protected double limitScaleFactor(double scaleFactor) {
+        Double inLimit = zoomInLimit;
+        Double outLimit = zoomOutLimit;
+
+        if (inLimit == null && scaleFactor < 1)
+            return scaleFactor;
+        if (outLimit == null && scaleFactor > 1)
+            return scaleFactor;
+
+        AffineTransform view = transform;
+        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;
+    }
+
+    @Override
+    public void render(Graphics2D g2d) {\r
+        Rectangle newBounds = g2d.getClipBounds(r);
+        if (!newBounds.equals(bounds)) {
+            if (bounds != null) {
+                if (Boolean.TRUE.equals(adaptViewportToResizedControl)) {
+                    double scale = Math.sqrt(newBounds.getWidth()*newBounds.getWidth() + newBounds.getHeight()*newBounds.getHeight()) / Math.sqrt(bounds.getWidth()*bounds.getWidth() + bounds.getHeight()*bounds.getHeight());
+                    AffineTransform tr = (AffineTransform) transform.clone();
+                    //tr.scale(scale, scale);
+                    tr.preConcatenate(new AffineTransform(new double[] {scale, 0.0, 0.0, scale, 0.0, 0.0}));
+                    setTransform(tr);
+                    transformChanged();
+                }
+            }
+            setBounds(newBounds); // FIXME: not very good idea to send bounds to server
+        }
+        if (bounds != null && performZoomTo != null) {\r
+            setTransform(GeometryUtils.fitArea(bounds, performZoomTo));
+            performZoomTo = null;
+            transformChanged();
+        }\r
+\r
+        if (visible) {\r
+            QualityHints origQualityHints = null;\r
+\r
+            Quality mode = null;\r
+            if (dynamicQuality) {\r
+                mode = qualityPaint ? highQualityMode : lowQualityMode;\r
+            } else if (staticQualityMode != null) {\r
+                mode = staticQualityMode;\r
+            }\r
+\r
+            if (mode != null) {\r
+                QualityHints qualityHints = QualityHints.getHints(mode);\r
+                if (qualityHints != null) {\r
+                    origQualityHints = QualityHints.getQuality(g2d);\r
+                    qualityHints.setQuality(g2d);\r
+                }\r
+            }\r
+\r
+            super.render(g2d);\r
+\r
+            if (origQualityHints != null)\r
+                origQualityHints.setQuality(g2d);\r
+        }
+    }
+
+    @ClientSide
+    public void zoomTo(Rectangle2D diagram) {
+        performZoomTo = diagram;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + " [visible=" + visible + ", bounds=" + bounds + ", zoomInLimit=" + zoomInLimit\r
+                + ", zoomOutLimit=" + zoomOutLimit + ", adaptViewportToResize=" + adaptViewportToResizedControl + "]";\r
+    }
+
+    private void transformChanged() {
+        if (transformListener != null) {\r
+            transformListener.transformChanged(transform);
+        }
+    }
+
+    public void setTransformListener(TransformListener listener) {
+        transformListener = listener;
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (transformListener != null && "transform".equals(evt.getPropertyName())) {\r
+            transformListener.transformChanged((AffineTransform)evt.getNewValue());
+        }
+    }\r
+\r
+    @Override\r
+    public boolean mouseWheelMoved(MouseWheelMovedEvent me) {\r
+        if (navigationEnabled && zoomEnabled) {\r
+            double scroll = Math.min(0.9, -me.wheelRotation / 20.0);\r
+            double z = 1 - scroll;\r
+            double dx = (me.controlPosition.getX() - transform.getTranslateX()) / transform.getScaleX();\r
+            double dy = (me.controlPosition.getY() - transform.getTranslateY()) / transform.getScaleY();\r
+            dx = dx * (1 - z);\r
+            dy = dy * (1 - z);\r
+            double limitedScale = limitScaleFactor(z);\r
+            if (limitedScale != 1.0) {\r
+                translate(dx, dy);\r
+                scale(z, z);\r
+                transformChanged();\r
+                dropQuality();\r
+                repaint();\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public boolean mouseButtonPressed(MouseButtonPressedEvent e) {\r
+        if (navigationEnabled) {\r
+            if (isPanState(e)) {\r
+                dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());\r
+                // TODO : why to repaint here? Mouse has not been dragged, so it is not necessary, an causes unnecessary delay in start of panning movement.\r
+                //repaint();\r
+                //return true; // hmm.. why?\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public boolean mouseMoved(MouseMovedEvent e) {\r
+        if (navigationEnabled && dragDelta != null) {\r
+            if (isPanState(e)) {\r
+                double x = (e.controlPosition.getX() - dragDelta.getX()) / transform.getScaleX();\r
+                double y = (e.controlPosition.getY() - dragDelta.getY()) / transform.getScaleY();\r
+                translate(x, y);\r
+                transformChanged();\r
+                dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());\r
+                dropQuality();\r
+                repaint();\r
+                return true;\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    protected boolean isPanState(MouseEvent e) {\r
+        boolean anyPanButton = e.hasAnyButton(MouseEvent.MIDDLE_MASK | MouseEvent.RIGHT_MASK);\r
+        boolean middle = e.hasAnyButton(MouseEvent.MIDDLE_MASK);\r
+        boolean shift = e.hasAnyModifier(MouseEvent.SHIFT_MASK);\r
+        return middle || (anyPanButton && shift);\r
+    }\r
+\r
+    /**\r
+     * Utility method for dropping the paint quality and scheduling repaint with good quality.\r
+     * This can be used to speed up rendering while navigating.\r
+     */\r
+    private void dropQuality() {\r
+        if (!dynamicQuality) return;\r
+        //System.out.println("dropQuality: " + qualityPaint);\r
+        if (pendingTask!=null) {\r
+            //System.out.println("cancel quality task");\r
+            pendingTask.cancel(false);\r
+            pendingTask = null;\r
+        }\r
+        // Render with better quality soon.\r
+        qualityPaint = false;\r
+        scheduleRepaint();\r
+    }\r
+\r
+    private void scheduleRepaint() {\r
+        //System.out.println("schedule quality improvement");\r
+        Executable exe = new Executable(AWTThread.getThreadAccess(), new Runnable() {\r
+            @Override\r
+            public void run() {\r
+                //System.out.println("run: " + qualityPaint);\r
+                // we have waited for [delay], now its time to render with good quality\r
+                // Render next time with good quality\r
+                qualityPaint = true;\r
+                repaint();\r
+            }\r
+        });\r
+        // Render with good quality later\r
+        pendingTask = ExecutorWorker.getInstance().timerExec(exe, REPAINT_DELAY);\r
+    }\r
+\r
+    @Override\r
+    public int getEventMask() {\r
+        return EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask | EventTypes.MouseWheelMask;\r
+    }\r
+
+}