/* * $Id$ * * Copyright (c) 2001-2008 Gaudenz Alder * */ package org.jgraph.graph; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.WeakHashMap; import javax.swing.event.EventListenerList; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.CompoundEdit; import javax.swing.undo.UndoableEdit; import org.jgraph.event.GraphLayoutCacheEvent; import org.jgraph.event.GraphLayoutCacheListener; import org.jgraph.event.GraphModelEvent; import org.jgraph.util.RectUtils; /** * An object that defines the view of a graphmodel. This object maps between * model cells and views and provides a set of methods to change these views. * The view may also contain its own set of attributes and is therefore an * extension of an Observable, which may be observed by the GraphUI. It uses the * model to send its changes to the command history. * * @version 1.0 1/1/02 * @author Gaudenz Alder */ @SuppressWarnings ( "all" ) public class GraphLayoutCache implements CellMapper, Serializable { /** * True if the cells should be auto-sized when their values change. Default * is false. */ protected boolean autoSizeOnValueChange = false; /** * Boolean indicating whether existing connections should me made visible if * their sources or targets are made visible, given the opposite end of the * edge is already visible or made visible, too. Default is true. */ protected boolean showsExistingConnections = true; /** * Boolean indicating whether connections should be made visible when * reconnected and their source and target port is visible. Default is true. */ protected boolean showsChangedConnections = true; /** * Boolean indicating whether edited cells should be made visible if they * are changed via * {@link #edit(Map, ConnectionSet, ParentMap, UndoableEdit[])}. Default is * true. */ protected boolean showsInvisibleEditedCells = true; /** * Boolean indicating whether inserted should be made visible if they are * inserted via * {@link #insert(Object[], Map, ConnectionSet, ParentMap, UndoableEdit[])}. * Default is true. */ protected boolean showsInsertedCells = true; /** * Boolean indicating whether inserted edges should me made visible if their * sources or targets are already visible. Default is true. */ protected boolean showsInsertedConnections = true; /** * Boolean indicating whether existing connections should be hidden if their * source or target and no parent of the ports is visible, either by hiding * the cell or by changing the source or target of the edge to a hidden * cell. Default is true. */ protected boolean hidesExistingConnections = true; /** * Boolean indicating whether existing connections should be hidden if their * source or target port is removed from the model. Default is false. */ protected boolean hidesDanglingConnections = false; /** * Boolean indicating whether cellviews should be remembered once visible in * this GraphLayoutCache. Default is true. */ protected boolean remembersCellViews = true; /** * Boolean indicating whether inserted cells should automatically be * selected. Default is true. This is ignored if the cache is partial. Note: * Despite the name of this field the implementation is located in the * BasicGraphUI.GraphModelHandler.graphChanged method. */ protected boolean selectsAllInsertedCells = false; /** * Boolean indicating whether cells that are inserted using the local insert * method should automatically be selected. Default is true. This is ignored * if the cache is not partial and selectsAllInsertedCells is true, in which * case the cells will be selected through another mechanism. Note: Despite * the name of this field the implementation is located in the * BasicGraphUI.GraphLayoutCacheObserver.changed method. */ protected boolean selectsLocalInsertedCells = false; /** * Boolean indicating whether children should be moved to the parent group's * origin on expand. Default is true. */ protected boolean movesChildrenOnExpand = true; /** * Boolean indicating whether parents should be moved to the child area * origin on collapse. Default is true. */ protected boolean movesParentsOnCollapse = true; /** * Boolean indicating whether parents should always be resized to the child * area on collapse. If false the size is only initially updated if it has * not yet been assigned. Default is false. */ protected boolean resizesParentsOnCollapse = false; /** * Specified the initial x- and y-scaling factor for initial collapsed group * bounds. Default is 1.0, ie. no scaling. */ protected double collapseXScale = 1.0, collapseYScale = 1.0; /** * Boolean indicating whether edges should be reconneted to visible parents * on collapse/expand. Default is false. * * @deprecated edges are moved to parent view and back automatically */ protected boolean reconnectsEdgesToVisibleParent = false; /** * The list of listeners that listen to the GraphLayoutCache. */ protected EventListenerList listenerList = new EventListenerList(); /** * Reference to the graphModel */ protected GraphModel graphModel; /** * Maps cells to views. */ protected Map mapping = new Hashtable(); /** * Maps cells to views. The hidden mapping is used to remembed cell views * that are hidden, based on the remembersCellViews setting. hiddenMapping * must use weak keys for the cells since when cells are removed * hiddenMapping is not updated. */ protected transient Map hiddenMapping = new WeakHashMap(); /** * Factory to create the views. */ protected CellViewFactory factory = null; /** * The set of visible cells. */ protected Set visibleSet = new HashSet(); /** * Ordered list of roots for the view. */ protected List roots = new ArrayList(); /** * Cached array of all ports for the view. */ protected PortView[] ports; /** * Only portions of the model are visible. */ protected boolean partial = false; /** * Controls if all attributes are local. If this is false then the * createLocalEdit will check the localAttributes set to see if a specific * attribute is local, otherwise it will assume that all attributes are * local. This allows to make all attributes local without actually knowing * them. Default is false. */ protected boolean allAttributesLocal = false; /** * A set containing all attribute keys that are stored in the cell views, in * other words, the view-local attributes. */ protected Set localAttributes = new HashSet(); /** * Constructs a graph layout cache. */ public GraphLayoutCache() { this(new DefaultGraphModel(), new DefaultCellViewFactory()); } /** * Constructs a view for the specified model that uses <code>factory</code> * to create its views. * * @param model * the model that constitues the data source */ public GraphLayoutCache(GraphModel model, CellViewFactory factory) { this(model, factory, false); } /** * Constructs a view for the specified model that uses <code>factory</code> * to create its views. * * @param model * the model that constitues the data source */ public GraphLayoutCache(GraphModel model, CellViewFactory factory, boolean partial) { this(model, factory, null, null, partial); } /** * Constructs a view for the specified model that uses <code>factory</code> * to create its views. * * @param model * the model that constitues the data source */ public GraphLayoutCache(GraphModel model, CellViewFactory factory, CellView[] cellViews, CellView[] hiddenCellViews, boolean partial) { this.factory = factory; this.partial = partial; if (cellViews != null) { graphModel = model; for (int i = 0; i < cellViews.length; i++) { if (cellViews[i] != null) { putMapping(cellViews[i].getCell(), cellViews[i]); if (partial) visibleSet.add(cellViews[i].getCell()); } } insertViews(cellViews); // Notify observers for autosizing? } else { setModel(model); } if (hiddenCellViews != null) { for (int i = 0; i < hiddenCellViews.length; i++) hiddenMapping.put(hiddenCellViews[i].getCell(), hiddenCellViews[i]); } } // // GraphLayoutCacheListeners // /** * Adds a listener for the GraphLayoutCacheEvent posted after the graph * layout cache changes. * * @see #removeGraphLayoutCacheListener * @param l * the listener to add */ public void addGraphLayoutCacheListener(GraphLayoutCacheListener l) { listenerList.add(GraphLayoutCacheListener.class, l); } /** * Removes a listener previously added with <B>addGraphLayoutCacheListener() * </B>. * * @see #addGraphLayoutCacheListener * @param l * the listener to remove */ public void removeGraphLayoutCacheListener(GraphLayoutCacheListener l) { listenerList.remove(GraphLayoutCacheListener.class, l); } /** * Invoke this method after you've changed how the cells are to be * represented in the graph. */ public void cellViewsChanged(final CellView[] cellViews) { if (cellViews != null) { fireGraphLayoutCacheChanged(this, new GraphLayoutCacheEvent.GraphLayoutCacheChange() { public Object[] getInserted() { return null; } public Object[] getRemoved() { return null; } public Map getPreviousAttributes() { return null; } public Object getSource() { return this; } public Object[] getChanged() { return cellViews; } public Map getAttributes() { return null; } public Object[] getContext() { return null; } public Rectangle2D getDirtyRegion() { return null; } public void setDirtyRegion(Rectangle2D dirty) {} }); } } /* * Notify all listeners that have registered interest for notification on * this event type. The event instance is lazily created using the * parameters passed into the fire method. * * @see EventListenerList */ protected void fireGraphLayoutCacheChanged(Object source, GraphLayoutCacheEvent.GraphLayoutCacheChange edit) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); GraphLayoutCacheEvent e = null; // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == GraphLayoutCacheListener.class) { // Lazily create the event: if (e == null) e = new GraphLayoutCacheEvent(source, edit); ((GraphLayoutCacheListener) listeners[i + 1]) .graphLayoutCacheChanged(e); } } } /** * Return an array of all GraphLayoutCacheListener that were added to this * model. */ public GraphLayoutCacheListener[] getGraphLayoutCacheListeners() { return (GraphLayoutCacheListener[]) listenerList .getListeners(GraphLayoutCacheListener.class); } // // Accessors // /** * Sets the factory that creates the cell views. */ public void setFactory(CellViewFactory factory) { this.factory = factory; } /** * Returns the factory that was passed to the constructor. */ public CellViewFactory getFactory() { return factory; } /** * Sets the current model. */ public void setModel(GraphModel model) { roots.clear(); mapping.clear(); hiddenMapping.clear(); visibleSet.clear(); graphModel = model; if (!isPartial()) { Object[] cells = DefaultGraphModel.getRoots(getModel()); CellView[] cellViews = getMapping(cells, true); insertViews(cellViews); } // Update PortView Cache and Notify Observers updatePorts(); cellViewsChanged(getRoots()); } /** * @return Returns an unordered array of all visible cellviews. */ public CellView[] getCellViews() { Collection coll = mapping.values(); CellView[] result = new CellView[coll.size()]; coll.toArray(result); return result; } /** * Returns the bounding box for the specified cell views. */ public static Rectangle2D getBounds(CellView[] views) { if (views != null && views.length > 0) { Rectangle2D r = (views[0] != null) ? views[0].getBounds() : null; Rectangle2D ret = (r != null) ? (Rectangle2D) r.clone() : null; for (int i = 1; i < views.length; i++) { r = (views[i] != null) ? views[i].getBounds() : null; if (r != null) { if (ret == null) ret = (r != null) ? (Rectangle2D) r.clone() : null; else Rectangle2D.union(ret, r, ret); } } return ret; } return null; } /** * A helper method to return various arrays of cells that are visible in * this cache. For example, to get all selected vertices in a graph, do * <code>graph.getSelectionCells(graph.getGraphLayoutCache().getCells(false, true, false, false));</code> */ public Object[] getCells(boolean groups, boolean vertices, boolean ports, boolean edges) { CellView[] views = getCellViews(); List result = new ArrayList(views.length); GraphModel model = getModel(); for (int i = 0; i < views.length; i++) { Object cell = views[i].getCell(); boolean isEdge = model.isEdge(cell); if ((ports || !model.isPort(cell))) { if (((((ports || vertices) && !isEdge) || (edges && isEdge)) && views[i] .isLeaf()) || (groups && !views[i].isLeaf())) result.add(views[i].getCell()); } } return result.toArray(); } /** * Returns a nested map of (cell, map) pairs that represent all attributes * of all cell views in this view. * * @see #getCellViews */ public Map createNestedMap() { CellView[] cellViews = getCellViews(); Map nested = new Hashtable(); for (int i = 0; i < cellViews.length; i++) { nested.put(cellViews[i].getCell(), new Hashtable((Map) cellViews[i] .getAllAttributes().clone())); } return nested; } /** * @return Returns an unordered array of all hidden cellviews. */ public CellView[] getHiddenCellViews() { Collection coll = hiddenMapping.values(); CellView[] result = new CellView[coll.size()]; coll.toArray(result); return result; } /** * Remaps all existing views using the CellViewFactory * and replaces the respective root views. */ public synchronized void reload() { List newRoots = new ArrayList(); Map oldMapping = new Hashtable(mapping); mapping.clear(); Iterator it = oldMapping.keySet().iterator(); Set rootsSet = new HashSet(roots); while (it.hasNext()) { Object cell = it.next(); CellView oldView = (CellView) oldMapping.get(cell); CellView newView = getMapping(cell, true); newView.changeAttributes(this, oldView.getAttributes()); // newView.refresh(getModel(), this, false); if (rootsSet.contains(oldView)) newRoots.add(newView); } // replace hidden hiddenMapping.clear(); roots = newRoots; } /** * Returns the current model. */ public GraphModel getModel() { return graphModel; } /** * Returns the roots of the view. */ public CellView[] getRoots() { CellView[] views = new CellView[roots.size()]; roots.toArray(views); return views; } /** * Return all root cells that intersect the given rectangle. */ public CellView[] getRoots(Rectangle2D clip) { java.util.List result = new ArrayList(); CellView[] views = getRoots(); for (int i = 0; i < views.length; i++) if (views[i].getBounds().intersects(clip)) result.add(views[i]); views = new CellView[result.size()]; result.toArray(views); return views; } /** * Returns a an array with the visible cells in <code>cells</code>. */ public Object[] getVisibleCells(Object[] cells) { if (cells != null) { List result = new ArrayList(cells.length); for (int i = 0; i < cells.length; i++) if (isVisible(cells[i])) result.add(cells[i]); return result.toArray(); } return null; } /** * Returns the ports of the view. */ public PortView[] getPorts() { return ports; } /** * Updates the cached array of ports. */ protected void updatePorts() { Object[] roots = DefaultGraphModel.getRoots(graphModel); List list = DefaultGraphModel.getDescendants(graphModel, roots); if (list != null) { ArrayList result = new ArrayList(); Iterator it = list.iterator(); while (it.hasNext()) { Object cell = it.next(); if (graphModel.isPort(cell)) { CellView portView = getMapping(cell, false); if (portView != null) { result.add(portView); portView.refresh(this, this, false); } } } ports = new PortView[result.size()]; result.toArray(ports); } } public void refresh(CellView[] views, boolean create) { if (views != null) for (int i = 0; i < views.length; i++) refresh(views[i], create); } public void refresh(CellView view, boolean create) { if (view != null) { view.refresh(this, this, create); CellView[] children = view.getChildViews(); for (int i = 0; i < children.length; i++) refresh(children[i], create); } } public void update(CellView[] views) { if (views != null) for (int i = 0; i < views.length; i++) update(views[i]); } public void update(CellView view) { if (view != null) { view.update(this); CellView[] children = view.getChildViews(); for (int i = 0; i < children.length; i++) update(children[i]); } } // // Update View based on Model Change // /** * Called from BasicGraphUI.ModelHandler to update the view based on the * specified GraphModelEvent. */ public void graphChanged(GraphModelEvent.GraphModelChange change) { // Get Old Attributes From GraphModelChange (Undo) -- used to remap // removed cells CellView[] views = change.getViews(this); if (views != null) { // Only ex-visible views are piggybacked for (int i = 0; i < views.length; i++) if (views[i] != null) { // Do not use putMapping because cells are invisible mapping.put(views[i].getCell(), views[i]); } // Ensure visible state setVisibleImpl(getCells(views), true); } // Fetch View Order Of Changed Cells (Before any changes) Object[] changed = change.getChanged(); // Fetch Views to Insert before Removal (Special case: two step process, // see setModel) getMapping(change.getInserted(), true); // Remove and Hide Roots views = removeCells(change.getRemoved()); // Store Removed Attributes In GraphModelChange (Undo) change.putViews(this, views); // Insert New Roots // insertViews(insertViews); // Hide edges with invisible source or target if (isPartial()) { // Then show showCellsForChange(change); // First hide hideCellsForChange(change); } // Refresh Changed Cells if (changed != null && changed.length > 0) { // Restore All Cells in Model Order (Replace Roots) for (int i = 0; i < changed.length; i++) { CellView view = getMapping(changed[i], false); if (view != null) { view.refresh(this, this, true); // Update child edges in groups (routing) update(view); } } } reloadRoots(); // Refresh Context of Changed Cells (=Connected Edges) refresh(getMapping(getContext(change), false), false); updatePorts(); } /** * Completely reloads all roots from the model in the order returned by * DefaultGraphModel.getAll. This uses the current visibleSet and mapping to * fetch the cell views for the cells. */ protected void reloadRoots() { // Reorder roots Object[] orderedCells = DefaultGraphModel.getAll(graphModel); List newRoots = new ArrayList(); for (int i = 0; i < orderedCells.length; i++) { CellView view = getMapping(orderedCells[i], false); if (view != null) { view.refresh(this, this, true); if (view.getParentView() == null) { newRoots.add(view); } } } roots = newRoots; } /** * Hook for subclassers to augment the context for a graphChange. This means * you can add additional cells that should be refreshed on a special change * event. eg. parallel edges when one is removed or added. */ protected Object[] getContext(GraphModelEvent.GraphModelChange change) { return change.getContext(); } protected void hideCellsForChange(GraphModelEvent.GraphModelChange change) { // Hide visible edges between invisible vertices // 1. Remove attached edges of removed cells // 2. Remove edges who's source or target has changed to // invisible. Object[] tmp = change.getRemoved(); Set removed = new HashSet(); if (tmp != null) for (int i = 0; i < tmp.length; i++) removed.add(tmp[i]); if (hidesDanglingConnections || hidesExistingConnections) { Object[] changed = change.getChanged(); for (int i = 0; i < changed.length; i++) { CellView view = getMapping(changed[i], false); if (view instanceof EdgeView) { EdgeView edge = (EdgeView) view; Object oldSource = (edge.getSource() == null) ? null : edge .getSource().getCell(); Object oldTarget = (edge.getTarget() == null) ? null : edge .getTarget().getCell(); Object newSource = graphModel.getSource(changed[i]); Object newTarget = graphModel.getTarget(changed[i]); boolean hideExisting = (hidesExistingConnections && ((newSource != null && !hasVisibleParent( newSource, null)) || (newTarget != null && !hasVisibleParent( newTarget, null)))); if ((hidesDanglingConnections && (removed .contains(oldSource) || removed.contains(oldTarget))) || hideExisting) { setVisibleImpl(new Object[] { changed[i] }, false); } } } } } /** * Checks if the port or one of its parents is visible. */ protected boolean hasVisibleParent(Object cell, Set invisible) { boolean isVisible = false; do { isVisible = (invisible == null || !invisible.contains(cell)) ? isVisible(cell) : false; cell = getModel().getParent(cell); } while (cell != null && !isVisible); return isVisible; } protected void showCellsForChange(GraphModelEvent.GraphModelChange change) { Object[] inserted = change.getInserted(); if (inserted != null && showsInsertedConnections) { for (int i = 0; i < inserted.length; i++) { if (!isVisible(inserted[i])) { Object source = graphModel.getSource(inserted[i]); Object target = graphModel.getTarget(inserted[i]); if ((source != null || target != null) && (isVisible(source) && isVisible(target))) setVisibleImpl(new Object[] { inserted[i] }, true); } } } if (change.getConnectionSet() != null) { Set changedSet = change.getConnectionSet().getChangedEdges(); if (changedSet != null && showsChangedConnections) { Object[] changed = changedSet.toArray(); for (int i = 0; i < changed.length; i++) { if (!isVisible(changed[i])) { Object source = graphModel.getSource(changed[i]); Object target = graphModel.getTarget(changed[i]); if ((source != null || target != null) && (isVisible(source) && isVisible(target)) && !isVisible(changed[i])) setVisibleImpl(new Object[] { changed[i] }, true); } } } } } /** * Adds the specified model root cells to the view. Do not add a view that * is already in roots. */ public void insertViews(CellView[] views) { if (views != null) { refresh(views, true); for (int i = 0; i < views.length; i++) { if (views[i] != null && getMapping(views[i].getCell(), false) != null) { CellView parentView = views[i].getParentView(); Object parent = (parentView != null) ? parentView.getCell() : null; if (!graphModel.isPort(views[i].getCell()) && parent == null) { roots.add(views[i]); } } } } } /** * Removes the specified model root cells from the view by removing the * mapping between the cell and its view and makes the cells invisible. */ public CellView[] removeCells(Object[] cells) { if (cells != null && cells.length > 0) { CellView[] views = new CellView[cells.length]; // Store views to be removed from roots in an intermediate // set for performance reasons Set removedRoots = null; for (int i = 0; i < cells.length; i++) { views[i] = removeMapping(cells[i]); if (views[i] != null) { views[i].removeFromParent(); if (removedRoots == null) { removedRoots = new HashSet(); } removedRoots.add(views[i]); visibleSet.remove(views[i].getCell()); } } if (removedRoots !=null && removedRoots.size() > 0) { // If any roots have been removed, reform the roots // lists appropriately, keeping the order the same int newRootsSize = roots.size() - removedRoots.size(); if (newRootsSize < 8) { newRootsSize = 8; } List newRoots = new ArrayList(newRootsSize); Iterator iter = roots.iterator(); while (iter.hasNext()) { Object cell = iter.next(); if (!removedRoots.contains(cell)) { newRoots.add(cell); } } roots = newRoots; } return views; } return null; } // // Cell Mapping // /** * Takes an array of views and returns the array of the corresponding cells * by using <code>getCell</code> for each view. */ public Object[] getCells(CellView[] views) { if (views != null) { Object[] cells = new Object[views.length]; for (int i = 0; i < views.length; i++) if (views[i] != null) cells[i] = views[i].getCell(); return cells; } return null; } /** * Returns the view for the specified cell. If create is true and no view is * found then a view is created using createView(Object). */ public CellView getMapping(Object cell, boolean create) { if (cell == null) return null; CellView view = (CellView) mapping.get(cell); if (view == null && create && isVisible(cell)) { view = (CellView) hiddenMapping.get(cell); if (view != null) { putMapping(cell, view); hiddenMapping.remove(cell); } else { view = factory.createView(graphModel, cell); putMapping(cell, view); view.refresh(this, this, true); // Create Dependent // Views view.update(this); } } return view; } /** * Returns the views for the specified array of cells without creating these * views on the fly. */ public CellView[] getMapping(Object[] cells) { return getMapping(cells, false); } /** * Returns the views for the specified array of cells. Returned array may * contain null pointers if the respective cell is not mapped in this view * and <code>create</code> is <code>false</code>. */ public CellView[] getMapping(Object[] cells, boolean create) { if (cells != null) { CellView[] result = new CellView[cells.length]; for (int i = 0; i < cells.length; i++) result[i] = getMapping(cells[i], create); return result; } return null; } /** * Associates the specified model cell with the specified view. */ public void putMapping(Object cell, CellView view) { if (cell != null && view != null) mapping.put(cell, view); } /** * Removes the associaten for the specified model cell and returns the view * that was previously associated with the cell. Updates the portlist if * necessary. */ public CellView removeMapping(Object cell) { if (cell != null) { CellView view = (CellView) mapping.remove(cell); return view; } return null; } // // Partial View // // Null is always visible! public boolean isVisible(Object cell) { return !isPartial() || visibleSet.contains(cell) || cell == null; } public Set getVisibleSet() { return new HashSet(visibleSet); } public void setVisibleSet(Set visible) { visibleSet = visible; } /** * Makes the specified cell visible or invisible depending on the flag * passed in. Note the cell really is a cell, not a cell view. * * @param cell * the cell whose visibility is to be changed * @param visible * <code>true</code> if cell is to be made visible */ public void setVisible(Object cell, boolean visible) { setVisible(new Object[] { cell }, visible); } /** * Makes the specified cells visible or invisible depending on the flag * passed in. Note the cells really are cells, not cell views. * * @param cells * the cells whose visibility is to be changed * @param visible * <code>true</code> if the cells are to be made visible */ public void setVisible(Object[] cells, boolean visible) { if (visible) setVisible(cells, null); else setVisible(null, cells); } /** * Changes the visibility state of the cells passed in. Note that the arrays * must contain cells, not cell views. * * @param visible * cells to be made visible * @param invisible * cells to be made invisible */ public void setVisible(Object[] visible, Object[] invisible) { setVisible(visible, invisible, null); } /** * Changes the visibility state of the cells passed in. Note that the arrays * must contain cells, not cell views. * * @param visible * cells to be made visible * @param invisible * cells to be made invisible * @param cs * a <code>ConnectionSet</code> describing the new state of * edge connections in the graph */ public void setVisible(Object[] visible, Object[] invisible, ConnectionSet cs) { GraphLayoutCacheEdit edit = new GraphLayoutCacheEdit(null, null, visible, invisible); edit.end(); graphModel.edit(null, cs, null, new UndoableEdit[] { edit }); } // This is used to augment the array passed to the setVisible method. protected Object[] addVisibleDependencies(Object[] cells, boolean visible) { if (cells != null) { if (visible) { // Make ports and source and target vertex visible Set all = new HashSet(); for (int i = 0; i < cells.length; i++) { all.add(cells[i]); // Add ports all.addAll(getPorts(cells[i])); // Add source vertex and ports Collection coll = getParentPorts(graphModel .getSource(cells[i])); if (coll != null) all.addAll(coll); // Add target vertex and ports coll = getParentPorts(graphModel.getTarget(cells[i])); if (coll != null) all.addAll(coll); } if (showsExistingConnections) { Set tmp = DefaultGraphModel.getEdges(getModel(), cells); Iterator it = tmp.iterator(); while (it.hasNext()) { Object obj = it.next(); Object source = graphModel.getSource(obj); Object target = graphModel.getTarget(obj); if ((isVisible(source) || all.contains(source)) && (isVisible(target) || all.contains(target))) all.add(obj); } } all.removeAll(visibleSet); all.remove(null); return all.toArray(); } else { if (hidesExistingConnections) { Set all = new HashSet(); for (int i = 0; i < cells.length; i++) { all.addAll(getPorts(cells[i])); all.add(cells[i]); } Iterator it = DefaultGraphModel.getEdges(graphModel, cells) .iterator(); while (it.hasNext()) { Object edge = it.next(); Object newSource = graphModel.getSource(edge); Object newTarget = graphModel.getTarget(edge); // Note: At this time the cells are not yet hidden if ((newSource != null && !hasVisibleParent(newSource, all)) || (newTarget != null && !hasVisibleParent( newTarget, all))) { all.add(edge); } } all.remove(null); return all.toArray(); } } } return cells; } /** * The actual implementation of changing cells' visibility state. This * method does not deal with creating the undo or updating the * GraphLayoutCache correctly. The <code>setVisible</code> methods in this * class are intended to be the main public way to change visiblilty. * However, if you do not require the undo to be formed, this method is much * quicker, just note that you must call <code>updatePorts</code> if this * method returns true. * * @param cells * @param visible * @return whether or not the ports needed updating in the calling method */ public boolean setVisibleImpl(Object[] cells, boolean visible) { cells = addVisibleDependencies(cells, visible); if (cells != null && isPartial()) { boolean updatePorts = false; // Update Visible Set CellView[] views = new CellView[cells.length]; if (!visible) { views = removeCells(cells); } // Set used for model roots contains call for performance Set modelRoots = null; for (int i = 0; i < cells.length; i++) { if (cells[i] != null) { if (visible) { visibleSet.add(cells[i]); views[i] = getMapping(cells[i], true); } else { if (views[i] != null) { if (modelRoots == null) { modelRoots = new HashSet(DefaultGraphModel .getRootsAsCollection(getModel())); } if (modelRoots.contains(views[i].getCell()) && remembersCellViews) { hiddenMapping.put(views[i].getCell(), views[i]); } updatePorts = true; } } } } // Make Cell Views Visible (if not already in place) if (visible) { Set parentSet = new HashSet(); Set removedRoots = null; for (int i = 0; i < views.length; i++) { if (views[i] != null) { CellView view = views[i]; // Remove all children from roots CellView[] children = AbstractCellView .getDescendantViews(new CellView[] { view }); for (int j = 0; j < children.length; j++) { if (removedRoots == null) { removedRoots = new HashSet(); } removedRoots.add(children[j]); } view.refresh(this, this, false); // Link cellView into graphLayoutCache CellView parentView = view.getParentView(); if (parentView != null) parentSet.add(parentView); updatePorts = true; } } if (removedRoots !=null && removedRoots.size() > 0) { // If any roots have been removed, reform the roots // lists appropriately, keeping the order the same List newRoots = new ArrayList(); Iterator iter = roots.iterator(); while (iter.hasNext()) { Object cell = iter.next(); if (!removedRoots.contains(cell)) { newRoots.add(cell); } } roots = newRoots; } CellView[] parentViews = new CellView[parentSet.size()]; parentSet.toArray(parentViews); refresh(parentViews, true); } return updatePorts; } return false; } protected Collection getParentPorts(Object cell) { // does nothing if a parent is already visible Object parent = graphModel.getParent(cell); while (parent != null) { if (isVisible(parent)) return null; parent = graphModel.getParent(parent); } // Else returns the parent and all ports parent = graphModel.getParent(cell); Collection collection = getPorts(parent); collection.add(parent); return collection; } protected Collection getPorts(Object cell) { LinkedList list = new LinkedList(); for (int i = 0; i < graphModel.getChildCount(cell); i++) { Object child = graphModel.getChild(cell, i); if (graphModel.isPort(child)) list.add(child); } return list; } // // Change Support // public boolean isPartial() { return partial; } /** * Required for XML persistence * * @return whether or not the cache is partial */ public boolean getPartial() { return isPartial(); } /** * Messaged when the user has altered the value for the item identified by * cell to newValue. If newValue signifies a truly new value the model * should post a graphCellsChanged event. This calls * augmentNestedMapForValueChange. */ public void valueForCellChanged(Object cell, Object newValue) { Map nested = null; if (isAutoSizeOnValueChange()) { CellView view = getMapping(cell, false); if (view != null) { AttributeMap attrs = view.getAllAttributes(); Rectangle2D bounds = GraphConstants.getBounds(attrs); Rectangle2D dummyBounds = null; // Force the model to store the old bounds if (bounds != null) { dummyBounds = attrs.createRect(bounds.getX(), bounds.getY(), 0, 0); } else { dummyBounds = attrs.createRect(0, 0, 0, 0); } nested = GraphConstants.createAttributes(new Object[] { cell }, new Object[] { GraphConstants.RESIZE, GraphConstants.BOUNDS }, new Object[] { Boolean.TRUE, dummyBounds }); } } else { nested = new Hashtable(); nested.put(cell, new Hashtable()); } augmentNestedMapForValueChange(nested, cell, newValue); edit(nested, null, null, null); } /** * Hook for subclassers to add more stuff for value changes. Currently this * adds the new value to the change. */ protected void augmentNestedMapForValueChange(Map nested, Object cell, Object newValue) { Map attrs = (Map) nested.get(cell); if (attrs != null) GraphConstants.setValue(attrs, newValue); } /** * Inserts the <code>cells</code> and connections into the model, and * absorbs the local attributes. This implementation sets the inserted cells * visible and selects the new roots depending on graph.selectNewCells. */ public void insert(Object[] roots, Map attributes, ConnectionSet cs, ParentMap pm, UndoableEdit[] e) { Object[] visible = null; if (isPartial() && showsInsertedCells) { List tmp = DefaultGraphModel.getDescendants(graphModel, roots); tmp.removeAll(visibleSet); if (!tmp.isEmpty()) visible = tmp.toArray(); } // Absorb local attributes GraphLayoutCacheEdit edit = createLocalEdit(roots, attributes, visible, null); if (edit != null) e = augment(e, edit); graphModel.insert(roots, attributes, cs, pm, e); } /** * Inserts the cloned cells from the clone map and clones the passed-in * arguments according to the clone map before insertion and returns the * clones in order of the cells. This example shows how to clone the current * selection and get a reference to the clones: * * <pre> * Object[] cells = graph.getDescendants(graph.order(graph.getSelectionCells())); * ConnectionSet cs = ConnectionSet.create(graphModel, cells, false); * ParentMap pm = ParentMap.create(graphModel, cells, false, true); * cells = graphLayoutCache.insertClones(cells, graph.cloneCells(cells), * attributes, cs, pm, 0, 0); * </pre> */ public Object[] insertClones(Object[] cells, Map clones, Map nested, ConnectionSet cs, ParentMap pm, double dx, double dy) { if (cells != null) { if (cs != null) cs = cs.clone(clones); if (pm != null) pm = pm.clone(clones); if (nested != null) { nested = GraphConstants.replaceKeys(clones, nested); AttributeMap.translate(nested.values(), dx, dy); } // Replace cells in order Object[] newCells = new Object[cells.length]; for (int i = 0; i < cells.length; i++) newCells[i] = clones.get(cells[i]); // Insert into cache/model insert(newCells, nested, cs, pm, null); return newCells; } return null; } /** * Inserts the specified vertex into the graph model. This method does in * fact nothing, it calls insert edge with the vertex and the source and * target port set to null. This example shows how to add a vertex with a * port and a black border: * * <pre> * DefaultGraphCell vertex = new DefaultGraphCell("Hello, world!"); * Map attrs = vertex.getAttributes(); * GraphConstants.setOpaque(attrs, false); * GraphConstants.setBorderColor(attrs, Color.black); * DefaultPort port = new DefaultPort(); * vertex.add(port); * port.setParent(vertex); * graph.getGraphLayoutCache().insert(vertex); * </pre> * * @param cell * inserts the specified cell in the cache */ public void insert(Object cell) { insert(new Object[] { cell }); } /** * Inserts the specified edge into the graph model. This method does in fact * nothing, it calls insert with a default connection set. * * @param edge * the edge to be inserted * @param source * the source port this edge is connected to * @param target * the target port this edge is connected to */ public void insertEdge(Object edge, Object source, Object target) { insert(new Object[] { edge }, new Hashtable(), new ConnectionSet(edge, source, target), new ParentMap()); } /** * Inserts the specified cells into the graph model. This method is a * general implementation of cell insertion. If the source and target port * are null, then no connection set is created. The method uses the * attributes from the specified edge and the egdge's children to construct * the insert call. This example shows how to insert an edge with a special * arrow between two known vertices: * * <pre> * Object source = graph.getDefaultPortForCell(sourceVertex).getCell(); * Object target = graph.getDefaultPortForCell(targetVertex).getCell(); * DefaultEdge edge = new DefaultEdge("Hello, world!"); * edge.setSource(source); * edge.setTarget(target); * Map attrs = edge.getAttributes(); * GraphConstants.setLineEnd(attrs, GraphConstants.ARROW_TECHNICAL); * graph.getGraphLayoutCache().insert(edge); * </pre> */ public void insert(Object[] cells) { insert(cells, new Hashtable(), new ConnectionSet(), new ParentMap()); } /** * Variant of the insert method that allows to pass a default connection set * and parent map and nested map. */ public void insert(Object[] cells, Map nested, ConnectionSet cs, ParentMap pm) { if (cells != null) { if (nested == null) nested = new Hashtable(); if (cs == null) cs = new ConnectionSet(); if (pm == null) pm = new ParentMap(); for (int i = 0; i < cells.length; i++) { // Using the children of the vertex we construct the parent map. int childCount = getModel().getChildCount(cells[i]); for (int j = 0; j < childCount; j++) { Object child = getModel().getChild(cells[i], j); pm.addEntry(child, cells[i]); // And add their attributes to the nested map AttributeMap attrs = getModel().getAttributes(child); if (attrs != null) nested.put(child, attrs); } // A nested map with the vertex as key // and its attributes as the value // is required for the model. Map attrsTmp = (Map) nested.get(cells[i]); Map attrs = getModel().getAttributes(cells[i]); if (attrsTmp != null) attrs.putAll(attrsTmp); nested.put(cells[i], attrs); // Check if we have parameters for a connection set. Object sourcePort = getModel().getSource(cells[i]); if (sourcePort != null) cs.connect(cells[i], sourcePort, true); Object targetPort = getModel().getTarget(cells[i]); if (targetPort != null) cs.connect(cells[i], targetPort, false); } // Create an array with the parent and its children. cells = DefaultGraphModel.getDescendants(getModel(), cells) .toArray(); // Finally call the insert method on the parent class. insert(cells, nested, cs, pm, null); } } /** * Inserts the specified cell as a parent of children. Note: All cells that * are not yet in the model will be inserted. This example shows how to * group the current selection and pass the group default bounds in case it * is later collapsed: * * <pre> * DefaultGraphCell group = new DefaultGraphCell("Hello, world!"); * Object[] cells = DefaultGraphModel.order(graph.getModel(), graph * .getSelectionCells()); * Rectangle2D bounds = graph.getCellBounds(cells); * if (bounds != null) { * bounds = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 4, * bounds.getY() + bounds.getHeight() / 4, bounds.getWidth() / 2, * bounds.getHeight() / 2); * GraphConstants.setBounds(group.getAttributes(), bounds); * } * graph.getGraphLayoutCache().insertGroup(group, cells); * </pre> */ public void insertGroup(Object group, Object[] children) { if (group != null && children != null && children.length > 0) { Map nested = new Hashtable(); // List to store all children that are not in the model List newCells = new ArrayList(children.length + 1); // Plus the group cell at pos 0 if (!getModel().contains(group)) { newCells.add(group); } // Create a parent map for the group and the children, and // store the children's attributes in the nested map. // Note: This implementation assumes that the children have // not yet been added to the group object. Therefore, // the insert method will only collect the group // attributes, but will ignore the child attributes. ParentMap pm = new ParentMap(); for (int i = 0; i < children.length; i++) { pm.addEntry(children[i], group); if (!getModel().contains(children[i])) { newCells.add(children[i]); AttributeMap attrs = getModel().getAttributes(children[i]); if (attrs != null) nested.put(children[i], attrs); } } if (newCells.isEmpty()) edit(nested, null, pm, null); else insert(newCells.toArray(), nested, null, pm); } } /** * Removes <code>cells</code> from the model. */ public void remove(Object[] cells) { graphModel.remove(cells); } /** * Removes cells from the model, including all children and connected edges * if <code>children</code> or <code>edges</code> is true, respectively. * * @param cells * The cells to remove. * @param descendants * Whether to remove all descendants as well. * @param edges * Whether to remove all connected edges as well. */ public void remove(Object[] cells, boolean descendants, boolean edges) { if (cells != null && cells.length > 0) { if (edges) { Object[] tmp = DefaultGraphModel.getEdges(getModel(), cells) .toArray(); Object[] newCells = new Object[cells.length + tmp.length]; System.arraycopy(cells, 0, newCells, 0, cells.length); System.arraycopy(tmp, 0, newCells, cells.length, tmp.length); cells = newCells; } if (descendants) cells = DefaultGraphModel.getDescendants(getModel(), cells) .toArray(); remove(cells); } } /** * Hides the specified cells with all children if <code>descandants</code> * is true. */ public void hideCells(Object[] cells, boolean descandants) { if (cells != null && cells.length > 0) { if (descandants) cells = DefaultGraphModel.getDescendants(getModel(), cells) .toArray(); setVisible(cells, false); } } /** * Shows the specified cells with all children if <code>descandants</code> * is true. */ public void showCells(Object[] cells, boolean descandants) { if (cells != null && cells.length > 0) { if (descandants) cells = DefaultGraphModel.getDescendants(getModel(), cells) .toArray(); setVisible(cells, true); } } /** * Ungroups all groups in cells and returns the children that are not ports. * Note: This replaces the parents with their group cells in the group * structure. */ public Object[] ungroup(Object[] cells) { if (cells != null && cells.length > 0) { ArrayList toRemove = new ArrayList(); ArrayList children = new ArrayList(); boolean groupExists = false; for (int i = 0; i < cells.length; i++) { boolean childExists = false; ArrayList tempPortList = new ArrayList(); for (int j = 0; j < getModel().getChildCount(cells[i]); j++) { Object child = getModel().getChild(cells[i], j); if (!getModel().isPort(child)) { children.add(child); childExists = true; } else { tempPortList.add(child); } } if (childExists) { toRemove.addAll(tempPortList); toRemove.add(cells[i]); groupExists = true; } } if (groupExists) remove(toRemove.toArray()); return children.toArray(); } return null; } /** * Toggles the collapsed state of the specified cells. * * @param cells * The cells to toggle the collapsed state for. * @param collapseOnly * Whether cells should only be collapsed. * @param expandOnly * Whether cells should only be expanded. * */ public void toggleCollapsedState(Object[] cells, boolean collapseOnly, boolean expandOnly) { List toExpand = new ArrayList(); List toCollapse = new ArrayList(); for (int i = 0; i < cells.length; i++) { Object cell = cells[i]; CellView view = getMapping(cell, false); if (view != null) { // Adds to list of expansion cells if it is a leaf in the layout // cache and we do not only want to collapse. if (view.isLeaf() && !collapseOnly) toExpand.add(view.getCell()); // Else adds to list of to-be-collapsed cells if it is not a // leaf in the layout cache we do not only want to expand. else if (!view.isLeaf() && !expandOnly) toCollapse.add(view.getCell()); } } if (!toCollapse.isEmpty() || !toExpand.isEmpty()) setCollapsedState(toCollapse.toArray(), toExpand.toArray()); } /** * Collapses all groups by hiding all their descendants. * * @param groups */ public void collapse(Object[] groups) { setCollapsedState(groups, null); } /** * Expands all groups by showing all children. (Note: This does not show all * descandants, but only the first generation of children.) */ public void expand(Object[] cells) { setCollapsedState(null, cells); } /** * Collapses and/or expands the specified cell(s) * * @param collapse * the cells to be collapsed * @param expand * the cells to be expanded */ public void setCollapsedState(Object[] collapse, Object[] expand) { // Get all descandants for the groups ConnectionSet cs = new ConnectionSet(); // Collapse cells List toHide = DefaultGraphModel.getDescendants(getModel(), collapse); if (collapse != null) { // Remove the groups themselfes for (int i = 0; i < collapse.length; i++) { toHide.remove(collapse[i]); cellWillCollapse(collapse[i]); } // Remove the ports (will be hidden automatically) for (int i = 0; i < collapse.length; i++) { int childCount = getModel().getChildCount(collapse[i]); if (childCount > 0) { for (int j = 0; j < childCount; j++) { Object child = getModel().getChild(collapse[i], j); if (getModel().isPort(child)) { toHide.remove(child); } } } } } // Expand cells Set toShow = new HashSet(); if (expand != null) { for (int i = 0; i < expand.length; i++) { int childCount = getModel().getChildCount(expand[i]); for (int j = 0; j < childCount; j++) { toShow.add(getModel().getChild(expand[i], j)); } } } setVisible(toShow.toArray(), (toHide != null) ? toHide.toArray() : null, cs); } /** * Hook for subclassers to return the first or last visible port to replace * the current source or target port of the edge. This is called when groups * are collapsed for the edges that cross the group, ie. go from a child * cell to a cell which is outside the group. This implementation returns * the first port of the parent group if source is true, otherwise it * returns the last port of the parent group. */ protected Object getParentPort(Object edge, boolean source) { // Contains the parent of the parent vertex, eg. the group Object parent = getModel().getParent( (source) ? DefaultGraphModel.getSourceVertex(getModel(), edge) : DefaultGraphModel.getTargetVertex(getModel(), edge)); // Finds a port in the group int c = getModel().getChildCount(parent); for (int i = (source) ? c - 1 : 0; i < getModel().getChildCount(parent) && i >= 0; i += (source) ? -1 : +1) { Object child = getModel().getChild(parent, i); if (getModel().isPort(child)) { return child; } } return null; } /** * Hook for subclassers to return the port to be used for edges that have * been connected to the group. This is called from expand. This returns the * first port of the first or last vertex depending on <code>source</code>. */ protected Object getChildPort(Object edge, boolean source) { GraphModel model = getModel(); // Contains the parent of the port, eg. the group Object parent = (source) ? DefaultGraphModel.getSourceVertex(model, edge) : DefaultGraphModel.getTargetVertex(model, edge); // Finds a vertex in the group int c = model.getChildCount(parent); for (int i = (source) ? c - 1 : 0; i < c && i >= 0; i += (source) ? -1 : +1) { Object child = model.getChild(parent, i); if (!model.isEdge(child) && !model.isPort(child)) { // Finds a port in the vertex for (int j = 0; j < model.getChildCount(child); j++) { Object port = model.getChild(child, j); if (model.isPort(port)) { return port; } } } } return null; } /** * Applies the <code>propertyMap</code> and the connection changes to the * model. The initial <code>edits</code> that triggered the call are * considered to be part of this transaction. Notifies the model- and undo * listeners of the change. Note: The passed in attributes may contain * PortViews. */ public void edit(Map attributes, ConnectionSet cs, ParentMap pm, UndoableEdit[] e) { if (attributes != null || cs != null || pm != null || e != null) { Object[] visible = null; if (isPartial() && showsInvisibleEditedCells) { Set tmp = new HashSet(); if (attributes != null) tmp.addAll(attributes.keySet()); if (cs != null) tmp.addAll(cs.getChangedEdges()); if (pm != null) tmp.addAll(pm.getChangedNodes()); tmp.removeAll(visibleSet); if (!tmp.isEmpty()) visible = tmp.toArray(); } GraphLayoutCacheEdit edit = createLocalEdit(null, attributes, visible, null); if (edit != null) e = augment(e, edit); // Pass to model graphModel.edit(attributes, cs, pm, e); } } /** * A shortcut method that takes a nested map and passes it to the edit * method. */ public void edit(Map attributes) { edit(attributes, null, null, null); } /** * Applies the <code>attributes</code> to all <code>cells</code> by * creating a map that contains the attributes for each cell and passing it * to edit on this layout cache. Example: * * <pre> * Map attrs = new java.util.Hashtable(); * GraphConstants.setBackground(attrs, Color.RED); * graph.getGraphLayoutCache().edit(graph.getSelectionCells(), attrs); * </pre> */ public void edit(Object[] cells, Map attributes) { if (attributes != null && cells != null && cells.length > 0) { Map nested = new Hashtable(); for (int i = 0; i < cells.length; i++) nested.put(cells[i], attributes); edit(nested, null, null, null); } } /** * Applies the <code>attributes</code> to a single <code>cell</code> by * creating a map that contains the attributes for this cell and passing it * to edit on this layout cache. Example: * * <pre> * Map attrs = new java.util.Hashtable(); * GraphConstants.setBackground(attrs, Color.RED); * graph.getGraphLayoutCache().editCell(graph.getSelectionCell(), attrs); * </pre> */ public void editCell(Object cell, Map attributes) { if (attributes != null && cell != null) { edit(new Object[] { cell }, attributes); } } protected UndoableEdit[] augment(UndoableEdit[] e, UndoableEdit edit) { if (edit != null) { int size = (e != null) ? e.length + 1 : 1; UndoableEdit[] result = new UndoableEdit[size]; if (e != null) System.arraycopy(e, 0, result, 0, size - 2); result[size - 1] = edit; return result; } return e; } /** * Sends <code>cells</code> to back. Note: This expects an array of cells! */ public void toBack(Object[] cells) { if (cells != null && cells.length > 0) { graphModel.toBack(cells); } } /** * Brings <code>cells</code> to front. Note: This expects an array of * cells! */ public void toFront(Object[] cells) { if (cells != null && cells.length > 0) { graphModel.toFront(cells); } } /** * Creates a local edit for the specified change. A local operation contains * all visibility changes, as well as all changes to attributes that are * local, and all control attributes. <br> * Note: You must use cells as keys for the nested map, not cell views. */ protected GraphLayoutCacheEdit createLocalEdit(Object[] inserted, Map nested, Object[] visible, Object[] invisible) { // Create an edit if there are any view-local attributes set if ((nested != null && !nested.isEmpty()) && (!localAttributes.isEmpty() || isAllAttributesLocal())) { // Move or Copy Local Attributes to Local View Map globalMap = new Hashtable(); Map localMap = new Hashtable(); Map localAttr; Iterator it = nested.entrySet().iterator(); while (it.hasNext()) { localAttr = new Hashtable(); Map.Entry entry = (Map.Entry) it.next(); // (cell, Hashtable) Object cell = entry.getKey(); Map attr = (Map) entry.getValue(); // Create Difference of Existing and New Attributes CellView tmpView = getMapping(cell, false); if (tmpView != null) attr = tmpView.getAllAttributes().diff(attr); // End of Diff Iterator it2 = attr.entrySet().iterator(); while (it2.hasNext()) { Map.Entry entry2 = (Map.Entry) it2.next(); // (key, value) Object key = entry2.getKey(); Object value = entry2.getValue(); boolean isControlAttribute = isControlAttribute(cell, key, value); if (isAllAttributesLocal() || isControlAttribute || isLocalAttribute(cell, key, value)) { localAttr.put(key, value); if (!isControlAttribute) it2.remove(); } } if (!localAttr.isEmpty()) localMap.put(cell, localAttr); if (!attr.isEmpty()) globalMap.put(cell, attr); } nested.clear(); nested.putAll(globalMap); if (visible != null || invisible != null || !localMap.isEmpty()) { GraphLayoutCacheEdit edit = new GraphLayoutCacheEdit(inserted, new Hashtable(localMap), visible, invisible); edit.end(); return edit; } } else if (visible != null || invisible != null) { GraphLayoutCacheEdit edit = new GraphLayoutCacheEdit(inserted, null, visible, invisible); edit.end(); return edit; } return null; } /** * Returns true if the set of local attributes contains <code>key</code> */ protected boolean isLocalAttribute(Object cell, Object key, Object value) { return localAttributes.contains(key); } /** * Returns true if <code>key</code> is a control attribute */ protected boolean isControlAttribute(Object cell, Object key, Object value) { return GraphConstants.REMOVEALL.equals(key) || GraphConstants.REMOVEATTRIBUTES.equals(key); } /** * Handles the removal of view local attributes. Since these attributes are * only being stored in the view, the option is provided to copy the values * for that key into the model. Without this, those values are lost. * * @param key * the key of the view local attribute * @param addToModel * whether or not to move the attribute values to the graph model * @param override * whether or not to override the key's value in the model cell's * attribute map if it exists * @return whether or not the operation completed sucessfully */ public boolean removeViewLocalAttribute(Object key, boolean addToModel, boolean override) { if (localAttributes.contains(key)) { if (addToModel) { // Iterate through all views copying this attribute to the // cell. copyRemovedViewValue(key, addToModel, override, mapping .values()); copyRemovedViewValue(key, addToModel, override, hiddenMapping .values()); } localAttributes.remove(key); return true; } return false; } /** * Helper method to copy removed view local attributes to model cell's * * @param key * the key of the view local attribute * @param addToModel * whether or not to move the attribute values to the graph model * @param override * whether or not to override the key's value in the model cell's * attribute map if it exists * @param coll * the current collection being analysed */ private void copyRemovedViewValue(Object key, boolean addToModel, boolean override, Collection coll) { Iterator iter = coll.iterator(); while (iter.hasNext()) { CellView cellView = (CellView) iter.next(); Map attributes = cellView.getAttributes(); if (attributes.containsKey(key)) { if (addToModel) { Object cell = cellView.getCell(); Map cellAttributes = graphModel.getAttributes(cell); if (cellAttributes != null) { boolean cellContainsKey = cellAttributes .containsKey(key); // Write the model cell's attribute map key // if overriding is enabled or if key doesn't // exist if (!override || !cellContainsKey) { Object value = attributes.get(key); cellAttributes.put(key, value); } } } attributes.remove(key); } } } /** * An implementation of GraphLayoutCacheChange. */ public class GraphLayoutCacheEdit extends CompoundEdit implements GraphLayoutCacheEvent.GraphLayoutCacheChange { protected Object[] cells, previousCells = null; protected CellView[] context, hidden; protected Map attributes, previousAttributes; protected Object[] visible, invisible; /** * The dirty region associated with this event prior to the change */ protected Rectangle2D dirtyRegion = null; // Remember which cells have changed for finding their context protected Set changedCells = new HashSet(); /** * Constructs a GraphViewEdit. This modifies the attributes of the * specified views and may be used to notify UndoListeners. * * @param nested * the map that defines the new attributes */ public GraphLayoutCacheEdit(Map nested) { this(null, nested, null, null); attributes = nested; } /** * Constructs a GraphViewEdit. This modifies the attributes of the * specified views and may be used to notify UndoListeners. This should * also take an array of removed cell views, but it is not possible to * add further UndoableEdits to an already executed CompoundEdit, such * as a GraphModel change. Thus, to handle implicit changes -- rather * than piggybacking on the model's event -- the CompoundEdit's addEdit * method should be extended to accept and instantly execute sub- * sequent edits (implicit changes to the view, such as removing a * mapping, hiding a view or the like). * * @param inserted * an array of inserted cells * @param attributes * the map that defines the new attributes * @param visible * an array defining which cells are visible * @param invisible * an array defining which cells are invisible */ public GraphLayoutCacheEdit(Object[] inserted, Map attributes, Object[] visible, Object[] invisible) { super(); this.attributes = attributes; this.previousAttributes = attributes; this.cells = inserted; this.visible = visible; this.invisible = invisible; } public Object getSource() { return GraphLayoutCache.this; } public boolean isSignificant() { return true; } /** * Returns the cell views that have changed. */ public Object[] getChanged() { return changedCells.toArray(); } /** * Returns the cells that habe been made visible. */ public Object[] getInserted() { return invisible; } /** * Returns the cells that have changed. */ public Object[] getRemoved() { return visible; } /** * Returns the views that have not changed explicitly, but implicitly * because one of their dependent cells has changed. */ public Object[] getContext() { return context; } /** * Returns a map of (cell view, attribute) pairs. */ public Map getAttributes() { return attributes; } /** * Returns a map of (cell view, attribute) pairs. */ public Map getPreviousAttributes() { return previousAttributes; } public Rectangle2D getDirtyRegion() { return dirtyRegion; } public void setDirtyRegion(Rectangle2D dirty) { this.dirtyRegion = dirty; } /** * Redoes a change. * * @exception CannotRedoException * if the change cannot be redone */ public void redo() throws CannotRedoException { super.redo(); execute(); } /** * Undoes a change. * * @exception CannotUndoException * if the change cannot be undone */ public void undo() throws CannotUndoException { super.undo(); execute(); } /** * Execute this edit such that the next invocation to this method will * invert the last execution. */ public void execute() { GraphModel model = getModel(); changedCells.clear(); // Remember or restore hidden cells if (hidden != null) for (int i = 0; i < hidden.length; i++) if (hidden[i] != null) mapping.put(hidden[i].getCell(), hidden[i]); if (invisible != null && invisible.length >0) { CellView[] invisibleViews = new CellView[invisible.length]; invisibleViews = getMapping(invisible, true); Rectangle2D changedBounds = getBounds(invisibleViews); dirtyRegion = RectUtils.union(dirtyRegion, changedBounds); } if (!remembersCellViews) // already remembered hidden = getMapping(invisible); // Handle visibility boolean updatePorts = setVisibleImpl(visible, true) | setVisibleImpl(invisible, false); if (visible != null) { for (int i = 0; i < visible.length; i++) { changedCells.add(visible[i]); // Only calls if not inserted if (cells == null) cellExpanded(visible[i]); } } if (invisible != null) for (int i = 0; i < invisible.length; i++) changedCells.add(invisible[i]); // Swap arrays Object[] tmp = visible; visible = invisible; invisible = tmp; // Handle attributes if (attributes != null) { previousAttributes = attributes; changedCells.addAll(attributes.keySet()); } if (updatePorts) updatePorts(); // Add ancestor cells to changed cells Set parentSet = new HashSet(); Iterator it = changedCells.iterator(); while (it.hasNext()) { Object parent = model.getParent(it.next()); while (parent != null) { parentSet.add(parent); parent = model.getParent(parent); } } changedCells.addAll(parentSet); Set ctx = DefaultGraphModel.getEdges(getModel(), changedCells .toArray()); context = getMapping(ctx.toArray()); Set allChangedCells = new HashSet(changedCells); allChangedCells.addAll(ctx); CellView[] allChangedCellViews = getMapping(allChangedCells.toArray()); Rectangle2D changedBounds = getBounds(allChangedCellViews); dirtyRegion = RectUtils.union(dirtyRegion, changedBounds); if (attributes != null) { attributes = handleAttributes(attributes); } // Refresh all changed cells refresh(getMapping(changedCells.toArray(), false), false); // Updates the connected edges. Make sure that changedCells // contains no edges, as these will be removed from the result. refresh(context, false); tmp = cells; cells = previousCells; previousCells = tmp; reloadRoots(); fireGraphLayoutCacheChanged(GraphLayoutCache.this, this); } } /** * Called when a child has been made visible by expanding its parent. This * implementation translates the child so that it reflects the offset of the * parent group since the child was last visible (see * {@link #movesChildrenOnExpand}). */ protected void cellExpanded(Object cell) { GraphModel model = getModel(); // Moves the child to the group origin if it is not a port if (movesChildrenOnExpand && !model.isPort(cell)) { CellView view = getMapping(cell, false); if (view != null) { CellView parent = getMapping(model.getParent(cell), false); if (parent != null) { if (DefaultGraphModel.isVertex(model, parent)) { // Computes the offset of the parent group Rectangle2D src = GraphConstants.getBounds(parent .getAllAttributes()); Rectangle2D rect = parent.getBounds(); double dx = src.getX() - rect.getX(); double dy = src.getY() - rect.getY(); // Gets the attributes from the cell view or // cell and translates the bounds or points AttributeMap attrs = view.getAttributes(); if (!attrs.contains(GraphConstants.BOUNDS)) attrs = model.getAttributes(view.getCell()); attrs.translate(dx, dy); } } } } } protected void cellWillCollapse(Object cell) { GraphModel model = getModel(); if (movesParentsOnCollapse) { CellView view = getMapping(cell, false); if (view != null && !view.isLeaf()) { // Uses view-local attribute if available AttributeMap attrs = view.getAttributes(); if (!attrs.contains(GraphConstants.BOUNDS) && !localAttributes.contains(GraphConstants.BOUNDS)) attrs = model.getAttributes(cell); // Moves the group to the origin of the children Rectangle2D src = GraphConstants.getBounds(attrs); Rectangle2D b = view.getBounds(); // FIXME: What if the group is exactly at "defaultBounds"? if (resizesParentsOnCollapse || src == null || src.equals(VertexView.defaultBounds)) { src = attrs.createRect(b.getX(), b.getY(), b.getWidth() * collapseXScale, b.getHeight() * collapseYScale); attrs.applyValue(GraphConstants.BOUNDS, src); } else { src.setFrame(b.getX(), b.getY(), src.getWidth(), src .getHeight()); } } } } /** * Attention: Undo will not work for routing-change if ROUTING and POINTS * are stored in different locations. This happens if the model holds the * routing attribute and the routing changes from unrouted to routed. In * this case the points in the view are already routed according to the new * scheme when written to the command history (-> no undo). */ protected Map handleAttributes(Map attributes) { Map undo = new Hashtable(); CellView[] views = new CellView[attributes.size()]; Iterator it = attributes.entrySet().iterator(); int i = 0; while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); CellView cv = getMapping(entry.getKey(), false); views[i] = cv; i += 1; if (cv != null && cv.getAttributes() != null) { Map deltaNew = (Map) entry.getValue(); // System.out.println("state=" + cv.getAttributes()); // System.out.println("change=" + deltaNew); Map deltaOld = cv.getAttributes().applyMap(deltaNew); cv.refresh(this, this, false); // System.out.println("state'=" + cv.getAttributes()); // System.out.println("change'=" + deltaOld); undo.put(cv.getCell(), deltaOld); } } // Re-route all child edges update(views); return undo; } // // Static Methods // /** * Translates the specified views by the given amount. * * @param views * an array of cell view to each be translated * @param dx * the amount to translate the views in the x-axis * @param dy * the amount to translate the views in the x-axis */ public static void translateViews(CellView[] views, double dx, double dy) { for (int i = 0; i < views.length; i++) { if (views[i] instanceof AbstractCellView) { ((AbstractCellView) views[i]).translate(dx, dy); } } } /** * Returns a collection of cells that are connected to the specified cell by * edges. Any cells specified in the exclude set will be ignored. * * @param cell * The cell from which the neighbours will be determined * @param exclude * The set of cells to ignore when searching * @param directed * whether or not direction of edges should be taken into account * @param visibleCells * whether or not to only consider visible cells * @return Returns the list of neighbours for <code>cell</code> */ public List getNeighbours(Object cell, Set exclude, boolean directed, boolean visibleCells) { // Traverse Graph GraphModel model = getModel(); Object[] fanout = (directed) ? DefaultGraphModel.getOutgoingEdges( model, cell) : DefaultGraphModel.getEdges(model, new Object[] { cell }).toArray(); List neighbours = new ArrayList(fanout.length); Set localExclude = new HashSet(fanout.length + 8, (float) 0.75); for (int i = 0; i < fanout.length; i++) { // if only visible cells are being processed, check that this // edge is visible before looking for neighbours with it if (!visibleCells || isVisible(fanout[i])) { Object neighbour = DefaultGraphModel.getOpposite(model, fanout[i], cell); if (neighbour != null && (exclude == null || !exclude.contains(neighbour)) && !localExclude.contains(neighbour) && (!visibleCells || isVisible(neighbour))) { localExclude.add(neighbour); neighbours.add(neighbour); } } } return neighbours; } /** * Returns the outgoing edges for cell. Cell should be a port or a vertex. * * @param cell * The cell from which the outgoing edges will be determined * @param exclude * The set of edges to ignore when searching * @param visibleCells * whether or not only visible cells should be processed * @param selfLoops * whether or not to include self loops in the returned list * @return Returns the list of outgoing edges for <code>cell</code> */ public List getOutgoingEdges(Object cell, Set exclude, boolean visibleCells, boolean selfLoops) { return getEdges(cell, exclude, visibleCells, selfLoops, false); } /** * Returns the incoming edges for cell. Cell should be a port or a vertex. * * @param cell * The cell from which the incoming edges will be determined * @param exclude * The set of edges to ignore when searching * @param visibleCells * whether or not only visible cells should be processed * @param selfLoops * whether or not to include self loops in the returned list * @return Returns the list of incoming edges for <code>cell</code> */ public List getIncomingEdges(Object cell, Set exclude, boolean visibleCells, boolean selfLoops) { return getEdges(cell, exclude, visibleCells, selfLoops, true); } /** * Returns the incoming or outgoing edges for cell. Cell should be a port or * a vertex. * * @param cell * The cell from which the edges will be determined * @param exclude * The set of edges to ignore when searching * @param visibleCells * whether or not only visible cells should be processed * @param selfLoops * whether or not to include self loops in the returned list * @param incoming * <code>true</code> if incoming edges are to be obtained, * <code>false</code> if outgoing edges are to be obtained * @return Returns the list of incoming or outgoing edges for * <code>cell</code> */ protected List getEdges(Object cell, Set exclude, boolean visibleCells, boolean selfLoops, boolean incoming) { GraphModel model = getModel(); Object[] edges = DefaultGraphModel.getEdges(model, cell, incoming); List edgeList = new ArrayList(edges.length); Set localExclude = new HashSet(edges.length); for (int i = 0; i < edges.length; i++) { // Check that the edge is neiter in the passed in exclude set or // the local exclude set. Also, if visibleCells is true check // the edge is visible in the cache. if ((exclude == null || !exclude.contains(edges[i])) && !localExclude.contains(edges[i]) && (!visibleCells || isVisible(edges[i]))) { // Add the edge to the list if all edges, including self loops // are allowed. If self loops are not allowed, ensure the // source and target of the edge are different if (selfLoops == true || model.getSource(edges[i]) != model .getTarget(edges[i])) { edgeList.add(edges[i]); } localExclude.add(edges[i]); } } return edgeList; } /** * Returns all views, shortcut to getAllDescendants(getRoots()) */ public CellView[] getAllViews() { return getAllDescendants(getRoots()); } /** * Returns all views, including descendants that have a parent in * <code>views</code>, especially the PortViews. Note: Iterative * Implementation using model.getChild and getMapping on this cell mapper. */ public CellView[] getAllDescendants(CellView[] views) { Stack stack = new Stack(); for (int i = 0; i < views.length; i++) if (views[i] != null) stack.add(views[i]); ArrayList result = new ArrayList(); while (!stack.isEmpty()) { CellView tmp = (CellView) stack.pop(); Object[] children = tmp.getChildViews(); for (int i = 0; i < children.length; i++) stack.add(children[i]); result.add(tmp); // Add Port Views for (int i = 0; i < graphModel.getChildCount(tmp.getCell()); i++) { Object child = graphModel.getChild(tmp.getCell(), i); if (graphModel.isPort(child)) { CellView view = getMapping(child, false); if (view != null) stack.add(view); } } } CellView[] ret = new CellView[result.size()]; result.toArray(ret); return ret; } /** * Returns the hiddenMapping. * * @return Map */ public Map getHiddenMapping() { return hiddenMapping; } /** * Sets the showsExistingConnections * * @param showsExistingConnections */ public void setShowsExistingConnections(boolean showsExistingConnections) { this.showsExistingConnections = showsExistingConnections; } /** * Returns the showsExistingConnections. * * @return boolean */ public boolean isShowsExistingConnections() { return showsExistingConnections; } /** * Sets the showsInsertedConnections * * @param showsInsertedConnections */ public void setShowsInsertedConnections(boolean showsInsertedConnections) { this.showsInsertedConnections = showsInsertedConnections; } /** * Returns the showsInsertedConnections. * * @return boolean */ public boolean isShowsInsertedConnections() { return showsInsertedConnections; } /** * Sets the hidesExistingConnections * * @param hidesExistingConnections */ public void setHidesExistingConnections(boolean hidesExistingConnections) { this.hidesExistingConnections = hidesExistingConnections; } /** * Returns the hidesExistingConnections. * * @return boolean */ public boolean isHidesExistingConnections() { return hidesExistingConnections; } /** * Sets the hidesDanglingConnections * * @param hidesDanglingConnections */ public void setHidesDanglingConnections(boolean hidesDanglingConnections) { this.hidesDanglingConnections = hidesDanglingConnections; } /** * Returns the hidesDanglingConnections. * * @return boolean */ public boolean isHidesDanglingConnections() { return hidesDanglingConnections; } /** * Sets the rememberCellViews. * * @param rememberCellViews * The rememberCellViews to set */ public void setRemembersCellViews(boolean rememberCellViews) { this.remembersCellViews = rememberCellViews; } /** * Returns the remembersCellViews. * * @return boolean */ public boolean isRemembersCellViews() { return remembersCellViews; } /** * Sets the hiddenSet. * * @param hiddenSet * The hiddenSet to set */ public void setHiddenSet(Map hiddenSet) { this.hiddenMapping = hiddenSet; } /** * @return Returns the localAttributes. */ public Set getLocalAttributes() { return localAttributes; } /** * @param localAttributes * The localAttributes to set. */ public void setLocalAttributes(Set localAttributes) { this.localAttributes = localAttributes; } /** * @return Returns the askLocalAttribute. */ public boolean isAllAttributesLocal() { return allAttributesLocal; } /** * @param allAttributesLocal * The allAttributesLocal to set. */ public void setAllAttributesLocal(boolean allAttributesLocal) { this.allAttributesLocal = allAttributesLocal; } /** * Returns true if cells should be auto-sized when their values change * * @return true if cells should be auto-sized when their values change */ public boolean isAutoSizeOnValueChange() { return autoSizeOnValueChange; } /** * Determines whether cells should be auto-sized when their values change. * Fires a property change event if the new setting is different from the * existing setting. * * @param flag * a boolean value, true if cells should be auto-sized when their * values change */ public void setAutoSizeOnValueChange(boolean flag) { this.autoSizeOnValueChange = flag; } /** * @return Returns the selectsAllInsertedCells. */ public boolean isSelectsAllInsertedCells() { return selectsAllInsertedCells; } /** * @param selectsAllInsertedCells * The selectsAllInsertedCells to set. */ public void setSelectsAllInsertedCells(boolean selectsAllInsertedCells) { this.selectsAllInsertedCells = selectsAllInsertedCells; } /** * @return Returns the selectsLocalInsertedCells. */ public boolean isSelectsLocalInsertedCells() { return selectsLocalInsertedCells; } /** * @param selectsLocalInsertedCells * The selectsLocalInsertedCells to set. */ public void setSelectsLocalInsertedCells(boolean selectsLocalInsertedCells) { this.selectsLocalInsertedCells = selectsLocalInsertedCells; } /** * @return Returns the reconnectsEdgesToVisibleParent. * @deprecated edges are moved to parent view and back automatically */ public boolean isReconnectsEdgesToVisibleParent() { return reconnectsEdgesToVisibleParent; } /** * @param reconnectsEdgesToVisibleParent * The reconnectsEdgesToVisibleParent to set. * @deprecated edges are moved to parent view and back automatically */ public void setReconnectsEdgesToVisibleParent( boolean reconnectsEdgesToVisibleParent) { this.reconnectsEdgesToVisibleParent = reconnectsEdgesToVisibleParent; } /** * @return Returns the showsChangedConnections. */ public boolean isShowsChangedConnections() { return showsChangedConnections; } /** * @param showsChangedConnections * The showsChangedConnections to set. */ public void setShowsChangedConnections(boolean showsChangedConnections) { this.showsChangedConnections = showsChangedConnections; } /** * @return Returns the moveChildrenOnExpand. */ public boolean isMovesChildrenOnExpand() { return movesChildrenOnExpand; } /** * @param moveChildrenOnExpand * The moveChildrenOnExpand to set. */ public void setMovesChildrenOnExpand(boolean moveChildrenOnExpand) { this.movesChildrenOnExpand = moveChildrenOnExpand; } /** * @return Returns the collapseXScale. */ public double getCollapseXScale() { return collapseXScale; } /** * @param collapseXScale * The collapseXScale to set. */ public void setCollapseXScale(double collapseXScale) { this.collapseXScale = collapseXScale; } /** * @return Returns the collapseYScale. */ public double getCollapseYScale() { return collapseYScale; } /** * @param collapseYScale * The collapseYScale to set. */ public void setCollapseYScale(double collapseYScale) { this.collapseYScale = collapseYScale; } /** * @return Returns the movesParentsOnCollapse. */ public boolean isMovesParentsOnCollapse() { return movesParentsOnCollapse; } /** * @param movesParentsOnCollapse * The movesParentsOnCollapse to set. */ public void setMovesParentsOnCollapse(boolean movesParentsOnCollapse) { this.movesParentsOnCollapse = movesParentsOnCollapse; } /** * @return Returns the resizesParentsOnCollapse. */ public boolean isResizesParentsOnCollapse() { return resizesParentsOnCollapse; } /** * @param resizesParentsOnCollapse * The resizesParentsOnCollapse to set. */ public void setResizesParentsOnCollapse(boolean resizesParentsOnCollapse) { this.resizesParentsOnCollapse = resizesParentsOnCollapse; } /** * Serialization support. */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); // Write out the hidden mapping Map map = new Hashtable(hiddenMapping); s.writeObject(map); } /** * Serialization support. */ private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); // Read the hidden mapping Map map = (Map) s.readObject(); hiddenMapping = new WeakHashMap(map); } }