1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.scenegraph.g2d.nodes;
14 import java.awt.Graphics2D;
\r
15 import java.awt.Rectangle;
\r
16 import java.awt.geom.AffineTransform;
\r
17 import java.awt.geom.Point2D;
\r
18 import java.awt.geom.Rectangle2D;
\r
19 import java.beans.PropertyChangeEvent;
\r
20 import java.beans.PropertyChangeListener;
\r
21 import java.util.concurrent.ScheduledFuture;
\r
23 import org.simantics.scenegraph.g2d.events.EventTypes;
\r
24 import org.simantics.scenegraph.g2d.events.MouseEvent;
\r
25 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
\r
26 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
\r
27 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent;
\r
28 import org.simantics.scenegraph.utils.GeometryUtils;
\r
29 import org.simantics.scenegraph.utils.Quality;
\r
30 import org.simantics.scenegraph.utils.QualityHints;
\r
31 import org.simantics.utils.threads.AWTThread;
\r
32 import org.simantics.utils.threads.Executable;
\r
33 import org.simantics.utils.threads.ExecutorWorker;
\r
36 * @author Tuukka Lehtonen
\r
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;
\r
60 protected Quality lowQualityMode = Quality.LOW;
\r
61 protected Quality highQualityMode = Quality.HIGH;
\r
64 * The rendering quality used when {@link #dynamicQuality} is false.
\r
66 protected Quality staticQualityMode = Quality.LOW;
\r
68 protected Boolean dynamicQuality = Boolean.TRUE;
\r
70 private TransformListener transformListener = null;
\r
72 private static final int REPAINT_DELAY = 250;
\r
73 private transient boolean qualityPaint = true;
\r
74 private transient ScheduledFuture<Object> pendingTask;
\r
76 protected transient Point2D dragDelta = null;
\r
77 transient Rectangle r = new Rectangle();
\r
78 protected transient Rectangle2D performZoomTo = null;
\r
81 public void init() {
\r
83 addEventHandler(this);
\r
87 public void cleanup() {
\r
88 removeEventHandler(this);
\r
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")
\r
114 public void setZoomEnabled(Boolean zoomEnabled) {
\r
115 this.zoomEnabled = zoomEnabled;
\r
118 @SyncField("lowQualityMode")
\r
119 public void setLowQualityMode(Quality mode) {
\r
120 this.lowQualityMode = mode;
\r
123 @SyncField("highQualityMode")
\r
124 public void setHighQualityMode(Quality mode) {
\r
125 this.highQualityMode = mode;
\r
129 * @param mode a quality to define a static quality mode that is used when
\r
130 * {@link #dynamicQuality} is false or <code>null</code> to undefine
\r
131 * static quality mode
\r
133 @SyncField("staticQualityMode")
\r
134 public void setStaticQualityMode(Quality mode) {
\r
135 this.staticQualityMode = mode;
\r
138 public Quality getStaticQualityMode() {
\r
139 return staticQualityMode;
\r
143 * With dynamic quality rendering will proceed with low quality settings
\r
144 * during interaction or when instructed to do so through Graphics2D
\r
145 * rendering hints. Without dynamic quality rendering will always proceed in
\r
146 * the mode set with {@link #setStaticQualityMode(Quality)}. If swtatic
\r
147 * quality mode is not set (i.e. <code>null</code>), rendering will proceed
\r
148 * with whatever settings are in the Graphics2D instance at that time.
\r
150 * @param dynamicQuality
\r
152 @SyncField("dynamicQuality")
\r
153 public void setDynamicQuality(Boolean dynamicQuality) {
\r
154 this.dynamicQuality = dynamicQuality;
\r
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) {
\r
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) {
\r
240 setTransform(GeometryUtils.fitArea(bounds, performZoomTo));
241 performZoomTo = null;
246 QualityHints origQualityHints = null;
\r
248 Quality mode = null;
\r
249 if (dynamicQuality) {
\r
250 mode = qualityPaint ? highQualityMode : lowQualityMode;
\r
251 } else if (staticQualityMode != null) {
\r
252 mode = staticQualityMode;
\r
255 if (mode != null) {
\r
256 QualityHints qualityHints = QualityHints.getHints(mode);
\r
257 if (qualityHints != null) {
\r
258 origQualityHints = QualityHints.getQuality(g2d);
\r
259 qualityHints.setQuality(g2d);
\r
265 if (origQualityHints != null)
\r
266 origQualityHints.setQuality(g2d);
\r
271 public void zoomTo(Rectangle2D diagram) {
272 performZoomTo = diagram;
276 public String toString() {
277 return super.toString() + " [visible=" + visible + ", bounds=" + bounds + ", zoomInLimit=" + zoomInLimit
\r
278 + ", zoomOutLimit=" + zoomOutLimit + ", adaptViewportToResize=" + adaptViewportToResizedControl + "]";
\r
281 private void transformChanged() {
282 if (transformListener != null) {
\r
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())) {
\r
294 transformListener.transformChanged((AffineTransform)evt.getNewValue());
299 public boolean mouseWheelMoved(MouseWheelMovedEvent me) {
\r
300 if (navigationEnabled && zoomEnabled) {
\r
301 double scroll = Math.min(0.9, -me.wheelRotation / 20.0);
\r
302 double z = 1 - scroll;
\r
303 double dx = (me.controlPosition.getX() - transform.getTranslateX()) / transform.getScaleX();
\r
304 double dy = (me.controlPosition.getY() - transform.getTranslateY()) / transform.getScaleY();
\r
307 double limitedScale = limitScaleFactor(z);
\r
308 if (limitedScale != 1.0) {
\r
311 transformChanged();
\r
320 public boolean mouseButtonPressed(MouseButtonPressedEvent e) {
\r
321 if (navigationEnabled) {
\r
322 if (isPanState(e)) {
\r
323 dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());
\r
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.
\r
326 //return true; // hmm.. why?
\r
333 public boolean mouseMoved(MouseMovedEvent e) {
\r
334 if (navigationEnabled && dragDelta != null) {
\r
335 if (isPanState(e)) {
\r
336 double x = (e.controlPosition.getX() - dragDelta.getX()) / transform.getScaleX();
\r
337 double y = (e.controlPosition.getY() - dragDelta.getY()) / transform.getScaleY();
\r
339 transformChanged();
\r
340 dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());
\r
349 protected boolean isPanState(MouseEvent e) {
\r
350 boolean anyPanButton = e.hasAnyButton(MouseEvent.MIDDLE_MASK | MouseEvent.RIGHT_MASK);
\r
351 boolean middle = e.hasAnyButton(MouseEvent.MIDDLE_MASK);
\r
352 boolean shift = e.hasAnyModifier(MouseEvent.SHIFT_MASK);
\r
353 return middle || (anyPanButton && shift);
\r
357 * Utility method for dropping the paint quality and scheduling repaint with good quality.
\r
358 * This can be used to speed up rendering while navigating.
\r
360 private void dropQuality() {
\r
361 if (!dynamicQuality) return;
\r
362 //System.out.println("dropQuality: " + qualityPaint);
\r
363 if (pendingTask!=null) {
\r
364 //System.out.println("cancel quality task");
\r
365 pendingTask.cancel(false);
\r
366 pendingTask = null;
\r
368 // Render with better quality soon.
\r
369 qualityPaint = false;
\r
373 private void scheduleRepaint() {
\r
374 //System.out.println("schedule quality improvement");
\r
375 Executable exe = new Executable(AWTThread.getThreadAccess(), new Runnable() {
\r
377 public void run() {
\r
378 //System.out.println("run: " + qualityPaint);
\r
379 // we have waited for [delay], now its time to render with good quality
\r
380 // Render next time with good quality
\r
381 qualityPaint = true;
\r
385 // Render with good quality later
\r
386 pendingTask = ExecutorWorker.getInstance().timerExec(exe, REPAINT_DELAY);
\r
390 public int getEventMask() {
\r
391 return EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask | EventTypes.MouseWheelMask;
\r