/******************************************************************************* * Copyright (c) 2009 the CHISEL group and contributors. * 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: * Del Myers -- initial API and implementation *******************************************************************************/ package org.eclipse.zest.custom.sequence.visuals; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.draw2d.AbstractLayout; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.geometry.Dimension; import org.eclipse.draw2d.geometry.Rectangle; /** * Lays-out figures in a tree structure, using a default size for each item in the tree. * The constraint on each figure is assumed to be its "parent" figure. figures with * null parents will be laid-out in a single row on the top. Note: it is expected that * the constraints set will create a properly formed forest without gaps or cycles. * Improper orderings may result in infinite loops, memory leaks, or deadlocks. * * The layout has a "focal" point which will be used to determine which figure should * have focus and be displayed at "full size" * * @author Del Myers * */ public class ContainmentTreeLayout extends AbstractLayout { public static interface ContainmentTreeConstraint { public IFigure getParentFigure(); } /** * The default size of a figure in the layout. Figures will be laid out as "icons" * on the screen. */ public static final int FIGURE_SIZE = 16; private static final int PADDING = 5; private HashMap<IFigure, ContainmentTreeConstraint> constraints = new HashMap<IFigure, ContainmentTreeConstraint>(); private List<Map<IFigure, List<IFigure>>> tree; Set<IFigure> focalPoint = new HashSet<IFigure>(); /* (non-Javadoc) * @see org.eclipse.draw2d.AbstractLayout#calculatePreferredSize(org.eclipse.draw2d.IFigure, int, int) */ @Override protected Dimension calculatePreferredSize(IFigure container, int whint, int hhint) { if (tree == null) { tree = createLayoutTree(container); } int maxHeight = 0; int maxWidth = 0; int height = (FIGURE_SIZE+PADDING)*tree.size(); ArrayList<IFigure> leaves = new ArrayList<IFigure>(); //collect the leaves for the tree. for (int i = 0; i < tree.size(); i++) { Map<IFigure, List<IFigure>> row = tree.get(i); for (List<IFigure> figures : row.values()) { for (IFigure fig : figures) { Dimension prefferred = fig.getPreferredSize(); if (prefferred.width > maxWidth) { maxWidth = prefferred.width; } if (prefferred.height > maxHeight) { maxHeight = prefferred.height; } boolean add = true; if (i < tree.size()-1) { Map<IFigure, List<IFigure>> nextRow = tree.get(i+1); add = !nextRow.containsKey(fig); } if (add) { leaves.add(fig); } } } } int width = leaves.size()*(FIGURE_SIZE+PADDING); Dimension result = new Dimension( ((whint > 0) ? Math.max(whint, width) : width), ((hhint > 0) ? Math.max(hhint, height) : height) ); result.width += maxWidth; result.height += maxHeight; return result; } /* (non-Javadoc) * @see org.eclipse.draw2d.LayoutManager#layout(org.eclipse.draw2d.IFigure) */ public void layout(IFigure container) { //Dimension preferred = getPreferredSize(container, -1, -1); Dimension size = container.getSize(); int startX = size.width/2;// - preferred.width/2; if (tree == null) { tree = createLayoutTree(container); } if (tree.size() > 0) { layoutSubTree(null, 0, startX); nudge(); } } /** * Searches through the tree to find the figure that contains the focal point, and * nudges everything else out of the way. */ private void nudge() { if (focalPoint == null) { return; } int yAdjust = 0; for (Map<IFigure, List<IFigure>> row : tree) { List<IFigure> rowFigures = new ArrayList<IFigure>(); for (IFigure parent : row.keySet()) { for (IFigure child : row.get(parent)) { rowFigures.add(child); } } Collections.sort(rowFigures, new Comparator<IFigure>(){ public int compare(IFigure o1, IFigure o2) { return o1.getBounds().x - o2.getBounds().x; } }); //the amount to nudge forward int maxRowY = 0; for (int i = 0; i < rowFigures.size(); i++) { IFigure figure = rowFigures.get(i); //adjust the bounds of this figure if (yAdjust != 0) { Rectangle bounds = figure.getBounds().getCopy(); bounds.y += yAdjust; figure.setBounds(bounds); } if (focalPoint.contains(figure)) { Dimension size = figure.getPreferredSize(); if (size.height > FIGURE_SIZE + PADDING) { if (size.height-FIGURE_SIZE > maxRowY) { maxRowY = size.height-FIGURE_SIZE; } } Rectangle bounds = figure.getBounds().getCopy(); bounds.x -= (size.width/2 - bounds.width/2); bounds.setSize(size); figure.setBounds(bounds); //push all the previous ones back. IFigure currentFigure = figure; for (int j = i-1; j >= 0; j--) { IFigure previousFigure = rowFigures.get(j); Rectangle previousBounds = previousFigure.getBounds().getCopy(); Rectangle currentBounds = currentFigure.getBounds().getCopy(); int nudge = (previousBounds.x + previousBounds.width + PADDING) - currentBounds.x; if (nudge > 0) { previousBounds.x -= nudge; previousFigure.setBounds(previousBounds); currentFigure = previousFigure; } else { break; } } //push all the next ones forward currentFigure = figure; for (int j = i+1; j < rowFigures.size(); j++) { IFigure nextFigure = rowFigures.get(j); Rectangle nextBounds = nextFigure.getBounds().getCopy(); Rectangle currentBounds = currentFigure.getBounds().getCopy(); int nudge = (currentBounds.x + currentBounds.width + PADDING) - nextBounds.x; if (nudge > 0) { nextBounds.x += nudge; nextFigure.setBounds(nextBounds); currentFigure = nextFigure; } else { break; } } } } yAdjust += maxRowY; } } private Rectangle layoutSubTree(IFigure parent, int rowIndex, int startX) { Rectangle containment = null; Map<IFigure, List<IFigure>> row = tree.get(rowIndex); List<IFigure> segment = row.get(parent); int y = rowIndex * (FIGURE_SIZE+PADDING); if (segment == null) return new Rectangle(startX, y, 0, 0); Map<IFigure, List<IFigure>> nextRow = null; if (rowIndex < tree.size()-1) { nextRow = tree.get(rowIndex+1); } for (IFigure figure : segment) { if (nextRow != null && nextRow.containsKey(figure)) { //for each segment with children, lay it out first Rectangle childContainment = layoutSubTree(figure, rowIndex+1, startX); if (containment == null) { containment = childContainment.getCopy(); } else { containment.union(childContainment); } //set this bounds to be just above the child containment, and in its middle Rectangle bounds = new Rectangle(childContainment .getCenter().x - (FIGURE_SIZE/2), y, FIGURE_SIZE, FIGURE_SIZE); containment.union(bounds); figure.setBounds(bounds); } else { Rectangle bounds = new Rectangle(startX, y, FIGURE_SIZE, FIGURE_SIZE); if (containment == null) { containment = bounds.getCopy(); } else { containment.union(bounds); } figure.setBounds(bounds); } startX = containment.x+containment.width+PADDING; } return containment; } /** * Creates a "tree" of figures sorted in rows. Each row is a map of "parent" figures * which lists its children. * @param container the container layout. * @return the "tree" structure of figures. */ private List<Map<IFigure, List<IFigure>>> createLayoutTree(IFigure container) { List<Map<IFigure, List<IFigure>>> result = new ArrayList<Map<IFigure, List<IFigure>>>(); List<?> children = container.getChildren(); for (int i = 0; i < children.size(); i++) { IFigure child = (IFigure) children.get(i); Object constraint = getConstraint(child); int row = 0; IFigure parent = null; if (constraint instanceof ContainmentTreeConstraint) { parent = ((ContainmentTreeConstraint) constraint).getParentFigure(); } IFigure currentParent = parent; while (currentParent != null) { row++; constraint = getConstraint(currentParent); if (!(constraint instanceof ContainmentTreeConstraint)) { currentParent = null; } else { currentParent = ((ContainmentTreeConstraint)constraint).getParentFigure(); } } while (row >= result.size()) { result.add(new HashMap<IFigure, List<IFigure>>()); } List<IFigure> segment = result.get(row).get(parent); if (segment == null) { segment = new ArrayList<IFigure>(); result.get(row).put(parent, segment); } segment.add(child); } return result; } /* (non-Javadoc) * @see org.eclipse.draw2d.AbstractLayout#setConstraint(org.eclipse.draw2d.IFigure, java.lang.Object) */ @Override public void setConstraint(IFigure child, Object constraint) { if (child == null) { return; } if (constraint == null || constraint instanceof ContainmentTreeConstraint) { constraints.put(child, (ContainmentTreeConstraint) constraint); } super.setConstraint(child, constraint); } /* (non-Javadoc) * @see org.eclipse.draw2d.AbstractLayout#getConstraint(org.eclipse.draw2d.IFigure) */ @Override public Object getConstraint(IFigure child) { return constraints.get(child); } /* (non-Javadoc) * @see org.eclipse.draw2d.AbstractLayout#invalidate() */ @Override public void invalidate() { tree = null; super.invalidate(); } /** * @param figure */ public void addFocus(IFigure container, IFigure figure) { if (container.getLayoutManager() == this && figure.getParent() == container) { focalPoint.add(figure); } super.invalidate(); layout(container); } /** * @param figure */ public void removeFocus(IFigure container, IFigure figure) { if (container.getLayoutManager() == this && figure.getParent() == container) { focalPoint.remove(figure); } super.invalidate(); layout(container); } }