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.Shape;
17 import java.awt.geom.AffineTransform;
18 import java.awt.geom.Point2D;
19 import java.awt.geom.Rectangle2D;
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;
57 * This participant handles pan, zoom, zoom to fit and rotate commands.
60 * KEY_TRANSLATE_AMOUNT
63 * KEY_ZOOM_TO_FIT_MARGINS
67 * @author Toni Kalajainen
68 * @author Tuukka Lehtonen
70 public class PanZoomRotateHandler extends AbstractCanvasParticipant {
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.
79 * If hint is not specified, the default value is <code>true</code>.
81 * See {@link NavigationNode} for the zoom level keep implementation.
83 public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL");
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.
90 public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT");
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.
97 public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT");
99 public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM");
101 public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN");
104 @Dependency CanvasGrab grab;
105 @Dependency TransformUtil util;
106 @Dependency KeyUtil keys;
107 @Reference Selection selection;
108 @Reference CanvasBoundsParticipant bounds;
110 // Capture center point
111 Point2D centerPointControl;
112 Point2D centerPointCanvas;
115 final Boolean navigationEnabled;
117 protected NavigationNode node = null;
118 protected G2DParentNode oldRoot = null;
120 public PanZoomRotateHandler() {
124 public PanZoomRotateHandler(boolean navigationEnabled) {
125 this.navigationEnabled = navigationEnabled;
128 NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() {
130 public void transformChanged(final AffineTransform transform) {
131 ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() {
136 //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform);
137 setHint(KEY_CANVAS_TRANSFORM, transform);
143 IHintListener hintListener = new HintListenerAdapter() {
145 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
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)));
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);
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);
188 protected Class<? extends NavigationNode> getNavigationNodeClass() {
189 return NavigationNode.class;
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);
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).
203 node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass());
205 node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
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);
213 util.setTransform(AffineTransform.getScaleInstance(z, z));
214 node.setTransform(AffineTransform.getScaleInstance(z, z));
216 boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
217 node.setVisible(visible);
218 oldRoot = getContext().getCanvasNode();
219 getContext().setCanvasNode(node);
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);
231 public TransformNode getNode() {
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.
242 public void setTransform(AffineTransform transform) {
243 getNode().setTransform(transform);
244 transformListener.transformChanged(transform);
248 public void cleanupSG() {
251 getContext().setCanvasNode(oldRoot);
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);
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;
267 public final static int ROTATE_GRAB_ID = -666;
269 @EventHandler(priority = 0)
270 public boolean handleEvent(CommandEvent e) {
271 assertDependencies();
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;
278 if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
279 util.translateWithControlCoordinates(
281 getTranslateAmount(), 0));
284 if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
285 util.translateWithControlCoordinates(
287 -getTranslateAmount(), 0));
290 if (Commands.PAN_UP.equals(c) && !panDisabled) {
291 util.translateWithControlCoordinates(
293 0, getTranslateAmount()));
296 if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
297 util.translateWithControlCoordinates(
298 new Point2D.Double(0, -getTranslateAmount()));
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);
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);
314 if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
315 if (centerPointCanvas == null) return false;
316 util.rotate(centerPointCanvas, -getRotateAmount());
320 if (Commands.ROTATE_CANVAS_CW.equals(c)) {
321 if (centerPointCanvas == null) return false;
322 util.rotate(centerPointCanvas, getRotateAmount());
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);
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);
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);
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);
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);
364 if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
365 boolean result = zoomToFit();
367 result = zoomToPage();
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;
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)
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);
389 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
390 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
393 if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
400 private boolean zoomToFit() {
401 if (controlSize==null) return false;
402 IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
403 if (d==null) return false;
405 Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
406 if (diagramRect==null) return false;
407 if (diagramRect.isEmpty())
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()));
418 private boolean zoomToPage() {
419 if (controlSize==null) return false;
420 PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
423 if (desc.isInfinite())
427 Rectangle2D diagramRect = new Rectangle2D.Double();
428 desc.getPageRectangle(diagramRect);
429 if (diagramRect.isEmpty())
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()));
438 public double getTranslateAmount()
440 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
441 if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
445 public double getZoomAmount()
447 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
448 if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
452 public double getRotateAmount()
454 Integer h = getHint(KEY_ROTATE_AMOUNT);
455 if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
459 public double limitScaleFactor(double scaleFactor) {
460 Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
461 Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
463 if (inLimit == null && scaleFactor < 1)
465 if (outLimit == null && scaleFactor > 1)
468 AffineTransform view = util.getTransform();
469 double currentScale = GeometryUtils.getScale(view) * 100.0;
470 double newScale = currentScale * scaleFactor;
472 if (inLimit != null && newScale > currentScale && newScale > inLimit) {
473 if (currentScale < inLimit)
474 scaleFactor = inLimit / currentScale;
477 } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
478 if (currentScale > outLimit)
479 scaleFactor = outLimit / currentScale;
486 public static Margins getZoomToFitMargins(IHintObservable hints) {
487 Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
489 Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
490 boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
492 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
494 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;