]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/NavigationNode.java
b46b4ad61c81089ff3f022cc6946bf299bf52d74
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / NavigationNode.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.scenegraph.g2d.nodes;
13
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;
22
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;
34
35 /**
36  * @author Tuukka Lehtonen
37  */
38 public class NavigationNode extends TransformNode implements PropertyChangeListener {
39
40     public interface TransformListener {
41         void transformChanged(AffineTransform transform);
42     }
43
44     private static final long serialVersionUID = -2561419753994187972L;
45
46     protected Rectangle2D bounds = null;
47
48     protected Boolean visible = Boolean.TRUE;
49
50     protected Boolean adaptViewportToResizedControl = Boolean.TRUE;
51
52     protected Double zoomInLimit = null;
53
54     protected Double zoomOutLimit = null;
55
56     protected Boolean navigationEnabled = Boolean.TRUE;
57
58     protected Boolean zoomEnabled = Boolean.TRUE;
59
60     protected Quality lowQualityMode = Quality.LOW;
61     protected Quality highQualityMode = Quality.HIGH;
62
63     /**
64      * The rendering quality used when {@link #dynamicQuality} is false.
65      */
66     protected Quality staticQualityMode = Quality.LOW;
67
68     protected Boolean dynamicQuality = Boolean.TRUE;
69
70     private TransformListener transformListener = null;
71
72     private static final int REPAINT_DELAY = 250;
73     private transient boolean qualityPaint = true;
74     private transient ScheduledFuture<Object> pendingTask;
75
76     protected transient Point2D dragDelta = null;
77     transient Rectangle r = new Rectangle();
78     protected transient Rectangle2D performZoomTo = null;
79
80     @Override
81     public void init() {
82         super.init();
83         addEventHandler(this);
84     }
85
86     @Override
87     public void cleanup() {
88         removeEventHandler(this);
89         super.cleanup();
90     }
91
92     @SyncField("bounds")
93     protected void setBounds(Rectangle2D bounds) {
94         this.bounds = (Rectangle2D)bounds.clone();
95     }
96
97     @Override
98     public Rectangle2D getBoundsInLocal() {
99         // In order to render everything under NavigationNode.
100         return null;
101     }
102
103     @SyncField("visible")
104     public void setVisible(Boolean visible) {
105         this.visible = visible;
106     }
107
108     @SyncField("navigationEnabled")
109     public void setNavigationEnabled(Boolean navigationEnabled) {
110         this.navigationEnabled = navigationEnabled;
111     }
112
113     @SyncField("zoomEnabled")
114     public void setZoomEnabled(Boolean zoomEnabled) {
115         this.zoomEnabled = zoomEnabled;
116     }
117
118     @SyncField("lowQualityMode")
119     public void setLowQualityMode(Quality mode) {
120         this.lowQualityMode = mode;
121     }
122
123     @SyncField("highQualityMode")
124     public void setHighQualityMode(Quality mode) {
125         this.highQualityMode = mode;
126     }
127
128     /**
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
132      */
133     @SyncField("staticQualityMode")
134     public void setStaticQualityMode(Quality mode) {
135         this.staticQualityMode = mode;
136     }
137
138     public Quality getStaticQualityMode() {
139         return staticQualityMode;
140     }
141
142     /**
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.
149      * 
150      * @param dynamicQuality
151      */
152     @SyncField("dynamicQuality")
153     public void setDynamicQuality(Boolean dynamicQuality) {
154         this.dynamicQuality = dynamicQuality;
155     }
156
157     public boolean isVisible() {
158         return visible;
159     }
160
161     /**
162      * Set whether the node should try its best to keep the viewport the same
163      * when the control is resized or not.
164      * 
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.
168      */
169     @SyncField("adaptViewportToResizedControl")
170     public void setAdaptViewportToResizedControl(Boolean adapt) {
171         this.adaptViewportToResizedControl = adapt;
172     }
173
174     public boolean getAdaptViewportToResizedControl() {
175         return adaptViewportToResizedControl;
176     }
177
178     @SyncField("zoomOutLimit")
179     public void setZoomOutLimit(Double zoomOutLimit) {
180         this.zoomOutLimit = zoomOutLimit;
181     }
182
183     @SyncField("zoomInLimit")
184     public void setZoomInLimit(Double zoomInLimit) {
185         this.zoomInLimit = zoomInLimit;
186     }
187
188     public Double getZoomInLimit() {
189         return zoomInLimit;
190     }
191
192     public Double getZoomOutLimit() {
193         return zoomOutLimit;
194     }
195
196     protected double limitScaleFactor(double scaleFactor) {
197         Double inLimit = zoomInLimit;
198         Double outLimit = zoomOutLimit;
199
200         if (inLimit == null && scaleFactor < 1)
201             return scaleFactor;
202         if (outLimit == null && scaleFactor > 1)
203             return scaleFactor;
204
205         AffineTransform view = transform;
206         double currentScale = GeometryUtils.getScale(view) * 100.0;
207         double newScale = currentScale * scaleFactor;
208
209         if (inLimit != null && newScale > currentScale && newScale > inLimit) {
210             if (currentScale < inLimit)
211                 scaleFactor = inLimit / currentScale;
212             else
213                 scaleFactor = 1.0;
214         } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
215             if (currentScale > outLimit)
216                 scaleFactor = outLimit / currentScale;
217             else
218                 scaleFactor = 1.0;
219         }
220         return scaleFactor;
221     }
222
223     @Override
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}));
233                     setTransform(tr);
234                     transformChanged();
235                 }
236             }
237             setBounds(newBounds); // FIXME: not very good idea to send bounds to server
238         }
239         if (bounds != null && performZoomTo != null) {
240             setTransform(GeometryUtils.fitArea(bounds, performZoomTo));
241             performZoomTo = null;
242             transformChanged();
243         }
244
245         if (visible) {
246             QualityHints origQualityHints = null;
247
248             Quality mode = null;
249             if (dynamicQuality) {
250                 mode = qualityPaint ? highQualityMode : lowQualityMode;
251             } else if (staticQualityMode != null) {
252                 mode = staticQualityMode;
253             }
254
255             if (mode != null) {
256                 QualityHints qualityHints = QualityHints.getHints(mode);
257                 if (qualityHints != null) {
258                     origQualityHints = QualityHints.getQuality(g2d);
259                     qualityHints.setQuality(g2d);
260                 }
261             }
262
263             super.render(g2d);
264
265             if (origQualityHints != null)
266                 origQualityHints.setQuality(g2d);
267         }
268     }
269
270     @ClientSide
271     public void zoomTo(Rectangle2D diagram) {
272         performZoomTo = diagram;
273     }
274
275     @Override
276     public String toString() {
277         return super.toString() + " [visible=" + visible + ", bounds=" + bounds + ", zoomInLimit=" + zoomInLimit
278                 + ", zoomOutLimit=" + zoomOutLimit + ", adaptViewportToResize=" + adaptViewportToResizedControl + "]";
279     }
280
281     private void transformChanged() {
282         if (transformListener != null) {
283             transformListener.transformChanged(transform);
284         }
285     }
286
287     public void setTransformListener(TransformListener listener) {
288         transformListener = listener;
289     }
290
291     @Override
292     public void propertyChange(PropertyChangeEvent evt) {
293         if (transformListener != null && "transform".equals(evt.getPropertyName())) {
294             transformListener.transformChanged((AffineTransform)evt.getNewValue());
295         }
296     }
297
298     @Override
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();
305             dx = dx * (1 - z);
306             dy = dy * (1 - z);
307             double limitedScale = limitScaleFactor(z);
308             if (limitedScale != 1.0) {
309                 translate(dx, dy);
310                 scale(z, z);
311                 transformChanged();
312                 dropQuality();
313                 repaint();
314             }
315         }
316         return false;
317     }
318
319     @Override
320     public boolean mouseButtonPressed(MouseButtonPressedEvent e) {
321         if (navigationEnabled) {
322             if (isPanState(e)) {
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.
325                 //repaint();
326                 //return true; // hmm.. why?
327             }
328         }
329         return false;
330     }
331
332     @Override
333     public boolean mouseMoved(MouseMovedEvent e) {
334         if (navigationEnabled && dragDelta != null) {
335             if (isPanState(e)) {
336                 double x = (e.controlPosition.getX() - dragDelta.getX()) / transform.getScaleX();
337                 double y = (e.controlPosition.getY() - dragDelta.getY()) / transform.getScaleY();
338                 translate(x, y);
339                 transformChanged();
340                 dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());
341                 dropQuality();
342                 repaint();
343                 return true;
344             }
345         }
346         return false;
347     }
348
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);
354     }
355
356     /**
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.
359      */
360     private 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);
366             pendingTask = null;
367         }
368         // Render with better quality soon.
369         qualityPaint = false;
370         scheduleRepaint();
371     }
372
373     private void scheduleRepaint() {
374         //System.out.println("schedule quality improvement");
375         Executable exe = new Executable(AWTThread.getThreadAccess(), new Runnable() {
376             @Override
377             public void run() {
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
381                 qualityPaint = true;
382                 repaint();
383             }
384         });
385         // Render with good quality later
386         pendingTask = ExecutorWorker.getInstance().timerExec(exe, REPAINT_DELAY);
387     }
388
389     @Override
390     public int getEventMask() {
391         return EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask | EventTypes.MouseWheelMask;
392     }
393
394 }