/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.participant; import static org.simantics.g2d.canvas.Hints.KEY_CANVAS_TRANSFORM; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Set; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant; import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency; import org.simantics.g2d.canvas.impl.DependencyReflection.Reference; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup; import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.DiagramUtils; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.participant.Selection; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.scenegraph.SceneGraphConstants; import org.simantics.g2d.utils.GeometryUtils; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler; import org.simantics.scenegraph.g2d.events.command.Command; import org.simantics.scenegraph.g2d.events.command.CommandEvent; import org.simantics.scenegraph.g2d.events.command.Commands; import org.simantics.scenegraph.g2d.nodes.NavigationNode; import org.simantics.scenegraph.g2d.nodes.TransformNode; import org.simantics.scenegraph.utils.NodeUtil; import org.simantics.utils.datastructures.hints.HintListenerAdapter; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; import org.simantics.utils.datastructures.hints.IHintListener; import org.simantics.utils.datastructures.hints.IHintObservable; import org.simantics.utils.page.MarginUtils; import org.simantics.utils.page.MarginUtils.Margins; import org.simantics.utils.page.PageDesc; import org.simantics.utils.threads.ThreadUtils; /** * This participant handles pan, zoom, zoom to fit and rotate commands. * * Hints: * KEY_TRANSLATE_AMOUNT * KEY_ZOOM_AMOUNT * KEY_ROTATE_AMOUNT * KEY_ZOOM_TO_FIT_MARGINS * KEY_ZOOM_OUT_LIMIT * KEY_ZOOM_IN_LIMIT * * @author Toni Kalajainen * @author Tuukka Lehtonen */ public class PanZoomRotateHandler extends AbstractCanvasParticipant { /** * Express whether or not the view should attempt to keep the current zoom * level when the canvas parenting control is resized. If the viewport is * set to be adapted to the resized control, the view transform will be * adjusted to accommodate for this. Otherwise the view transform will be * left alone when the control is resized. * * If hint is not specified, the default value is true. * * See {@link NavigationNode} for the zoom level keep implementation. */ public final static Key KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL = new KeyOf(Boolean.class, "ADAPT_VIEWPORT_TO_RESIZED_CONTROL"); /** * Limit for zooming in expressed as a percentage (100% == 1:1 == identity * view transform). If null, there is no limit. Used with an * ICanvasContext's hint context. */ public final static Key KEY_ZOOM_OUT_LIMIT = new KeyOf(Double.class, "ZOOM_OUT_LIMIT"); /** * Limit for zooming in expressed as a percentage (100% == 1:1 == identity * view transform). If null there is no limit. Used with an * ICanvasContext's hint context. */ public final static Key KEY_ZOOM_IN_LIMIT = new KeyOf(Double.class, "ZOOM_IN_LIMIT"); public final static Key KEY_DISABLE_ZOOM = new KeyOf(Boolean.class, "DISABLE_ZOOM"); public final static Key KEY_DISABLE_PAN = new KeyOf(Boolean.class, "DISABLE_PAN"); @Dependency CanvasGrab grab; @Dependency TransformUtil util; @Dependency KeyUtil keys; @Reference Selection selection; @Reference CanvasBoundsParticipant bounds; // Capture center point Point2D centerPointControl; Point2D centerPointCanvas; Point2D controlSize; final Boolean navigationEnabled; protected NavigationNode node = null; protected G2DParentNode oldRoot = null; public PanZoomRotateHandler() { this(true); } public PanZoomRotateHandler(boolean navigationEnabled) { this.navigationEnabled = navigationEnabled; } NavigationNode.TransformListener transformListener = new NavigationNode.TransformListener() { @Override public void transformChanged(final AffineTransform transform) { ThreadUtils.asyncExec(PanZoomRotateHandler.this.getContext().getThreadAccess(), new Runnable() { @Override public void run() { if (isRemoved()) return; //System.out.println("PanZoomRotateHandler: set canvas transform: " + transform); setHint(KEY_CANVAS_TRANSFORM, transform); } }); } }; IHintListener hintListener = new HintListenerAdapter() { @Override public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { if (node != null) { if (key == Hints.KEY_DISABLE_PAINTING) { boolean visible = !Boolean.TRUE.equals(newValue); if (visible != node.isVisible()) node.setVisible(Boolean.valueOf(visible)); } else if (key == KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL) { boolean noKeepZoom = Boolean.FALSE.equals(newValue); if (noKeepZoom == node.getAdaptViewportToResizedControl()) node.setAdaptViewportToResizedControl(Boolean.valueOf(!noKeepZoom)); } else if (key == KEY_ZOOM_OUT_LIMIT) { node.setZoomOutLimit((Double) newValue); } else if (key == KEY_ZOOM_IN_LIMIT) { node.setZoomInLimit((Double) newValue); } else if (key == KEY_DISABLE_ZOOM) { node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM))); } } } }; @Override public void addedToContext(ICanvasContext ctx) { super.addedToContext(ctx); ctx.getDefaultHintContext().addKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener); ctx.getDefaultHintContext().addKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener); ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener); ctx.getDefaultHintContext().addKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener); ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_ZOOM, hintListener); ctx.getDefaultHintContext().addKeyHintListener(KEY_DISABLE_PAN, hintListener); } @Override public void removedFromContext(ICanvasContext ctx) { ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_IN_LIMIT, hintListener); ctx.getDefaultHintContext().removeKeyHintListener(KEY_ZOOM_OUT_LIMIT, hintListener); ctx.getDefaultHintContext().removeKeyHintListener(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL, hintListener); ctx.getDefaultHintContext().removeKeyHintListener(Hints.KEY_DISABLE_PAINTING, hintListener); ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_ZOOM, hintListener); ctx.getDefaultHintContext().removeKeyHintListener(KEY_DISABLE_PAN, hintListener); super.removedFromContext(ctx); } protected Class getNavigationNodeClass() { return NavigationNode.class; } @SGInit public void initSG(G2DParentNode parent) { // Replace old NAVIGATION_NODE with a new one INode oldnav = NodeUtil.getRootNode(parent).getNode(SceneGraphConstants.NAVIGATION_NODE_NAME); if(oldnav != null) { node = oldnav.appendParent(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass()); // FIXME : oldnav seems to be the same node as parent (most of the cases). // Deleting it will cause plenty of code to fail, since they refer to the node directly. // The bug was not shown, since deleting() a Node did not actually wipe its structures (until now). // oldnav.delete(); } else { node = parent.addNode(SceneGraphConstants.NAVIGATION_NODE_NAME, getNavigationNodeClass()); } node.setLookupId(SceneGraphConstants.NAVIGATION_NODE_NAME); node.setZIndex(0); node.setTransformListener(transformListener); node.setNavigationEnabled(navigationEnabled); node.setZoomEnabled(!Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM))); node.setAdaptViewportToResizedControl(!Boolean.FALSE.equals(getHint(KEY_ADAPT_VIEWPORT_TO_RESIZED_CONTROL))); Double z = getHint(KEY_ZOOM_AMOUNT); if(z != null) { util.setTransform(AffineTransform.getScaleInstance(z, z)); node.setTransform(AffineTransform.getScaleInstance(z, z)); } boolean visible = !Boolean.TRUE.equals(getHint(Hints.KEY_DISABLE_PAINTING)); node.setVisible(visible); oldRoot = getContext().getCanvasNode(); getContext().setCanvasNode(node); } public void update() { if (bounds != null) { Rectangle2D vp = bounds.getControlBounds(); controlSize = new Point2D.Double(vp.getMaxX(), vp.getMaxY()); centerPointControl = new Point2D.Double(vp.getCenterX(), vp.getCenterY()); centerPointCanvas = util.controlToCanvas(centerPointControl, null); } } public TransformNode getNode() { return node; } /** * Ensures that the navigation node handled by this participant contains the * specified transform and that {@link Hints#KEY_CANVAS_TRANSFORM} will * contain the same value. * * @param transform */ public void setTransform(AffineTransform transform) { getNode().setTransform(transform); transformListener.transformChanged(transform); } @SGCleanup public void cleanupSG() { node.remove(); node = null; getContext().setCanvasNode(oldRoot); } /** Arrow key translate */ public final static Key KEY_TRANSLATE_AMOUNT = new KeyOf(Integer.class); public final static Key KEY_ZOOM_AMOUNT = new KeyOf(Double.class); public final static Key KEY_ROTATE_AMOUNT = new KeyOf(Double.class); /** Amount of arrow key translate */ public final static int DEFAULT_KEYBOARD_TRANSLATE_AMOUNT = 30; public final static double DEFAULT_KEYBOARD_ZOOM_AMOUNT = 1.2; public final static double DEFAULT_KEYBOARD_ROTATE_AMOUNT = 0.1; public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS = RulerPainter.RULER_MARINGS2; public final static Margins DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER = MarginUtils.MARGINS2; public final static int ROTATE_GRAB_ID = -666; @EventHandler(priority = 0) public boolean handleEvent(CommandEvent e) { assertDependencies(); update(); Command c = e.command; boolean panDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_PAN)) ? true : false; boolean zoomDisabled = Boolean.TRUE.equals(getHint(KEY_DISABLE_ZOOM)) ? true : false; // Arrow key panning if (Commands.PAN_LEFT.equals(c) && !panDisabled) { util.translateWithControlCoordinates( new Point2D.Double( getTranslateAmount(), 0)); return true; } if (Commands.PAN_RIGHT.equals(c) && !panDisabled) { util.translateWithControlCoordinates( new Point2D.Double( -getTranslateAmount(), 0)); return true; } if (Commands.PAN_UP.equals(c) && !panDisabled) { util.translateWithControlCoordinates( new Point2D.Double( 0, getTranslateAmount())); return true; } if (Commands.PAN_DOWN.equals(c) && !panDisabled) { util.translateWithControlCoordinates( new Point2D.Double(0, -getTranslateAmount())); return true; } if (Commands.ZOOM_IN.equals(c) && !zoomDisabled) { if (centerPointControl == null) return false; double scaleFactor = getZoomAmount(); scaleFactor = limitScaleFactor(scaleFactor); util.zoomAroundControlPoint(scaleFactor, centerPointControl); } if (Commands.ZOOM_OUT.equals(c) && !zoomDisabled) { if (centerPointControl == null) return false; double scaleFactor = 1 / getZoomAmount(); scaleFactor = limitScaleFactor(scaleFactor); util.zoomAroundControlPoint(scaleFactor, centerPointControl); } if (Commands.ROTATE_CANVAS_CCW.equals(c)) { if (centerPointCanvas == null) return false; util.rotate(centerPointCanvas, -getRotateAmount()); setDirty(); return true; } if (Commands.ROTATE_CANVAS_CW.equals(c)) { if (centerPointCanvas == null) return false; util.rotate(centerPointCanvas, getRotateAmount()); setDirty(); return true; } if (Commands.ROTATE_CANVAS_CCW_GRAB.equals(c)) { if (centerPointCanvas == null) return false; util.rotate(centerPointCanvas, -getRotateAmount()); grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas); grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas); setDirty(); return true; } if (Commands.ROTATE_CANVAS_CW_GRAB.equals(c)) { if (centerPointCanvas == null) return false; util.rotate(centerPointCanvas, getRotateAmount()); grab.grabCanvas(ROTATE_GRAB_ID, centerPointCanvas); grab.grabCanvas(ROTATE_GRAB_ID - 1, centerPointCanvas); setDirty(); return true; } if (Commands.ROTATE_CANVAS_CCW_RELEASE.equals(c)) { if (centerPointCanvas == null) return false; grab.releaseCanvas(ROTATE_GRAB_ID); grab.releaseCanvas(ROTATE_GRAB_ID - 1); setDirty(); return true; } if (Commands.ROTATE_CANVAS_CW_RELEASE.equals(c)) { if (centerPointCanvas == null) return false; grab.releaseCanvas(ROTATE_GRAB_ID); grab.releaseCanvas(ROTATE_GRAB_ID - 1); setDirty(); return true; } if (Commands.ENABLE_PAINTING.equals(c)) { Boolean t = getHint(Hints.KEY_DISABLE_PAINTING); removeHint(Hints.KEY_DISABLE_PAINTING); boolean processed = Boolean.TRUE.equals(t); if (processed) setDirty(); return processed; } if (Commands.ZOOM_TO_FIT.equals(c) && !zoomDisabled) { boolean result = zoomToFit(); if (!result) result = zoomToPage(); return result; } if (Commands.ZOOM_TO_SELECTION.equals(c) && !zoomDisabled && selection != null) { if (controlSize==null) return false; IDiagram d = getHint(DiagramHints.KEY_DIAGRAM); if (d==null) return false; Set selections = selection.getAllSelections(); Rectangle2D diagramRect = ElementUtils.getSurroundingElementBoundsOnDiagram(selections); if (diagramRect == null) return false; if (diagramRect.getWidth() <= 0 && diagramRect.getHeight() <= 0) return false; // HACK: prevents straight connections from being unzoomable. if (diagramRect.getWidth() <= 0) org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 0, 0, 1, 1); if (diagramRect.getHeight() <= 0) org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(diagramRect, 1, 1, 0, 0); // Show area Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY()); util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack())); return true; } if (Commands.ZOOM_TO_PAGE.equals(c) && !zoomDisabled) { return zoomToPage(); } return false; } private boolean zoomToFit() { if (controlSize==null) return false; IDiagram d = getHint(DiagramHints.KEY_DIAGRAM); if (d==null) return false; Rectangle2D diagramRect = DiagramUtils.getContentRect(d); if (diagramRect==null) return false; if (diagramRect.isEmpty()) return false; // Show area Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY()); //System.out.println("zoomToFit(" + controlArea + ", " + diagramRect + ")"); util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack())); return true; } private boolean zoomToPage() { if (controlSize==null) return false; PageDesc desc = getHint(Hints.KEY_PAGE_DESC); if (desc == null) return false; if (desc.isInfinite()) return false; // Show page Rectangle2D diagramRect = new Rectangle2D.Double(); desc.getPageRectangle(diagramRect); if (diagramRect.isEmpty()) return false; Rectangle2D controlArea = new Rectangle2D.Double(0, 0, controlSize.getX(), controlSize.getY()); //System.out.println("zoomToPage(" + controlArea + ", " + diagramRect + ")"); util.fitArea(controlArea, diagramRect, getZoomToFitMargins(getHintStack())); return true; } public double getTranslateAmount() { Integer h = getHint(KEY_TRANSLATE_AMOUNT); if (h==null) return DEFAULT_KEYBOARD_TRANSLATE_AMOUNT; return h; } public double getZoomAmount() { Integer h = getHint(KEY_TRANSLATE_AMOUNT); if (h==null) return DEFAULT_KEYBOARD_ZOOM_AMOUNT; return h; } public double getRotateAmount() { Integer h = getHint(KEY_ROTATE_AMOUNT); if (h==null) return DEFAULT_KEYBOARD_ROTATE_AMOUNT; return h; } public double limitScaleFactor(double scaleFactor) { Double inLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT); Double outLimit = getHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT); if (inLimit == null && scaleFactor < 1) return scaleFactor; if (outLimit == null && scaleFactor > 1) return scaleFactor; AffineTransform view = util.getTransform(); double currentScale = GeometryUtils.getScale(view) * 100.0; double newScale = currentScale * scaleFactor; if (inLimit != null && newScale > currentScale && newScale > inLimit) { if (currentScale < inLimit) scaleFactor = inLimit / currentScale; else scaleFactor = 1.0; } else if (outLimit != null && newScale < currentScale && newScale < outLimit) { if (currentScale > outLimit) scaleFactor = outLimit / currentScale; else scaleFactor = 1.0; } return scaleFactor; } public static Margins getZoomToFitMargins(IHintObservable hints) { Margins h = hints.getHint(DiagramHints.KEY_MARGINS); if (h == null) { Boolean b = hints.getHint(RulerPainter.KEY_RULER_ENABLED); boolean rulerEnabled = b == null || Boolean.TRUE.equals(b); if (rulerEnabled) { return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS; } else { return PanZoomRotateHandler.DEFAULT_ZOOM_TO_FIT_MARGINS_NO_RULER; } } return h; } }