/******************************************************************************* * Copyright (c) 2014, 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: * Alexander Nyßen (itemis AG) - initial API and implementation * Matthias Wienand (itemis AG) - initial API and implementation * *******************************************************************************/ package org.eclipse.gef.fx.anchors; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.eclipse.gef.common.adapt.IAdaptable; import org.eclipse.gef.common.beans.property.ReadOnlyMapWrapperEx; import org.eclipse.gef.fx.listeners.VisualChangeListener; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.geometry.planar.Point; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import javafx.beans.property.ReadOnlyMapProperty; import javafx.beans.property.ReadOnlyMapWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.transform.Transform; /** * {@link AbstractAnchor} is the abstract base implementation for * {@link IAnchor}s. It provides the facility to bind an anchor to an anchorage * {@link Node} ({@link #anchorageProperty()}), to attach and detach * {@link Node}s via {@link AnchorKey}s, and to provide positions ( * {@link #positionsUnmodifiableProperty()}) for the attached {@link AnchorKey} * s. * <p> * It also registers the necessary listeners at the anchorage {@link Node} and * the attached {@link Node}s as well as relevant ancestor {@link Node}s, to * trigger the (re-)computation of positions. * <p> * The actual computation of positions for attached nodes is delegated to * {@link #computePosition(AnchorKey)}, thus left to subclasses. If a subclass * needs additional information to compute positions for attached * {@link AnchorKey}s, it may request that an {@link IAdaptable} info gets * passed into {@link #attach(AnchorKey)} and {@link #detach(AnchorKey)}, and * may overwrite both methods to get access to it. * * @author anyssen * @author mwienand * */ public abstract class AbstractAnchor implements IAnchor { private ReadOnlyObjectWrapper<Node> anchorageProperty = new ReadOnlyObjectWrapper<>(); private SetMultimap<Node, AnchorKey> keysByNode = HashMultimap.create(); private ObservableMap<AnchorKey, Point> positions = FXCollections .observableHashMap(); private ObservableMap<AnchorKey, Point> positionsUnmodifiable; private ReadOnlyMapWrapper<AnchorKey, Point> positionsUnmodifiableProperty; // TODO: push this down to dynamic anchor (as its only needed there) private Map<Node, VisualChangeListener> vcls = new HashMap<>(); private ChangeListener<Scene> anchoredSceneChangeListener = new ChangeListener<Scene>() { @Override public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) { // determine which anchored changed for (Node anchored : keysByNode.keySet()) { if (anchored.sceneProperty() == observable) { if (oldValue == newValue) { return; } if (oldValue != null) { // System.out.println( // "Try to unregister VCL because anchored " // + anchored + " lost scene reference."); unregisterVCL(anchored); } if (newValue != null) { // System.out // .println("Try to register VCL because anchored " // + anchored // + " obtained scene reference."); registerVCL(anchored); } break; } } } }; private ChangeListener<Node> anchorageChangeListener = new ChangeListener<Node>() { private ChangeListener<Scene> anchorageSceneChangeListener = new ChangeListener<Scene>() { @Override public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) { if (oldValue != null) { // System.out.println("Try to unregister VCLs because // anchorage // " // + getAnchorage() + " lost scene reference."); unregisterVCLs(); } if (newValue != null) { // System.out.println("Try to register VCLs because // anchorage " // + getAnchorage() + " obtained scene reference."); registerVCLs(); } } }; @Override public void changed(ObservableValue<? extends Node> observable, Node oldAnchorage, Node newAnchorage) { if (oldAnchorage != null) { // System.out // .println("Try to unregister VCLS because old anchorage " // + oldAnchorage + " was removed."); unregisterVCLs(); oldAnchorage.sceneProperty() .removeListener(anchorageSceneChangeListener); } if (newAnchorage != null) { // register listener on scene property, so we can react to // changes of the scene property of the anchorage node newAnchorage.sceneProperty() .addListener(anchorageSceneChangeListener); // System.out.println("Try to register VCLS because new // anchorage " // + newAnchorage + " was set."); registerVCLs(); } } }; /** * Creates a new {@link AbstractAnchor} for the given <i>anchorage</i> * {@link Node}. * * @param anchorage * The anchorage {@link Node} for this {@link AbstractAnchor}. */ public AbstractAnchor(Node anchorage) { anchorageProperty.addListener(anchorageChangeListener); // XXX Set anchorage after registering the anchorageChangeListener, so // that its addition is properly tracked (and change listeners are // attached) setAnchorage(anchorage); } @Override public ReadOnlyObjectProperty<Node> anchorageProperty() { return anchorageProperty.getReadOnlyProperty(); } @Override public void attach(AnchorKey key) { Node anchored = key.getAnchored(); if (!keysByNode.containsKey(anchored)) { anchored.sceneProperty().addListener(anchoredSceneChangeListener); } keysByNode.put(anchored, key); if (!vcls.containsKey(anchored)) { VisualChangeListener vcl = createVCL(anchored); vcls.put(anchored, vcl); // System.out.println( // "Try to register VCL, because anchored " + key.getAnchored() // + " was attached to anchorage " + getAnchorage()); registerVCL(anchored); } updatePosition(key); } private boolean canRegister(Node anchored) { // we can register if there is a common ancestor if (getAnchorage() == null || anchored == null) { return false; } return NodeUtils.getNearestCommonAncestor(getAnchorage(), anchored) != null; } /** * Recomputes the position for the given attached {@link AnchorKey} by * delegating to the respective {@link IComputationStrategy}. * * @param key * The {@link AnchorKey} for which to compute an anchor position. * @return The point for the given {@link AnchorKey} in local coordinates of * the anchored {@link Node}. */ protected abstract Point computePosition(AnchorKey key); private VisualChangeListener createVCL(final Node anchored) { return new VisualChangeListener() { @Override protected void boundsInLocalChanged(Bounds oldBounds, Bounds newBounds) { updatePositions(anchored); } @Override protected void localToParentTransformChanged(Node observed, Transform oldTransform, Transform newTransform) { updatePositions(anchored); } @Override public void register(Node observed, Node observer) { super.register(observed, observer); /* * The visual change listener is registered when the anchorage * is attached to a scene. Therefore, the anchorages * bounds/transformation could have "changed" until * registration, so we have to recompute anchored's positions * now. */ updatePositions(anchored); } }; } @Override public void detach(AnchorKey key) { Node anchored = key.getAnchored(); if (!isAttached(key)) { throw new IllegalArgumentException( "The given AnchorKey was not previously attached to this IAnchor."); } // remove from positions map so that a change event is fired when it is // attached again positions.remove(key); // remove from keysByNode to indicate it is detached keysByNode.remove(anchored, key); // clean-up for this anchored if necessary if (keysByNode.get(anchored).isEmpty()) { anchored.sceneProperty() .removeListener(anchoredSceneChangeListener); keysByNode.removeAll(anchored); // System.out.println("Trying to unregister VCL as anchored " // + anchored + " has been detached from anchorage " // + getAnchorage()); unregisterVCL(anchored); vcls.remove(anchored); } } @Override public Node getAnchorage() { return anchorageProperty.get(); } /** * Returns all keys maintained by this anchor. * * @return A set containing all {@link AnchorKey}s. */ protected Set<AnchorKey> getKeys() { Set<AnchorKey> allKeys = new HashSet<>(); for (Node n : keysByNode.keySet()) { allKeys.addAll(keysByNode.get(n)); } return allKeys; } /** * Returns the {@link Map} which stores the registered {@link AnchorKey}s * per {@link Node} by reference. * * @return The {@link Map} which stores the registered {@link AnchorKey}s * per {@link Node} by reference. */ protected SetMultimap<Node, AnchorKey> getKeysByNode() { return keysByNode; } @Override public Point getPosition(AnchorKey key) { if (!isAttached(key)) { throw new IllegalArgumentException( "The AnchorKey is not attached to this anchor."); } return positions.get(key); } @Override public ObservableMap<AnchorKey, Point> getPositionsUnmodifiable() { if (positionsUnmodifiable == null) { positionsUnmodifiable = FXCollections .unmodifiableObservableMap(positions); } return positionsUnmodifiable; } @Override public boolean isAttached(AnchorKey key) { return keysByNode.containsKey(key.getAnchored()) && keysByNode.get(key.getAnchored()).contains(key); } @Override public ReadOnlyMapProperty<AnchorKey, Point> positionsUnmodifiableProperty() { if (positionsUnmodifiableProperty == null) { positionsUnmodifiableProperty = new ReadOnlyMapWrapperEx<>( getPositionsUnmodifiable()); } return positionsUnmodifiableProperty.getReadOnlyProperty(); } /** * Registers a {@link VisualChangeListener} for the given anchored * {@link Node}. * * @param anchored * The anchored {@link Node} to register a * {@link VisualChangeListener} at. */ protected void registerVCL(Node anchored) { if (canRegister(anchored)) { // System.out.println("Register VCL between anchorage " // + getAnchorage() + " and anchored " + anchored); VisualChangeListener vcl = vcls.get(anchored); if (!vcl.isRegistered()) { vcl.register(getAnchorage(), anchored); updatePositions(anchored); } // else { // System.out.println("VCL is already registered, thus skipping."); // } } } /** * Registers {@link VisualChangeListener}s for all anchored {@link Node}s, * or schedules their registration if the VCL cannot be registered yet. */ protected void registerVCLs() { for (Node anchored : vcls.keySet().toArray(new Node[] {})) { registerVCL(anchored); } } /** * Sets the anchorage of this {@link AbstractAnchor} to the given value. * * @param anchorage * The new anchorage for this {@link AbstractAnchor}. */ protected void setAnchorage(Node anchorage) { anchorageProperty.set(anchorage); } /** * Unregisters the {@link VisualChangeListener}s for the given anchored * {@link Node}. * * @param anchored * The anchored Node to unregister a {@link VisualChangeListener} * from. */ protected void unregisterVCL(Node anchored) { // System.out.println("Unregister VCL between anchorage " + // getAnchorage() // + " and anchored " + anchored); VisualChangeListener vcl = vcls.get(anchored); if (vcl.isRegistered()) { vcl.unregister(); } // else { // System.out.println("VCL is not registered, thus skipping."); // } } /** * Unregisters the {@link VisualChangeListener}s for all anchored * {@link Node}s. */ protected void unregisterVCLs() { for (Node anchored : vcls.keySet().toArray(new Node[] {})) { unregisterVCL(anchored); } } /** * Updates the position for the given {@link AnchorKey}, i.e. * <ol> * <li>Queries its current position.</li> * <li>Computes its new position.</li> * <li>Checks if the position changed, and fires an appropriate event by * putting the new position into the * {@link #positionsUnmodifiableProperty()}</li> * </ol> * * @param key * The {@link AnchorKey} for which the position is updated. */ protected void updatePosition(AnchorKey key) { // only update position if key is attached if (!isAttached(key)) { return; } // compute new position to see if it has changed Point oldPosition = getPosition(key); Point newPosition = computePosition(key); // System.out.print("[" + key.getId() + "] old = " + oldPosition // + ", new = " + newPosition); if (oldPosition == null || !oldPosition.equals(newPosition)) { // TODO: we could enforce that computePosition may never return // null or an invalid position if (newPosition != null && !Double.isNaN(newPosition.x) && !Double.isInfinite(newPosition.x) && !Double.isNaN(newPosition.y) && !Double.isInfinite(newPosition.y)) { // System.out.println(" !!!"); positions.put(key, newPosition); // return; } } // System.out.println(); } /** * Updates the positions for all attached {@link AnchorKey}s. */ protected void updatePositions() { for (AnchorKey key : getKeys()) { updatePosition(key); } } private void updatePositions(Node anchored) { SetMultimap<Node, AnchorKey> keys = getKeysByNode(); if (keys.containsKey(anchored)) { Set<AnchorKey> keysCopy = new HashSet<>(keys.get(anchored)); for (AnchorKey key : keysCopy) { updatePosition(key); } } } }