1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.scenegraph.g2d.nodes;
14 import java.awt.Graphics2D;
15 import java.awt.Rectangle;
16 import java.awt.geom.AffineTransform;
17 import java.awt.geom.Point2D;
18 import java.awt.geom.Rectangle2D;
19 import java.beans.PropertyChangeEvent;
20 import java.beans.PropertyChangeListener;
21 import java.util.concurrent.ScheduledFuture;
23 import org.simantics.scenegraph.g2d.events.EventTypes;
24 import org.simantics.scenegraph.g2d.events.MouseEvent;
25 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
26 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
27 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent;
28 import org.simantics.scenegraph.utils.GeometryUtils;
29 import org.simantics.scenegraph.utils.Quality;
30 import org.simantics.scenegraph.utils.QualityHints;
31 import org.simantics.utils.threads.AWTThread;
32 import org.simantics.utils.threads.Executable;
33 import org.simantics.utils.threads.ExecutorWorker;
36 * @author Tuukka Lehtonen
38 public class NavigationNode extends TransformNode implements PropertyChangeListener {
40 public interface TransformListener {
41 void transformChanged(AffineTransform transform);
44 private static final long serialVersionUID = -2561419753994187972L;
46 protected Rectangle2D bounds = null;
48 protected Boolean visible = Boolean.TRUE;
50 protected Boolean adaptViewportToResizedControl = Boolean.TRUE;
52 protected Double zoomInLimit = null;
54 protected Double zoomOutLimit = null;
56 protected Boolean navigationEnabled = Boolean.TRUE;
58 protected Boolean zoomEnabled = Boolean.TRUE;
60 protected Quality lowQualityMode = Quality.LOW;
61 protected Quality highQualityMode = Quality.HIGH;
64 * The rendering quality used when {@link #dynamicQuality} is false.
66 protected Quality staticQualityMode = Quality.LOW;
68 protected Boolean dynamicQuality = Boolean.TRUE;
70 private TransformListener transformListener = null;
72 private static final int REPAINT_DELAY = 250;
73 private transient boolean qualityPaint = true;
74 private transient ScheduledFuture<Object> pendingTask;
76 protected transient Point2D dragDelta = null;
77 transient Rectangle r = new Rectangle();
78 protected transient Rectangle2D performZoomTo = null;
83 addEventHandler(this);
87 public void cleanup() {
88 removeEventHandler(this);
93 protected void setBounds(Rectangle2D bounds) {
94 this.bounds = (Rectangle2D)bounds.clone();
98 public Rectangle2D getBoundsInLocal() {
99 // In order to render everything under NavigationNode.
103 @SyncField("visible")
104 public void setVisible(Boolean visible) {
105 this.visible = visible;
108 @SyncField("navigationEnabled")
109 public void setNavigationEnabled(Boolean navigationEnabled) {
110 this.navigationEnabled = navigationEnabled;
113 @SyncField("zoomEnabled")
114 public void setZoomEnabled(Boolean zoomEnabled) {
115 this.zoomEnabled = zoomEnabled;
118 @SyncField("lowQualityMode")
119 public void setLowQualityMode(Quality mode) {
120 this.lowQualityMode = mode;
123 @SyncField("highQualityMode")
124 public void setHighQualityMode(Quality mode) {
125 this.highQualityMode = mode;
129 * @param mode a quality to define a static quality mode that is used when
130 * {@link #dynamicQuality} is false or <code>null</code> to undefine
131 * static quality mode
133 @SyncField("staticQualityMode")
134 public void setStaticQualityMode(Quality mode) {
135 this.staticQualityMode = mode;
138 public Quality getStaticQualityMode() {
139 return staticQualityMode;
143 * With dynamic quality rendering will proceed with low quality settings
144 * during interaction or when instructed to do so through Graphics2D
145 * rendering hints. Without dynamic quality rendering will always proceed in
146 * the mode set with {@link #setStaticQualityMode(Quality)}. If swtatic
147 * quality mode is not set (i.e. <code>null</code>), rendering will proceed
148 * with whatever settings are in the Graphics2D instance at that time.
150 * @param dynamicQuality
152 @SyncField("dynamicQuality")
153 public void setDynamicQuality(Boolean dynamicQuality) {
154 this.dynamicQuality = dynamicQuality;
157 public boolean isVisible() {
162 * Set whether the node should try its best to keep the viewport the same
163 * when the control is resized or not.
165 * @param adapt <code>true</code> to attempt to keep the viewport, i.e.
166 * adjust the view transform or <code>false</code> to leave the view
167 * transform as is and let the viewport change.
169 @SyncField("adaptViewportToResizedControl")
170 public void setAdaptViewportToResizedControl(Boolean adapt) {
171 this.adaptViewportToResizedControl = adapt;
174 public boolean getAdaptViewportToResizedControl() {
175 return adaptViewportToResizedControl;
178 @SyncField("zoomOutLimit")
179 public void setZoomOutLimit(Double zoomOutLimit) {
180 this.zoomOutLimit = zoomOutLimit;
183 @SyncField("zoomInLimit")
184 public void setZoomInLimit(Double zoomInLimit) {
185 this.zoomInLimit = zoomInLimit;
188 public Double getZoomInLimit() {
192 public Double getZoomOutLimit() {
196 protected double limitScaleFactor(double scaleFactor) {
197 Double inLimit = zoomInLimit;
198 Double outLimit = zoomOutLimit;
200 if (inLimit == null && scaleFactor < 1)
202 if (outLimit == null && scaleFactor > 1)
205 AffineTransform view = transform;
206 double currentScale = GeometryUtils.getScale(view) * 100.0;
207 double newScale = currentScale * scaleFactor;
209 if (inLimit != null && newScale > currentScale && newScale > inLimit) {
210 if (currentScale < inLimit)
211 scaleFactor = inLimit / currentScale;
214 } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
215 if (currentScale > outLimit)
216 scaleFactor = outLimit / currentScale;
224 public void render(Graphics2D g2d) {
225 Rectangle newBounds = g2d.getClipBounds(r);
226 if (!newBounds.equals(bounds)) {
227 if (bounds != null) {
228 if (Boolean.TRUE.equals(adaptViewportToResizedControl)) {
229 double scale = Math.sqrt(newBounds.getWidth()*newBounds.getWidth() + newBounds.getHeight()*newBounds.getHeight()) / Math.sqrt(bounds.getWidth()*bounds.getWidth() + bounds.getHeight()*bounds.getHeight());
230 AffineTransform tr = (AffineTransform) transform.clone();
231 //tr.scale(scale, scale);
232 tr.preConcatenate(new AffineTransform(new double[] {scale, 0.0, 0.0, scale, 0.0, 0.0}));
237 setBounds(newBounds); // FIXME: not very good idea to send bounds to server
239 if (bounds != null && performZoomTo != null) {
240 setTransform(GeometryUtils.fitArea(bounds, performZoomTo));
241 performZoomTo = null;
246 QualityHints origQualityHints = null;
249 if (dynamicQuality) {
250 mode = qualityPaint ? highQualityMode : lowQualityMode;
251 } else if (staticQualityMode != null) {
252 mode = staticQualityMode;
256 QualityHints qualityHints = QualityHints.getHints(mode);
257 if (qualityHints != null) {
258 origQualityHints = QualityHints.getQuality(g2d);
259 qualityHints.setQuality(g2d);
265 if (origQualityHints != null)
266 origQualityHints.setQuality(g2d);
271 public void zoomTo(Rectangle2D diagram) {
272 performZoomTo = diagram;
276 public String toString() {
277 return super.toString() + " [visible=" + visible + ", bounds=" + bounds + ", zoomInLimit=" + zoomInLimit
278 + ", zoomOutLimit=" + zoomOutLimit + ", adaptViewportToResize=" + adaptViewportToResizedControl + "]";
281 protected void transformChanged() {
282 if (transformListener != null) {
283 transformListener.transformChanged(transform);
287 public void setTransformListener(TransformListener listener) {
288 transformListener = listener;
292 public void propertyChange(PropertyChangeEvent evt) {
293 if (transformListener != null && "transform".equals(evt.getPropertyName())) {
294 transformListener.transformChanged((AffineTransform)evt.getNewValue());
299 public boolean mouseWheelMoved(MouseWheelMovedEvent me) {
300 if (navigationEnabled && zoomEnabled) {
301 double scroll = Math.min(0.9, -me.wheelRotation / 20.0);
302 double z = 1 - scroll;
303 double dx = (me.controlPosition.getX() - transform.getTranslateX()) / transform.getScaleX();
304 double dy = (me.controlPosition.getY() - transform.getTranslateY()) / transform.getScaleY();
307 double limitedScale = limitScaleFactor(z);
308 if (limitedScale != 1.0) {
320 public boolean mouseButtonPressed(MouseButtonPressedEvent e) {
321 if (navigationEnabled) {
323 dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());
324 // TODO : why to repaint here? Mouse has not been dragged, so it is not necessary, an causes unnecessary delay in start of panning movement.
326 //return true; // hmm.. why?
333 public boolean mouseMoved(MouseMovedEvent e) {
334 if (navigationEnabled && dragDelta != null) {
336 double x = (e.controlPosition.getX() - dragDelta.getX()) / transform.getScaleX();
337 double y = (e.controlPosition.getY() - dragDelta.getY()) / transform.getScaleY();
340 dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());
349 protected boolean isPanState(MouseEvent e) {
350 boolean anyPanButton = e.hasAnyButton(MouseEvent.MIDDLE_MASK | MouseEvent.RIGHT_MASK);
351 boolean middle = e.hasAnyButton(MouseEvent.MIDDLE_MASK);
352 boolean shift = e.hasAnyModifier(MouseEvent.SHIFT_MASK);
353 return middle || (anyPanButton && shift);
357 * Utility method for dropping the paint quality and scheduling repaint with good quality.
358 * This can be used to speed up rendering while navigating.
360 protected void dropQuality() {
361 if (!dynamicQuality) return;
362 //System.out.println("dropQuality: " + qualityPaint);
363 if (pendingTask!=null) {
364 //System.out.println("cancel quality task");
365 pendingTask.cancel(false);
368 // Render with better quality soon.
369 qualityPaint = false;
373 private void scheduleRepaint() {
374 //System.out.println("schedule quality improvement");
375 Executable exe = new Executable(AWTThread.getThreadAccess(), new Runnable() {
378 //System.out.println("run: " + qualityPaint);
379 // we have waited for [delay], now its time to render with good quality
380 // Render next time with good quality
385 // Render with good quality later
386 pendingTask = ExecutorWorker.getInstance().timerExec(exe, REPAINT_DELAY);
390 public int getEventMask() {
391 return EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask | EventTypes.MouseWheelMask;