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