/* 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. * * */