--- /dev/null
+/*******************************************************************************\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
+
+}