/******************************************************************************* * 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.zest.fx.behaviors; import java.util.List; import java.util.Map; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.geometry.planar.Rectangle; import org.eclipse.gef.graph.Edge; import org.eclipse.gef.graph.Graph; import org.eclipse.gef.layout.ILayoutAlgorithm; import org.eclipse.gef.layout.ILayoutFilter; import org.eclipse.gef.layout.LayoutContext; import org.eclipse.gef.layout.LayoutProperties; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.parts.PartUtils; import org.eclipse.gef.mvc.fx.viewer.IViewer; import org.eclipse.gef.mvc.fx.viewer.InfiniteCanvasViewer; import org.eclipse.gef.zest.fx.ZestProperties; import org.eclipse.gef.zest.fx.models.HidingModel; import org.eclipse.gef.zest.fx.models.NavigationModel; import org.eclipse.gef.zest.fx.models.NavigationModel.ViewportState; import org.eclipse.gef.zest.fx.parts.GraphPart; import org.eclipse.gef.zest.fx.parts.NodePart; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.SetChangeListener; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.Parent; /** * The {@link GraphLayoutBehavior} is responsible for initiating layout passes. * It is only applicable to {@link GraphPart}. * * @author mwienand * */ // only applicable for GraphPart (see #getHost()) public class GraphLayoutBehavior extends AbstractLayoutBehavior { private Runnable postLayout = new Runnable() { @Override public void run() { postLayout(); } }; private Runnable preLayout = new Runnable() { @Override public void run() { preLayout(); } }; private Parent nestingVisual; private ChangeListener<? super Bounds> nestingVisualLayoutBoundsChangeListener = new ChangeListener<Bounds>() { @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldLayoutBounds, Bounds newLayoutBounds) { updateBounds(); } }; private ChangeListener<? super Bounds> viewportBoundsChangeListener = new ChangeListener<Bounds>() { @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldLayoutBounds, Bounds newLayoutBounds) { updateBounds(); } }; private ListChangeListener<IVisualPart<? extends Node>> childrenObserver = new ListChangeListener<IVisualPart<? extends Node>>() { @Override public void onChanged(ListChangeListener.Change<? extends IVisualPart<? extends Node>> c) { applyLayout(true, null); } }; private SetChangeListener<org.eclipse.gef.graph.Node> hidingModelObserver = new SetChangeListener<org.eclipse.gef.graph.Node>() { @Override public void onChanged(SetChangeListener.Change<? extends org.eclipse.gef.graph.Node> change) { applyLayout(true, null); } }; private boolean skipNextLayout; /** * Performs one layout pass using the static layout algorithm that is * configured for the layout context. * * @param clean * Whether to fully re-compute the layout or not. * @param extra * An extra {@link Object} that is passed-on to the * {@link ILayoutAlgorithm}. */ @SuppressWarnings("unchecked") public void applyLayout(boolean clean, Object extra) { // check child parts exist for all content children if (getHost().getChildrenUnmodifiable().size() != getHost().getContentChildrenUnmodifiable().size()) { return; } else { List<IContentPart<? extends Node>> childContentParts = PartUtils .filterParts(getHost().getChildrenUnmodifiable(), IContentPart.class); for (IContentPart<? extends Node> cp : childContentParts) { if (!getHost().getContentChildrenUnmodifiable().contains(cp.getContent())) { return; } } } if (skipNextLayout) { skipNextLayout = false; return; } Graph graph = getHost().getContent(); // update layout algorithm (apply layout will depend on it) LayoutContext layoutContext = getLayoutContext(); ILayoutAlgorithm layoutAlgorithm = ZestProperties.getLayoutAlgorithm(graph); if (layoutAlgorithm != null) { if (layoutContext.getLayoutAlgorithm() != layoutAlgorithm) { layoutContext.setLayoutAlgorithm(layoutAlgorithm); } } else { if (layoutContext.getLayoutAlgorithm() != null) { layoutContext.setLayoutAlgorithm(null); } } // update the graph if (layoutContext.getGraph() != graph) { layoutContext.setGraph(graph); } // apply layout (if no algorithm is set, will be a no-op) layoutContext.applyLayout(true); } /** * Determines the layout bounds for the graph. * * @return The bounds used to layout the graph. */ protected Rectangle computeLayoutBounds() { Rectangle newBounds = new Rectangle(); if (nestingVisual != null) { // nested graph uses layout bounds of nesting node Bounds layoutBounds = nestingVisual.getLayoutBounds(); newBounds = new Rectangle(0, 0, layoutBounds.getWidth() / NodePart.DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR, layoutBounds.getHeight() / NodePart.DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR); } else { // root graph uses infinite canvas bounds InfiniteCanvas canvas = getInfiniteCanvas(); // XXX: Use minimum of window size and canvas size, because the // canvas size is invalid when its scene is changed. double windowWidth = canvas.getScene().getWindow().getWidth(); double windowHeight = canvas.getScene().getWindow().getHeight(); newBounds = new Rectangle(0, 0, Double.isFinite(windowWidth) ? Math.min(canvas.getWidth(), windowWidth) : canvas.getWidth(), Double.isFinite(windowHeight) ? Math.min(canvas.getHeight(), windowHeight) : canvas.getHeight()); } return newBounds; } @Override protected void doActivate() { getHost().getChildrenUnmodifiable().addListener(childrenObserver); LayoutContext layoutContext = getLayoutContext(); layoutContext.schedulePreLayoutPass(preLayout); layoutContext.schedulePostLayoutPass(postLayout); // register listener for bounds changes if (getHost().getParent() == getHost().getRoot()) { /* * Our graph is the root graph, therefore we listen to viewport * changes to update the layout bounds in the context accordingly. */ getInfiniteCanvas().scrollableBoundsProperty().addListener(viewportBoundsChangeListener); } else { /* * Our graph is nested inside a node of another graph, therefore we * listen to changes of that node's layout-bounds. */ nestingVisual = getHost().getVisual().getParent(); nestingVisual.layoutBoundsProperty().addListener(nestingVisualLayoutBoundsChangeListener); } // add layout filter for hidden/layout irrelevant elements final HidingModel hidingModel = getHost().getRoot().getViewer().getAdapter(HidingModel.class); if (hidingModel != null) { getLayoutContext().addLayoutFilter(new ILayoutFilter() { Map<Object, IContentPart<? extends Node>> contentPartMap = getHost().getViewer().getContentPartMap(); @Override public boolean isLayoutIrrelevant(Edge edge) { if (!contentPartMap.containsKey(edge)) { return true; } if (!contentPartMap.get(edge).isActive()) { return true; } return Boolean.TRUE.equals(ZestProperties.getLayoutIrrelevant(edge)) || isLayoutIrrelevant(edge.getSource()) || hidingModel.isHidden(edge.getSource()) || isLayoutIrrelevant(edge.getTarget()) || hidingModel.isHidden(edge.getTarget()); } @Override public boolean isLayoutIrrelevant(org.eclipse.gef.graph.Node node) { if (!contentPartMap.containsKey(node)) { return true; } if (!contentPartMap.get(node).isActive()) { return true; } return Boolean.TRUE.equals(ZestProperties.getLayoutIrrelevant(node)) || hidingModel.isHidden(node); } }); hidingModel.hiddenProperty().addListener(hidingModelObserver); } // initially apply layout if no viewport state is saved for this graph, // or we are nested inside a node, or the saved viewport is outdated NavigationModel navigationModel = getHost().getRoot().getViewer().getAdapter(NavigationModel.class); ViewportState savedViewport = navigationModel == null ? null : navigationModel.getViewportState(getHost().getContent()); InfiniteCanvas canvas = ((InfiniteCanvasViewer) getHost().getRoot().getViewer()).getCanvas(); boolean isNested = getNestingPart() != null; boolean isViewportChanged = savedViewport != null && (savedViewport.getWidth() != canvas.getWidth() || savedViewport.getHeight() != canvas.getHeight()); // TODO: we should store one viewport state for the viewport of the // nesting part and one for the viewport of the graph part, so that // nested graphs are not unnecessarily layouted skipNextLayout = savedViewport != null; if (savedViewport == null || isNested || isViewportChanged) { LayoutProperties.setBounds(getHost().getContent(), computeLayoutBounds()); applyLayout(true, null); } } @Override protected void doDeactivate() { getHost().getChildrenUnmodifiable().removeListener(childrenObserver); final HidingModel hidingModel = getHost().getRoot().getViewer().getAdapter(HidingModel.class); if (hidingModel != null) { hidingModel.hiddenProperty().removeListener(hidingModelObserver); } LayoutContext layoutContext = getLayoutContext(); layoutContext.unschedulePreLayoutPass(preLayout); layoutContext.unschedulePostLayoutPass(postLayout); if (nestingVisual != null) { // remove layout change listener from nesting visual nestingVisual.layoutBoundsProperty().removeListener(nestingVisualLayoutBoundsChangeListener); } else { // remove change listener from infinite canvas getInfiniteCanvas().scrollableBoundsProperty().removeListener(viewportBoundsChangeListener); } nestingVisual = null; } @Override public GraphPart getHost() { return (GraphPart) super.getHost(); } /** * Returns the {@link InfiniteCanvas} of the {@link IViewer} of the * {@link #getHost() host}. * * @return The {@link InfiniteCanvas} of the {@link IViewer} of the * {@link #getHost() host}. */ protected InfiniteCanvas getInfiniteCanvas() { return ((InfiniteCanvasViewer) getHost().getRoot().getViewer()).getCanvas(); } @Override protected LayoutContext getLayoutContext() { return getHost().getAdapter(LayoutContext.class); } /** * Returns the {@link NodePart} that contains the nested graph to which the * behavior corresponds, if this behavior is related to a nested graph. * * @return The {@link NodePart} that contains the nested graph to which the * behavior corresponds. */ protected NodePart getNestingPart() { if (getHost().getParent() instanceof NodePart) { return (NodePart) getHost().getParent(); } return null; } @Override protected void postLayout() { // execute post-layout of all nodes and edges for (IVisualPart<? extends Node> child : getHost().getChildrenUnmodifiable()) { // FIXME: Layout should only be triggered when content-part-map // is changed, not when the children are changed. if (child.getViewer() == null) { continue; } AbstractLayoutBehavior childLayoutBehavior = child.getAdapter(AbstractLayoutBehavior.class); if (childLayoutBehavior != null) { childLayoutBehavior.postLayout(); } } } @Override protected void preLayout() { // execute pre-layout of all nodes and edges for (IVisualPart<? extends Node> child : getHost().getChildrenUnmodifiable()) { // FIXME: Layout should only be triggered when content-part-map // is changed, not when the children are changed. if (child.getViewer() == null) { continue; } AbstractLayoutBehavior childLayoutBehavior = child.getAdapter(AbstractLayoutBehavior.class); if (childLayoutBehavior != null) { childLayoutBehavior.preLayout(); } } } /** * Updates the bounds property from the visual (viewport or nesting node) */ protected void updateBounds() { Rectangle newBounds = computeLayoutBounds(); Rectangle oldBounds = LayoutProperties.getBounds(getHost().getContent()); if (oldBounds != newBounds && (oldBounds == null || !oldBounds.equals(newBounds))) { LayoutProperties.setBounds(getHost().getContent(), newBounds); applyLayout(true, null); } } }