summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java
diff options
context:
space:
mode:
authorBenjamin Culkin <scorpress@gmail.com>2024-05-19 17:56:33 -0400
committerBenjamin Culkin <scorpress@gmail.com>2024-05-19 17:56:33 -0400
commitaedc34d55462a75e329bbf342251ff6504cd117e (patch)
treebcc8f1f2352582717b484df302aeea6696b8f000 /projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java
Initial import from SVN
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java')
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java1751
1 files changed, 1751 insertions, 0 deletions
diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java
new file mode 100644
index 0000000..86bfa69
--- /dev/null
+++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java
@@ -0,0 +1,1751 @@
+/*
+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.
+*
+* <ul>
+*
+* <li>titles: a property convertable to a string for
+* display in the nodes of the tree. The objects in
+* the bound display group will be used to populate the
+* initial root nodes of the tree (more accurately,
+* children of the offscreen root node in the tree).</li>
+*
+* <li>children: a property of a node value that returns
+* zero, one or many objects, each of which will correspond
+* to a child node for the corresponding node in the tree.
+* The data source of the bound display group is replaced
+* a data source that populates the display group with
+* the selection in the tree component as determined by
+* calling fetchObjectsIntoChildrenGroup.
+* If this aspect is not bound, the tree behaves like a list.
+* <br><br>
+* Binding this aspect with a null display group is the same
+* as binding it with the titles display group.
+* In this configuration the contents of the titles
+* display group will be replaced with the selection in the
+* tree component, as specified above, replacing the existing
+* data source.
+* <br><br>
+* In that case, the display groups for the nodes in
+* the tree will still use the original data source
+* for resolving their children key, and programmatically
+* setting the contents of the display group will still
+* repopulate the root nodes of the tree.
+* </li>
+*
+* <li>isLeaf: a property of a node value that returns
+* a value convertable to a boolean value (aside from
+* an actual boolean value, zeroes evaluate to true,
+* as does any String containing "yes" or "true" or that
+* is convertable to a number equal to zero; other values
+* evaluate to false).
+* <br><br>
+* If the isLeaf aspect is not bound,
+* the tree must force nodes to load their children to
+* determine whether they are leaf nodes (in effect
+* loading the grandchildren for any expanded node).
+* If bound, child loading is deferred until the node
+* is actually expanded.
+* <br><br>
+* For example, binding this value to a null
+* display group and the key "false" will result in a
+* deferred-loading tree that works much like Windows
+* Explorer's network volume browser - all nodes appear
+* with "pluses" until they are expanded.
+* <br><br>
+* Note that the display group is ignored: the property
+* will be applied directly to the object corresponding
+* to the node.</li>
+*
+* </ul>
+*
+* 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.<br><br>
+*
+* 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.
+* <br><br>
+*
+* 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. <br><br>
+*
+* 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.<br><br>
+*
+* 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.<br><br>
+*
+* 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". <br><br>
+ *
+ * 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. <ul>
+ * <li>"A" attribute: the aspect can be bound to
+ * an attribute.</li>
+ * <li>"1" to-one: the aspect can be bound to a
+ * property that returns a single object.</li>
+ * <li>"M" to-one: the aspect can be bound to a
+ * property that returns multiple objects.</li>
+ * </ul>
+ * 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.
+ * <br><br>
+ * 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. <br><br>
+ * 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. <br><br>
+ * 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.
+ *
+ *
+ */
+