]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java
eeab18069717aaa250ffe5ffb5d0e06320b47871
[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     @SGInit
189     public void initSG(G2DParentNode parent) {
190         // Replace old NAVIGATION_NODE with a new one
191         INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME);
192         if(oldnav != null) {
193             node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);
194             // FIXME : oldnav seems to be the same node as parent (most of the cases).
195             // Deleting it will cause plenty of code to fail, since they refer to the node directly.
196             // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now).             
197             // oldnav.delete();
198         } else {
199             node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);
200         }
201         node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
202         node.setZIndex(0);
203         node.setTransformListener(transformListener);
204         node.setNavigationEnabled(navigationEnabled);
205         node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
206         node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL)));
207         Double z = getHint(KEY_ZOOM_AMOUNT);
208         if(z != null) {
209             util.setTransform(AffineTransform.getScaleInstance(z, z));
210             node.setTransform(AffineTransform.getScaleInstance(z, z));
211         }
212         boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
213         node.setVisible(visible);
214         oldRoot = getContext().getCanvasNode();
215         getContext().setCanvasNode(node);
216     }
217
218     public void update() {
219         if (bounds != null) {
220             Rectangle2D vp = bounds.getControlBounds();
221             controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY());
222             centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY());
223             centerPointCanvas = util.controlToCanvas(centerPointControl, null);
224         }
225     }
226
227     public TransformNode getNode() {
228         return node;
229     }
230
231     /**
232      * Ensures that the navigation node handled by this participant contains the
233      * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will
234      * contain the same value.
235      * 
236      * @param transform
237      */
238     public void setTransform(AffineTransform transform) {
239         getNode().setTransform(transform);
240         transformListener.transformChanged(transform);
241     }
242
243     @SGCleanup
244     public void cleanupSG() {
245         node.remove();
246         node = null;
247         getContext().setCanvasNode(oldRoot);
248     }
249
250
251     /** Arrow key translate */
252     public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class);
253     public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class);
254     public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class);
255
256     /** Amount of arrow key translate */
257     public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30;
258     public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2;
259     public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1;
260     public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2;
261     public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2;
262
263     public final static int ROTATE_GRAB_ID = -666;
264
265     @EventHandler(priority = 0)
266     public boolean handleEvent(CommandEvent e) {
267         assertDependencies();
268         update();
269         Command c = e.command;
270         boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false;
271         boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false;
272
273         // Arrow key panning
274         if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
275             util.translateWithControlCoordinates(
276                     new Point2D.Double(
277                             getTranslateAmount(), 0));
278             return true;
279         }
280         if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
281             util.translateWithControlCoordinates(
282                     new Point2D.Double(
283                             -getTranslateAmount(), 0));
284             return true;
285         }
286         if (Commands.PAN_UP.equals(c) && !panDisabled) {
287             util.translateWithControlCoordinates(
288                     new Point2D.Double(
289                             0, getTranslateAmount()));
290             return true;
291         }
292         if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
293             util.translateWithControlCoordinates(
294                     new Point2D.Double(0, -getTranslateAmount()));
295             return true;
296         }
297         if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) {
298             if (centerPointControl == null) return false;
299             double scaleFactor = getZoomAmount();
300             scaleFactor = limitScaleFactor(scaleFactor);
301             util.zoomAroundControlPoint(scaleFactor, centerPointControl);
302         }
303         if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) {
304             if (centerPointControl == null) return false;
305             double scaleFactor = 1 / getZoomAmount();
306             scaleFactor = limitScaleFactor(scaleFactor);
307             util.zoomAroundControlPoint(scaleFactor, centerPointControl);
308         }
309
310         if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
311             if (centerPointCanvas == null) return false;
312             util.rotate(centerPointCanvas, -getRotateAmount());
313             setDirty();
314             return true;
315         }
316         if (Commands.ROTATE_CANVAS_CW.equals(c)) {
317             if (centerPointCanvas == null) return false;
318             util.rotate(centerPointCanvas, getRotateAmount());
319             setDirty();
320             return true;
321         }
322         if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) {
323             if (centerPointCanvas == null) return false;
324             util.rotate(centerPointCanvas, -getRotateAmount());
325             grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
326             grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
327             setDirty();
328             return true;
329         }
330         if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) {
331             if (centerPointCanvas == null) return false;
332             util.rotate(centerPointCanvas, getRotateAmount());
333             grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
334             grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
335             setDirty();
336             return true;
337         }
338         if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) {
339             if (centerPointCanvas == null) return false;
340             grab.releaseCanvas(ROTATE_GRAB_ID);
341             grab.releaseCanvas(ROTATE_GRAB_ID - 1);
342             setDirty();
343             return true;
344         }
345         if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) {
346             if (centerPointCanvas == null) return false;
347             grab.releaseCanvas(ROTATE_GRAB_ID);
348             grab.releaseCanvas(ROTATE_GRAB_ID - 1);
349             setDirty();
350             return true;
351         }
352         if (Commands.ENABLE_PAINTING.equals(c)) {
353             Boolean t = getHint(Hints.KEY_DISABLE_PAINTING);
354             removeHint(Hints.KEY_DISABLE_PAINTING);
355             boolean processed = Boolean.TRUE.equals(t);
356             if (processed)
357                 setDirty();
358             return processed;
359         }
360         if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
361             boolean result = zoomToFit();
362             if (!result)
363                 result = zoomToPage();
364             return result;
365         }
366         if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) {
367             if (controlSize==null) return false;
368             IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
369             if (d==null) return false;
370
371             Set<IElement> selections = selection.getAllSelections();
372             Shape bounds = ElementUtils.getElementBoundsOnDiagram(selections);
373             if (bounds == null) return false;
374             Rectangle2D diagramRect = bounds.getBounds2D();
375             if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0)
376                 return false;
377
378             // HACK: prevents straight connections from being unzoomable.
379             if (diagramRect.getWidth() <= 0)
380                 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1);
381             if (diagramRect.getHeight() <= 0)
382                 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0);
383
384             // Show area
385             Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
386             util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
387             return true;
388         }
389         if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
390             return zoomToPage();
391         }
392
393         return false;
394     }
395
396     private boolean zoomToFit() {
397         if (controlSize==null) return false;
398         IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
399         if (d==null) return false;
400
401         Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
402         if (diagramRect==null) return false;
403         if (diagramRect.isEmpty())
404             return false;
405
406         // Show area
407         Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
408         //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")");
409         util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
410
411         return true;
412     }
413
414     private boolean zoomToPage() {
415         if (controlSize==null) return false;
416         PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
417         if (desc == null)
418             return false;
419         if (desc.isInfinite())
420             return false;
421
422         // Show page
423         Rectangle2D diagramRect = new Rectangle2D.Double();
424         desc.getPageRectangle(diagramRect);
425         if (diagramRect.isEmpty())
426             return false;
427
428         Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
429         //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")");
430         util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
431         return true;
432     }
433
434     public double getTranslateAmount()
435     {
436         Integer h = getHint(KEY_TRANSLATE_AMOUNT);
437         if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
438         return h;
439     }
440
441     public double getZoomAmount()
442     {
443         Integer h = getHint(KEY_TRANSLATE_AMOUNT);
444         if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
445         return h;
446     }
447
448     public double getRotateAmount()
449     {
450         Integer h = getHint(KEY_ROTATE_AMOUNT);
451         if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
452         return h;
453     }
454
455     public double limitScaleFactor(double scaleFactor) {
456         Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
457         Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
458
459         if (inLimit == null && scaleFactor < 1)
460             return scaleFactor;
461         if (outLimit == null && scaleFactor > 1)
462             return scaleFactor;
463
464         AffineTransform view = util.getTransform();
465         double currentScale = GeometryUtils.getScale(view) * 100.0;
466         double newScale = currentScale * scaleFactor;
467
468         if (inLimit != null && newScale > currentScale && newScale > inLimit) {
469             if (currentScale < inLimit)
470                 scaleFactor = inLimit / currentScale;
471             else
472                 scaleFactor = 1.0;
473         } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
474             if (currentScale > outLimit)
475                 scaleFactor = outLimit / currentScale;
476             else
477                 scaleFactor = 1.0;
478         }
479         return scaleFactor;
480     }
481
482     public static Margins getZoomToFitMargins(IHintObservable hints) {
483         Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
484         if (h == null) {
485             Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
486             boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
487             if (rulerEnabled) {
488                 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
489             } else {
490                 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;
491             }
492         }
493         return h;
494     }
495     
496 }