summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java
diff options
context:
space:
mode:
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.java1518
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.
+ *
+ *
+ */
+