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