]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/NavigationNode.java
Sync git svn branch with SVN repository r33269.
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / NavigationNode.java
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
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.scenegraph.g2d.nodes;
13 \r
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
22 \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
34
35 /**\r
36  * @author Tuukka Lehtonen\r
37  */\r
38 public class NavigationNode extends TransformNode implements PropertyChangeListener {
39 \r
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;\r
59 \r
60     protected Quality lowQualityMode = Quality.LOW;\r
61     protected Quality highQualityMode = Quality.HIGH;\r
62 \r
63     /**\r
64      * The rendering quality used when {@link #dynamicQuality} is false.\r
65      */\r
66     protected Quality staticQualityMode = Quality.LOW;\r
67 \r
68     protected Boolean dynamicQuality = Boolean.TRUE;\r
69 \r
70     private TransformListener transformListener = null;\r
71 \r
72     private static final int REPAINT_DELAY = 250;\r
73     private transient boolean qualityPaint = true;\r
74     private transient ScheduledFuture<Object> pendingTask;\r
75 \r
76     protected transient Point2D dragDelta = null;\r
77     transient Rectangle r = new Rectangle();\r
78     protected transient Rectangle2D performZoomTo = null;\r
79
80     @Override\r
81     public void init() {\r
82         super.init();\r
83         addEventHandler(this);\r
84     }\r
85 \r
86     @Override\r
87     public void cleanup() {\r
88         removeEventHandler(this);\r
89         super.cleanup();\r
90     }\r
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     }\r
112
113     @SyncField("zoomEnabled")\r
114     public void setZoomEnabled(Boolean zoomEnabled) {\r
115         this.zoomEnabled = zoomEnabled;\r
116     }\r
117 \r
118     @SyncField("lowQualityMode")\r
119     public void setLowQualityMode(Quality mode) {\r
120         this.lowQualityMode = mode;\r
121     }\r
122 \r
123     @SyncField("highQualityMode")\r
124     public void setHighQualityMode(Quality mode) {\r
125         this.highQualityMode = mode;\r
126     }\r
127 \r
128     /**\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
132      */\r
133     @SyncField("staticQualityMode")\r
134     public void setStaticQualityMode(Quality mode) {\r
135         this.staticQualityMode = mode;\r
136     }\r
137 \r
138     public Quality getStaticQualityMode() {\r
139         return staticQualityMode;\r
140     }\r
141 \r
142     /**\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
149      * \r
150      * @param dynamicQuality\r
151      */\r
152     @SyncField("dynamicQuality")\r
153     public void setDynamicQuality(Boolean dynamicQuality) {\r
154         this.dynamicQuality = dynamicQuality;\r
155     }\r
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) {\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}));
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) {\r
240             setTransform(GeometryUtils.fitArea(bounds, performZoomTo));
241             performZoomTo = null;
242             transformChanged();
243         }\r
244 \r
245         if (visible) {\r
246             QualityHints origQualityHints = null;\r
247 \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
253             }\r
254 \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
260                 }\r
261             }\r
262 \r
263             super.render(g2d);\r
264 \r
265             if (origQualityHints != null)\r
266                 origQualityHints.setQuality(g2d);\r
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\r
278                 + ", zoomOutLimit=" + zoomOutLimit + ", adaptViewportToResize=" + adaptViewportToResizedControl + "]";\r
279     }
280
281     private void transformChanged() {
282         if (transformListener != null) {\r
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())) {\r
294             transformListener.transformChanged((AffineTransform)evt.getNewValue());
295         }
296     }\r
297 \r
298     @Override\r
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
305             dx = dx * (1 - z);\r
306             dy = dy * (1 - z);\r
307             double limitedScale = limitScaleFactor(z);\r
308             if (limitedScale != 1.0) {\r
309                 translate(dx, dy);\r
310                 scale(z, z);\r
311                 transformChanged();\r
312                 dropQuality();\r
313                 repaint();\r
314             }\r
315         }\r
316         return false;\r
317     }\r
318 \r
319     @Override\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
325                 //repaint();\r
326                 //return true; // hmm.. why?\r
327             }\r
328         }\r
329         return false;\r
330     }\r
331 \r
332     @Override\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
338                 translate(x, y);\r
339                 transformChanged();\r
340                 dragDelta = new Point2D.Double(e.controlPosition.getX(), e.controlPosition.getY());\r
341                 dropQuality();\r
342                 repaint();\r
343                 return true;\r
344             }\r
345         }\r
346         return false;\r
347     }\r
348 \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
354     }\r
355 \r
356     /**\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
359      */\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
367         }\r
368         // Render with better quality soon.\r
369         qualityPaint = false;\r
370         scheduleRepaint();\r
371     }\r
372 \r
373     private void scheduleRepaint() {\r
374         //System.out.println("schedule quality improvement");\r
375         Executable exe = new Executable(AWTThread.getThreadAccess(), new Runnable() {\r
376             @Override\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
382                 repaint();\r
383             }\r
384         });\r
385         // Render with good quality later\r
386         pendingTask = ExecutorWorker.getInstance().timerExec(exe, REPAINT_DELAY);\r
387     }\r
388 \r
389     @Override\r
390     public int getEventMask() {\r
391         return EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask | EventTypes.MouseWheelMask;\r
392     }\r
393
394 }