/*******************************************************************************
* Copyright (c) 2014, 2017 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:
* Alexander Nyßen (itemis AG) - initial API and implementation
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.fx.listeners;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.gef.fx.nodes.InfiniteCanvas;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.transform.Transform;
/**
* You can use a VisualChangeListener to register/unregister specific listeners
* for catching changes in the visual representation of a JavaFX {@link Node}.
* Depending on the changed property, either the
* {@link #boundsInLocalChanged(Bounds, Bounds)} or the
* {@link #localToParentTransformChanged(Node, Transform, Transform)} method is
* called. A bounds-in-local change occurs when the target node's effect, clip,
* stroke, local transformations, or geometric bounds change. A
* local-to-parent-transform change occurs when the node undergoes a
* transformation change. Transformation listeners are registered for all nodes
* in the hierarchy up to a specific parent.
*
* @author anyssen
* @author mwienand
*
*/
public abstract class VisualChangeListener {
private Node observed;
private Node parent;
private HashMap<ChangeListener<Transform>, Node> localToParentTransformListeners = new HashMap<>();
private boolean layoutBoundsChanged = false;
private boolean boundsInLocalChanged = false;
private boolean boundsInParentChanged = false;
private Bounds oldBoundsInLocal = null;
private Bounds newBoundsInLocal = null;
private ChangeListener<? super Bounds> layoutBoundsListener = new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
// only fire a visual change event if the new bounds are valid
if (isValidBounds(newValue)) {
layoutBoundsChanged = true;
onBoundsChanged();
}
}
};
private final ChangeListener<? super Bounds> boundsInLocalListener = new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
// only fire a visual change event if the new bounds are valid
if (isValidBounds(newValue)) {
oldBoundsInLocal = oldValue;
newBoundsInLocal = newValue;
boundsInLocalChanged = true;
onBoundsChanged();
}
}
};
private ChangeListener<? super Bounds> boundsInParentListener = new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
// only fire a visual change event if the new bounds are valid
if (isValidBounds(newValue)) {
boundsInParentChanged = true;
onBoundsChanged();
}
}
};
/**
* This method is called upon a bounds-in-local change.
*
* @param oldBounds
* The old {@link Bounds}.
* @param newBounds
* The new {@link Bounds}.
*/
protected abstract void boundsInLocalChanged(Bounds oldBounds,
Bounds newBounds);
private Node getNearestCommonAncestor(Node source, Node target) {
if (source == target) {
return source;
}
Set<Node> parents = new HashSet<>();
Node m = source;
Node n = target;
while (m != null || n != null) {
if (m != null) {
if (parents.contains(m)) {
return m;
}
parents.add(m);
if (n != null && parents.contains(n)) {
return n;
}
m = m.getParent();
}
if (n != null) {
if (parents.contains(n)) {
return n;
}
parents.add(n);
if (m != null && parents.contains(m)) {
return m;
}
n = n.getParent();
}
}
// could not find a common parent
return null;
}
/**
* Returns <code>true</code> if this {@link VisualChangeListener} is
* currently registered, otherwise returns <code>false</code>.
*
* @return <code>true</code> if this {@link VisualChangeListener} is
* currently registered, otherwise <code>false</code>.
*/
public boolean isRegistered() {
return parent != null;
}
/**
* Checks if the given Bounds contain NaN values. Returns <code>true</code>
* if no NaN values are found, otherwise <code>false</code>.
*
* @param b
* @return
*/
private boolean isValidBounds(Bounds b) {
if (Double.isNaN(b.getMinX()) || Double.isInfinite(b.getMinX())) {
return false;
}
if (Double.isNaN(b.getMinY()) || Double.isInfinite(b.getMinY())) {
return false;
}
if (Double.isNaN(b.getMaxX()) || Double.isInfinite(b.getMaxX())) {
return false;
}
if (Double.isNaN(b.getMaxY()) || Double.isInfinite(b.getMaxY())) {
return false;
}
return true;
}
/**
* Checks if the given Transform contains NaN values. Returns
* <code>true</code> if no NaN values are found, otherwise <code>false/
* <code>.
*
* @param t
* @return
*/
private boolean isValidTransform(Transform t) {
if (Double.isNaN(t.getMxx()) || Double.isInfinite(t.getMxx())) {
return false;
}
if (Double.isNaN(t.getMxy()) || Double.isInfinite(t.getMxy())) {
return false;
}
if (Double.isNaN(t.getMxz()) || Double.isInfinite(t.getMxz())) {
return false;
}
if (Double.isNaN(t.getMyx()) || Double.isInfinite(t.getMyx())) {
return false;
}
if (Double.isNaN(t.getMyy()) || Double.isInfinite(t.getMyy())) {
return false;
}
if (Double.isNaN(t.getMyz()) || Double.isInfinite(t.getMyz())) {
return false;
}
if (Double.isNaN(t.getMzx()) || Double.isInfinite(t.getMzx())) {
return false;
}
if (Double.isNaN(t.getMzy()) || Double.isInfinite(t.getMzy())) {
return false;
}
if (Double.isNaN(t.getMzz()) || Double.isInfinite(t.getMzz())) {
return false;
}
if (Double.isNaN(t.getTx()) || Double.isInfinite(t.getTx())) {
return false;
}
if (Double.isNaN(t.getTy()) || Double.isInfinite(t.getTy())) {
return false;
}
if (Double.isNaN(t.getTz()) || Double.isInfinite(t.getTz())) {
return false;
}
return true;
}
/**
* This method is called upon a local-to-parent-transform change.
*
* @param observed
* The {@link Node} whose local-to-parent-transform changed.
* @param oldTransform
* The old {@link Transform}.
* @param newTransform
* The new {@link Transform}.
*/
protected abstract void localToParentTransformChanged(Node observed,
Transform oldTransform, Transform newTransform);
/**
* Called upon changes to any of the following properties: "layout-bounds",
* "bounds-in-local", and "bounds-in-parent". Calls the
* {@link #boundsInLocalChanged(Bounds, Bounds)} method if all bounds
* properties are changed.
*/
protected void onBoundsChanged() {
if (layoutBoundsChanged && boundsInLocalChanged
&& boundsInParentChanged) {
layoutBoundsChanged = false;
boundsInLocalChanged = false;
boundsInParentChanged = false;
boundsInLocalChanged(oldBoundsInLocal, newBoundsInLocal);
}
}
/**
* Registers this listener on the given pair of observed and observer nodes
* to recognize visual changes of the observed node relative to the common
* parent of observer and observed node.
* <p>
* In detail, two kind of changes will be reported as visual changes:
* <ul>
* <li>changes to the bounds-in-local property of the observed node (
* {@link #boundsInLocalChanged(Bounds, Bounds)}) itself</li>
* <li>changes to the local-to-parent-transform property of any node in the
* observed node hierarchy up to (but excluding) the common parent of the
* observed and observer nodes (
* {@link #localToParentTransformChanged(Node, Transform, Transform)}).</li>
* </ul>
* <p>
* The use of a visual change lister allows to react to relative transform
* changes only. If the common parent of both nodes is for instance nested
* below an {@link InfiniteCanvas}, this allows to ignore transform changes
* that result from scrolling, as these will (in most cases) not indicate a
* visual change.
*
* @param observed
* The observed {@link Node} to be observed for visual changes,
* which includes bounds-in-local changes for the source node
* itself, as well as local-to-parent-transform changes for all
* ancestor nodes (including the source node) up to (but
* excluding) the common parent node of source and target.
* @param observer
* A {@link Node} in the same {@link Scene} as the given observed
* node, relative to which transform changes will be reported.
* That is, local-to-parent-transform changes will only be
* reported for all nodes in the hierarchy up to (but excluding)
* the common parent of observed and observer.
*/
public void register(Node observed, Node observer) {
if (observed == null) {
throw new IllegalArgumentException("Observed may not be null.");
}
if (observer == null) {
throw new IllegalArgumentException("Observer not be null.");
}
Node commonAncestor = getNearestCommonAncestor(observed, observer);
if (commonAncestor == null) {
throw new IllegalArgumentException(
"Source and target do not share a common ancestor.");
}
Node tmp = observed;
while (tmp != null && tmp != commonAncestor) {
tmp = tmp.getParent();
}
if (tmp == null) {
throw new IllegalArgumentException(
"TransformReference needs to be ancestor of the given observed node.");
}
// unregister old listeners
if (this.observed != null) {
unregister();
}
// assign new nodes
this.observed = observed;
parent = commonAncestor;
// add bounds listeners
observed.layoutBoundsProperty().addListener(layoutBoundsListener);
observed.boundsInLocalProperty().addListener(boundsInLocalListener);
observed.boundsInParentProperty().addListener(boundsInParentListener);
// add transform listeners
tmp = observed;
while (tmp != null && tmp != parent) {
final Node current = tmp;
ChangeListener<Transform> transformChangeListener = new ChangeListener<Transform>() {
@Override
public void changed(
ObservableValue<? extends Transform> observable,
Transform oldValue, Transform newValue) {
// only fire a visual change event if the new transform is
// valid
if (isValidTransform(newValue)) {
localToParentTransformChanged(current, oldValue,
newValue);
}
}
};
tmp.localToParentTransformProperty()
.addListener(transformChangeListener);
localToParentTransformListeners.put(transformChangeListener, tmp);
tmp = tmp.getParent();
}
// add transform listeners
// FIXME: Duplicate code!
tmp = observer;
while (tmp != null && tmp != parent) {
final Node current = tmp;
ChangeListener<Transform> transformChangeListener = new ChangeListener<Transform>() {
@Override
public void changed(
ObservableValue<? extends Transform> observable,
Transform oldValue, Transform newValue) {
// only fire a visual change event if the new transform is
// valid
if (isValidTransform(newValue)) {
localToParentTransformChanged(current, oldValue,
newValue);
}
}
};
tmp.localToParentTransformProperty()
.addListener(transformChangeListener);
localToParentTransformListeners.put(transformChangeListener, tmp);
tmp = tmp.getParent();
}
}
/**
* Unregisters all previously registered listeners.
*/
public void unregister() {
if (!isRegistered()) {
return;
}
// remove bounds listener
observed.layoutBoundsProperty().removeListener(layoutBoundsListener);
observed.boundsInLocalProperty().removeListener(boundsInLocalListener);
observed.boundsInParentProperty()
.removeListener(boundsInParentListener);
// remove transform listeners
for (ChangeListener<Transform> l : localToParentTransformListeners
.keySet()) {
localToParentTransformListeners.get(l)
.localToParentTransformProperty().removeListener(l);
}
// reset fields
parent = null;
observed = null;
localToParentTransformListeners.clear();
}
}