/*******************************************************************************
* 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;
}
}