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);
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);
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).
199 node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);
201 node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
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);
209 util.setTransform(AffineTransform.getScaleInstance(z, z));
210 node.setTransform(AffineTransform.getScaleInstance(z, z));
212 boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
213 node.setVisible(visible);
214 oldRoot = getContext().getCanvasNode();
215 getContext().setCanvasNode(node);
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);
227 public TransformNode getNode() {
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.
238 public void setTransform(AffineTransform transform) {
239 getNode().setTransform(transform);
240 transformListener.transformChanged(transform);
244 public void cleanupSG() {
247 getContext().setCanvasNode(oldRoot);
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);
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;
263 public final static int ROTATE_GRAB_ID = -666;
265 @EventHandler(priority = 0)
266 public boolean handleEvent(CommandEvent e) {
267 assertDependencies();
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;
274 if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
275 util.translateWithControlCoordinates(
277 getTranslateAmount(), 0));
280 if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
281 util.translateWithControlCoordinates(
283 -getTranslateAmount(), 0));
286 if (Commands.PAN_UP.equals(c) && !panDisabled) {
287 util.translateWithControlCoordinates(
289 0, getTranslateAmount()));
292 if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
293 util.translateWithControlCoordinates(
294 new Point2D.Double(0, -getTranslateAmount()));
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);
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);
310 if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
311 if (centerPointCanvas == null) return false;
312 util.rotate(centerPointCanvas, -getRotateAmount());
316 if (Commands.ROTATE_CANVAS_CW.equals(c)) {
317 if (centerPointCanvas == null) return false;
318 util.rotate(centerPointCanvas, getRotateAmount());
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);
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);
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);
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);
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);
360 if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
361 boolean result = zoomToFit();
363 result = zoomToPage();
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;
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)
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);
385 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
386 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
389 if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
396 private boolean zoomToFit() {
397 if (controlSize==null) return false;
398 IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
399 if (d==null) return false;
401 Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
402 if (diagramRect==null) return false;
403 if (diagramRect.isEmpty())
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()));
414 private boolean zoomToPage() {
415 if (controlSize==null) return false;
416 PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
419 if (desc.isInfinite())
423 Rectangle2D diagramRect = new Rectangle2D.Double();
424 desc.getPageRectangle(diagramRect);
425 if (diagramRect.isEmpty())
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()));
434 public double getTranslateAmount()
436 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
437 if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
441 public double getZoomAmount()
443 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
444 if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
448 public double getRotateAmount()
450 Integer h = getHint(KEY_ROTATE_AMOUNT);
451 if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
455 public double limitScaleFactor(double scaleFactor) {
456 Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
457 Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
459 if (inLimit == null && scaleFactor < 1)
461 if (outLimit == null && scaleFactor > 1)
464 AffineTransform view = util.getTransform();
465 double currentScale = GeometryUtils.getScale(view) * 100.0;
466 double newScale = currentScale * scaleFactor;
468 if (inLimit != null && newScale > currentScale && newScale > inLimit) {
469 if (currentScale < inLimit)
470 scaleFactor = inLimit / currentScale;
473 } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
474 if (currentScale > outLimit)
475 scaleFactor = outLimit / currentScale;
482 public static Margins getZoomToFitMargins(IHintObservable hints) {
483 Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
485 Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
486 boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
488 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
490 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;