1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.g2d.participant;
\r
14 import static org.simantics.g2d.canvas.Hints.KEY_CANVAS_TRANSFORM;
\r
16 import java.awt.Shape;
\r
17 import java.awt.geom.AffineTransform;
\r
18 import java.awt.geom.Point2D;
\r
19 import java.awt.geom.Rectangle2D;
\r
20 import java.util.Set;
\r
22 import org.simantics.g2d.canvas.Hints;
\r
23 import org.simantics.g2d.canvas.ICanvasContext;
\r
24 import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;
\r
25 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
\r
26 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
\r
27 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
\r
28 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
\r
29 import org.simantics.g2d.diagram.DiagramHints;
\r
30 import org.simantics.g2d.diagram.DiagramUtils;
\r
31 import org.simantics.g2d.diagram.IDiagram;
\r
32 import org.simantics.g2d.diagram.participant.Selection;
\r
33 import org.simantics.g2d.element.ElementUtils;
\r
34 import org.simantics.g2d.element.IElement;
\r
35 import org.simantics.g2d.scenegraph.SceneGraphConstants;
\r
36 import org.simantics.g2d.utils.GeometryUtils;
\r
37 import org.simantics.scenegraph.INode;
\r
38 import org.simantics.scenegraph.g2d.G2DParentNode;
\r
39 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
\r
40 import org.simantics.scenegraph.g2d.events.command.Command;
\r
41 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
\r
42 import org.simantics.scenegraph.g2d.events.command.Commands;
\r
43 import org.simantics.scenegraph.g2d.nodes.NavigationNode;
\r
44 import org.simantics.scenegraph.g2d.nodes.TransformNode;
\r
45 import org.simantics.scenegraph.utils.NodeUtil;
\r
46 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
\r
47 import org.simantics.utils.datastructures.hints.IHintContext.Key;
\r
48 import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;
\r
49 import org.simantics.utils.datastructures.hints.IHintListener;
\r
50 import org.simantics.utils.datastructures.hints.IHintObservable;
\r
51 import org.simantics.utils.page.MarginUtils;
\r
52 import org.simantics.utils.page.MarginUtils.Margins;
\r
53 import org.simantics.utils.page.PageDesc;
\r
54 import org.simantics.utils.threads.ThreadUtils;
\r
57 * This participant handles pan, zoom, zoom to fit and rotate commands.
\r
60 * KEY_TRANSLATE_AMOUNT
\r
63 * KEY_ZOOM_TO_FIT_MARGINS
\r
64 * KEY_ZOOM_OUT_LIMIT
\r
67 * @author Toni Kalajainen
\r
68 * @author Tuukka Lehtonen
\r
70 public class PanZoomRotateHandler extends AbstractCanvasParticipant {
\r
73 * Express whether or not the view should attempt to keep the current zoom
\r
74 * level when the canvas parenting control is resized. If the viewport is
\r
75 * set to be adapted to the resized control, the view transform will be
\r
76 * adjusted to accommodate for this. Otherwise the view transform will be
\r
77 * left alone when the control is resized.
\r
79 * If hint is not specified, the default value is <code>true</code>.
\r
81 * See {@link NavigationNode} for the zoom level keep implementation.
\r
83 public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL");
\r
86 * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
\r
87 * view transform). If null, there is no limit. Used with an
\r
88 * ICanvasContext's hint context.
\r
90 public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT");
\r
93 * Limit for zooming in expressed as a percentage (100% == 1:1 == identity
\r
94 * view transform). If null there is no limit. Used with an
\r
95 * ICanvasContext's hint context.
\r
97 public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT");
\r
99 public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM");
\r
101 public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN");
\r
104 @Dependency CanvasGrab grab;
\r
105 @Dependency TransformUtil util;
\r
106 @Dependency KeyUtil keys;
\r
107 @Reference Selection selection;
\r
108 @Reference CanvasBoundsParticipant bounds;
\r
110 // Capture center point
\r
111 Point2D centerPointControl;
\r
112 Point2D centerPointCanvas;
\r
113 Point2D controlSize;
\r
115 final Boolean navigationEnabled;
\r
117 protected NavigationNode node = null;
\r
118 protected G2DParentNode oldRoot = null;
\r
120 public PanZoomRotateHandler() {
\r
124 public PanZoomRotateHandler(boolean navigationEnabled) {
\r
125 this.navigationEnabled = navigationEnabled;
\r
128 NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() {
\r
130 public void transformChanged(final AffineTransform transform) {
\r
131 ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() {
\r
133 public void run() {
\r
136 //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform);
\r
137 setHint(KEY_CANVAS_TRANSFORM, transform);
\r
143 IHintListener hintListener = new HintListenerAdapter() {
\r
145 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
\r
146 if (node != null) {
\r
147 if (key == Hints.KEY_DISABLE_PAINTING) {
\r
148 boolean visible = !Boolean.TRUE.equals(newValue);
\r
149 if (visible != node.isVisible())
\r
150 node.setVisible(Boolean.valueOf(visible));
\r
151 } else if (key == KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL) {
\r
152 boolean noKeepZoom = Boolean.FALSE.equals(newValue);
\r
153 if (noKeepZoom == node.getAdaptViewportToResizedControl())
\r
154 node.setAdaptViewportToResizedControl(Boolean.valueOf(!noKeepZoom));
\r
155 } else if (key == KEY_ZOOM_OUT_LIMIT) {
\r
156 node.setZoomOutLimit((Double) newValue);
\r
157 } else if (key == KEY_ZOOM_IN_LIMIT) {
\r
158 node.setZoomInLimit((Double) newValue);
\r
159 } else if (key == KEY_DISABLE_ZOOM) {
\r
160 node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
\r
167 public void addedToContext(ICanvasContext ctx) {
\r
168 super.addedToContext(ctx);
\r
169 ctx.getDefaultHintContext().addKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
\r
170 ctx.getDefaultHintContext().addKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
\r
171 ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
\r
172 ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
\r
173 ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
\r
174 ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_PAN, hintListener);
\r
178 public void removedFromContext(ICanvasContext ctx) {
\r
179 ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener);
\r
180 ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener);
\r
181 ctx.getDefaultHintContext().removeKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener);
\r
182 ctx.getDefaultHintContext().removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener);
\r
183 ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_ZOOM, hintListener);
\r
184 ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_PAN, hintListener);
\r
185 super.removedFromContext(ctx);
\r
189 public void initSG(G2DParentNode parent) {
\r
190 // Replace old NAVIGATION_NODE with a new one
\r
191 INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME);
\r
192 if(oldnav != null) {
\r
193 node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);
\r
194 // FIXME : oldnav seems to be the same node as parent (most of the cases).
\r
195 // Deleting it will cause plenty of code to fail, since they refer to the node directly.
\r
196 // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now).
\r
197 // oldnav.delete();
\r
199 node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, NavigationNode.class);
\r
201 node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME);
\r
203 node.setTransformListener(transformListener);
\r
204 node.setNavigationEnabled(navigationEnabled);
\r
205 node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)));
\r
206 node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL)));
\r
207 Double z = getHint(KEY_ZOOM_AMOUNT);
\r
209 util.setTransform(AffineTransform.getScaleInstance(z, z));
\r
210 node.setTransform(AffineTransform.getScaleInstance(z, z));
\r
212 boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING));
\r
213 node.setVisible(visible);
\r
214 oldRoot = getContext().getCanvasNode();
\r
215 getContext().setCanvasNode(node);
\r
218 public void update() {
\r
219 if (bounds != null) {
\r
220 Rectangle2D vp = bounds.getControlBounds();
\r
221 controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY());
\r
222 centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY());
\r
223 centerPointCanvas = util.controlToCanvas(centerPointControl, null);
\r
227 public TransformNode getNode() {
\r
232 * Ensures that the navigation node handled by this participant contains the
\r
233 * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will
\r
234 * contain the same value.
\r
238 public void setTransform(AffineTransform transform) {
\r
239 getNode().setTransform(transform);
\r
240 transformListener.transformChanged(transform);
\r
244 public void cleanupSG() {
\r
247 getContext().setCanvasNode(oldRoot);
\r
251 /** Arrow key translate */
\r
252 public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class);
\r
253 public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class);
\r
254 public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class);
\r
256 /** Amount of arrow key translate */
\r
257 public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30;
\r
258 public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2;
\r
259 public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1;
\r
260 public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2;
\r
261 public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2;
\r
263 public final static int ROTATE_GRAB_ID = -666;
\r
265 @EventHandler(priority = 0)
\r
266 public boolean handleEvent(CommandEvent e) {
\r
267 assertDependencies();
\r
269 Command c = e.command;
\r
270 boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false;
\r
271 boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false;
\r
273 // Arrow key panning
\r
274 if (Commands.PAN_LEFT.equals(c) && !panDisabled) {
\r
275 util.translateWithControlCoordinates(
\r
276 new Point2D.Double(
\r
277 getTranslateAmount(), 0));
\r
280 if (Commands.PAN_RIGHT.equals(c) && !panDisabled) {
\r
281 util.translateWithControlCoordinates(
\r
282 new Point2D.Double(
\r
283 -getTranslateAmount(), 0));
\r
286 if (Commands.PAN_UP.equals(c) && !panDisabled) {
\r
287 util.translateWithControlCoordinates(
\r
288 new Point2D.Double(
\r
289 0, getTranslateAmount()));
\r
292 if (Commands.PAN_DOWN.equals(c) && !panDisabled) {
\r
293 util.translateWithControlCoordinates(
\r
294 new Point2D.Double(0, -getTranslateAmount()));
\r
297 if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) {
\r
298 if (centerPointControl == null) return false;
\r
299 double scaleFactor = getZoomAmount();
\r
300 scaleFactor = limitScaleFactor(scaleFactor);
\r
301 util.zoomAroundControlPoint(scaleFactor, centerPointControl);
\r
303 if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) {
\r
304 if (centerPointControl == null) return false;
\r
305 double scaleFactor = 1 / getZoomAmount();
\r
306 scaleFactor = limitScaleFactor(scaleFactor);
\r
307 util.zoomAroundControlPoint(scaleFactor, centerPointControl);
\r
310 if (Commands.ROTATE_CANVAS_CCW.equals(c)) {
\r
311 if (centerPointCanvas == null) return false;
\r
312 util.rotate(centerPointCanvas, -getRotateAmount());
\r
316 if (Commands.ROTATE_CANVAS_CW.equals(c)) {
\r
317 if (centerPointCanvas == null) return false;
\r
318 util.rotate(centerPointCanvas, getRotateAmount());
\r
322 if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) {
\r
323 if (centerPointCanvas == null) return false;
\r
324 util.rotate(centerPointCanvas, -getRotateAmount());
\r
325 grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
\r
326 grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
\r
330 if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) {
\r
331 if (centerPointCanvas == null) return false;
\r
332 util.rotate(centerPointCanvas, getRotateAmount());
\r
333 grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas);
\r
334 grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas);
\r
338 if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) {
\r
339 if (centerPointCanvas == null) return false;
\r
340 grab.releaseCanvas(ROTATE_GRAB_ID);
\r
341 grab.releaseCanvas(ROTATE_GRAB_ID - 1);
\r
345 if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) {
\r
346 if (centerPointCanvas == null) return false;
\r
347 grab.releaseCanvas(ROTATE_GRAB_ID);
\r
348 grab.releaseCanvas(ROTATE_GRAB_ID - 1);
\r
352 if (Commands.ENABLE_PAINTING.equals(c)) {
\r
353 Boolean t = getHint(Hints.KEY_DISABLE_PAINTING);
\r
354 removeHint(Hints.KEY_DISABLE_PAINTING);
\r
355 boolean processed = Boolean.TRUE.equals(t);
\r
360 if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) {
\r
361 boolean result = zoomToFit();
\r
363 result = zoomToPage();
\r
366 if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) {
\r
367 if (controlSize==null) return false;
\r
368 IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
\r
369 if (d==null) return false;
\r
371 Set<IElement> selections = selection.getAllSelections();
\r
372 Shape bounds = ElementUtils.getElementBoundsOnDiagram(selections);
\r
373 if (bounds == null) return false;
\r
374 Rectangle2D diagramRect = bounds.getBounds2D();
\r
375 if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0)
\r
378 // HACK: prevents straight connections from being unzoomable.
\r
379 if (diagramRect.getWidth() <= 0)
\r
380 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1);
\r
381 if (diagramRect.getHeight() <= 0)
\r
382 org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0);
\r
385 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
\r
386 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
\r
389 if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) {
\r
390 return zoomToPage();
\r
396 private boolean zoomToFit() {
\r
397 if (controlSize==null) return false;
\r
398 IDiagram d = getHint(DiagramHints.KEY_DIAGRAM);
\r
399 if (d==null) return false;
\r
401 Rectangle2D diagramRect = DiagramUtils.getContentRect(d);
\r
402 if (diagramRect==null) return false;
\r
403 if (diagramRect.isEmpty())
\r
407 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
\r
408 //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")");
\r
409 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
\r
414 private boolean zoomToPage() {
\r
415 if (controlSize==null) return false;
\r
416 PageDesc desc = getHint(Hints.KEY_PAGE_DESC);
\r
419 if (desc.isInfinite())
\r
423 Rectangle2D diagramRect = new Rectangle2D.Double();
\r
424 desc.getPageRectangle(diagramRect);
\r
425 if (diagramRect.isEmpty())
\r
428 Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY());
\r
429 //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")");
\r
430 util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack()));
\r
434 public double getTranslateAmount()
\r
436 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
\r
437 if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT;
\r
441 public double getZoomAmount()
\r
443 Integer h = getHint(KEY_TRANSLATE_AMOUNT);
\r
444 if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT;
\r
448 public double getRotateAmount()
\r
450 Integer h = getHint(KEY_ROTATE_AMOUNT);
\r
451 if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT;
\r
455 public double limitScaleFactor(double scaleFactor) {
\r
456 Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT);
\r
457 Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT);
\r
459 if (inLimit == null && scaleFactor < 1)
\r
460 return scaleFactor;
\r
461 if (outLimit == null && scaleFactor > 1)
\r
462 return scaleFactor;
\r
464 AffineTransform view = util.getTransform();
\r
465 double currentScale = GeometryUtils.getScale(view) * 100.0;
\r
466 double newScale = currentScale * scaleFactor;
\r
468 if (inLimit != null && newScale > currentScale && newScale > inLimit) {
\r
469 if (currentScale < inLimit)
\r
470 scaleFactor = inLimit / currentScale;
\r
473 } else if (outLimit != null && newScale < currentScale && newScale < outLimit) {
\r
474 if (currentScale > outLimit)
\r
475 scaleFactor = outLimit / currentScale;
\r
479 return scaleFactor;
\r
482 public static Margins getZoomToFitMargins(IHintObservable hints) {
\r
483 Margins h = hints.getHint(DiagramHints.KEY_MARGINS);
\r
485 Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED);
\r
486 boolean rulerEnabled = b == null || Boolean.TRUE.equals(b);
\r
487 if (rulerEnabled) {
\r
488 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS;
\r
490 return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER;
\r