/******************************************************************************* * 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.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.eclipse.gef.common.beans.property.ReadOnlySetMultimapProperty; import org.eclipse.gef.common.beans.property.ReadOnlySetMultimapWrapper; import org.eclipse.gef.common.beans.property.ReadOnlySetWrapperEx; import org.eclipse.gef.common.collections.CollectionUtils; import org.eclipse.gef.common.collections.ObservableSetMultimap; import org.eclipse.gef.common.collections.SetMultimapChangeListener; import org.eclipse.gef.fx.anchors.IComputationStrategy.Parameter; import org.eclipse.gef.fx.anchors.IComputationStrategy.Parameter.Kind; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.geometry.planar.IGeometry; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.planar.Rectangle; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.ReadOnlySetProperty; import javafx.beans.property.ReadOnlySetWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import javafx.collections.SetChangeListener; import javafx.geometry.Orientation; import javafx.scene.Node; /** * The {@link DynamicAnchor} computes anchor positions through a * {@link IComputationStrategy}. The strategy performs the position calculation * based on {@link Parameter}s, which are controlled by the * {@link DynamicAnchor}. * * @author anyssen * @author mwienand * */ public class DynamicAnchor extends AbstractAnchor { /** * An {@link IComputationStrategy.Parameter} that encapsulates an * (anchorage) reference geometry. */ public static class AnchorageReferenceGeometry extends Parameter<IGeometry> { /** * Creates a new {@link AnchorageReferenceGeometry} with no default * value. */ public AnchorageReferenceGeometry() { this(new Rectangle()); } /** * Creates a {@link AnchorageReferenceGeometry} that encapsulates the * given {@link IGeometry}. * * @param defaultValue * The {@link IGeometry} to use by default. */ public AnchorageReferenceGeometry(IGeometry defaultValue) { super(Kind.ANCHORAGE); set(defaultValue); } } /** * An {@link IComputationStrategy.Parameter} that encapsulates an * (anchorage) reference point. */ public static class AnchorageReferencePosition extends Parameter<Point> { /** * Creates a new {@link AnchorageReferencePosition} without default * value. */ public AnchorageReferencePosition() { this(null); } /** * Creates a {@link AnchorageReferencePosition} that encapsulates the * given {@link Point}. * * @param defaultValue * The {@link Point} to encapsulate. */ public AnchorageReferencePosition(Point defaultValue) { super(Kind.ANCHORAGE); set(defaultValue); } } /** * An {@link IComputationStrategy.Parameter} that encapsulates a projection * reference point. */ public static class AnchoredReferencePoint extends Parameter<Point> { /** * Creates a new {@link AnchoredReferencePoint} with no default value. */ public AnchoredReferencePoint() { this(new Point()); } /** * Creates a {@link AnchoredReferencePoint} that encapsulates the given * {@link Point}. * * @param defaultValue * The {@link Point} to encapsulate. */ public AnchoredReferencePoint(Point defaultValue) { super(Kind.ANCHORED); set(defaultValue); } } /** * An {@link IComputationStrategy.Parameter} that encapsulates the preferred * orientation to be used for orthogonal projections. */ public static class PreferredOrientation extends Parameter<Orientation> { /** * Creates a new {@link PreferredOrientation} without default value. */ public PreferredOrientation() { this(Orientation.VERTICAL); } /** * Creates a {@link PreferredOrientation} that encapsulates the given * {@link Orientation}. * * @param orientation * The {@link Orientation} to encapsulate. */ public PreferredOrientation(Orientation orientation) { super(Kind.ANCHORED, true); // optional set(orientation); } } private SetMultimapChangeListener<AnchorKey, IComputationStrategy.Parameter<?>> anchoredComputationParametersChangeListener = new SetMultimapChangeListener<AnchorKey, IComputationStrategy.Parameter<?>>() { // keep track of the change listeners registered at the individual // parameters private Map<AnchorKey, ChangeListener<Object>> valueChangeListeners = new HashMap<>(); @Override public void onChanged( final SetMultimapChangeListener.Change<? extends AnchorKey, ? extends Parameter<?>> change) { while (change.next()) { if (change.wasAdded()) { // prevent null from being put into the map if (change.getKey() == null) { throw new IllegalStateException( "Attempt to put <null> key into reference point map!"); } if (change.getValuesAdded().contains(null)) { throw new IllegalStateException( "Attempt to put <null> value for key " + change.getKey() + " into reference point map!"); } for (Parameter<?> p : change.getValuesAdded()) { // add change listener to each added parameter, so we // can recompute the position upon changes final AnchorKey key = change.getKey(); ChangeListener<Object> l = valueChangeListeners .get(key); if (l == null) { l = new ChangeListener<Object>() { @Override public void changed( ObservableValue<? extends Object> observable, Object oldValue, Object newValue) { // if (inUpdatePosition) { // deferredUpdates.add(key); // } else { updatePosition(key); // } } }; valueChangeListeners.put(key, l); } p.addListener(l); } } else if (change.wasRemoved()) { // unregister change listener from removed parameter for (Parameter<?> p : change.getValuesRemoved()) { p.removeListener( valueChangeListeners.get(change.getKey())); } } // update position for this key updatePosition(change.getKey()); } } }; private IComputationStrategy computationStrategy; private ObservableSet<IComputationStrategy.Parameter<?>> anchorageComputationParameters = FXCollections .observableSet(new HashSet<IComputationStrategy.Parameter<?>>()); private ReadOnlySetWrapper<IComputationStrategy.Parameter<?>> anchorageComputationParametersProperty = new ReadOnlySetWrapperEx<>( anchorageComputationParameters); private ObservableSetMultimap<AnchorKey, IComputationStrategy.Parameter<?>> anchoredComputationParameters = CollectionUtils .observableHashMultimap(); private ReadOnlySetMultimapWrapper<AnchorKey, IComputationStrategy.Parameter<?>> anchoredComputationParametersProperty = new ReadOnlySetMultimapWrapper<>( anchoredComputationParameters); private SetChangeListener<IComputationStrategy.Parameter<?>> anchorageComputationParametersChangeListener = new SetChangeListener<IComputationStrategy.Parameter<?>>() { private ChangeListener<Object> valueChangeListener = new ChangeListener<Object>() { @Override public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) { // recompute positions for all anchor keys updatePositions(); } }; @Override public void onChanged( final SetChangeListener.Change<? extends Parameter<?>> change) { if (change.wasRemoved()) { change.getElementRemoved().removeListener(valueChangeListener); } if (change.wasAdded()) { change.getElementAdded().addListener(valueChangeListener); } // if the list of anchorage parameters was changed, recompute // positions updatePositions(); } }; /** * Constructs a new {@link DynamicAnchor} for the given anchorage visual * that uses a {@link ChopBoxStrategy} as computation strategy. The anchor * will also add a default binding for the * {@link AnchorageReferenceGeometry} computation parameter, which is * required by the {@link ChopBoxStrategy}, that infers the geometry from * the anchorage's shape outline. * * @param anchorage * The anchorage visual. */ public DynamicAnchor(final Node anchorage) { this(anchorage, new ChopBoxStrategy()); } /** * Constructs a new {@link DynamicAnchor} for the given anchorage visual * using the given {@link IComputationStrategy}. The anchor will also add a * default binding for the {@link AnchorageReferenceGeometry} computation * parameter, inferring the geometry from the anchorage's shape outline, in * case this parameter is required by the given * {@link IComputationStrategy}. * * @param anchorage * The anchorage visual. * @param computationStrategy * The {@link IComputationStrategy} to use. */ public DynamicAnchor(final Node anchorage, IComputationStrategy computationStrategy) { super(anchorage); anchorageComputationParameters .addListener(anchorageComputationParametersChangeListener); anchoredComputationParameters .addListener(anchoredComputationParametersChangeListener); // XXX: Set computation strategy after adding parameter change // listeners, because setting the computation strategy does initialize // some parameters, for which otherwise no change listeners would be // registered. setComputationStrategy(computationStrategy); // add default binding for the anchorage reference geometry (if required // by the given computation strategy) if (computationStrategy.getRequiredParameters() .contains(AnchorageReferenceGeometry.class)) { getComputationParameter(AnchorageReferenceGeometry.class) .bind(new ObjectBinding<IGeometry>() { { bind(anchorage.layoutBoundsProperty()); } @Override protected IGeometry computeValue() { return NodeUtils.getShapeOutline(anchorage); } }); } } /** * Returns a {@link ReadOnlySetProperty} that provides the * {@link IComputationStrategy.Parameter computation parameters} of kind * {@link Kind#ANCHORAGE}. * * @return A {@link ReadOnlySetProperty} providing the {@link Parameter}s. */ protected ReadOnlySetProperty<IComputationStrategy.Parameter<?>> anchorageComputationParametersProperty() { return anchorageComputationParametersProperty.getReadOnlyProperty(); } /** * Returns a {@link ReadOnlySetMultimapProperty} that provides the * {@link IComputationStrategy.Parameter computation parameters} of kind * {@link Kind#ANCHORED} per {@link AnchorKey}. The set of computation * parameters for each {@link AnchorKey} is initialed by the responsible * computation strategy. * * @return A {@link ReadOnlySetMultimapProperty} that provides an * {@link Object} per {@link AnchorKey}. */ protected ReadOnlySetMultimapProperty<AnchorKey, IComputationStrategy.Parameter<?>> anchoredComputationParametersProperty() { return anchoredComputationParametersProperty.getReadOnlyProperty(); } @Override public void attach(AnchorKey key) { initAnchoredParameters(key); super.attach(key); } private void clearAnchoredParameters(AnchorKey key) { anchoredComputationParameters.removeAll(key); } /** * 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}. */ @Override protected Point computePosition(AnchorKey key) { // check for availability of (anchorage) parameters Set<IComputationStrategy.Parameter<?>> parameters = getParameters(key); for (Class<? extends Parameter<?>> parameterType : computationStrategy .getRequiredParameters()) { Parameter<?> p = Parameter.get(parameters, parameterType); // check that parameter values are provided if (p == null || (p.get() == null && !p.isOptional())) { // as long as all required parameters are not provided, we // cannot compute a position. // System.out.println("Skipping computation of position for key // " // + key + " because mandatory parameter " + p // + " has no value."); return null; } } // only invoke strategy if all required parameters are provided Point positionInScene = computationStrategy.computePositionInScene( getAnchorage(), key.getAnchored(), parameters); Point position = FX2Geometry.toPoint(key.getAnchored() .sceneToLocal(Geometry2FX.toFXPoint(positionInScene))); return position; } @Override public void detach(AnchorKey key) { super.detach(key); clearAnchoredParameters(key); } /** * Retrieves a computation parameter of the respective type for the given * {@link AnchorKey}. * * @param <T> * The value type of the computation parameter. * @param key * The {@link AnchorKey} for which to retrieve the anchored * parameter. * @param parameterType * The type of computation parameter. * @return The anchored computation parameter. */ public <T extends Parameter<?>> T getComputationParameter(AnchorKey key, Class<T> parameterType) { // check anchorage parameters T parameter = Parameter.get(anchorageComputationParametersProperty(), parameterType); if (parameter != null) { return parameter; } // check anchored parameters parameter = Parameter.get( anchoredComputationParametersProperty().get(key), parameterType); if (parameter != null) { return parameter; } // create a new parameter instance try { parameter = parameterType.getDeclaredConstructor().newInstance(); if (Kind.ANCHORED.equals(parameter.getKind())) { anchoredComputationParametersProperty().put(key, parameter); } else { anchorageComputationParametersProperty().add(parameter); } } catch (Exception e) { e.printStackTrace(); } return parameter; } /** * Retrieves a computation parameter of the respective type. * * @param <T> * The value type of the computation parameter. * @param parameterType * The type of computation parameter. * @return The anchored computation parameter. */ public <T extends Parameter<?>> T getComputationParameter( Class<T> parameterType) { // check anchorage parameters T parameter = Parameter.get(anchorageComputationParametersProperty(), parameterType); if (parameter != null) { return parameter; } // create a new parameter instance try { parameter = parameterType.getDeclaredConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } if (Kind.ANCHORAGE.equals(parameter.getKind())) { anchorageComputationParametersProperty().add(parameter); } else { throw new IllegalArgumentException("Specified parameter type " + parameterType.getSimpleName() + " is anchored, it has to be queried per AdapterKey."); } return parameter; } /** * Returns the {@link IComputationStrategy} used by this * {@link DynamicAnchor}. * * @return The {@link IComputationStrategy}. */ public IComputationStrategy getComputationStrategy() { return computationStrategy; } /** * Retrieves the relevant parameters for the computation of the given * {@link AnchorKey}. * * @param key * The {@link AnchorKey} of relevance. * @return The parameters required by the computation strategy to compute * the position for the given {@link AnchorKey}. * */ protected Set<Parameter<?>> getParameters(AnchorKey key) { Set<Parameter<?>> parameters = new HashSet<>(); parameters.addAll(anchorageComputationParameters); parameters.addAll(anchoredComputationParameters.get(key)); return parameters; } private void initAnchorageParameters() { for (Class<? extends Parameter<?>> paramType : computationStrategy .getRequiredParameters()) { if (Kind.ANCHORAGE.equals(Parameter.getKind(paramType))) { if (Parameter.get(anchorageComputationParameters, paramType) == null) { // parameter is not already contained try { Parameter<?> p = paramType.getDeclaredConstructor() .newInstance(); anchorageComputationParameters.add(p); } catch (Exception e) { throw new IllegalStateException( "Could not create instance of parameter type " + paramType, e); } } } } } private void initAnchoredParameters(AnchorKey key) { Set<Parameter<?>> parameters = getParameters(key); for (Class<? extends Parameter<?>> paramType : computationStrategy .getRequiredParameters()) { if (Kind.ANCHORED.equals(Parameter.getKind(paramType))) { if (Parameter.get(parameters, paramType) == null) { // parameter is not already contained Parameter<?> p; try { p = paramType.getDeclaredConstructor().newInstance(); if (Kind.ANCHORED.equals(p.getKind())) { anchoredComputationParameters.put(key, p); } } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { throw new IllegalStateException( "Could not create instance of parameter type " + paramType, e); } } } } } /** * Sets the given {@link IComputationStrategy} to be used by this * {@link IAnchor}. * * @param computationStrategy * The {@link IComputationStrategy} that will be used to compute * positions for all attached {@link AnchorKey}s. */ public void setComputationStrategy( IComputationStrategy computationStrategy) { for (AnchorKey key : getKeys()) { clearAnchoredParameters(key); } this.computationStrategy = computationStrategy; initAnchorageParameters(); for (AnchorKey key : getKeys()) { initAnchoredParameters(key); } } }