From aedc34d55462a75e329bbf342251ff6504cd117e Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Sun, 19 May 2024 17:56:33 -0400 Subject: Initial import from SVN --- .../wotonomy/ui/swing/TreeModelAssociation.java | 1751 ++++++++++++++++++++ 1 file changed, 1751 insertions(+) create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java') 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. +* +* +* +* 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. + * + * + */ + -- cgit v1.2.3