1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
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
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.g2d.participant;
14 import static org.simantics.g2d.canvas.Hints.KEY_CANVAS_TRANSFORM;
16 import java.awt.geom.AffineTransform;
17 import java.awt.geom.Point2D;
18 import java.awt.geom.Rectangle2D;
21 import org.simantics.g2d.canvas.Hints;
22 import org.simantics.g2d.canvas.ICanvasContext;
23 import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;
24 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
25 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
26 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
27 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
28 import org.simantics.g2d.diagram.DiagramHints;
29 import org.simantics.g2d.diagram.DiagramUtils;
30 import org.simantics.g2d.diagram.IDiagram;
31 import org.simantics.g2d.diagram.participant.Selection;
32 import org.simantics.g2d.element.ElementUtils;
33 import org.simantics.g2d.element.IElement;
34 import org.simantics.g2d.scenegraph.SceneGraphConstants;
35 import org.simantics.g2d.utils.GeometryUtils;
36 import org.simantics.scenegraph.INode;
37 import org.simantics.scenegraph.g2d.G2DParentNode;
38 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
39 import org.simantics.scenegraph.g2d.events.command.Command;
40 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
41 import org.simantics.scenegraph.g2d.events.command.Commands;
42 import org.simantics.scenegraph.g2d.nodes.NavigationNode;
43 import org.simantics.scenegraph.g2d.nodes.TransformNode;
44 import org.simantics.scenegraph.utils.NodeUtil;
45 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
46 import org.simantics.utils.datastructures.hints.IHintContext.Key;
47 import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;
48 import org.simantics.utils.datastructures.hints.IHintListener;
49 import org.simantics.utils.datastructures.hints.IHintObservable;
50 import org.simantics.utils.page.MarginUtils;
51 import org.simantics.utils.page.MarginUtils.Margins;
52 import org.simantics.utils.page.PageDesc;
53 import org.simantics.utils.threads.ThreadUtils;
56 * This participant handles pan, zoom, zoom to fit and rotate commands.
59 * KEY_TRANSLATE_AMOUNT
62 * KEY_ZOOM_TO_FIT_MARGINS
66 * @author Toni Kalajainen
67 * @author Tuukka Lehtonen
69 public class PanZoomRotateHandler extends AbstractCanvasParticipant {
72 * Express whether or not the view should attempt to keep the current zoom
73 * level when the canvas parenting control is resized. If the viewport is
74 * set to be adapted to the resized control, the view transform will be
75 * adjusted to accommodate for this. Otherwise the view transform will be
76 * left alone when the control is resized.
78 * If hint is not specified, the default value is <code>true</code>.
80 * See {@link NavigationNode} for the zoom level keep implementation.
82 public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL");
85 * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
86 * view transform). If null, there is no limit. Used with an
87 * ICanvasContext's hint context.
89 public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT");
92 * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
93 * view transform). If null there is no limit. Used with an
94 * ICanvasContext's hint context.
96 public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT");
98 public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM");
100 public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN");
103 @Dependency CanvasGrab grab;
104 @Dependency TransformUtil util;
105 @Dependency KeyUtil keys;
106 @Reference Selection selection;
107 @Reference CanvasBoundsParticipant bounds;
109 // Capture center point
110 Point2D centerPointControl;
111 Point2D centerPointCanvas;
114 final Boolean navigationEnabled;
116 protected NavigationNode node = null;
117 protected G2DParentNode oldRoot = null;
119 public PanZoomRotateHandler() {
123 public PanZoomRotateHandler(boolean navigationEnabled) {
124 this.navigationEnabled = navigationEnabled;
127 NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() {
129 public void transformChanged(final AffineTransform transform) {
130 ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() {
135 //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform);
136 setHint(KEY_CANVAS_TRANSFORM, transform);
142 IHintListener hintListener = new HintListenerAdapter() {
144 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
146 if (key == Hints.KEY_DISABLE_PAINTING) {
147 boolean visible = !Boolean.TRUE.equals(newValue);
148 if (visible != node.isVisible())
149 node.setVisible(Boolean.valueOf(visible));
150 } else if (key == KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL) {
151 boolean noKeepZoom = Boolean.FALSE.equals(newValue);
152 if (noKeepZoom == node.getAdaptViewportToResizedControl())
153 node.setAdaptViewportToResizedControl(Boolean.valueOf(!noKeepZoom));
154 } else if (key == KEY_ZOOM_OUT_LIMIT) {
155 node.setZoomOutLimit((Double) newValue);
156 } else if (key == KEY_ZOOM_IN_LIMIT) {
157 node.setZoomInLimit((Double) newValue);
158 } else if (key == KEY_DISABLE_ZOOM) {
159 node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
166 public void addedToContext(ICanvasContext ctx) {
167 super.addedToContext(ctx);
168 ctx.getDefaultHintContext().addKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
169 ctx.getDefaultHintContext().addKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
170 ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
171 ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
172 ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
173 ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_PAN, hintListener);
177 public void removedFromContext(ICanvasContext ctx) {
178 ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
179 ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
180 ctx.getDefaultHintContext().removeKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
181 ctx.getDefaultHintContext().removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
182 ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
183 ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_PAN, hintListener);
184 super.removedFromContext(ctx);
187 protected Class<? extends NavigationNode> getNavigationNodeClass() {
188 return NavigationNode.class;
192 public void initSG(G2DParentNode parent) {
193 // Replace old NAVIGATION_NODE with a new one
194 INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME);
196 node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
197 // FIXME : oldnav seems to be the same node as parent (most of the cases).
198 // Deleting it will cause plenty of code to fail, since they refer to the node directly.
199 // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now).
202 node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
204 node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
206 node.setTransformListener(transformListener);
207 node.setNavigationEnabled(navigationEnabled);
208 node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
209 node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL)));
210 Double z = getHint(KEY_ZOOM_AMOUNT);
212 util.setTransform(AffineTransform.getScaleInstance(z, z));
213 node.setTransform(AffineTransform.getScaleInstance(z, z));
215 boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
216 node.setVisible(visible);
217 oldRoot = getContext().getCanvasNode();
218 getContext().setCanvasNode(node);
221 public void update() {
222 if (bounds != null) {
223 Rectangle2D vp = bounds.getControlBounds();
224 controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY());
225 centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY());
226 centerPointCanvas = util.controlToCanvas(centerPointControl, null);
230 public TransformNode getNode() {
235 * Ensures that the navigation node handled by this participant contains the
236 * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will
237 * contain the same value.
241 public void setTransform(AffineTransform transform) {
242 getNode().setTransform(transform);
243 transformListener.transformChanged(transform);
247 public void cleanupSG() {
250 getContext().setCanvasNode(oldRoot);
254 /** Arrow key translate */
255 public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class);
256 public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class);
257 public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class);
259 /** Amount of arrow key translate */
260 public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30;
261 public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2;
262 public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1;
263 public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2;
264 public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2;
266 public final static int ROTATE_GRAB_ID = -666;
268 @EventHandler(priority = 0)
269 public boolean handleEvent(CommandEvent e) {
270 assertDependencies();
272 Command c = e.command;
273 boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false;
274 boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false;
277 if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
278 util.translateWithControlCoordinates(
280 getTranslateAmount(), 0));
283 if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
284 util.translateWithControlCoordinates(
286 -getTranslateAmount(), 0));
289 if (Commands.PAN_UP.equals(c) && !panDisabled) {
290 util.translateWithControlCoordinates(
292 0, getTranslateAmount()));
295 if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
296 util.translateWithControlCoordinates(
297 new Point2D.Double(0, -getTranslateAmount()));
300 if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) {
301 if (centerPointControl == null) return false;
302 double scaleFactor = getZoomAmount();
303 scaleFactor = limitScaleFactor(scaleFactor);
304 util.zoomAroundControlPoint(scaleFactor, centerPointControl);
306 if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) {
307 if (centerPointControl == null) return false;
308 double scaleFactor = 1 / getZoomAmount();
309 scaleFactor = limitScaleFactor(scaleFactor);
310 util.zoomAroundControlPoint(scaleFactor, centerPointControl);
313 if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
314 if (centerPointCanvas == null) return false;
315 util.rotate(centerPointCanvas, -getRotateAmount());
319 if (Commands.ROTATE_CANVAS_CW.equals(c)) {
320 if (centerPointCanvas == null) return false;
321 util.rotate(centerPointCanvas, getRotateAmount());
325 if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) {
326 if (centerPointCanvas == null) return false;
327 util.rotate(centerPointCanvas, -getRotateAmount());
328 grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
329 grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
333 if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) {
334 if (centerPointCanvas == null) return false;
335 util.rotate(centerPointCanvas, getRotateAmount());
336 grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
337 grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
341 if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) {
342 if (centerPointCanvas == null) return false;
343 grab.releaseCanvas(ROTATE_GRAB_ID);
344 grab.releaseCanvas(ROTATE_GRAB_ID - 1);
348 if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) {
349 if (centerPointCanvas == null) return false;
350 grab.releaseCanvas(ROTATE_GRAB_ID);
351 grab.releaseCanvas(ROTATE_GRAB_ID - 1);
355 if (Commands.ENABLE_PAINTING.equals(c)) {
356 Boolean t = getHint(Hints.KEY_DISABLE_PAINTING);
357 removeHint(Hints.KEY_DISABLE_PAINTING);
358 boolean processed = Boolean.TRUE.equals(t);
363 if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
364 boolean result = zoomToFit();
366 result = zoomToPage();
369 if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) {
370 if (controlSize==null) return false;
371 IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
372 if (d==null) return false;
374 Set<IElement> selections = selection.getAllSelections();
375 Rectangle2D diagramRect = ElementUtils.getSurroundingElementBoundsOnDiagram(selections);
376 if (diagramRect == null) return false;
377 if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0)
380 // HACK: prevents straight connections from being unzoomable.
381 if (diagramRect.getWidth() <= 0)
382 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1);
383 if (diagramRect.getHeight() <= 0)
384 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0);
387 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
388 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
391 if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
398 private boolean zoomToFit() {
399 if (controlSize==null) return false;
400 IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
401 if (d==null) return false;
403 Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
404 if (diagramRect==null) return false;
405 if (diagramRect.isEmpty())
409 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
410 //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")");
411 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
416 private boolean zoomToPage() {
417 if (controlSize==null) return false;
418 PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
421 if (desc.isInfinite())
425 Rectangle2D diagramRect = new Rectangle2D.Double();
426 desc.getPageRectangle(diagramRect);
427 if (diagramRect.isEmpty())
430 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
431 //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")");
432 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
436 public double getTranslateAmount()
438 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
439 if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
443 public double getZoomAmount()
445 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
446 if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
450 public double getRotateAmount()
452 Integer h = getHint(KEY_ROTATE_AMOUNT);
453 if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
457 public double limitScaleFactor(double scaleFactor) {
458 Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
459 Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
461 if (inLimit == null && scaleFactor < 1)
463 if (outLimit == null && scaleFactor > 1)
466 AffineTransform view = util.getTransform();
467 double currentScale = GeometryUtils.getScale(view) * 100.0;
468 double newScale = currentScale * scaleFactor;
470 if (inLimit != null && newScale > currentScale && newScale > inLimit) {
471 if (currentScale < inLimit)
472 scaleFactor = inLimit / currentScale;
475 } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
476 if (currentScale > outLimit)
477 scaleFactor = outLimit / currentScale;
484 public static Margins getZoomToFitMargins(IHintObservable hints) {
485 Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
487 Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
488 boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
490 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
492 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;