diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
| commit | aedc34d55462a75e329bbf342251ff6504cd117e (patch) | |
| tree | bcc8f1f2352582717b484df302aeea6696b8f000 /projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java | |
Initial import from SVN
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java')
| -rw-r--r-- | projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java | 1518 |
1 files changed, 1518 insertions, 0 deletions
diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java new file mode 100644 index 0000000..1756285 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java @@ -0,0 +1,1518 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 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.awt.Component; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Vector; + +import javax.swing.tree.TreePath; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.TreeModelAssociation.DelegatingTreeDataSource; + +/** +* DisplayGroupNodes are used as nodes in the +* TreeModelAssociation's implementation of TreeModel, +* and is tightly coupled with TreeModelAssociation +* and MasterDetailAssociation. <br><br> +* +* Even though it is no longer package access, +* don't rely on this class because we want to +* have the option of completely replacing this +* approach in the future. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + abstract public class DisplayGroupNode + extends EODisplayGroup + { + protected TreeModelAssociation parentAssociation; + protected EODelayedObserver targetObserver; + protected NSMutableDictionary childNodes; + protected EODisplayGroup parentGroup; + protected Object target; + protected boolean isFetched; + protected boolean isFetchNeeded; + protected boolean useParentOrderings; + protected boolean useParentQualifier; + + /** + * Constructor for all nodes. + * Root node must have a null target. + */ + public DisplayGroupNode( + TreeModelAssociation aParentAssociation, + EODisplayGroup aParentGroup, + Object aTarget ) + { +//new net.wotonomy.ui.swing.util.StackTraceInspector( ""+aTarget ); +//System.out.println( "DisplayGroupNode.new: " + aTarget ); + parentAssociation = aParentAssociation; + target = null; + targetObserver = null; + parentGroup = aParentGroup; + childNodes = new NSMutableDictionary(); + isFetched = false; + isFetchNeeded = false; + useParentOrderings = true; + useParentQualifier = true; + + EODataSource parentSource = null; + if ( parentGroup != null ) + { + parentSource = parentGroup.dataSource(); + } + else + if ( parentAssociation.titlesDisplayGroup != null ) + { + parentSource = parentAssociation.titlesDisplayGroup.dataSource(); + } + + // create child datasource + if ( aTarget != null ) // not root node + { + if ( parentAssociation.childrenKey != null ) + { + if ( parentSource == null ) + { + throw new WotonomyException( + "Need a data source when children aspect is bound." ); + } + + NSArray displayedObjects = parentGroup.displayedObjects(); + EODataSource childSource = parentSource.dataSourceQualifiedByKey( + parentAssociation.childrenKey ); + childSource.qualifyWithRelationshipKey( + parentAssociation.childrenKey, aTarget ); + + // create new display group using child data source + this.setDataSource( childSource ); + + // establish observer for target object + setTarget( aTarget ); + } + else // only titles is bound + { + // establish observer for target object + setTarget( aTarget ); + + setDataSource( new PropertyDataSource() + { + public NSArray fetchObjects() + { + return new NSArray(); + } + } ); + } + } + else // else root node + { + // root node uses PropertyDataSource by default + if ( parentSource == null ) + { + setDataSource( new PropertyDataSource() + { + public NSArray fetchObjects() + { + if ( parentGroup != null ) + { + return parentGroup.displayedObjects(); + } + return null; + } + } ); + } + else + { + // root node uses parent source directly + setDataSource( parentSource ); + } + } + } + + /** + * Overridden to unregister as an editor of the editing context, + * since we don't directly present a user interface. + */ + public void setDataSource ( EODataSource aDataSource ) + { + super.setDataSource( aDataSource ); + if ( ( aDataSource != null ) + && ( aDataSource.editingContext() != null ) ) + { + aDataSource.editingContext().removeEditor( this ); + } + } + + /** + * Returns whether the node should call fetch(). + */ + protected boolean isFetched() + { + if ( isFetchNeeded() ) + { + setFetchNeeded( false ); + fetch(); + } + return isFetched; + } + + /** + * Sets whether the node should call fetch(). + */ + protected void setFetched( boolean fetched ) + { +//System.out.println( "DisplayGroupNode.setFetched: " + fetched + " : " + this + " : " + target ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + isFetched = fetched; + } + + /** + * Returns whether the node is in need of a refetch. + */ + protected boolean isFetchNeeded() + { + return isFetchNeeded; + } + + /** + * Returns whether the node should call fetch(). + */ + protected void setFetchNeeded( boolean fetchNeeded ) + { +//System.out.println( "DisplayGroupNode.setFetchNeeded: " + fetchNeeded + " : " + this + " : " + target ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + isFetchNeeded = fetchNeeded; + } + + /** + * Subclasses should override this method to fire an appropriate insertion event. + */ + protected void fireNodesInserted( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesInserted: " + this ); + parentAssociation.fireTreeNodesInserted( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate change event. + */ + protected void fireNodesChanged( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesChanged: " + this ); + parentAssociation.fireTreeNodesChanged( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate deletion event. + */ + protected void fireNodesRemoved( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesRemoved: " + this ); + parentAssociation.fireTreeNodesRemoved( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate event. + */ + protected void fireStructureChanged( Object[] path, int[] indexes, Object[] objects ) + { + parentAssociation.fireTreeStructureChanged( + this, path, indexes, objects ); + } + + /** + * Overridden to broadcast a tree event after super executes. + */ + public void insertObjectAtIndex ( Object anObject, int anIndex ) + { + int count = getChildCount(); // gets old count + if ( target == null ) + { + // if root node, forward to parent: + // circumventing delegating data source, if any + EODataSource dataSource = parentGroup.dataSource(); + if ( dataSource instanceof DelegatingTreeDataSource ) + { + parentGroup.setDataSource( + ((DelegatingTreeDataSource)dataSource).delegateDataSource ); + } + parentGroup.insertObjectAtIndex( anObject, anIndex ); + if ( dataSource instanceof DelegatingTreeDataSource ) + { + parentGroup.setDataSource( dataSource ); + } + return; // prevent event from firing (?) + } + else // not root node + { + super.insertObjectAtIndex( anObject, anIndex ); + } + } + + /** + * Overridden to broadcast a tree event after super executes. + */ + public boolean deleteObjectAtIndex ( int anIndex ) + { + boolean result; + Object node = getChildNodeAt( anIndex ); + if ( target == null ) + { + // if root node, forward to parent: + result = parentGroup.deleteObjectAtIndex( anIndex ); + } + else // not root node + { + result = super.deleteObjectAtIndex( anIndex ); + } + + return result; + } + + /** + * Returns the child node that corresponds to the + * specified index, creating it if necessary. + * The index must be within bounds or an exception + * is thrown. + */ + public DisplayGroupNode getChildNodeAt( int anIndex ) + { + boolean wasFetched = isFetched(); + if ( ! wasFetched ) fetch(); + Object o = displayedObjects.objectAtIndex( anIndex ); + DisplayGroupNode result = getChildNodeForObject( o ); + if ( result == null ) + { + result = createChildNodeForObject( o ); + } + return result; + } + + /** + * Returns a child node that corresponds to the + * specified object, returning null if not found. + */ + protected DisplayGroupNode getChildNodeForObject( Object anObject ) + { + return (DisplayGroupNode) + childNodes.objectForKey( new ReferenceKey( anObject ) ); + } + + /** + * Creates a child node that corresponds to the + * specified object. + */ + private DisplayGroupNode createChildNodeForObject( Object anObject ) + { + DisplayGroupNode result = parentAssociation.createNode( this, anObject ); + childNodes.setObjectForKey( result, new ReferenceKey( anObject ) ); + return result; + } + + /** + * Returns a tree path of all DisplayGroupNodes leading + * to this node, including the root node (but excluding the + * titles display group). + */ + public TreePath treePath() + { + List path = new LinkedList(); + EODisplayGroup node = this; + while ( node instanceof DisplayGroupNode ) + { + // insert at head of list + path.add( 0, node ); + node = ((DisplayGroupNode)node).parentGroup; + } + return new TreePath( path.toArray() ); + } + + /** + * Overridden to return the parent group's + * sort ordering if useParentOrderings is true. + * useParentOrderings is true by default. + */ + public NSArray sortOrderings() + { + if ( ( useParentOrderings ) + && ( parentGroup != null ) ) + { + return parentGroup.sortOrderings(); + } + return super.sortOrderings(); + } + + /** + * Overridden to set useParentOrderings to false, + * or true if aList is null. + */ + public void setSortOrderings ( List aList ) + { + if ( aList == null ) + { + useParentOrderings = true; + } + else + { + useParentOrderings = false; + super.setSortOrderings( aList ); + } + } + + /** + * Overridden to return the parent group's + * qualifier if useParentQualifier is true. + * useParentQualifier is true by default. + */ + public EOQualifier qualifier() + { + if ( ( useParentQualifier ) + && ( parentGroup != null ) ) + { + return parentGroup.qualifier(); + } + return super.qualifier(); + } + + /** + * Overridden to set useParentQualifier to false, + * or true if aList is null. + */ + public void setQualifier ( EOQualifier aQualifier ) + { + if ( aQualifier == null ) + { + useParentQualifier = true; + } + else + { + useParentQualifier = false; + super.setQualifier( aQualifier ); + } + } + + /** + * Overridden to set isFetched to true. + */ + public boolean fetch() + { +//System.out.println( "DisplayGroupNode.fetch: " + this + " : " ); +//if ( getClass().getName().indexOf( "Activity" ) != -1 ) +//{ +// new net.wotonomy.ui.swing.util.StackTraceInspector( this.toString() ); +//} + // set flag + setFetched( true ); + + // skip root node + if ( target == null ) return true; + + // requalify + dataSource().qualifyWithRelationshipKey( + parentAssociation.childrenKey, target ); + + // call to super + return super.fetch(); + +//boolean result = super.fetch(); +//System.out.println( displayedObjects() ); +//return result; + } + + /** + * Returns the object at the appropriate index + * in the parent display group. + */ + public Object object() + { + // if root node + if ( target == null ) + { + return parentAssociation.rootLabel(); + } + return target; + } + + /** + * Returns the string value of the title property + * on the object in the parent display group corresponding + * to this index. The tree renderer asks JTrees to + * call this method to retrieve a value for display. + */ + public String toString() + { + Object result = getUserObject(); + if ( result == null ) result = "[null]"; + return result.toString(); + } + + // parts of interface TreeNode + + public int getChildCount() + { + if ( ! isFetched() ) fetch(); +//if ( toString().indexOf("154.16406")!=-1){ +//System.out.println( "getChildCount: " + displayedObjects.count() + " : " + this ); +//new RuntimeException().printStackTrace(); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); +//} + return displayedObjects.count(); + } + + public int getIndex(DisplayGroupNode node) + { + if ( ! isFetched() ) fetch(); + return displayedObjects.indexOfObject( + ((DisplayGroupNode)node).target ); + } + + public boolean getAllowsChildren() + { + return true; + } + + public boolean isLeaf() + { + // if not root node and isLeaf aspect is bound + if ( ( target != null ) + && ( parentGroup != null ) + && ( parentAssociation.leafKey != null ) ) + { + Object value; + if ( parentAssociation.leafDisplayGroup != null ) + { + value = parentGroup.valueForObject( + target, parentAssociation.leafKey ); + } + else + { + value = parentAssociation.leafKey; + } + + // getBoolean returns true for zero, among other things + Object result = ValueConverter.getBoolean( value ); + if ( result != null ) + { + return ((Boolean)result).booleanValue(); + } + } + + // otherwise, we have to fetch and return count + return ( getChildCount() == 0 ); + } + + public Enumeration children() + { + int count = getChildCount(); + Vector v = new Vector(); + for ( int i = 0; i < count; i++ ) + { + v.add( getChildNodeAt( i ) ); + } + return v.elements(); + } + + // parts of interface MutableTreeNode + + public void insert(DisplayGroupNode aChild, int anIndex) + { + insertObjectAtIndex( + ((DisplayGroupNode)aChild).object(), anIndex ); + } + + public void remove(int index) + { + deleteObjectAtIndex( index ); + } + + /** + * Removes the node at the index corresponding + * to the index of the object. + */ + public void remove(DisplayGroupNode node) + { + remove( getIndex( node ) ); + } + + /** + * Removes our object from the parent display group. + */ + public void removeFromParent() + { + int index = parentGroup.displayedObjects().indexOfIdenticalObject( target ); + if ( index != NSArray.NotFound ) + { + parentGroup.deleteObjectAtIndex( index ); + } + else + { + throw new WotonomyException( + "Object not found in parent group: " + target ); + } + } + + /** + * Removes our object from the parent display group + * and adds it to the end of the specified node's children. + */ + public void setParent(DisplayGroupNode newParent) + { + removeFromParent(); + newParent.insertObjectAtIndex( + object(), newParent.displayedObjects.size() ); + } + + /** + * Returns the value of the displayed property in the parent display group + * at the index that corresponds to the index of this node. + */ + public Object getUserObject() + { + return valueForKey( parentAssociation.titlesKey ); + } + + /** + * Sets the value of the displayed property in the parent display group + * at the index that corresponds to the index of this node. + */ + public void setUserObject( Object aValue ) + { + setValueForKey( aValue, parentAssociation.titlesKey ); + } + + /** + * Returns a value from the object in the parent display group + * at the index that corresponds to the index of this node. + * For the root node, if the titles key is specified, the root + * label is returned, otherwise null is returned. + */ + public Object valueForKey( String aKey ) + { + // if root node + if ( target == null ) + { + // compare by ref is okay for strings + if ( aKey == parentAssociation.titlesKey ) + { + return parentAssociation.rootLabel(); + } + return null; + } + return parentGroup.valueForObject( target, aKey ); + } + + /** + * Sets a value on the object in the parent display group + * at the index that corresponds to the index of this node. + * For the root node, this method only works if aKey is the + * titlesAspect's key, otherwise does nothing. + */ + public void setValueForKey(Object aValue, String aKey) + { + // if root node, return. + if ( target == null ) + { + // compare by ref is okay for strings + if ( aKey == parentAssociation.titlesKey ) + { + parentAssociation.setRootLabel( aValue ); + + // how to handle root node? tree event docs don't say. + fireNodesChanged ( treePath().getPath(), + new int[] { 0 }, + new Object[] { this } ); + } + return; + } + + parentGroup.setValueForObject( + aValue, target, aKey ); + } + + /** + * Perform any clean up in this method. + * The node will not be reused after this method is called. + * This implementation removes itself from the parent's + * set of child nodes, sets target and datasource to null, + * and then calls disposeChildNodes(). + */ + protected void dispose() + { //System.out.println( "dispose: " + this.getClass().getName() + " : " + this ); + if ( parentGroup != null ) + { + ((DisplayGroupNode)parentGroup).childNodes.remove( + new ReferenceKey( target ) ); + } + setTarget( (Object) null ); + setDataSource( null ); + disposeChildNodes(); + } + + /** + * Calls dispose() on all child nodes. + */ + protected void disposeChildNodes() + { + Iterator i = new LinkedList(childNodes.values()).iterator(); + while ( i.hasNext() ) + { + ((DisplayGroupNode) i.next()).dispose(); + } + } + + /** + * Called after the target object posts a change notification. + * This implementation re-fetches which triggers + * updateDisplayedObjects to broadcast any tree events. + * This method marks the parent object as changed if: + * (1) this object is not registered in the editing context + * of the titles display group's data source (if any), AND + * (2) the children key is not in the list of attributes + * of the parent object's EOClassDescription. + */ + public void targetChanged() + { + // if not root node + if ( target != null ) + { + // if we're not root and not fetched, stop here. + //FIXME: with this, some nodes have old values when moved. + //FIXME: without this, nodes are unnecessarily fetched. + //FIXME: might have parent modify isFetched of certain child nodes. + if ( isFetched() ) + { + fetch(); + } + else // not fetched - just update the display + { + updateDisplayedObjects(); + } +/* +//disabling this for performance reasons: +//might reenable later or find an alternate approach + // check to see if we need to mark the parent object as changed + EOEditingContext context = dataSource().editingContext(); + if ( ( context == null ) + || ( context.globalIDForObject( target ) == null ) ) + { + DisplayGroupNode parentNode = (DisplayGroupNode) parentGroup; + if ( parentNode.target != null ) + { + // only notify if childrenKey is an attribute of parentDesc + // (and therefore not a toOne or toMany relationship) + EOClassDescription parentDesc = + EOClassDescription.classDescriptionForClass( + parentNode.target.getClass() ); + if ( parentDesc.attributeKeys().contains( parentAssociation.childrenKey ) ) + { + // only notify if no context is already observing the object + // and we are an attribute key + EOObserverCenter.notifyObserversObjectWillChange( parentNode.target ); + } + } + } +*/ + } + else // root node + { + setObjectArray( parentAssociation.titlesDisplayGroup.displayedObjects() ); + } + + // finally, broadcast change event for this node + // even though we're not sure if the displayed value changed. + fireNodeChanged(); + } + + /** + * Fires a change event for this node. + */ + public void fireNodeChanged() + { + // if not root node + if ( target != null ) + { + int index = ((DisplayGroupNode)parentGroup).getIndex( this ); + if ( ( index != -1 ) + && ( treePath().getParentPath() != null ) ) + { + fireNodesChanged ( + treePath().getParentPath().getPath(), + new int[] { index }, + new Object[] { this } ); + } + } + } + +Object[] previouslyDisplayedObjects = new Object[0]; + /** + * Overridden to call to super, fire any tree events, and then + * call updateDisplayedObjects on all fetched child nodes. + * This method compares this node's displayed objects against + * the list of child nodes, synchronizes them, and then broadcasts + * only the necessary events to bring the view component up to date. + */ + public void updateDisplayedObjects() + { +//System.out.println( "updateDisplayedObjects: " + " : " + this ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); +//new RuntimeException().printStackTrace(); + super.updateDisplayedObjects(); + + // diff lists + boolean proceed = true; + Object[] oldObjects = previouslyDisplayedObjects; + Object[] newObjects = displayedObjects.toArray(); + if ( oldObjects.length == newObjects.length ) + { + proceed = false; + for ( int i = 0; i < newObjects.length; i++ ) + { + if ( oldObjects[i] != newObjects[i] ) + { + proceed = true; + break; + } + } + } + + // this should be set before firing the change events + // in case some clients end up calling this again. + previouslyDisplayedObjects = newObjects; + + DisplayGroupNode node; + Iterator i = childNodes.values().iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + if ( !node.isFetchNeeded() ) + { + node.updateDisplayedObjects(); + } + } + + if ( proceed ) + { +//System.out.println( "DisplayGroupNode.firingEventsForChanges: " ); +//new RuntimeException().printStackTrace(); + fireEventsForChanges( oldObjects, newObjects ); + } + + } + + /** + * Called by processRecentChanges to analyze the + * differences between the lists and broadcast the + * appropriate events. + */ + protected void fireEventsForChanges( + Object[] oldObjects, Object[] newObjects ) + { + // structure changed causes havoc while + // establishing connection in some cases + //if ( oldObjects.length == 0 || newObjects.length == 0 ) + //{ + // fireStructureChanged( treePath().getPath(), null, null ); + // return; + //} + + int insertCount = 0; + int deleteCount = 0; + Object[] inserts = new Object[ newObjects.length ]; + Object[] deletes = new Object[ oldObjects.length ]; + + int i; + int n = -1, o = -1; // last match + int n1 = 0, o1 = 0; // current match test + int n2 = 0, o2 = 0; // scan ahead + + while ( o1 < oldObjects.length && n1 < newObjects.length ) + { + if ( newObjects[n1] == oldObjects[o1] ) + { + // mark as match and continue + o = o1; + n = n1; + } + else + { + // scan ahead for the next match, if any + o2 = o1; + n2 = n1; + + while ( o2 < oldObjects.length || n2 < newObjects.length ) + { + if ( o2 < oldObjects.length && newObjects[n1] == oldObjects[o2] ) + { + // run o1 to o2: mark as deletes + for ( i = o1; i < o2; i++ ) + { // System.out.println( "delete : " + i ); + deletes[i] = oldObjects[i]; + deleteCount++; + } + o1 = o2; // reset test + o = o1; // set match + n = n1; // set match + break; + } + if ( n2 < newObjects.length && newObjects[n2] == oldObjects[o1] ) + { + // run n1 to n2: mark as inserts + for ( i = n1; i < n2; i++ ) + { // System.out.println( "insert : " + i ); + inserts[i] = newObjects[i]; + insertCount++; + } + n1 = n2; // reset test + n = n1; // set match + o = o1; // set match + break; + } + o2++; + n2++; + } + } + if (n != n1) + { + inserts[n1] = newObjects[n1]; + insertCount++; + deletes[o1] = oldObjects[o1]; + deleteCount++; + //increment even though no match: + //the new object was marked as inserted and + //the old object was marked as deleted. + n = n1; + o = o1; + } + o1++; + n1++; + } + + // run o to end of oldObjects: mark as deletes + for ( i = o+1; i < oldObjects.length; i++ ) + { // System.out.println( "delete : " + i ); + deletes[i] = oldObjects[i]; + deleteCount++; + } + + // run n to end of newObjects: mark as inserts + for ( i = n+1; i < newObjects.length; i++ ) + { // System.out.println( "insert : " + i ); + inserts[i] = newObjects[i]; + insertCount++; + } + +//System.out.println( "done : " +//+ o + " : " + o1 + " : " + o2 + " :: " + n + " : " + n1 + " : " + n2 ); +//System.out.println( new NSArray( newObjects ) ); +//System.out.println( new NSArray( inserts ) ); +//System.out.println( new NSArray( deletes ) ); +//System.out.println( new NSArray( oldObjects ) ); + + int c; + Object[] nodes; + int[] indices; + + // broadcast delete event + c = 0; + nodes = new Object[ deleteCount ]; + indices = new int[ deleteCount ]; + for ( i = 0; i < deletes.length; i++ ) + { + if ( deletes[i] != null ) + { + indices[c] = i; + nodes[c] = getChildNodeForObject( deletes[i] ); + c++; + } + } + if ( c > 0 ) + { +// fireNodeChanged(); // force the jtree to get the correct child count + fireNodesRemoved( treePath().getPath(), indices, nodes ); + } + deletes = nodes; // retain for dispose check + + // broadcast insert event + c = 0; + nodes = new Object[ insertCount ]; + indices = new int[ insertCount ]; + for ( i = 0; i < inserts.length; i++ ) + { + if ( inserts[i] != null ) + { + indices[c] = i; + nodes[c] = getChildNodeForObject( inserts[i] ); + if ( nodes[c] == null ) + { + nodes[c] = createChildNodeForObject( newObjects[i] ); + } + c++; + } + } + if ( c > 0 ) + { + fireNodesInserted( treePath().getPath(), indices, nodes ); + } + + // dispose any delete nodes not on insert list + int j; + boolean found; + for ( i = 0; i < deletes.length; i++ ) + { + for ( j = 0; j < nodes.length; j++ ) + { + if ( deletes[i] == nodes[j] ) break; + } + + // did not break early, so not found, so dispose + if ( j == nodes.length ) + { + ((DisplayGroupNode)deletes[i]).dispose(); + } + } + } + + /** + * Sets the target object and creates an registers a target observer. + * If target was not previously null, the existing observer is unregistered. + * Protected access so subclasses and TreeModelAssociation can update our target. + */ + public void setTarget( Object aTarget ) + { + if ( target != null ) + { + EOObserverCenter.removeObserver( targetObserver, target ); + targetObserver.discardPendingNotification(); + } + + if ( aTarget != null ) + { + target = aTarget; + targetObserver = new TargetObserver( this ); + EOObserverCenter.addObserver( targetObserver, target ); + } + } + + /** + * Returns the parent display group, or null if parent is root. + */ + public DisplayGroupNode getParentGroup() + { + if ( parentGroup instanceof DisplayGroupNode ) + { + return (DisplayGroupNode)parentGroup; + } + // presumably the root node + return null; + } + + /** + * Gets all descendants of the this node. + */ + public List getDescendants() + { + return getDescendants( this, true ); + } + + /** + * Gets only the descendants of the this node + * whose children has been loaded - no fetching + * will occur. Useful for load-on-demand trees. + */ + public List getLoadedDescendants() + { + return getDescendants( this, false ); + } + + // breadth first traversal implementation + + /** + * Returns a list of all descendants of the + * specified node. Unfetched nodes are traversed + * only if forceLoad is true. + * This implementation is a breadth-first traversal + * of the nodes starting at the specified node. + */ + static private List getDescendants( DisplayGroupNode aNode, boolean forceLoad ) + { + if ( !forceLoad && !aNode.isFetched ) return NSArray.EmptyArray; + + LinkedList result = new LinkedList(); + LinkedList queue = new LinkedList(); + + queue.add( aNode ); + while ( ! queue.isEmpty() ) + { + checkNode( (DisplayGroupNode) queue.removeFirst(), + queue, result, forceLoad ); + } + + return result; + } + + /** + * Adds each fetched child node of the specified node to + * the result set (optionally forcing the child node to load) + * and adding child node to the end of the queue. + */ + static private void checkNode( DisplayGroupNode aNode, + LinkedList aQueue, LinkedList aResult, boolean forceLoad ) + { + DisplayGroupNode child; + int count = aNode.getChildCount(); + + for ( int i = 0; i < count; i++ ) + { + child = aNode.getChildNodeAt( i ); + + // add to queue if node has fetched children + if ( ( !child.isFetched ) && ( forceLoad ) ) + { + child.fetch(); + } + if ( child.isFetched ) + { + aQueue.addLast( child ); + } + + aResult.add( child ); + } + } + + /** + * Overridden to not fetch on InvalidateAllObjectsInStoreNotification + * unless we've already been fetched, preserving the load-on-demand + * functionality. + */ + public void objectsInvalidatedInEditingContext( NSNotification aNotification ) + { + if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + else + if ( ( EOEditingContext.ObjectsChangedInEditingContextNotification + .equals( aNotification.name() ) ) + || ( EOEditingContext.EditingContextDidSaveChangesNotification + .equals( aNotification.name() ) ) ) + { + int index; + Enumeration e; + boolean didChange = false; + NSDictionary userInfo = aNotification.userInfo(); + + // if our target object was deleted + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + if ( deletes.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: delete: " + this + " : " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + + // if our target object was invalidated + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + if ( invalidates != null && + invalidates.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: invalidate: " + this + " : " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + + // if our target object was updated, set fetchNeeded plus fire changed event + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + if ( updates.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + if ( object() instanceof Component ) ((Component)object()).repaint(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + } + + super.objectsInvalidatedInEditingContext( aNotification ); + + } + + // inner classes + + /** + * Private class used to force a hashmap to + * perform key comparisons by reference. + */ + private class ReferenceKey + { + private int hashCode; + private Object referent; + + public ReferenceKey( Object anObject ) + { + referent = anObject; + hashCode = anObject.hashCode(); + } + + /** + * Returns the actual key's hash code. + */ + public int hashCode() + { + return hashCode; + } + + /** + * Compares by reference. + */ + public boolean equals( Object anObject ) + { + if ( anObject instanceof ReferenceKey ) + { + return ((ReferenceKey)anObject).referent == referent; + } + return false; + } + } + + /** + * A private class to observe the target object of this node. + */ + private class TargetObserver extends EODelayedObserver + { + Reference ref; + + /** + * Pass in the display group node that will be updated + * when the target changes. + */ + public TargetObserver( DisplayGroupNode aDisplayGroup ) + { + ref = new WeakReference( aDisplayGroup ); + } + + /** + * Repopulate our display group, and calculate the deltas + * so we can broadcast appropriate events. + */ + public void subjectChanged () + { + DisplayGroupNode node = (DisplayGroupNode) ref.get(); + if ( node == null ) return; // node is null if gc'd. + //FIXME: should un-register self from observer center?? + + node.targetChanged(); + } + } + +} +/* + * $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.64 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.63 2003/06/06 14:20:07 mpowers + * getLoadedDescendants was forcing a fetch of the node it was called on. + * + * Revision 1.62 2003/06/03 14:48:33 mpowers + * Clean-up of notification handling for updates/invalidation/etc. + * Now fetching immediately on notification if the node is visible. + * This averts the infamous IndexOutOfBoundsException that occurs + * if fetching happens during repaint, because the BasicTreeUI is + * caching the number of child nodes before painting begins. + * + * Revision 1.61 2003/01/18 23:33:29 mpowers + * Fixing the build. + * + * Revision 1.60 2002/05/31 15:03:10 mpowers + * Fixes for the previous fix. Fat props to yjcheung. + * + * Revision 1.59 2002/05/28 15:31:36 mpowers + * Fix for updateDisplayedObjects for a subtle case where a node appears in + * the position that another node was moved from. + * + * Revision 1.58 2002/05/24 14:42:02 mpowers + * Prevent repeat events from firing if firing events loops back. + * + * Revision 1.57 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.56 2002/04/19 21:18:45 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.55 2002/04/19 20:53:22 mpowers + * Now firing event fewer events in fireEventsForChanges. + * + * Revision 1.54 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.53 2002/04/12 20:35:20 mpowers + * Now correctly setting parent display group and data source on creation. + * + * Revision 1.52 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.51 2002/04/03 20:13:36 mpowers + * Now differentiating between node instantiation caused by model expansion + * (user initiated) and by modifications to the model. + * Dispose now disposes all children. + * + * Revision 1.50 2002/03/23 16:20:27 mpowers + * Optimized processRecentChanges, minimized tree events. + * + * Revision 1.49 2002/03/11 03:15:06 mpowers + * Optimized processRecentChanges, minimize event firing, coalescing changes. + * Still need a better diff algorithm to avoid removing nodes. + * + * Revision 1.48 2002/03/10 00:59:39 mpowers + * Interim version: coalesces calls to process recent changes. + * Still does not handle rearranged nodes. + * + * Revision 1.47 2002/03/09 17:33:45 mpowers + * Nodes now track their child nodes by reference, not index. + * + * Revision 1.46 2002/03/08 23:19:07 mpowers + * Added getParentGroup to DisplayGroupNode. + * + * Revision 1.45 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.44 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.42 2002/02/19 22:28:46 mpowers + * DisplayGroupNodes immediately unregister themselves as editors. + * + * Revision 1.41 2002/02/13 16:27:38 mpowers + * Exposing setTarget. + * + * Revision 1.40 2001/11/02 20:55:46 mpowers + * Now using fixed index to send node removed events. This preserves the + * expanded state of the nodes in the corresponding jtree. + * + * Revision 1.39 2001/09/21 21:09:25 mpowers + * Exposed more fields as protected. + * + * Revision 1.38 2001/09/19 15:36:08 mpowers + * Refined behavior for isFetched after notification handling. + * + * Revision 1.37 2001/09/13 14:51:18 mpowers + * DisplayGroupNodes now dispose themselves and mark their parent for update + * when they receive notification that their target has been deleted. + * + * Revision 1.36 2001/09/10 14:10:24 mpowers + * Fix for notification handling. + * + * Revision 1.35 2001/07/30 16:17:01 mpowers + * Minor code cleanup. + * + * Revision 1.34 2001/07/18 22:13:39 mpowers + * getLoadedDescendants now works as advertised. + * Now correctly handling invalidateAllObjects notification. + * + * Revision 1.33 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.32 2001/06/18 14:10:28 mpowers + * Cleaned up event firing: no longer firing insert or remove events twice. + * + * Revision 1.31 2001/06/09 16:15:39 mpowers + * Revised the targetChanged scheme because oldObjects and newObjects were + * identical after the target object is invalidated. + * + * Revision 1.30 2001/05/21 22:17:19 mpowers + * Fix for tree out-of-synch problems when nodes are inserted. + * + * Revision 1.29 2001/05/18 21:07:46 mpowers + * Playing with refresh options. + * + * Revision 1.28 2001/05/14 15:25:43 mpowers + * DisplayGroupNodes now only respond to InvalidateAllObjectsInStore + * if they are already fetched. + * + * Revision 1.27 2001/05/08 19:55:58 mpowers + * Fix for node children not refreshing after sibling was inserted. + * + * Revision 1.26 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.25 2001/05/06 22:22:55 mpowers + * Debugging. + * + * Revision 1.24 2001/05/04 14:42:58 mpowers + * Now getting stored values in KeyValueCoding. + * MasterDetail now marks dirty based on whether it's an attribute + * or relation. + * Implemented editing context marker. + * + * Revision 1.23 2001/05/02 18:00:43 mpowers + * Removed debug code. + * + * Revision 1.22 2001/05/02 17:31:20 mpowers + * DisplayGroupNode now does a better job determining when to mark its + * parent dirty. + * + * Revision 1.21 2001/05/01 00:52:32 mpowers + * Implemented breadth-first traversal of tree for node. + * + * Revision 1.20 2001/04/26 01:15:19 mpowers + * Major clean-up of DisplayGroupNode: fitter, happier, more productive. + * + * Revision 1.19 2001/04/22 23:13:35 mpowers + * Minor bug. + * + * Revision 1.18 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.17 2001/04/21 23:05:12 mpowers + * A fairly major revisiting. I've decided to scrap the pass-thru approach + * where every node simply represents an index and not an object. + * The next update will have each node correspond to a specific object. + * + * Revision 1.16 2001/04/13 16:37:37 mpowers + * Handling bounds checking. + * + * Revision 1.15 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.14 2001/03/27 17:45:51 mpowers + * More index bounds checking. + * + * Revision 1.13 2001/03/22 21:25:42 mpowers + * Fixed some nasty issues with jtree's internal state and array bounds. + * + * Revision 1.12 2001/03/19 22:18:58 mpowers + * Root node now mirrors contents of titles display group. + * + * Revision 1.11 2001/03/19 21:38:36 mpowers + * Improved redisplay after edit. Editing nodes off root now works. + * + * Revision 1.10 2001/03/09 22:08:38 mpowers + * Removed unused line. + * + * Revision 1.9 2001/03/07 16:41:04 mpowers + * Now checking size of parent displayed objects array so that we don't + * get array out of bounds execeptions from isLeaf() or object() when + * those messages are called after the TreeAssociation fires a + * nodesDeleted event. I believe that JTree is mistakenly rendering + * those nodes one last time before erasing them. + * + * Revision 1.8 2001/03/06 23:21:27 mpowers + * Now only notifying parent if the object is not registered in the + * editing context, if any. + * + * Revision 1.7 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.6 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.5 2001/01/31 17:59:52 mpowers + * Fixed isLeaf aspect of TreeAssociation. + * + * Revision 1.4 2001/01/25 02:16:25 mpowers + * TreeAssociation now returns DisplayGroupNode.getUserObject. + * + * Revision 1.3 2001/01/24 18:14:40 mpowers + * Fixed problem with leaving children aspect unspecified. + * + * Revision 1.2 2001/01/24 16:35:37 mpowers + * Improved documentation on TreeAssociation. + * SortOrderings are now inherited from parent nodes. + * Updates after sorting are still lost on TreeController. + * + * Revision 1.1 2001/01/24 14:17:12 mpowers + * Major revision to TreeAssociation. Can now add and remove nodes. + * DisplayGroupNode is now it's own class. + * + * + */ + |
