X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.g2d%2Fsrc%2Forg%2Fsimantics%2Fg2d%2Fparticipant%2FPanZoomRotateHandler.java;h=6ca4f786247b9e4cc82db6892ad150e0db685a41;hb=3598f987e691cb4f35ab8f283d4dfd760bcdd410;hp=b7321a386e6e919430bd28ac03d533037049774d;hpb=969bd23cab98a79ca9101af33334000879fb60c5;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java b/bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java index b7321a386..6ca4f7862 100644 --- a/bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java +++ b/bundles/org.simantics.g2d/src/org/simantics/g2d/participant/PanZoomRotateHandler.java @@ -1,496 +1,498 @@ -/******************************************************************************* - * 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.Shape; -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); - } - - @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, NavigationNode.class); - // 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, NavigationNode.class); - } - 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(); - Shape bounds = ElementUtils.getElementBoundsOnDiagram(selections); - if (bounds == null) return false; - Rectangle2D diagramRect = bounds.getBounds2D(); - 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; - } - +/******************************************************************************* + * 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; + } + } \ No newline at end of file