/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.ui.swing; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.Vector; import javax.swing.SwingUtilities; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import net.wotonomy.control.EOClassDescription; import net.wotonomy.control.EODataSource; import net.wotonomy.control.EODelayedObserver; import net.wotonomy.control.EOEditingContext; import net.wotonomy.control.EOObserverCenter; import net.wotonomy.control.EOObserverProxy; import net.wotonomy.control.OrderedDataSource; import net.wotonomy.control.PropertyDataSource; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSMutableArray; import net.wotonomy.foundation.NSSelector; import net.wotonomy.foundation.internal.WotonomyException; import net.wotonomy.ui.EOAssociation; import net.wotonomy.ui.EODisplayGroup; /** * TreeModelAssociation binds a JTree or similar component that uses a TreeModel * to a display group's list of displayable objects, each of which may have a * list of child objects managed by another display group, and so on. * TreeModelAssociation works exactly like a ListAssociation, with the * additional capability to specify a "Children" aspect, that will allow child * objects to be retrieved from a parent display group. * * * * This class acts as the TreeModel for the controlled component: calling * yourcomponent.getModel() will return this association. The tree model methods * on this class are public and may be used to affect changes on the controlled * components.
*
* * The titles display group's contents are inserted into a new display group * that acts as the root node. After that point, changes in the titles display * group will cause the tree model to reset itself, creating a new display group * for the root node.
*
* * If a separate display group is bound to the children aspect, it will be used * to hold the selected objects and their siblings and selection will be * maintained there, and the titles display group selection will not be updated. * Any editing or detail associations should in that case be attached to the * children display group, not the titles group.
*
* * Each node in the tree is an EODisplayGroup that contains the child objects of * the object it represents in the tree. These objects can be programmatically * inserted, updated, or removed using DisplayGroup methods. Each node's takes * its parent group's sortOrderings until a sort ordering is explicitly * specified - setting a sort ordering to null will resume using the parent * group's sort ordering.
*
* * Each node in the tree also implements MutableTreeNode. The value that a node * represents is the titles property value of the object in the parent's * displayed objects list at the index corresponding to the index of the node. * Calling toString on a node returns the string representation of the titles * property value, and setUserObject will update that value directly in the * corresponding object. Moving a node from one parent to another will remove * the actual object in the parent display group and insert it into the * destination display group.
*
* * In short, any nodes obtained from this class' implementation of TreeModel may * be cast as either EODisplayGroup or MutableTreeNode and maybe be * programmatically manipulated in either manner. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ public class TreeModelAssociation extends EOAssociation implements TreeModel, TreeSelectionListener { static final NSArray aspects = new NSArray(new Object[] { TitlesAspect, ChildrenAspect, IsLeafAspect }); static final NSArray aspectSignatures = new NSArray(new Object[] { AttributeToOneAspectSignature }); static final NSArray objectKeysTaken = new NSArray(new Object[] { "model" }); private final static NSSelector getSelectionModel = new NSSelector("getSelectionModel"); private final static NSSelector setModel = new NSSelector("setModel", new Class[] { TreeModel.class }); EODisplayGroup titlesDisplayGroup, childrenDisplayGroup, leafDisplayGroup; String titlesKey, childrenKey, leafKey; DisplayGroupNode rootNode; Vector listeners; Object rootLabel; TreeSelectionModel selectionModel; boolean selectionPaintedImmediately; boolean insertingChild; boolean insertingAfter; EOObserverProxy recentChangesObserver; private boolean pleaseSelectRootNode; /** * Constructor expecting a JTree or any other object that has void * setModel(TreeModel) and TreeModel getSelectionModel() methods. This tree * association will be used for the TreeModel. The root node will be labeled * "Root".
*
* * As an alternate way to use a TreeModelAssociation, you may pass a * TreeSelectionModel to the constructor and then manually set your component to * use this class as its TreeModel. */ public TreeModelAssociation(Object anObject) { super(anObject); titlesDisplayGroup = null; titlesKey = null; childrenDisplayGroup = null; childrenKey = null; leafDisplayGroup = null; leafKey = null; listeners = new Vector(); selectionPaintedImmediately = false; // after display group nodes process recent changes recentChangesObserver = new EOObserverProxy(this, new NSSelector("processRecentChanges"), EODelayedObserver.ObserverPrioritySixth); EOObserverCenter.addObserver(recentChangesObserver, this); insertingChild = true; insertingAfter = true; pleaseSelectRootNode = false; rootLabel = "Root"; rootNode = createNode(null, null); } /** * Constructor expecting a JTree or similar component and specifying a label for * the root node. */ public TreeModelAssociation(Object anObject, Object aRootLabel) { this(anObject); rootLabel = aRootLabel; rootNode.setUserObject(aRootLabel); } /** * Gets the current root label. */ public Object rootLabel() { return rootLabel; } /** * Gets the current root label. */ public Object getRootLabel() { return rootLabel(); } /** * Sets the root label. */ public void setRootLabel(Object aLabel) { rootLabel = aLabel; } /** * Returns a List of aspect signatures whose contents correspond with the * aspects list. Each element is a string whose characters represent a * capability of the corresponding aspect. * * An empty signature "" means that the aspect can bind without needing a key. * This implementation returns "A1M" for each element in the aspects array. */ public static NSArray aspectSignatures() { return aspectSignatures; } /** * Returns a List that describes the aspects supported by this class. Each * element in the list is the string name of the aspect. This implementation * returns an empty list. */ public static NSArray aspects() { return aspects; } /** * Returns a List of EOAssociation subclasses that, for the objects that are * usable for this association, are less suitable than this association. */ public static NSArray associationClassesSuperseded() { return new NSArray(); } /** * Returns whether this class can control the specified object. */ public static boolean isUsableWithObject(Object anObject) { return setModel.implementedByObject(anObject); } /** * Returns a List of properties of the controlled object that are controlled by * this class. For example, "stringValue", or "selected". */ public static NSArray objectKeysTaken() { return objectKeysTaken; } /** * Returns the aspect that is considered primary or default. This is typically * "value" or somesuch. */ public static String primaryAspect() { return TitlesAspect; } /** * Returns whether this association can bind to the specified display group on * the specified key for the specified aspect. */ public boolean canBindAspect(String anAspect, EODisplayGroup aDisplayGroup, String aKey) { return (aspects.containsObject(anAspect)); } /** * Binds the specified aspect of this association to the specified key on the * specified display group. */ public void bindAspect(String anAspect, EODisplayGroup aDisplayGroup, String aKey) { if (TitlesAspect.equals(anAspect)) { titlesDisplayGroup = aDisplayGroup; titlesKey = aKey; } if (ChildrenAspect.equals(anAspect)) { childrenDisplayGroup = aDisplayGroup; childrenKey = aKey; } if (IsLeafAspect.equals(anAspect)) { leafDisplayGroup = aDisplayGroup; leafKey = aKey; } if (childrenDisplayGroup == null) { childrenDisplayGroup = titlesDisplayGroup; } super.bindAspect(anAspect, aDisplayGroup, aKey); } /** * Establishes a connection between this association and the controlled object. * Subclasses should begin listening for events from their controlled object * here. */ public void establishConnection() { if (titlesDisplayGroup == null) { throw new WotonomyException("TreeModelAssociation: Titles aspect must be bound"); } // populate the root node rootNode = createNode(titlesDisplayGroup, null); rootNode.setObjectArray(titlesDisplayGroup.displayedObjects()); rootNode.setSortOrderings(titlesDisplayGroup.sortOrderings()); EODataSource dataSource = childrenDisplayGroup.dataSource(); if (dataSource == null) dataSource = titlesDisplayGroup.dataSource(); while (dataSource instanceof DelegatingTreeDataSource) { // unwrap any existing delegating data sources dataSource = ((DelegatingTreeDataSource) dataSource).delegateDataSource; } // create a new delegating data source childrenDisplayGroup.setDataSource(new DelegatingTreeDataSource(this, dataSource)); // TODO: find out why omitting this line causes weird selection behavior childrenDisplayGroup.setSortOrderings(new NSArray()); // check for alternate usage if (object() instanceof TreeSelectionModel) { selectionModel = (TreeSelectionModel) object(); } else // use specified object { try { setModel.invoke(object(), new Object[] { this }); selectionModel = (TreeSelectionModel) getSelectionModel.invoke(object(), new Object[] {}); } catch (Exception exc) { throw new WotonomyException(exc); } } addAsListener(); super.establishConnection(); /* * fireRootStructureChanged(); * * // titlesGroupChanged = true; // subjectChanged(); * * // update the children group removeAsListener(); * childrenDisplayGroup.fetch(); addAsListener(); * * // update selection selectFromDisplayGroup( titlesDisplayGroup ); */ } protected void fireRootStructureChanged() { int count = rootNode.displayedObjects().count(); int[] childIndices = new int[count]; Object[] children = new Object[count]; for (int i = 0; i < count; i++) { childIndices[i] = i; children[i] = rootNode.getChildNodeAt(i); } // must fire a tree structure changed with children, // otherwise the tree gets weird selection behavior fireTreeStructureChanged(this, new Object[] { rootNode }, childIndices, children); } /** * Breaks the connection between this association and its object. Override to * stop listening for events from the object. */ public void breakConnection() { if (childrenDisplayGroup != null) { if (childrenDisplayGroup.dataSource() instanceof DelegatingTreeDataSource) { if (titlesDisplayGroup == childrenDisplayGroup) { titlesDisplayGroup.setDataSource( ((DelegatingTreeDataSource) childrenDisplayGroup.dataSource()).delegateDataSource); } else { childrenDisplayGroup.setDataSource(null); } } } removeAsListener(); super.breakConnection(); } protected void addAsListener() { isListening = true; selectionModel.addTreeSelectionListener(this); } protected void removeAsListener() { isListening = false; selectionModel.removeTreeSelectionListener(this); } protected boolean isListening = false; private boolean pleaseIgnore = false; protected boolean titlesGroupChanged = false; protected boolean childrenGroupChanged = false; /** * Overridden to better discriminate what is changed. */ public void objectWillChange(Object anObject) { if (!isListening) return; if (anObject == titlesDisplayGroup) { titlesGroupChanged = true; } if (anObject == childrenDisplayGroup) { childrenGroupChanged = true; if (childrenDisplayGroup.qualifier() != null) { if ((rootNode.qualifier() == null) || !childrenDisplayGroup.qualifier().equals(rootNode.qualifier())) { // quietly move qualifier from children group to root node rootNode.setQualifier(childrenDisplayGroup.qualifier()); childrenDisplayGroup.setQualifier(null); rootNode.updateDisplayedObjects(); } } } super.objectWillChange(anObject); } /** * Called when either the selection or the contents of an associated display * group have changed. */ public void subjectChanged() { // titles aspect if (titlesGroupChanged) { if (titlesDisplayGroup.contentsChanged()) { NSArray displayedObjects = titlesDisplayGroup.displayedObjects(); NSArray childrenObjects; if (titlesDisplayGroup != childrenDisplayGroup || displayedObjects.count() != (childrenObjects = objectsFetchedIntoChildrenGroup()).count() || !displayedObjects.containsAll(childrenObjects)) { populateFromDisplayGroup(displayedObjects); } } } if (childrenDisplayGroup.selectionChanged() && !childrenDisplayGroup.contentsChanged()) { selectFromDisplayGroup(childrenDisplayGroup); } titlesGroupChanged = false; childrenGroupChanged = false; } /** * Called by subjectChanged() in response to an external change in the titles * display group. */ void populateFromDisplayGroup(List displayedObjects) { // trigger processRecentChanges willChange(); // workaround: see below int previousCount = rootNode.previouslyDisplayedObjects.length; // update the root node rootNode.setObjectArray(displayedObjects); // FIXME: workaround for what appears to be a bug in JTree: // if root node is not visible and has no children, insert events are ignored if (previousCount == 0) { fireRootStructureChanged(); } } /** * Package access so DisplayGroupNode can replace the selection after an update. */ void selectFromDisplayGroup(EODisplayGroup aDisplayGroup) { // System.out.println( "selectFromDisplayGroup: " + // aDisplayGroup.selectedObjects() ); removeAsListener(); TreePath[] paths = selectionModel.getSelectionPaths(); NSArray selectedObjects = aDisplayGroup.selectedObjects(); // assemble current selection list List treeSelection = new LinkedList(); if (paths != null) { for (int i = 0; i < paths.length; i++) { treeSelection.add(((DisplayGroupNode) paths[i].getLastPathComponent()).getUserObject()); } } if (!(selectedObjects.size() == treeSelection.size() && treeSelection.containsAll(selectedObjects))) { selectionModel.clearSelection(); // workaround to select root node from valueChanged() if (pleaseSelectRootNode) { selectionModel.addSelectionPath(new TreePath(this.getRoot())); pleaseSelectRootNode = false; } // FIXME: display group is assumed to have only one instance of each object for (int i = 0; i < selectedObjects.count(); i++) { // FIXME: selects only the first instance for now // selectionModel.addSelectionPaths( // getPathsForObject( // selectedObjects.objectAtIndex( i ) ) ); selectionModel.addSelectionPath(getPathForObject(selectedObjects.objectAtIndex(i))); } } addAsListener(); } /** * Returns the first node found that represents the specified object, or null if * not found. This implementation simply calls getPathForObject. */ public Object getNodeForObject(Object anObject) { TreePath result = getPathForObject(anObject); if (result != null) { return result.getLastPathComponent(); } return null; } /** * Returns the object represented by the specified node which must be a display * group node from this tree. */ public Object getObjectForNode(Object aNode) { if (aNode instanceof DisplayGroupNode) { return ((DisplayGroupNode) aNode).getUserObject(); } // not a display group node throw new WotonomyException("Not a display group node: " + aNode); } /** * Returns the tree path for the specified node, which must be a display group * node from this tree. */ public TreePath getPathForNode(Object aNode) { if (aNode instanceof DisplayGroupNode) { return ((DisplayGroupNode) aNode).treePath(); } // not a display group node throw new WotonomyException("Not a display group node: " + aNode); } /** * Returns the first tree path for the node that represents the specified * object, or null if the object does not exist in this tree. This * implementation does a breadth-first search of the tree for the object, * looking only at nodes that have been loaded. This means that if the object * does not exist in the tree, the entire tree must be traversed. */ public TreePath getPathForObject(Object anObject) { return getPathForObjectInPath(anObject, new TreePath(this.getRoot())); } /** * Returns the tree path for the node that represents the specified object, or * null if the object does not exist in this tree. This implementation does a * breadth-first search of the tree for the object, looking only at nodes that * have been loaded. This means that the entire tree is traversed. */ public TreePath[] getPathsForObject(Object anObject) { return getPathsForObjectInPath(anObject, new TreePath(this.getRoot())); } /** * A breadth-first search of the tree starting at the specified tree path, * comparing by reference. Returns immediately with the first match. */ private TreePath getPathForObjectInPath(Object anObject, TreePath aPath) { LinkedList queue = new LinkedList(); // add the specified path queue.addLast(aPath); return processQueue(anObject, queue, null); } /** * A breadth-first search of the tree starting at the specified tree path, * comparing by reference. The entire branch is searched before returning an * array of all matches. */ private TreePath[] getPathsForObjectInPath(Object anObject, TreePath aPath) { LinkedList queue = new LinkedList(); // add the specified path queue.addLast(aPath); List result = new LinkedList(); processQueue(anObject, queue, result); TreePath[] paths = new TreePath[result.size()]; for (int i = 0; i < paths.length; i++) { paths[i] = (TreePath) result.get(i); } return paths; } /** * Processes the specified queue, appending results to aResult if it exists, or * returning immediately with a TreePath is aResult is null. */ private TreePath processQueue(Object anObject, LinkedList aQueue, List aResult) { TreePath path; while (!aQueue.isEmpty()) { path = (TreePath) aQueue.removeFirst(); path = checkNode(anObject, path, aQueue); if (path != null) { if (aResult != null) { aResult.add(path); } else { return path; } } } return null; } /** * Compares the specified object by reference each of the children of the node * at the end of the specified tree path, adding nodes that do not match but * have fetched object to the end of the specified queue. Returns the path of * the first child node that matches the specified object, or null if no match * was found. */ private TreePath checkNode(Object anObject, TreePath aPath, LinkedList aQueue) { TreePath result = null; Object child; Object parent = aPath.getLastPathComponent(); int count = getChildCount(parent); for (int i = 0; i < count; i++) { child = getChild(parent, i); // add to queue if node has fetched children if (((DisplayGroupNode) child).isFetched) { aQueue.addLast(aPath.pathByAddingChild(child)); } // compares by reference if (((DisplayGroupNode) child).object() == anObject) { // assumes same object cannot be in display group twice result = aPath.pathByAddingChild(child); //System.out.println( "TRUE: " + ((DisplayGroupNode)child).object() + " == " + anObject ); } // else // { //System.out.println( ((DisplayGroupNode)child).object() + " != " + anObject ); // } } return result; } // interface TreeSelectionListener public void valueChanged(TreeSelectionEvent e) { if (!isListening) return; selectFromSelectionModel(); } /** * Determines whether the selection should be painted immediately after the user * clicks and therefore before the children display group is updated. When the * children group is bound to many associations or is bound to a master-detail * association, updating the display group can take a perceptibly long time. * This property defaults to false. * * @see #setSelectionPaintedImmediately */ public boolean isSelectionPaintedImmediately() { return selectionPaintedImmediately; } /** * Sets whether the selection should be painted immediately. Setting this * property to true will let the tree paint first before the display group is * updated. This means that any tree selection listers will also be notified * before the display group is updated and will have to invokeLater if they want * to see the updated display group. */ public void setSelectionPaintedImmediately(boolean isImmediate) { selectionPaintedImmediately = isImmediate; } /** * Package access so DisplayGroupNode can replace the selection. Returns the * display group containing the current selection: titles or children. */ EODisplayGroup selectFromSelectionModel() { // System.out.print( "selectFromSelectionModel: " ); removeAsListener(); DisplayGroupNode node; TreePath parentPath; TreePath[] selectedPaths = selectionModel.getSelectionPaths(); NSMutableArray selectionList = new NSMutableArray(); if (selectedPaths != null) { for (int i = 0; i < selectedPaths.length; i++) { // root node is zero - ignore root node if ((selectedPaths[i].getLastPathComponent() == rootNode)) { // select root in selectFromDisplayGroup() pleaseSelectRootNode = true; } else { node = (DisplayGroupNode) selectedPaths[i].getLastPathComponent(); Object o = node.object(); if (selectionList.indexOfIdenticalObject(o) == NSArray.NotFound) { selectionList.addObject(o); } } } } childrenDisplayGroup.fetch(); // note that we're not currently listening for changes if (!childrenDisplayGroup.selectObjectsIdenticalTo(selectionList)) { addAsListener(); // because we don't have a listener stack selectFromDisplayGroup(childrenDisplayGroup); removeAsListener(); } addAsListener(); return childrenDisplayGroup; // titles is now children if children not explicitly set } // interface TreeModel public Object getRoot() { return rootNode; } public Object getChild(Object parent, int index) { // interestingly, this gets called by // BasicTreeUI.paintVerticalPartOfLeg for // the last child of each expanded tree node. Object result = ((DisplayGroupNode) parent).getChildNodeAt(index); //((DisplayGroupNode)parent).suppressRecentChangeProcessing(); return result; // return ((DisplayGroupNode)parent).getChildNodeAt( index ); } public int getChildCount(Object parent) { int result = ((DisplayGroupNode) parent).getChildCount(); //((DisplayGroupNode)parent).suppressRecentChangeProcessing(); return result; // return ((DisplayGroupNode)parent).getChildCount(); } public boolean isLeaf(Object node) { boolean result = ((DisplayGroupNode) node).isLeaf(); //((DisplayGroupNode)node).suppressRecentChangeProcessing(); return result; // return ((DisplayGroupNode)node).isLeaf(); } /** * Returns whether this node is visible in the UI. This implementation returns * true.
*
* Subclasses should return false if they can determine that the node is not * displayed or expanded or otherwise visible. Non-visible nodes will fetch only * when they are shown. */ public boolean isVisible(Object node) { return true; } public void valueForPathChanged(TreePath path, Object newValue) { ((DisplayGroupNode) path.getLastPathComponent()).setUserObject(newValue); } public int getIndexOfChild(Object parent, Object child) { int result = ((DisplayGroupNode) parent).getIndex((DisplayGroupNode) child); //((DisplayGroupNode)parent).suppressRecentChangeProcessing(); return result; // return ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child ); } public void addTreeModelListener(TreeModelListener aListener) { listeners.add(aListener); } public void removeTreeModelListener(TreeModelListener aListener) { listeners.remove(aListener); } /** * Fires a tree nodes changed event to all listeners. Provided as a convenience * if you need to make manual changes to the tree model. */ public void fireTreeNodesChanged(Object source, Object[] path, int[] childIndices, Object[] children) { willChange(); // queue processRecentChanges TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children); //System.out.println( "fireTreeNodesChanged: " + event ); Enumeration it = listeners.elements(); while (it.hasMoreElements()) { try { ((TreeModelListener) it.nextElement()).treeNodesChanged(event); } catch (Exception exc) { System.out.println("TreeModelAssociation.fireTreeNodesChanged: caught: " + exc); System.out.println("Source:" + source); System.out.println("Path:"); for (int i = 0; i < path.length; i++) { System.out.print(path[i] + "-"); } System.out.println(); System.out.println("Indices:"); for (int i = 0; i < childIndices.length; i++) { System.out.print(childIndices[i] + "-"); } System.out.println(); System.out.println("Children:"); for (int i = 0; i < children.length; i++) { System.out.print(children[i] + "-"); } System.out.println(); exc.printStackTrace(); } } } /** * Fires a tree nodes inserted event to all listeners. Provided as a convenience * if you need to make manual changes to the tree model. */ public void fireTreeNodesInserted(Object source, Object[] path, int[] childIndices, Object[] children) { willChange(); // queue processRecentChanges TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children); //System.out.println( "fireTreeNodesInserted: " + event ); Enumeration it = listeners.elements(); while (it.hasMoreElements()) { try { ((TreeModelListener) it.nextElement()).treeNodesInserted(event); } catch (Exception exc) { System.out.println("TreeModelAssociation.fireTreeNodesInserted: caught: " + exc); } } } /** * Fires a tree nodes removed event to all listeners. Provided as a convenience * if you need to make manual changes to the tree model. */ public void fireTreeNodesRemoved(Object source, Object[] path, int[] childIndices, Object[] children) { willChange(); // queue processRecentChanges TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children); //System.out.println( "fireTreeNodesRemoved: " + event ); Enumeration it = listeners.elements(); while (it.hasMoreElements()) { try { // NOTE: removing nodes causes tree to fire selection change event // which confuses us if we're rearranging nodes (when sorting, for example). boolean wasListening = isListening; if (wasListening) isListening = false; ((TreeModelListener) it.nextElement()).treeNodesRemoved(event); if (wasListening) isListening = true; } catch (Exception exc) { System.out.println("TreeModelAssociation.fireTreeNodesRemoved: caught: " + exc); } } } /** * Fires a tree structure changed event to all listeners. Provided as a * convenience if you need to make manual changes to the tree model. */ public void fireTreeStructureChanged(Object source, Object[] path, int[] childIndices, Object[] children) { willChange(); // queue processRecentChanges TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children); //System.out.println( "fireStructureChanged: " + event ); Enumeration it = listeners.elements(); while (it.hasMoreElements()) { ((TreeModelListener) it.nextElement()).treeStructureChanged(event); } } /** * Creates and returns a new display group node. */ public DisplayGroupNode createNode(EODisplayGroup aParentGroup, Object anObject) { return new MutableDisplayGroupNode(this, aParentGroup, anObject); } /** * Gets whether new objects programmatically inserted into the children display * group should be inserted as a child of the first selected node. If false, new * objects are inserted as siblings of the first selected node. Default value is * true. */ public boolean isInsertingChild() { return insertingChild; } /** * Sets whether new objects programmatically inserted into the children display * group should be inserted as a child of the first selected node. If false, new * objects are inserted as siblings of the first selected node. Default value is * true. */ public void setInsertingChild(boolean asChild) { insertingChild = asChild; } /** * Determines where new objects programmatically inserted into the children * display group should be inserted, based on the value of insertingChild. If * insertingChild, isInsertingAfter causes objects to be inserted at the end of * the selected node's child list; otherwise, objects are inserted at the * beginning of the list. If inserting as a sibling, isInsertingAfter causes * objects to be inserted before the selected node in the selected node's * parent's child list; otherwise, objects are inserted after the selected node * in the child list. Default value is true. */ public boolean isInsertingAfter() { return insertingAfter; } /** * Determines where new objects programmatically inserted into the children * display group should be inserted, based on the value of insertingChild. If * insertingChild, isInsertingAfter causes objects to be inserted at the end of * the selected node's child list; otherwise, objects are inserted at the * beginning of the list. If inserting as a sibling, isInsertingAfter causes * objects to be inserted before the selected node in the selected node's * parent's child list; otherwise, objects are inserted after the selected node * in the child list. Default value is true. */ public void setInsertingAfter(boolean after) { insertingAfter = after; } /** * Called to by the children group's data source when it receives an * insertObject message, usually after an object has been inserted into the * children display group. Return the object that should be passed to the titles * display group's data source's implementation of insertObject, or return null * to prevent that method from being called.
*
* This implementation inserts the specified object into the tree as determined * by calling isInsertingChild and isInsertingAfter, then returns the unmodified * object. If there's no selection, or no selection model, the root node is * assumed to be selected. And if the root node is selected, the new node will * obviously be inserted as a child. Override to customize. */ protected Object objectInsertedIntoChildrenGroup(Object anObject) { // determine selection DisplayGroupNode selectedNode = (DisplayGroupNode) getRoot(); if (selectionModel != null) { // get selected path TreePath path = selectionModel.getSelectionPath(); // get selected node if (path != null) { selectedNode = (DisplayGroupNode) path.getLastPathComponent(); } } // determine location of insertion int index = 0; if ((isInsertingChild()) || (selectedNode == getRoot())) { if (isInsertingAfter()) { index = selectedNode.getChildCount(); } } else // inserting as sibling { DisplayGroupNode parentNode = selectedNode.getParentGroup(); index = parentNode.getIndex(selectedNode); if (isInsertingAfter()) { index++; } selectedNode = parentNode; } // insert and return selectedNode.insertObjectAtIndex(anObject, index); return anObject; } /** * Called to by the children group's data source when it receives a deleteObject * message, usually after an object has been deleted from the children display * group. Return the object that should be passed to the titles display group's * data source's implementation of deleteObject, or return null to prevent that * method from being called.
*
* This implementation deletes all instances of the selected object from the * tree nodes that are currently loaded, and returns the unmodified object. * Override to customize. */ protected Object objectDeletedFromChildrenGroup(Object anObject) { TreePath[] paths = getPathsForObject(anObject); if (paths != null) { for (int i = 0; i < paths.length; i++) { ((DisplayGroupNode) paths[i].getLastPathComponent()).removeFromParent(); } } return anObject; } /** * Called to by the children group's data source to populate it with all * selected nodes and their siblings. To customize, override this method, or * specify a different data source for the children display group. */ protected NSArray objectsFetchedIntoChildrenGroup() { DisplayGroupNode node; TreePath parentPath; TreePath[] selectedPaths = selectionModel.getSelectionPaths(); NSMutableArray objectList = new NSMutableArray(); if (selectedPaths != null) { for (int i = 0; i < selectedPaths.length; i++) { // root node is zero - ignore root node if ((selectedPaths[i].getLastPathComponent() == rootNode)) { // select root in selectFromDisplayGroup() pleaseSelectRootNode = true; } else { node = (DisplayGroupNode) selectedPaths[i].getLastPathComponent(); Object o = node.object(); // add all children of parent to object list - includes self if (node.parentGroup != null) { Enumeration e = node.parentGroup.displayedObjects().objectEnumerator(); while (e.hasMoreElements()) { // add only if not already in list o = e.nextElement(); if (objectList.indexOfIdenticalObject(o) == NSArray.NotFound) { objectList.addObject(o); } } } else // no parent node - add the node by itself { // add only if not already in list if (objectList.indexOfIdenticalObject(o) == NSArray.NotFound) { objectList.addObject(o); } } } } } // if no selection if (objectList.size() == 0) { // populate with children of root objectList.addAll(rootNode.displayedObjects()); } return objectList; } /** * Queues processRecentChanges to be run in the event queue. */ private void willChange() { EOObserverCenter.notifyObserversObjectWillChange(this); } /** * Tells the children display group to refetch, so that it reflects any changes * that were made in the node tree, and then updates the selection in the * selection model. Triggered in response to willChange(). */ public void processRecentChanges() { Runnable update = new Runnable() { public void run() { removeAsListener(); // prevent data source refetch: see fetchObjects() childrenDisplayGroup.fetch(); addAsListener(); selectFromDisplayGroup(childrenDisplayGroup); } }; if (isListening) { if (selectionPaintedImmediately) { // if painting selection immediately, run even later // so that AWT's repaint event fires before we do. SwingUtilities.invokeLater(update); } else { // otherwise run now update.run(); } } } /** * Delegates most behaviors to the specified data source, except fetchObjects, * which calls fetchObjectsIntoChildrenGroup on the tree model association. If * delegate is null, calls are passed to the superclass which is a * PropertyDataSource. */ static class DelegatingTreeDataSource extends PropertyDataSource { TreeModelAssociation parentAssociation; EODataSource delegateDataSource; public DelegatingTreeDataSource(TreeModelAssociation aTreeModelAssociation, EODataSource aDataSource) { parentAssociation = aTreeModelAssociation; delegateDataSource = aDataSource; } /** * Calls to delegateDataSource if it exists, otherwise calls to super. */ public Object createObject() { if (delegateDataSource != null) { return delegateDataSource.createObject(); } return super.createObject(); } /** * Calls objectInsertedIntoChildrenGroup, and if not null calls to * delegateDataSource.insertObject if it exists, and super.insertObjectAtIndex * if not. */ public void insertObjectAtIndex(Object anObject, int anIndex) { anObject = parentAssociation.objectInsertedIntoChildrenGroup(anObject); if (anObject != null) { if (delegateDataSource != null) { if (delegateDataSource instanceof OrderedDataSource) { ((OrderedDataSource) delegateDataSource).insertObjectAtIndex(anObject, anIndex); } else { delegateDataSource.insertObject(anObject); } } else { super.insertObjectAtIndex(anObject, anIndex); } } } /** * Calls objectDeletedIntoChildrenGroup, and if not null calls to * delegateDataSource if it exists. */ public void deleteObject(Object anObject) { anObject = parentAssociation.objectDeletedFromChildrenGroup(anObject); if (anObject != null) { if (delegateDataSource != null) { delegateDataSource.deleteObject(anObject); } super.deleteObject(anObject); } } /** * Overridden to return the delegate's editing context, the titles display * group's editing context, and failing that calling to super. */ public EOEditingContext editingContext() { EOEditingContext result = null; if (delegateDataSource != null) { result = delegateDataSource.editingContext(); } if (result == null) { EODataSource parentDataSource = parentAssociation.titlesDisplayGroup.dataSource(); if (parentDataSource != this && parentDataSource != null) { result = parentAssociation.titlesDisplayGroup.dataSource().editingContext(); } } if (result == null) { result = super.editingContext(); } return result; } /** * Returns a List containing the objects in this data source. */ public NSArray fetchObjects() { // if titles group is doing double-duty as children group if (parentAssociation.titlesDisplayGroup == parentAssociation.childrenDisplayGroup) { // if we're not initiating this fetch if (parentAssociation.isListening) { // need to call to delegate to see if we should update values if (delegateDataSource != null) { // System.out.println( "fetching from delegate (slow!)" ); NSArray result = delegateDataSource.fetchObjects(); NSArray rootObjects = parentAssociation.rootNode.displayedObjects(); // if titles data source has different objects, return them if (rootObjects.count() != result.count() || !rootObjects.containsAll(result)) { // this will force the root node to repopulate in subjectChanged() //System.out.println( "fetchObjects: data source" ); return result; } } } } // otherwise: just repopulate the titles group //System.out.println( "fetchObjects: objectsFetchedIntoChildrenGroup" ); return parentAssociation.objectsFetchedIntoChildrenGroup(); } /** * Returns a data source that is capable of manipulating objects of the type * returned by applying the specified key to objects vended by this data source. * * @see #qualifyWithRelationshipKey */ public EODataSource dataSourceQualifiedByKey(String aKey) { if (delegateDataSource != null) { return delegateDataSource.dataSourceQualifiedByKey(aKey); } return null; } /** * Restricts this data source to vend those objects that are associated with the * specified key on the specified object. */ public void qualifyWithRelationshipKey(String aKey, Object anObject) { if (delegateDataSource != null) { delegateDataSource.qualifyWithRelationshipKey(aKey, anObject); } } /** * Returns the value from the delegateDataSource, if it exists. Otherwise calls * super. */ public EOClassDescription classDescriptionForObjects() { if (delegateDataSource != null) { return delegateDataSource.classDescriptionForObjects(); } return super.classDescriptionForObjects(); } } } /* * $Log$ Revision 1.2 2006/02/18 23:19:05 cgruber Update imports and maven * dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.20 2003/08/06 23:07:52 chochos general code cleanup (mostly, * removing unused imports) * * Revision 1.19 2002/05/03 21:41:18 mpowers No longer clearing the selection * model when updating from display group: we now only modify if a change needs * to be made. No longer listening for selection change during firing of delete * events: delete events cause JTree's to update their selection model. Fix for * paintsSelectionImmediately: TreeAssociation.processRecentChanges() must * happen after the screen is painted, or the selection is not displayed. * * Revision 1.18 2002/04/23 19:12:28 mpowers Reimplemented fireEventsForChanges. * Fitter and happier. * * Revision 1.17 2002/04/19 21:18:46 mpowers Removed tree event coalescing, * which was causing way too many problems. The fireChangeEvent algorithm is way * faster than before, so we should still be better off than before. At least * now, we don't have to track whether the view component has encountered a * particular node. * * Revision 1.16 2002/04/18 20:36:11 mpowers TreeModelAssociation now populates * children group before selected objects. Got rid of the forceOnSync workaround * for cancelled selection change. * * Revision 1.15 2002/04/15 21:52:50 mpowers Tightening up TreeModelAssociation * and DisplayGroupNode. Now only firing root structure changed once. Now * disposing of root's children. Better event coalescing. * * Revision 1.14 2002/04/12 21:05:58 mpowers Now distinguishing changes in * titles group even better. * * Revision 1.11 2002/04/10 21:20:04 mpowers Better handling for tree nodes when * working with editing contexts. Better handling for invalidation. No longer * broadcasting events when nodes have not been "registered" in the tree. * * Revision 1.10 2002/04/03 20:01:24 mpowers Removed printlns. * * Revision 1.8 2002/03/11 03:16:28 mpowers Better handling of change events; * coalescing changes to children group. * * Revision 1.7 2002/03/08 23:19:57 mpowers Refactoring of * DelegatingTreeDataSource to facilitate binding of titles and children aspects * to the same display group. * * Revision 1.6 2002/03/07 23:04:36 mpowers Refining TreeColumnAssociation. * * Revision 1.5 2002/03/06 13:04:16 mpowers Implemented cascading qualifiers in * tree nodes. * * Revision 1.4 2002/03/04 22:47:48 mpowers Fixed sort ordering for titles * group. Optimization for delegate selection. * * Revision 1.3 2002/03/04 12:28:47 mpowers Revised case where children and * titles are bound to same display group. * * Revision 1.2 2002/03/01 23:42:09 mpowers Implemented TreeColumnAssociation, * and updated documentation. * * Revision 1.1 2002/02/27 23:19:17 mpowers Refactoring of TreeAssociation to * create TreeModelAssociation parent. * * Revision 1.38 2002/02/18 03:46:08 mpowers Implemented TreeTableCellRenderer. * * Revision 1.37 2002/02/13 21:20:15 mpowers Updated comments. * * Revision 1.36 2001/11/21 15:13:25 mpowers Better repainting for * selectionPaintedImmediately. Better handling for selection with multiple * instances of the same object in the tree (from yjcheung). * * Revision 1.35 2001/11/20 19:13:51 mpowers Finished implementation of children * group's specialized data source. * * Revision 1.34 2001/11/19 16:30:37 mpowers Tree repaint strategy is now a * preference: selectionPaintedImmediately. * * Revision 1.33 2001/11/15 17:56:41 mpowers Initial implementation of data * source for the children display group. * * Revision 1.32 2001/11/14 00:05:54 mpowers Eliminated the run later in favor * of repainting the component immediately. This makes things more predictable * for users of the association that want to listen to mouse or selection events * on the tree. * * Revision 1.31 2001/11/02 20:43:15 mpowers Fixes for delegate's * shouldChangeSelection veto (from yjcheung). * * Revision 1.30 2001/10/29 20:42:56 mpowers On selection change, repainting * tree before notifying display group; using NSRunLoop instead of * SwingUtilities. * * Revision 1.29 2001/10/12 20:12:53 mpowers Better handling of selection change * vetoing when changing selection to a node that is not the sibling of the * originally selected node. * * Revision 1.28 2001/09/14 13:40:26 mpowers User-initiated selection changes * are now handled on the next event loop so that the component repaints the new * selection before any potentially lengthy logic is triggered by the selection * change. * * Revision 1.27 2001/09/10 14:10:03 mpowers Tree now handles multiple instances * of the same object. * * Revision 1.26 2001/07/18 13:03:32 mpowers TreeNodes now refetch only on * demand. Previously, once a node had been fetched, it was always refetched * after an invalidate, even if the node was not being displayed. * * Revision 1.25 2001/05/14 15:25:35 mpowers No longer copying titles group's * data source to children group. * * Revision 1.24 2001/05/08 18:47:34 mpowers Minor fixes for d3. * * Revision 1.23 2001/05/01 00:52:32 mpowers Implemented breadth-first traversal * of tree for node. * * Revision 1.22 2001/04/26 01:15:19 mpowers Major clean-up of DisplayGroupNode: * fitter, happier, more productive. * * Revision 1.21 2001/04/22 23:13:35 mpowers Minor bug. * * Revision 1.20 2001/04/22 23:05:33 mpowers Totally revised DisplayGroupNode so * each object gets its own node (so the nodes are no longer fixed by index). * * Revision 1.19 2001/04/21 23:06:33 mpowers A major revisiting to support the * revising of DisplayGroupNode. * * Revision 1.18 2001/04/03 20:36:01 mpowers Fixed * refaulting/reverting/invalidating to be self-consistent. * * Revision 1.17 2001/03/29 21:35:08 mpowers Now handling circular references in * the graph. * * Revision 1.16 2001/03/22 21:25:42 mpowers Fixed some nasty issues with * jtree's internal state and array bounds. * * Revision 1.15 2001/03/19 21:37:58 mpowers Improved refresh of titles display * group. Fixed dangling selection problem after refresh. * * Revision 1.14 2001/03/09 22:08:57 mpowers Trying to handle the dangling * reference problem after an update. * * Revision 1.13 2001/02/17 17:23:49 mpowers More changes to support compiling * with jdk1.1 collections. * * Revision 1.12 2001/01/25 02:16:25 mpowers TreeModelAssociation now returns * DisplayGroupNode.getUserObject. * * Revision 1.11 2001/01/24 18:14:40 mpowers Fixed problem with leaving children * aspect unspecified. * * Revision 1.10 2001/01/24 17:49:15 mpowers Added getObjectForNode and * getNodeForObject convenience methods. * * Revision 1.9 2001/01/24 17:44:11 mpowers Renamed getPathForNode to * getPathForObject to be more precise. And created a new getPathForNode method. * * Revision 1.8 2001/01/24 17:20:29 mpowers Children display group now holds * siblings of selected objects in addition to the selected objects. * * Revision 1.5 2001/01/19 23:21:15 mpowers Fine tuning events broadcast from * TreeModelAssociation. * * Revision 1.4 2001/01/18 21:27:29 mpowers Major rework of * TreeModelAssociation. * * Revision 1.2 2001/01/11 20:29:19 mpowers Expanded access to tree event firing * methods. * * Revision 1.1.1.1 2000/12/21 15:49:18 mpowers Contributing wotonomy. * * Revision 1.20 2000/12/20 16:25:42 michael Added log to all files. * * */