/******************************************************************************* * 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; } }