/******************************************************************************* * 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: * Matthias Wienand (itemis AG) - initial API & implementation * *******************************************************************************/ package org.eclipse.gef.layout; import java.util.ArrayList; import java.util.List; import org.eclipse.gef.graph.Edge; import org.eclipse.gef.graph.Graph; import org.eclipse.gef.graph.Node; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; /** * The {@link LayoutContext} provides the context in which to layout a * {@link Graph}. It maintains an {@link ILayoutAlgorithm} that can be applied, * supports the handling and execution of pre and post {@link Runnable}s, and * filtering of layout objects using {@link ILayoutFilter}. * * @author mwienand * */ public class LayoutContext { /** * An {@link LayoutContext} notifies registered listeners about changes to * the layout algorithm using this property name. */ public static final String LAYOUT_ALGORITHM_PROPERTY = "layoutAlgorithm"; private ObjectProperty<ILayoutAlgorithm> layoutAlgorithmProperty = new SimpleObjectProperty<>( this, LAYOUT_ALGORITHM_PROPERTY); private Graph graph; private final List<Runnable> postLayoutPass = new ArrayList<>(); private final List<Runnable> preLayoutPass = new ArrayList<>(); private final List<ILayoutFilter> layoutFilters = new ArrayList<>(); /** * Adds the given ILayoutFilter to this {@link LayoutContext}. * * @param layoutFilter * The ILayoutFilter to add to this context. */ public void addLayoutFilter(ILayoutFilter layoutFilter) { layoutFilters.add(layoutFilter); } /** * Applies the layout algorithm of this LayoutContext. The clean flag is * passed-in to the layout algorithm to indicate whether the context changed * significantly since the last layout pass. * * @param clear * <code>true</code> to indicate that the algorithm has to fully * re-compute the layout, otherwise <code>false</code>. */ public void applyLayout(boolean clear) { ILayoutAlgorithm layoutAlgorithm = layoutAlgorithmProperty.get(); if (layoutAlgorithm != null) { preLayout(); layoutAlgorithm.applyLayout(this, clear); postLayout(); } } /** * Initiated by the context or by an {@link ILayoutAlgorithm} to perform * steps that are scheduled to be run after the layout pass. Should not be * called by clients. */ public void postLayout() { for (Runnable r : new ArrayList<>(postLayoutPass)) { r.run(); } } /** * Initiated by the context or by an {@link ILayoutAlgorithm} to perform * steps that are scheduled to be run before the layout pass. Should not be * called by clients. */ public void preLayout() { for (Runnable r : preLayoutPass) { r.run(); } } /** * Returns the graph that is to be layouted. * * @return The {@link Graph} that is to be layouted. */ public Graph getGraph() { return graph; } /** * Sets the graph that is to be layouted by this context. * * @param graph * The {@link Graph} to layout. */ public void setGraph(Graph graph) { // TODO: we should not allow to pass in null here. Instead we should // guard ourselves against null. if (graph == null) { graph = new Graph(); } this.graph = graph; } /** * Returns all the nodes that should be laid out. Replacing elements in the * returned array does not affect this context. * * @return array of nodes to lay out */ // TODO: remove this (algorithms should use getGraph().getNodes()) public Node[] getNodes() { ObservableList<Node> nodes = graph.getNodes(); List<Node> layoutRelevantNodes = new ArrayList<>(); for (Node n : nodes) { if (!isLayoutIrrelevant(n)) { layoutRelevantNodes.add(n); } } return layoutRelevantNodes.toArray(new Node[] {}); } /** * Returns all the connections between nodes that should be laid out. * Replacing elements in the returned array does not affect this context. * * @return array of connections between nodes */ public Edge[] getEdges() { ObservableList<Edge> edges = graph.getEdges(); List<Edge> layoutRelevantEdges = new ArrayList<>(); for (Edge e : edges) { if (!isLayoutIrrelevant(e)) { layoutRelevantEdges.add(e); } } return layoutRelevantEdges.toArray(new Edge[] {}); } /** * Returns the static layout algorithm used to layout a newly initialized * graph or after heavy changes to it. * * @return The layout algorithm that is used by this {@link LayoutContext}. */ public ILayoutAlgorithm getLayoutAlgorithm() { return layoutAlgorithmProperty.get(); } /** * Returns <code>true</code> when the given {@link Edge} is not relevant for * layout according to the configured {@link ILayoutFilter layout filters}. * Otherwise returns <code>false</code>. * * @param edge * The {@link Edge} in question. * @return <code>true</code> when the given {@link Edge} is not relevant for * layout according to the configure layout filters, otherwise * <code>false</code>. */ public boolean isLayoutIrrelevant(Edge edge) { for (ILayoutFilter filter : layoutFilters) { if (filter.isLayoutIrrelevant(edge)) { return true; } } return false; } /** * Returns <code>true</code> when the given {@link Node} is not relevant for * layout according to the configured {@link ILayoutFilter layout filters}. * Otherwise returns <code>false</code>. * * @param nodeLayout * The {@link Node} in question. * @return <code>true</code> when the given {@link Node} is not relevant for * layout according to the configure layout filters, otherwise * <code>false</code>. */ public boolean isLayoutIrrelevant(Node nodeLayout) { for (ILayoutFilter filter : layoutFilters) { if (filter.isLayoutIrrelevant(nodeLayout)) { return true; } } return false; } /** * A property representing the layout algorithm used by this * {@link LayoutContext}. * * @see #getLayoutAlgorithm() * @see #setLayoutAlgorithm(ILayoutAlgorithm) * * @return A property named {@link #LAYOUT_ALGORITHM_PROPERTY}. */ public ObjectProperty<ILayoutAlgorithm> layoutAlgorithmProperty() { return layoutAlgorithmProperty; }; /** * Removes the given ILayoutFilter from this {@link LayoutContext}. * * @param layoutFilter * The ILayoutFilter to remove to this context. */ public void removeLayoutFilter(ILayoutFilter layoutFilter) { layoutFilters.remove(layoutFilter); } /** * Adds the given {@link Runnable} to the list of runnables which are called * when this {@link LayoutContext} is asked to apply all changes made to its * elements to the display. * * @param runnable * A {@link Runnable} called whenever this context is asked to * apply all changes made to its elements to the display. */ public void schedulePostLayoutPass(Runnable runnable) { if (runnable == null) { throw new IllegalArgumentException("Runnable may not be null."); } if (!postLayoutPass.contains(runnable)) { postLayoutPass.add(runnable); } } /** * Adds the given {@link Runnable} to the list of {@link Runnable}s which * are executed before applying a layout, i.e. before * {@link #applyLayout(boolean)}. * * @param runnable * The {@link Runnable} to add to the list of {@link Runnable}s * which are executed before applying a layout. */ public void schedulePreLayoutPass(Runnable runnable) { if (runnable == null) { throw new IllegalArgumentException("Runnable may not be null."); } if (!preLayoutPass.contains(runnable)) { preLayoutPass.add(runnable); } } /** * Sets the layout algorithm for this context. * * @param algorithm * The new {@link ILayoutAlgorithm} for this * {@link LayoutContext}. */ public void setLayoutAlgorithm(ILayoutAlgorithm algorithm) { layoutAlgorithmProperty.set(algorithm); } /** * Removes the given {@link Runnable} from the list of runnables which are * called when this {@link LayoutContext} is asked to apply all changes made * to its elements to the display. * * @param runnable * The {@link Runnable} that should no longer get called when * flushing changes. */ public void unschedulePostLayoutPass(Runnable runnable) { if (runnable == null) { throw new IllegalArgumentException("Runnable may not be null."); } if (postLayoutPass.contains(runnable)) { postLayoutPass.remove(runnable); } } /** * Removes the given {@link Runnable} from the list of {@link Runnable}s * which are executed before applying a layout, i.e. before * {@link #applyLayout(boolean)}. * * @param runnable * The {@link Runnable} to remove from the list of * {@link Runnable}s which are executed before applying a layout. */ public void unschedulePreLayoutPass(Runnable runnable) { if (runnable == null) { throw new IllegalArgumentException("Runnable may not be null."); } if (preLayoutPass.contains(runnable)) { preLayoutPass.remove(runnable); } } }