/*******************************************************************************
* Copyright (c) 2015, 2016 itemis AG and others.
* 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:
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.policies;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.convert.fx.Geometry2FX;
import org.eclipse.gef.geometry.euclidean.Angle;
import org.eclipse.gef.geometry.planar.AffineTransform;
import org.eclipse.gef.mvc.fx.operations.ForwardUndoCompositeOperation;
import org.eclipse.gef.mvc.fx.operations.ITransactionalOperation;
import org.eclipse.gef.mvc.fx.operations.TransformContentOperation;
import org.eclipse.gef.mvc.fx.operations.TransformVisualOperation;
import org.eclipse.gef.mvc.fx.parts.ITransformableContentPart;
import javafx.scene.Node;
/**
* The {@link TransformPolicy} is a JavaFX-specific
* {@link AbstractPolicy} that handles the transformation of its
* {@link #getHost() host}.
* <p>
* When working with transformations, the order in which the individual
* transformations are concatenated is important. The transformation that is
* concatenated last will be applied first. For example, the rotation around a
* pivot point consists of 3 steps:
* <ol>
* <li>Translate the coordinate system, so that the pivot point is in the origin
* <code>(-px, -py)</code>.
* <li>Rotate the coordinate system.
* <li>Translate back to the original position <code>(px, py)</code>.
* </ol>
* But the corresponding transformations have to be concatenated in reverse
* order, i.e. translate back first, rotate then, translate pivot to origin
* last. This is easy to confuse, that's why this policy manages a list of
* pre-transforms and a list of post-transforms. These transformations (as well
* as the initial node transformation) are concatenated as follows to yield the
* new node transformation for the host:
*
* <pre>
* --> --> --> direction of concatenation --> --> -->
*
* postTransforms initialNodeTransform preTransforms
* |------------| |-----------|
* postIndex: n, n-1, ... 0 preIndex: 0, 1, ... m
*
* <-- <-- <-- <-- direction of effect <-- <-- <-- <--
* </pre>
* <p>
* As you can see, the last pre-transform is concatenated last, and therefore,
* will affect the host first. Generally, a post-transform manipulates the
* transformed node, while a pre-transform manipulates the coordinate system
* before the node is transformed.
* <p>
* You can use the {@link #createPreTransform()} and
* {@link #createPostTransform()} methods to create a pre- or a post-transform
* and append it to the respective list. Therefore, the most recently created
* pre-transform will be applied first, and the most recently created
* post-transform will be applied last. When creating a pre- or post-transform,
* the index of that transform within the respective list will be returned. This
* index can later be used to manipulate the transform.
* <p>
* The {@link #setPostRotate(int, Angle)},
* {@link #setPostScale(int, double, double)},
* {@link #setPostTransform(int, AffineTransform)},
* {@link #setPostTranslate(int, double, double)},
* {@link #setPreRotate(int, Angle)}, {@link #setPreScale(int, double, double)},
* {@link #setPreTransform(int, AffineTransform)}, and
* {@link #setPreTranslate(int, double, double)} methods can be used to change a
* previously created pre- or post-transform.
*
* @author mwienand
*
*/
public class TransformPolicy extends AbstractPolicy {
/**
* The initial node transformation of the manipulated part.
*/
private AffineTransform initialTransform;
/**
* The {@link List} of transformations that are applied before the old
* transformation.
*/
private List<AffineTransform> preTransforms = new ArrayList<>();
/**
* The {@link List} of transformations that are applied after the old
* transformation.
*/
private List<AffineTransform> postTransforms = new ArrayList<>();
/**
* Applies the given {@link AffineTransform} as the new transformation
* matrix to the {@link #getHost() host}. All transformation changes are
* applied via this method. Therefore, subclasses can override this method
* to perform adjustments that are necessary for its {@link #getHost() host}
* .
*
* @param finalTransform
* The new transformation matrix for the {@link #getHost() host}.
*/
protected void applyTransform(AffineTransform finalTransform) {
updateTransformOperation(finalTransform);
// locally execute operation
locallyExecuteOperation();
}
@Override
public ITransactionalOperation commit() {
ITransactionalOperation commitOperation = super.commit();
if (commitOperation != null && !commitOperation.isNoOp()
&& isContentTransformable()) {
// chain content changes
ForwardUndoCompositeOperation composite = new ForwardUndoCompositeOperation(
"Transform Content");
composite.add(commitOperation);
// compute delta between new and initial transform and apply it
composite.add(createTransformContentOperation());
commitOperation = composite;
}
preTransforms.clear();
postTransforms.clear();
initialTransform = null;
return commitOperation;
}
@Override
protected ITransactionalOperation createOperation() {
return new TransformVisualOperation(getHost());
}
/**
* Creates a new {@link AffineTransform} and appends it to the
* postTransforms list. Therefore, the new {@link AffineTransform} will
* affect the host after all other transforms, as shown below:
*
* <pre>
* --> --> --> direction of concatenation --> --> -->
*
* postTransforms initialTransform preTransforms
* |------------| |-----------|
* postIndex: n, n-1, ... 0 preIndex: 0, 1, ... m
*
* <-- <-- <-- <-- direction of effect <-- <-- <-- <--
* </pre>
*
* A post-transform manipulates the transformed node, while a pre-transform
* manipulates the coordinate system before the node is transformed.
*
* @return A new {@link AffineTransform} that is appended to the
* postTransforms list.
*/
public int createPostTransform() {
checkInitialized();
postTransforms.add(new AffineTransform());
return postTransforms.size() - 1;
}
/**
* Creates a new {@link AffineTransform} and appends it to the preTransforms
* list. Therefore, the new {@link AffineTransform} will affect the host
* before all other transforms, as shown below:
*
* <pre>
* --> --> --> direction of concatenation --> --> -->
*
* postTransforms initialTransform preTransforms
* |------------| |-----------|
* postIndex: n, n-1, ... 0 preIndex: 0, 1, ... m
*
* <-- <-- <-- <-- direction of effect <-- <-- <-- <--
* </pre>
*
* A post-transform manipulates the transformed node, while a pre-transform
* manipulates the coordinate system before the node is transformed.
*
* @return A new {@link AffineTransform} that is appended to the
* preTransforms list.
*/
public int createPreTransform() {
checkInitialized();
preTransforms.add(new AffineTransform());
return preTransforms.size() - 1;
}
/**
* Returns an operation to transform the content.
*
* @return The ITransactionalOperation to transform the content.
*/
protected ITransactionalOperation createTransformContentOperation() {
return new TransformContentOperation(getHost(), getCurrentTransform());
}
/**
* Returns the {@link AffineTransform} that matches the node transformation
* of the {@link #getHost() host}.
*
* @return The host's {@link AffineTransform}.
*/
public AffineTransform getCurrentTransform() {
return FX2Geometry.toAffineTransform(getHost().getVisualTransform());
}
@Override
public ITransformableContentPart<? extends Node> getHost() {
return (ITransformableContentPart<? extends Node>) super.getHost();
}
/**
* Returns a copy of the initial node transformation of the host (obtained
* via {@link #getCurrentTransform()}).
*
* @return A copy of the initial node transformation of the host (obtained
* via {@link #getCurrentTransform()}).
*/
public AffineTransform getInitialTransform() {
return initialTransform;
}
@Override
public void init() {
preTransforms.clear();
postTransforms.clear();
initialTransform = getCurrentTransform();
super.init();
}
/**
* Returns whether the content can be transformed.
*
* @return <code>true</code> if the content can be transformed,
* <code>false</code> otherwise.
*/
protected boolean isContentTransformable() {
return getHost() instanceof ITransformableContentPart;
}
/**
* Sets the specified post-transform to a rotation by the given angle.
*
* @param index
* The index of the post-transform to manipulate.
* @param rotation
* The counter clock-wise rotation {@link Angle}.
*/
public void setPostRotate(int index, Angle rotation) {
checkInitialized();
postTransforms.get(index).setToRotation(rotation.rad());
updateTransform();
}
/**
* Sets the specified post-transform to a scaling by the given factors.
*
* @param index
* The index of the post-transform to manipulate.
* @param sx
* The horizontal scale factor.
* @param sy
* The vertical scale factor.
*/
public void setPostScale(int index, double sx, double sy) {
checkInitialized();
postTransforms.get(index).setToScale(sx, sy);
updateTransform();
}
/**
* Sets the specified post-transform to the given {@link AffineTransform}.
*
* @param postTransformIndex
* The index of the post-transform to manipulate.
* @param transform
* The {@link AffineTransform} that replaces the specified
* post-transform.
*/
public void setPostTransform(int postTransformIndex,
AffineTransform transform) {
checkInitialized();
postTransforms.get(postTransformIndex).setTransform(transform);
updateTransform();
}
/**
* Sets the specified post-transform to a translation by the given offsets.
*
* @param index
* The index of the post-transform to manipulate.
* @param tx
* The horizontal translation offset (in local coordinates).
* @param ty
* The vertical translation offset (in local coordinates).
*/
public void setPostTranslate(int index, double tx, double ty) {
checkInitialized();
// TODO: snap to grid
postTransforms.get(index).setToTranslation(tx, ty);
updateTransform();
}
/**
* Sets the specified pre-transform to a rotation by the given angle.
*
* @param index
* The index of the pre-transform to manipulate.
* @param rotation
* The counter clock-wise rotation {@link Angle}.
*/
public void setPreRotate(int index, Angle rotation) {
checkInitialized();
preTransforms.get(index).setToRotation(rotation.rad());
updateTransform();
}
/**
* Sets the specified pre-transform to a scaling by the given factors.
*
* @param index
* The index of the pre-transform to manipulate.
* @param sx
* The horizontal scale factor.
* @param sy
* The vertical scale factor.
*/
public void setPreScale(int index, double sx, double sy) {
checkInitialized();
preTransforms.get(index).setToScale(sx, sy);
updateTransform();
}
/**
* Sets the specified pre-transform to the given {@link AffineTransform}.
*
* @param preTransformIndex
* The index of the pre-transform to manipulate.
* @param transform
* The {@link AffineTransform} that replaces the specified
* pre-transform.
*/
public void setPreTransform(int preTransformIndex,
AffineTransform transform) {
checkInitialized();
preTransforms.get(preTransformIndex).setTransform(transform);
updateTransform();
}
/**
* Sets the specified pre-transform to a translation by the given offsets.
*
* @param index
* The index of the pre-transform to manipulate.
* @param tx
* The horizontal translation offset (in parent coordinates).
* @param ty
* The vertical translation offset (in parent coordinates).
*/
public void setPreTranslate(int index, double tx, double ty) {
checkInitialized();
// TODO: snap to grid
preTransforms.get(index).setToTranslation(tx, ty);
updateTransform();
}
/**
* Changes the {@link #getHost() host's} transformation to the given
* {@link AffineTransform}. Clears the pre- and post-transforms lists.
*
* @param finalTransform
* The new {@link AffineTransform} for the {@link #getHost()
* host}.
*/
public void setTransform(AffineTransform finalTransform) {
checkInitialized();
// clear pre- and post-transforms lists
preTransforms.clear();
postTransforms.clear();
// apply new transform to host (and update the operation)
applyTransform(finalTransform);
}
/**
* Composes the pre- and post-transforms lists and the initial node
* transform to one composite transformation. This composite transformation
* is then applied to the host using
* {@link #applyTransform(AffineTransform)}.
*
* <pre>
* --> --> --> direction of concatenation --> --> -->
*
* postTransforms initialTransform preTransforms
* |------------| |-----------|
* postIndex: n, n-1, ... 0 preIndex: 0, 1, ... m
*
* <-- <-- <-- <-- direction of effect <-- <-- <-- <--
* </pre>
*/
protected void updateTransform() {
// compose transformations to one composite transformation
AffineTransform composite = new AffineTransform();
// concatenate pre transforms (in reverse order as the last pre
// transform should be applied first)
for (int i = postTransforms.size() - 1; i >= 0; i--) {
composite.concatenate(postTransforms.get(i));
}
// concatenate old transform
composite.concatenate(initialTransform);
// concatenate post transforms
for (AffineTransform pre : preTransforms) {
composite.concatenate(pre);
}
// apply composite transform to host
applyTransform(composite);
}
/**
* Updates the operation that was created within {@link #createOperation()}
* so that it will set the {@link #getHost() host's} transformation to match
* the given {@link AffineTransform} upon execution.
*
* @param newTransform
* The new transformation for the host.
*/
protected void updateTransformOperation(AffineTransform newTransform) {
((TransformVisualOperation) getOperation())
.setFinalTransform(Geometry2FX.toFXAffine(newTransform));
}
}