]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java
Customize NavigationNode for PanZoomRotateHandler
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / participant / PanZoomRotateHandler.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.g2d.participant;
13
14 import static org.simantics.g2d.canvas.Hints.KEY_CANVAS_TRANSFORM;
15
16 import java.awt.Shape;
17 import java.awt.geom.AffineTransform;
18 import java.awt.geom.Point2D;
19 import java.awt.geom.Rectangle2D;
20 import java.util.Set;
21
22 import org.simantics.g2d.canvas.Hints;
23 import org.simantics.g2d.canvas.ICanvasContext;
24 import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;
25 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
26 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
27 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
28 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
29 import org.simantics.g2d.diagram.DiagramHints;
30 import org.simantics.g2d.diagram.DiagramUtils;
31 import org.simantics.g2d.diagram.IDiagram;
32 import org.simantics.g2d.diagram.participant.Selection;
33 import org.simantics.g2d.element.ElementUtils;
34 import org.simantics.g2d.element.IElement;
35 import org.simantics.g2d.scenegraph.SceneGraphConstants;
36 import org.simantics.g2d.utils.GeometryUtils;
37 import org.simantics.scenegraph.INode;
38 import org.simantics.scenegraph.g2d.G2DParentNode;
39 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
40 import org.simantics.scenegraph.g2d.events.command.Command;
41 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
42 import org.simantics.scenegraph.g2d.events.command.Commands;
43 import org.simantics.scenegraph.g2d.nodes.NavigationNode;
44 import org.simantics.scenegraph.g2d.nodes.TransformNode;
45 import org.simantics.scenegraph.utils.NodeUtil;
46 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
47 import org.simantics.utils.datastructures.hints.IHintContext.Key;
48 import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;
49 import org.simantics.utils.datastructures.hints.IHintListener;
50 import org.simantics.utils.datastructures.hints.IHintObservable;
51 import org.simantics.utils.page.MarginUtils;
52 import org.simantics.utils.page.MarginUtils.Margins;
53 import org.simantics.utils.page.PageDesc;
54 import org.simantics.utils.threads.ThreadUtils;
55
56 /**
57  * This participant handles pan, zoom, zoom to fit and rotate commands.
58  * 
59  * Hints:
60  *  KEY_TRANSLATE_AMOUNT
61  *  KEY_ZOOM_AMOUNT
62  *  KEY_ROTATE_AMOUNT
63  *  KEY_ZOOM_TO_FIT_MARGINS
64  *  KEY_ZOOM_OUT_LIMIT
65  *  KEY_ZOOM_IN_LIMIT
66  * 
67  * @author Toni Kalajainen
68  * @author Tuukka Lehtonen
69  */
70 public class PanZoomRotateHandler extends AbstractCanvasParticipant {
71
72     /**
73      * Express whether or not the view should attempt to keep the current zoom
74      * level when the canvas parenting control is resized. If the viewport is
75      * set to be adapted to the resized control, the view transform will be
76      * adjusted to accommodate for this. Otherwise the view transform will be
77      * left alone when the control is resized.
78      * 
79      * If hint is not specified, the default value is <code>true</code>.
80      * 
81      * See {@link NavigationNode} for the zoom level keep implementation.
82      */
83     public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL");
84
85     /**
86      * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
87      * view transform). If null, there is no limit. Used with an
88      * ICanvasContext's hint context.
89      */
90     public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT");
91
92     /**
93      * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
94      * view transform). If null there is no limit. Used with an
95      * ICanvasContext's hint context.
96      */
97     public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT");
98
99     public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM");
100
101     public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN");
102
103
104     @Dependency CanvasGrab grab;
105     @Dependency TransformUtil util;
106     @Dependency KeyUtil keys;
107     @Reference  Selection selection;
108     @Reference  CanvasBoundsParticipant bounds;
109
110     // Capture center point
111     Point2D centerPointControl;
112     Point2D centerPointCanvas;
113     Point2D controlSize;
114
115     final Boolean navigationEnabled;
116
117     protected NavigationNode node = null;
118     protected G2DParentNode oldRoot = null;
119
120     public PanZoomRotateHandler() {
121         this(true);
122     }
123
124     public PanZoomRotateHandler(boolean navigationEnabled) {
125         this.navigationEnabled = navigationEnabled;
126     }
127
128     NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() {
129         @Override
130         public void transformChanged(final AffineTransform transform) {
131             ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() {
132                 @Override
133                 public void run() {
134                     if (isRemoved())
135                         return;
136                     //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform);
137                     setHint(KEY_CANVAS_TRANSFORM, transform);
138                 }
139             });
140         }
141     };
142
143     IHintListener hintListener = new HintListenerAdapter() {
144         @Override
145         public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
146             if (node != null) {
147                 if (key == Hints.KEY_DISABLE_PAINTING) {
148                     boolean visible = !Boolean.TRUE.equals(newValue);
149                     if (visible != node.isVisible())
150                         node.setVisible(Boolean.valueOf(visible));
151                 } else if (key == KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL) {
152                     boolean noKeepZoom = Boolean.FALSE.equals(newValue);
153                     if (noKeepZoom == node.getAdaptViewportToResizedControl())
154                         node.setAdaptViewportToResizedControl(Boolean.valueOf(!noKeepZoom));
155                 } else if (key == KEY_ZOOM_OUT_LIMIT) {
156                     node.setZoomOutLimit((Double) newValue);
157                 } else if (key == KEY_ZOOM_IN_LIMIT) {
158                     node.setZoomInLimit((Double) newValue);
159                 } else if (key == KEY_DISABLE_ZOOM) {
160                     node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
161                 }
162             }
163         }
164     };
165
166     @Override
167     public void addedToContext(ICanvasContext ctx) {
168         super.addedToContext(ctx);
169         ctx.getDefaultHintContext().addKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
170         ctx.getDefaultHintContext().addKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
171         ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
172         ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
173         ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
174         ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_PAN, hintListener);
175     }
176
177     @Override
178     public void removedFromContext(ICanvasContext ctx) {
179         ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
180         ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
181         ctx.getDefaultHintContext().removeKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
182         ctx.getDefaultHintContext().removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
183         ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
184         ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_PAN, hintListener);
185         super.removedFromContext(ctx);
186     }
187
188     protected Class<? extends NavigationNode> getNavigationNodeClass() {
189         return NavigationNode.class;
190     }
191
192     @SGInit
193     public void initSG(G2DParentNode parent) {
194         // Replace old NAVIGATION_NODE with a new one
195         INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME);
196         if(oldnav != null) {
197             node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
198             // FIXME : oldnav seems to be the same node as parent (most of the cases).
199             // Deleting it will cause plenty of code to fail, since they refer to the node directly.
200             // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now).             
201             // oldnav.delete();
202         } else {
203             node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
204         }
205         node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
206         node.setZIndex(0);
207         node.setTransformListener(transformListener);
208         node.setNavigationEnabled(navigationEnabled);
209         node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
210         node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL)));
211         Double z = getHint(KEY_ZOOM_AMOUNT);
212         if(z != null) {
213             util.setTransform(AffineTransform.getScaleInstance(z, z));
214             node.setTransform(AffineTransform.getScaleInstance(z, z));
215         }
216         boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
217         node.setVisible(visible);
218         oldRoot = getContext().getCanvasNode();
219         getContext().setCanvasNode(node);
220     }
221
222     public void update() {
223         if (bounds != null) {
224             Rectangle2D vp = bounds.getControlBounds();
225             controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY());
226             centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY());
227             centerPointCanvas = util.controlToCanvas(centerPointControl, null);
228         }
229     }
230
231     public TransformNode getNode() {
232         return node;
233     }
234
235     /**
236      * Ensures that the navigation node handled by this participant contains the
237      * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will
238      * contain the same value.
239      * 
240      * @param transform
241      */
242     public void setTransform(AffineTransform transform) {
243         getNode().setTransform(transform);
244         transformListener.transformChanged(transform);
245     }
246
247     @SGCleanup
248     public void cleanupSG() {
249         node.remove();
250         node = null;
251         getContext().setCanvasNode(oldRoot);
252     }
253
254
255     /** Arrow key translate */
256     public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class);
257     public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class);
258     public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class);
259
260     /** Amount of arrow key translate */
261     public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30;
262     public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2;
263     public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1;
264     public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2;
265     public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2;
266
267     public final static int ROTATE_GRAB_ID = -666;
268
269     @EventHandler(priority = 0)
270     public boolean handleEvent(CommandEvent e) {
271         assertDependencies();
272         update();
273         Command c = e.command;
274         boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false;
275         boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false;
276
277         // Arrow key panning
278         if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
279             util.translateWithControlCoordinates(
280                     new Point2D.Double(
281                             getTranslateAmount(), 0));
282             return true;
283         }
284         if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
285             util.translateWithControlCoordinates(
286                     new Point2D.Double(
287                             -getTranslateAmount(), 0));
288             return true;
289         }
290         if (Commands.PAN_UP.equals(c) && !panDisabled) {
291             util.translateWithControlCoordinates(
292                     new Point2D.Double(
293                             0, getTranslateAmount()));
294             return true;
295         }
296         if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
297             util.translateWithControlCoordinates(
298                     new Point2D.Double(0, -getTranslateAmount()));
299             return true;
300         }
301         if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) {
302             if (centerPointControl == null) return false;
303             double scaleFactor = getZoomAmount();
304             scaleFactor = limitScaleFactor(scaleFactor);
305             util.zoomAroundControlPoint(scaleFactor, centerPointControl);
306         }
307         if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) {
308             if (centerPointControl == null) return false;
309             double scaleFactor = 1 / getZoomAmount();
310             scaleFactor = limitScaleFactor(scaleFactor);
311             util.zoomAroundControlPoint(scaleFactor, centerPointControl);
312         }
313
314         if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
315             if (centerPointCanvas == null) return false;
316             util.rotate(centerPointCanvas, -getRotateAmount());
317             setDirty();
318             return true;
319         }
320         if (Commands.ROTATE_CANVAS_CW.equals(c)) {
321             if (centerPointCanvas == null) return false;
322             util.rotate(centerPointCanvas, getRotateAmount());
323             setDirty();
324             return true;
325         }
326         if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) {
327             if (centerPointCanvas == null) return false;
328             util.rotate(centerPointCanvas, -getRotateAmount());
329             grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
330             grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
331             setDirty();
332             return true;
333         }
334         if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) {
335             if (centerPointCanvas == null) return false;
336             util.rotate(centerPointCanvas, getRotateAmount());
337             grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
338             grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
339             setDirty();
340             return true;
341         }
342         if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) {
343             if (centerPointCanvas == null) return false;
344             grab.releaseCanvas(ROTATE_GRAB_ID);
345             grab.releaseCanvas(ROTATE_GRAB_ID - 1);
346             setDirty();
347             return true;
348         }
349         if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) {
350             if (centerPointCanvas == null) return false;
351             grab.releaseCanvas(ROTATE_GRAB_ID);
352             grab.releaseCanvas(ROTATE_GRAB_ID - 1);
353             setDirty();
354             return true;
355         }
356         if (Commands.ENABLE_PAINTING.equals(c)) {
357             Boolean t = getHint(Hints.KEY_DISABLE_PAINTING);
358             removeHint(Hints.KEY_DISABLE_PAINTING);
359             boolean processed = Boolean.TRUE.equals(t);
360             if (processed)
361                 setDirty();
362             return processed;
363         }
364         if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
365             boolean result = zoomToFit();
366             if (!result)
367                 result = zoomToPage();
368             return result;
369         }
370         if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) {
371             if (controlSize==null) return false;
372             IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
373             if (d==null) return false;
374
375             Set<IElement> selections = selection.getAllSelections();
376             Shape bounds = ElementUtils.getElementBoundsOnDiagram(selections);
377             if (bounds == null) return false;
378             Rectangle2D diagramRect = bounds.getBounds2D();
379             if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0)
380                 return false;
381
382             // HACK: prevents straight connections from being unzoomable.
383             if (diagramRect.getWidth() <= 0)
384                 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1);
385             if (diagramRect.getHeight() <= 0)
386                 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0);
387
388             // Show area
389             Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
390             util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
391             return true;
392         }
393         if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
394             return zoomToPage();
395         }
396
397         return false;
398     }
399
400     private boolean zoomToFit() {
401         if (controlSize==null) return false;
402         IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
403         if (d==null) return false;
404
405         Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
406         if (diagramRect==null) return false;
407         if (diagramRect.isEmpty())
408             return false;
409
410         // Show area
411         Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
412         //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")");
413         util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
414
415         return true;
416     }
417
418     private boolean zoomToPage() {
419         if (controlSize==null) return false;
420         PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
421         if (desc == null)
422             return false;
423         if (desc.isInfinite())
424             return false;
425
426         // Show page
427         Rectangle2D diagramRect = new Rectangle2D.Double();
428         desc.getPageRectangle(diagramRect);
429         if (diagramRect.isEmpty())
430             return false;
431
432         Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
433         //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")");
434         util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
435         return true;
436     }
437
438     public double getTranslateAmount()
439     {
440         Integer h = getHint(KEY_TRANSLATE_AMOUNT);
441         if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
442         return h;
443     }
444
445     public double getZoomAmount()
446     {
447         Integer h = getHint(KEY_TRANSLATE_AMOUNT);
448         if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
449         return h;
450     }
451
452     public double getRotateAmount()
453     {
454         Integer h = getHint(KEY_ROTATE_AMOUNT);
455         if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
456         return h;
457     }
458
459     public double limitScaleFactor(double scaleFactor) {
460         Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
461         Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
462
463         if (inLimit == null && scaleFactor < 1)
464             return scaleFactor;
465         if (outLimit == null && scaleFactor > 1)
466             return scaleFactor;
467
468         AffineTransform view = util.getTransform();
469         double currentScale = GeometryUtils.getScale(view) * 100.0;
470         double newScale = currentScale * scaleFactor;
471
472         if (inLimit != null && newScale > currentScale && newScale > inLimit) {
473             if (currentScale < inLimit)
474                 scaleFactor = inLimit / currentScale;
475             else
476                 scaleFactor = 1.0;
477         } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
478             if (currentScale > outLimit)
479                 scaleFactor = outLimit / currentScale;
480             else
481                 scaleFactor = 1.0;
482         }
483         return scaleFactor;
484     }
485
486     public static Margins getZoomToFitMargins(IHintObservable hints) {
487         Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
488         if (h == null) {
489             Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
490             boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
491             if (rulerEnabled) {
492                 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
493             } else {
494                 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;
495             }
496         }
497         return h;
498     }
499     
500 }