X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.scenegraph%2Fsrc%2Forg%2Fsimantics%2Fscenegraph%2Fg2d%2Fnodes%2FNavigationNode.java;fp=bundles%2Forg.simantics.scenegraph%2Fsrc%2Forg%2Fsimantics%2Fscenegraph%2Fg2d%2Fnodes%2FNavigationNode.java;h=e51ff35adec4c3ee6aca49d815a50777549eeafc;hb=969bd23cab98a79ca9101af33334000879fb60c5;hp=0000000000000000000000000000000000000000;hpb=866dba5cd5a3929bbeae85991796acb212338a08;p=simantics%2Fplatform.git 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 index 000000000..e51ff35ad --- /dev/null +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/NavigationNode.java @@ -0,0 +1,394 @@ +/******************************************************************************* + * 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.scenegraph.g2d.nodes; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.concurrent.ScheduledFuture; + +import org.simantics.scenegraph.g2d.events.EventTypes; +import org.simantics.scenegraph.g2d.events.MouseEvent; +import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; +import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; +import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent; +import org.simantics.scenegraph.utils.GeometryUtils; +import org.simantics.scenegraph.utils.Quality; +import org.simantics.scenegraph.utils.QualityHints; +import org.simantics.utils.threads.AWTThread; +import org.simantics.utils.threads.Executable; +import org.simantics.utils.threads.ExecutorWorker; + +/** + * @author Tuukka Lehtonen + */ +public class NavigationNode extends TransformNode implements PropertyChangeListener { + + 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; + + protected Quality lowQualityMode = Quality.LOW; + protected Quality highQualityMode = Quality.HIGH; + + /** + * The rendering quality used when {@link #dynamicQuality} is false. + */ + protected Quality staticQualityMode = Quality.LOW; + + protected Boolean dynamicQuality = Boolean.TRUE; + + private TransformListener transformListener = null; + + private static final int REPAINT_DELAY = 250; + private transient boolean qualityPaint = true; + private transient ScheduledFuture pendingTask; + + protected transient Point2D dragDelta = null; + transient Rectangle r = new Rectangle(); + protected transient Rectangle2D performZoomTo = null; + + @Override + public void init() { + super.init(); + addEventHandler(this); + } + + @Override + public void cleanup() { + removeEventHandler(this); + super.cleanup(); + } + + @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; + } + + @SyncField("zoomEnabled") + public void setZoomEnabled(Boolean zoomEnabled) { + this.zoomEnabled = zoomEnabled; + } + + @SyncField("lowQualityMode") + public void setLowQualityMode(Quality mode) { + this.lowQualityMode = mode; + } + + @SyncField("highQualityMode") + public void setHighQualityMode(Quality mode) { + this.highQualityMode = mode; + } + + /** + * @param mode a quality to define a static quality mode that is used when + * {@link #dynamicQuality} is false or null to undefine + * static quality mode + */ + @SyncField("staticQualityMode") + public void setStaticQualityMode(Quality mode) { + this.staticQualityMode = mode; + } + + public Quality getStaticQualityMode() { + return staticQualityMode; + } + + /** + * With dynamic quality rendering will proceed with low quality settings + * during interaction or when instructed to do so through Graphics2D + * rendering hints. Without dynamic quality rendering will always proceed in + * the mode set with {@link #setStaticQualityMode(Quality)}. If swtatic + * quality mode is not set (i.e. null), rendering will proceed + * with whatever settings are in the Graphics2D instance at that time. + * + * @param dynamicQuality + */ + @SyncField("dynamicQuality") + public void setDynamicQuality(Boolean dynamicQuality) { + this.dynamicQuality = dynamicQuality; + } + + 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 true to attempt to keep the viewport, i.e. + * adjust the view transform or false 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) { + 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) { + setTransform(GeometryUtils.fitArea(bounds, performZoomTo)); + performZoomTo = null; + transformChanged(); + } + + if (visible) { + QualityHints origQualityHints = null; + + Quality mode = null; + if (dynamicQuality) { + mode = qualityPaint ? highQualityMode : lowQualityMode; + } else if (staticQualityMode != null) { + mode = staticQualityMode; + } + + if (mode != null) { + QualityHints qualityHints = QualityHints.getHints(mode); + if (qualityHints != null) { + origQualityHints = QualityHints.getQuality(g2d); + qualityHints.setQuality(g2d); + } + } + + super.render(g2d); + + if (origQualityHints != null) + origQualityHints.setQuality(g2d); + } + } + + @ClientSide + public void zoomTo(Rectangle2D diagram) { + performZoomTo = diagram; + } + + @Override + public String toString() { + return super.toString() + " [visible=" + visible + ", bounds=" + bounds + ", zoomInLimit=" + zoomInLimit + + ", zoomOutLimit=" + zoomOutLimit + ", adaptViewportToResize=" + adaptViewportToResizedControl + "]"; + } + + private void transformChanged() { + if (transformListener != null) { + transformListener.transformChanged(transform); + } + } + + public void setTransformListener(TransformListener listener) { + transformListener = listener; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (transformListener != null && "transform".equals(evt.getPropertyName())) { + transformListener.transformChanged((AffineTransform)evt.getNewValue()); + } + } + + @Override + public boolean mouseWheelMoved(MouseWheelMovedEvent me) { + if (navigationEnabled && zoomEnabled) { + double scroll = Math.min(0.9, -me.wheelRotation / 20.0); + double z = 1 - scroll; + double dx = (me.controlPosition.getX() - transform.getTranslateX()) / transform.getScaleX(); + double dy = (me.controlPosition.getY() - transform.getTranslateY()) / transform.getScaleY(); + dx = dx * (1 - z); + dy = dy * (1 - z); + double limitedScale = limitScaleFactor(z); + if (limitedScale != 1.0) { + translate(dx, dy); + scale(z, z); + transformChanged(); + dropQuality(); + repaint(); + } + } + return false; + } + + @Override + public boolean mouseButtonPressed(MouseButtonPressedEvent e) { + if (navigationEnabled) { + if (isPanState(e)) { + dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY()); + // TODO : why to repaint here? Mouse has not been dragged, so it is not necessary, an causes unnecessary delay in start of panning movement. + //repaint(); + //return true; // hmm.. why? + } + } + return false; + } + + @Override + public boolean mouseMoved(MouseMovedEvent e) { + if (navigationEnabled && dragDelta != null) { + if (isPanState(e)) { + double x = (e.controlPosition.getX() - dragDelta.getX()) / transform.getScaleX(); + double y = (e.controlPosition.getY() - dragDelta.getY()) / transform.getScaleY(); + translate(x, y); + transformChanged(); + dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY()); + dropQuality(); + repaint(); + return true; + } + } + return false; + } + + protected boolean isPanState(MouseEvent e) { + boolean anyPanButton = e.hasAnyButton(MouseEvent.MIDDLE_MASK | MouseEvent.RIGHT_MASK); + boolean middle = e.hasAnyButton(MouseEvent.MIDDLE_MASK); + boolean shift = e.hasAnyModifier(MouseEvent.SHIFT_MASK); + return middle || (anyPanButton && shift); + } + + /** + * Utility method for dropping the paint quality and scheduling repaint with good quality. + * This can be used to speed up rendering while navigating. + */ + private void dropQuality() { + if (!dynamicQuality) return; + //System.out.println("dropQuality: " + qualityPaint); + if (pendingTask!=null) { + //System.out.println("cancel quality task"); + pendingTask.cancel(false); + pendingTask = null; + } + // Render with better quality soon. + qualityPaint = false; + scheduleRepaint(); + } + + private void scheduleRepaint() { + //System.out.println("schedule quality improvement"); + Executable exe = new Executable(AWTThread.getThreadAccess(), new Runnable() { + @Override + public void run() { + //System.out.println("run: " + qualityPaint); + // we have waited for [delay], now its time to render with good quality + // Render next time with good quality + qualityPaint = true; + repaint(); + } + }); + // Render with good quality later + pendingTask = ExecutorWorker.getInstance().timerExec(exe, REPAINT_DELAY); + } + + @Override + public int getEventMask() { + return EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask | EventTypes.MouseWheelMask; + } + +}