/*
Wotonomy: OpenStep design patterns for pure Java applications.
Copyright (C) 2000 Intersect Software Corporation
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, see http://www.gnu.org
*/
package net.wotonomy.ui.swing;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import net.wotonomy.control.EOClassDescription;
import net.wotonomy.control.EODataSource;
import net.wotonomy.control.EODelayedObserver;
import net.wotonomy.control.EOEditingContext;
import net.wotonomy.control.EOObserverCenter;
import net.wotonomy.control.EOObserverProxy;
import net.wotonomy.control.OrderedDataSource;
import net.wotonomy.control.PropertyDataSource;
import net.wotonomy.foundation.NSArray;
import net.wotonomy.foundation.NSMutableArray;
import net.wotonomy.foundation.NSSelector;
import net.wotonomy.foundation.internal.WotonomyException;
import net.wotonomy.ui.EOAssociation;
import net.wotonomy.ui.EODisplayGroup;
/**
* TreeModelAssociation binds a JTree or similar component
* that uses a TreeModel to a display group's
* list of displayable objects, each of which may have
* a list of child objects managed by another display
* group, and so on. TreeModelAssociation works exactly
* like a ListAssociation, with the additional capability
* to specify a "Children" aspect, that will allow child
* objects to be retrieved from a parent display group.
*
*
*
* - titles: a property convertable to a string for
* display in the nodes of the tree. The objects in
* the bound display group will be used to populate the
* initial root nodes of the tree (more accurately,
* children of the offscreen root node in the tree).
*
* - children: a property of a node value that returns
* zero, one or many objects, each of which will correspond
* to a child node for the corresponding node in the tree.
* The data source of the bound display group is replaced
* a data source that populates the display group with
* the selection in the tree component as determined by
* calling fetchObjectsIntoChildrenGroup.
* If this aspect is not bound, the tree behaves like a list.
*
* Binding this aspect with a null display group is the same
* as binding it with the titles display group.
* In this configuration the contents of the titles
* display group will be replaced with the selection in the
* tree component, as specified above, replacing the existing
* data source.
*
* In that case, the display groups for the nodes in
* the tree will still use the original data source
* for resolving their children key, and programmatically
* setting the contents of the display group will still
* repopulate the root nodes of the tree.
*
*
* - isLeaf: a property of a node value that returns
* a value convertable to a boolean value (aside from
* an actual boolean value, zeroes evaluate to true,
* as does any String containing "yes" or "true" or that
* is convertable to a number equal to zero; other values
* evaluate to false).
*
* If the isLeaf aspect is not bound,
* the tree must force nodes to load their children to
* determine whether they are leaf nodes (in effect
* loading the grandchildren for any expanded node).
* If bound, child loading is deferred until the node
* is actually expanded.
*
* For example, binding this value to a null
* display group and the key "false" will result in a
* deferred-loading tree that works much like Windows
* Explorer's network volume browser - all nodes appear
* with "pluses" until they are expanded.
*
* Note that the display group is ignored: the property
* will be applied directly to the object corresponding
* to the node.
*
*
*
* This class acts as the TreeModel for the controlled
* component: calling yourcomponent.getModel() will
* return this association. The tree model methods on
* this class are public and may be used to affect changes
* on the controlled components.
*
* The titles display group's contents are inserted
* into a new display group that acts as the root node.
* After that point, changes in the titles display group
* will cause the tree model to reset itself, creating
* a new display group for the root node.
*
*
* If a separate display group is bound to the children
* aspect, it will
* be used to hold the selected objects and their siblings
* and selection will be maintained there, and the titles
* display group selection will not be updated.
* Any editing or detail associations should in that case
* be attached to the children display group, not the titles
* group.
*
* Each node in the tree is an EODisplayGroup that
* contains the child objects of the object it represents
* in the tree. These objects can be programmatically
* inserted, updated, or removed using DisplayGroup
* methods. Each node's takes its parent group's
* sortOrderings until a sort ordering is explicitly
* specified - setting a sort ordering to null will resume
* using the parent group's sort ordering.
*
* Each node in the tree also implements MutableTreeNode.
* The value that a node represents is the titles property
* value of the object in the parent's displayed objects
* list at the index corresponding to the index of the node.
* Calling toString on a node returns the string representation
* of the titles property value, and setUserObject will update
* that value directly in the corresponding object.
* Moving a node from one parent to another will remove the
* actual object in the parent display group and insert it
* into the destination display group.
*
* In short, any nodes obtained from this class'
* implementation of TreeModel may be cast as either
* EODisplayGroup or MutableTreeNode and maybe be
* programmatically manipulated in either manner.
*
* @author michael@mpowers.net
* @author $Author: cgruber $
* @version $Revision: 904 $
*/
public class TreeModelAssociation extends EOAssociation
implements TreeModel, TreeSelectionListener
{
static final NSArray aspects =
new NSArray( new Object[] {
TitlesAspect, ChildrenAspect, IsLeafAspect
} );
static final NSArray aspectSignatures =
new NSArray( new Object[] {
AttributeToOneAspectSignature
} );
static final NSArray objectKeysTaken =
new NSArray( new Object[] {
"model"
} );
private final static NSSelector getSelectionModel =
new NSSelector( "getSelectionModel" );
private final static NSSelector setModel =
new NSSelector( "setModel",
new Class[] { TreeModel.class } );
EODisplayGroup titlesDisplayGroup, childrenDisplayGroup, leafDisplayGroup;
String titlesKey, childrenKey, leafKey;
DisplayGroupNode rootNode;
Vector listeners;
Object rootLabel;
TreeSelectionModel selectionModel;
boolean selectionPaintedImmediately;
boolean insertingChild;
boolean insertingAfter;
EOObserverProxy recentChangesObserver;
private boolean pleaseSelectRootNode;
/**
* Constructor expecting a JTree or any other object
* that has void setModel(TreeModel) and TreeModel getSelectionModel()
* methods. This tree association will be used for the TreeModel.
* The root node will be labeled "Root".
*
* As an alternate way to use a TreeModelAssociation, you may pass a
* TreeSelectionModel to the constructor and then manually set your
* component to use this class as its TreeModel.
*/
public TreeModelAssociation ( Object anObject )
{
super( anObject );
titlesDisplayGroup = null;
titlesKey = null;
childrenDisplayGroup = null;
childrenKey = null;
leafDisplayGroup = null;
leafKey = null;
listeners = new Vector();
selectionPaintedImmediately = false;
// after display group nodes process recent changes
recentChangesObserver = new EOObserverProxy(
this, new NSSelector( "processRecentChanges" ),
EODelayedObserver.ObserverPrioritySixth );
EOObserverCenter.addObserver( recentChangesObserver, this );
insertingChild = true;
insertingAfter = true;
pleaseSelectRootNode = false;
rootLabel = "Root";
rootNode = createNode( null, null );
}
/**
* Constructor expecting a JTree or similar component
* and specifying a label for the root node.
*/
public TreeModelAssociation( Object anObject, Object aRootLabel )
{
this( anObject );
rootLabel = aRootLabel;
rootNode.setUserObject( aRootLabel );
}
/**
* Gets the current root label.
*/
public Object rootLabel()
{
return rootLabel;
}
/**
* Gets the current root label.
*/
public Object getRootLabel()
{
return rootLabel();
}
/**
* Sets the root label.
*/
public void setRootLabel( Object aLabel )
{
rootLabel = aLabel;
}
/**
* Returns a List of aspect signatures whose contents
* correspond with the aspects list. Each element is
* a string whose characters represent a capability of
* the corresponding aspect.
* - "A" attribute: the aspect can be bound to
* an attribute.
* - "1" to-one: the aspect can be bound to a
* property that returns a single object.
* - "M" to-one: the aspect can be bound to a
* property that returns multiple objects.
*
* An empty signature "" means that the aspect can
* bind without needing a key.
* This implementation returns "A1M" for each
* element in the aspects array.
*/
public static NSArray aspectSignatures ()
{
return aspectSignatures;
}
/**
* Returns a List that describes the aspects supported
* by this class. Each element in the list is the string
* name of the aspect. This implementation returns an
* empty list.
*/
public static NSArray aspects ()
{
return aspects;
}
/**
* Returns a List of EOAssociation subclasses that,
* for the objects that are usable for this association,
* are less suitable than this association.
*/
public static NSArray associationClassesSuperseded ()
{
return new NSArray();
}
/**
* Returns whether this class can control the specified
* object.
*/
public static boolean isUsableWithObject ( Object anObject )
{
return setModel.implementedByObject( anObject );
}
/**
* Returns a List of properties of the controlled object
* that are controlled by this class. For example,
* "stringValue", or "selected".
*/
public static NSArray objectKeysTaken ()
{
return objectKeysTaken;
}
/**
* Returns the aspect that is considered primary
* or default. This is typically "value" or somesuch.
*/
public static String primaryAspect ()
{
return TitlesAspect;
}
/**
* Returns whether this association can bind to the
* specified display group on the specified key for
* the specified aspect.
*/
public boolean canBindAspect (
String anAspect, EODisplayGroup aDisplayGroup, String aKey )
{
return ( aspects.containsObject( anAspect ) );
}
/**
* Binds the specified aspect of this association to the
* specified key on the specified display group.
*/
public void bindAspect (
String anAspect, EODisplayGroup aDisplayGroup, String aKey )
{
if ( TitlesAspect.equals( anAspect ) )
{
titlesDisplayGroup = aDisplayGroup;
titlesKey = aKey;
}
if ( ChildrenAspect.equals( anAspect ) )
{
childrenDisplayGroup = aDisplayGroup;
childrenKey = aKey;
}
if ( IsLeafAspect.equals( anAspect ) )
{
leafDisplayGroup = aDisplayGroup;
leafKey = aKey;
}
if ( childrenDisplayGroup == null )
{
childrenDisplayGroup = titlesDisplayGroup;
}
super.bindAspect( anAspect, aDisplayGroup, aKey );
}
/**
* Establishes a connection between this association
* and the controlled object. Subclasses should begin
* listening for events from their controlled object here.
*/
public void establishConnection ()
{
if ( titlesDisplayGroup == null )
{
throw new WotonomyException(
"TreeModelAssociation: Titles aspect must be bound" );
}
// populate the root node
rootNode = createNode( titlesDisplayGroup, null );
rootNode.setObjectArray( titlesDisplayGroup.displayedObjects() );
rootNode.setSortOrderings( titlesDisplayGroup.sortOrderings() );
EODataSource dataSource = childrenDisplayGroup.dataSource();
if ( dataSource == null ) dataSource = titlesDisplayGroup.dataSource();
while ( dataSource instanceof DelegatingTreeDataSource )
{ // unwrap any existing delegating data sources
dataSource = ((DelegatingTreeDataSource)dataSource).delegateDataSource;
}
// create a new delegating data source
childrenDisplayGroup.setDataSource(
new DelegatingTreeDataSource( this, dataSource ) );
//TODO: find out why omitting this line causes weird selection behavior
childrenDisplayGroup.setSortOrderings( new NSArray() );
// check for alternate usage
if ( object() instanceof TreeSelectionModel )
{
selectionModel = (TreeSelectionModel) object();
}
else // use specified object
{
try
{
setModel.invoke( object(), new Object[] { this } );
selectionModel = (TreeSelectionModel)
getSelectionModel.invoke( object(), new Object[] {} );
}
catch ( Exception exc )
{
throw new WotonomyException( exc );
}
}
addAsListener();
super.establishConnection();
/*
fireRootStructureChanged();
// titlesGroupChanged = true;
// subjectChanged();
// update the children group
removeAsListener();
childrenDisplayGroup.fetch();
addAsListener();
// update selection
selectFromDisplayGroup( titlesDisplayGroup );
*/
}
protected void fireRootStructureChanged()
{
int count = rootNode.displayedObjects().count();
int[] childIndices = new int[ count ];
Object[] children = new Object[ count ];
for ( int i = 0; i < count; i++ )
{
childIndices[i] = i;
children[i] = rootNode.getChildNodeAt( i );
}
// must fire a tree structure changed with children,
// otherwise the tree gets weird selection behavior
fireTreeStructureChanged( this, new Object[] { rootNode },
childIndices, children );
}
/**
* Breaks the connection between this association and
* its object. Override to stop listening for events
* from the object.
*/
public void breakConnection ()
{
if ( childrenDisplayGroup != null )
{
if ( childrenDisplayGroup.dataSource() instanceof DelegatingTreeDataSource )
{
if ( titlesDisplayGroup == childrenDisplayGroup )
{
titlesDisplayGroup.setDataSource( ((DelegatingTreeDataSource)
childrenDisplayGroup.dataSource()).delegateDataSource );
}
else
{
childrenDisplayGroup.setDataSource( null );
}
}
}
removeAsListener();
super.breakConnection();
}
protected void addAsListener()
{
isListening = true;
selectionModel.addTreeSelectionListener( this );
}
protected void removeAsListener()
{
isListening = false;
selectionModel.removeTreeSelectionListener( this );
}
protected boolean isListening = false;
private boolean pleaseIgnore = false;
protected boolean titlesGroupChanged = false;
protected boolean childrenGroupChanged = false;
/**
* Overridden to better discriminate what is changed.
*/
public void objectWillChange( Object anObject )
{
if ( ! isListening ) return;
if ( anObject == titlesDisplayGroup )
{
titlesGroupChanged = true;
}
if ( anObject == childrenDisplayGroup )
{
childrenGroupChanged = true;
if ( childrenDisplayGroup.qualifier() != null )
{
if ( ( rootNode.qualifier() == null ) ||
! childrenDisplayGroup.qualifier().equals( rootNode.qualifier() ) )
{
// quietly move qualifier from children group to root node
rootNode.setQualifier( childrenDisplayGroup.qualifier() );
childrenDisplayGroup.setQualifier( null );
rootNode.updateDisplayedObjects();
}
}
}
super.objectWillChange( anObject );
}
/**
* Called when either the selection or the contents
* of an associated display group have changed.
*/
public void subjectChanged ()
{
// titles aspect
if ( titlesGroupChanged )
{
if ( titlesDisplayGroup.contentsChanged() )
{
NSArray displayedObjects = titlesDisplayGroup.displayedObjects();
NSArray childrenObjects;
if ( titlesDisplayGroup != childrenDisplayGroup
|| displayedObjects.count()
!= (childrenObjects = objectsFetchedIntoChildrenGroup()).count()
|| ! displayedObjects.containsAll( childrenObjects ) )
{
populateFromDisplayGroup( displayedObjects );
}
}
}
if ( childrenDisplayGroup.selectionChanged() && !childrenDisplayGroup.contentsChanged() )
{
selectFromDisplayGroup( childrenDisplayGroup );
}
titlesGroupChanged = false;
childrenGroupChanged = false;
}
/**
* Called by subjectChanged() in response to an external change in the titles display group.
*/
void populateFromDisplayGroup( List displayedObjects )
{
// trigger processRecentChanges
willChange();
// workaround: see below
int previousCount = rootNode.previouslyDisplayedObjects.length;
// update the root node
rootNode.setObjectArray( displayedObjects );
//FIXME: workaround for what appears to be a bug in JTree:
// if root node is not visible and has no children, insert events are ignored
if ( previousCount == 0 )
{
fireRootStructureChanged();
}
}
/**
* Package access so DisplayGroupNode can replace the selection after an update.
*/
void selectFromDisplayGroup( EODisplayGroup aDisplayGroup )
{ // System.out.println( "selectFromDisplayGroup: " + aDisplayGroup.selectedObjects() );
removeAsListener();
TreePath[] paths = selectionModel.getSelectionPaths();
NSArray selectedObjects = aDisplayGroup.selectedObjects();
// assemble current selection list
List treeSelection = new LinkedList();
if ( paths != null )
{
for ( int i = 0; i < paths.length; i ++ )
{
treeSelection.add(
((DisplayGroupNode)paths[i].getLastPathComponent()).getUserObject() );
}
}
if ( ! ( selectedObjects.size() == treeSelection.size()
&& treeSelection.containsAll( selectedObjects ) ) )
{
selectionModel.clearSelection();
// workaround to select root node from valueChanged()
if ( pleaseSelectRootNode )
{
selectionModel.addSelectionPath( new TreePath( this.getRoot() ) );
pleaseSelectRootNode = false;
}
//FIXME: display group is assumed to have only one instance of each object
for ( int i = 0; i < selectedObjects.count(); i++ )
{
//FIXME: selects only the first instance for now
//selectionModel.addSelectionPaths(
// getPathsForObject(
// selectedObjects.objectAtIndex( i ) ) );
selectionModel.addSelectionPath(
getPathForObject(
selectedObjects.objectAtIndex( i ) ) );
}
}
addAsListener();
}
/**
* Returns the first node found that represents the
* specified object, or null if not found.
* This implementation simply calls getPathForObject.
*/
public Object getNodeForObject( Object anObject )
{
TreePath result = getPathForObject( anObject );
if ( result != null )
{
return result.getLastPathComponent();
}
return null;
}
/**
* Returns the object represented by the specified node
* which must be a display group node from this tree.
*/
public Object getObjectForNode( Object aNode )
{
if ( aNode instanceof DisplayGroupNode )
{
return ((DisplayGroupNode)aNode).getUserObject();
}
// not a display group node
throw new WotonomyException(
"Not a display group node: " + aNode );
}
/**
* Returns the tree path for the specified node,
* which must be a display group node from this tree.
*/
public TreePath getPathForNode( Object aNode )
{
if ( aNode instanceof DisplayGroupNode )
{
return ((DisplayGroupNode)aNode).treePath();
}
// not a display group node
throw new WotonomyException(
"Not a display group node: " + aNode );
}
/**
* Returns the first tree path for the node that represents
* the specified object, or null if the object does not exist in this tree.
* This implementation does a breadth-first search of the tree
* for the object, looking only at nodes that have been loaded.
* This means that if the object does not exist in the tree,
* the entire tree must be traversed.
*/
public TreePath getPathForObject( Object anObject )
{
return getPathForObjectInPath( anObject, new TreePath( this.getRoot() ) );
}
/**
* Returns the tree path for the node that represents
* the specified object,
* or null if the object does not exist in this tree.
* This implementation does a breadth-first search of the tree
* for the object, looking only at nodes that have been loaded.
* This means that the entire tree is traversed.
*/
public TreePath[] getPathsForObject( Object anObject )
{
return getPathsForObjectInPath( anObject, new TreePath( this.getRoot() ) );
}
/**
* A breadth-first search of the tree starting
* at the specified tree path, comparing by reference.
* Returns immediately with the first match.
*/
private TreePath getPathForObjectInPath( Object anObject, TreePath aPath )
{
LinkedList queue = new LinkedList();
// add the specified path
queue.addLast( aPath );
return processQueue( anObject, queue, null );
}
/**
* A breadth-first search of the tree starting
* at the specified tree path, comparing by reference.
* The entire branch is searched before returning
* an array of all matches.
*/
private TreePath[] getPathsForObjectInPath( Object anObject, TreePath aPath )
{
LinkedList queue = new LinkedList();
// add the specified path
queue.addLast( aPath );
List result = new LinkedList();
processQueue( anObject, queue, result );
TreePath[] paths = new TreePath[ result.size() ];
for ( int i = 0; i < paths.length; i++ )
{
paths[i] = (TreePath) result.get(i);
}
return paths;
}
/**
* Processes the specified queue, appending results to aResult if it exists,
* or returning immediately with a TreePath is aResult is null.
*/
private TreePath processQueue( Object anObject, LinkedList aQueue, List aResult )
{
TreePath path;
while ( ! aQueue.isEmpty() )
{
path = (TreePath) aQueue.removeFirst();
path = checkNode( anObject, path, aQueue );
if ( path != null )
{
if ( aResult != null )
{
aResult.add( path );
}
else
{
return path;
}
}
}
return null;
}
/**
* Compares the specified object by reference each of the children of
* the node at the end of the specified tree path, adding nodes that
* do not match but have fetched object to the end of the specified queue.
* Returns the path of the first child node that matches the specified object,
* or null if no match was found.
*/
private TreePath checkNode( Object anObject, TreePath aPath, LinkedList aQueue )
{
TreePath result = null;
Object child;
Object parent = aPath.getLastPathComponent();
int count = getChildCount( parent );
for ( int i = 0; i < count; i++ )
{
child = getChild( parent, i );
// add to queue if node has fetched children
if ( ((DisplayGroupNode)child).isFetched )
{
aQueue.addLast( aPath.pathByAddingChild( child ) );
}
// compares by reference
if ( ((DisplayGroupNode)child).object() == anObject )
{
// assumes same object cannot be in display group twice
result = aPath.pathByAddingChild( child );
//System.out.println( "TRUE: " + ((DisplayGroupNode)child).object() + " == " + anObject );
}
// else
// {
//System.out.println( ((DisplayGroupNode)child).object() + " != " + anObject );
// }
}
return result;
}
// interface TreeSelectionListener
public void valueChanged(TreeSelectionEvent e)
{
if ( ! isListening ) return;
selectFromSelectionModel();
}
/**
* Determines whether the selection should be painted
* immediately after the user clicks and therefore
* before the children display group is updated.
* When the children group is bound to many associations
* or is bound to a master-detail association, updating
* the display group can take a perceptibly long time.
* This property defaults to false.
* @see #setSelectionPaintedImmediately
*/
public boolean isSelectionPaintedImmediately()
{
return selectionPaintedImmediately;
}
/**
* Sets whether the selection should be painted immediately.
* Setting this property to true will let the tree paint
* first before the display group is updated.
* This means that any tree selection listers will
* also be notified before the display group is updated
* and will have to invokeLater if they want to see the
* updated display group.
*/
public void setSelectionPaintedImmediately( boolean isImmediate )
{
selectionPaintedImmediately = isImmediate;
}
/**
* Package access so DisplayGroupNode can replace the selection.
* Returns the display group containing the current selection: titles or children.
*/
EODisplayGroup selectFromSelectionModel()
{ // System.out.print( "selectFromSelectionModel: " );
removeAsListener();
DisplayGroupNode node;
TreePath parentPath;
TreePath[] selectedPaths = selectionModel.getSelectionPaths();
NSMutableArray selectionList = new NSMutableArray();
if ( selectedPaths != null )
{
for ( int i = 0; i < selectedPaths.length; i++ )
{
// root node is zero - ignore root node
if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) )
{
// select root in selectFromDisplayGroup()
pleaseSelectRootNode = true;
}
else
{
node = (DisplayGroupNode)
selectedPaths[i].getLastPathComponent();
Object o = node.object();
if ( selectionList.indexOfIdenticalObject(o) == NSArray.NotFound )
{
selectionList.addObject( o );
}
}
}
}
childrenDisplayGroup.fetch(); //note that we're not currently listening for changes
if ( ! childrenDisplayGroup.selectObjectsIdenticalTo( selectionList ) )
{
addAsListener(); // because we don't have a listener stack
selectFromDisplayGroup( childrenDisplayGroup );
removeAsListener();
}
addAsListener();
return childrenDisplayGroup; // titles is now children if children not explicitly set
}
// interface TreeModel
public Object getRoot()
{
return rootNode;
}
public Object getChild(Object parent, int index)
{
// interestingly, this gets called by
// BasicTreeUI.paintVerticalPartOfLeg for
// the last child of each expanded tree node.
Object result = ((DisplayGroupNode)parent).getChildNodeAt( index );
//((DisplayGroupNode)parent).suppressRecentChangeProcessing();
return result;
// return ((DisplayGroupNode)parent).getChildNodeAt( index );
}
public int getChildCount(Object parent)
{
int result = ((DisplayGroupNode)parent).getChildCount();
//((DisplayGroupNode)parent).suppressRecentChangeProcessing();
return result;
// return ((DisplayGroupNode)parent).getChildCount();
}
public boolean isLeaf(Object node)
{
boolean result = ((DisplayGroupNode)node).isLeaf();
//((DisplayGroupNode)node).suppressRecentChangeProcessing();
return result;
// return ((DisplayGroupNode)node).isLeaf();
}
/**
* Returns whether this node is visible in the UI.
* This implementation returns true.
*
* Subclasses should return false if they can
* determine that the node is not displayed or
* expanded or otherwise visible. Non-visible
* nodes will fetch only when they are shown.
*/
public boolean isVisible(Object node)
{
return true;
}
public void valueForPathChanged(TreePath path, Object newValue)
{
((DisplayGroupNode)path.getLastPathComponent()).setUserObject( newValue );
}
public int getIndexOfChild(Object parent, Object child)
{
int result = ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child );
//((DisplayGroupNode)parent).suppressRecentChangeProcessing();
return result;
// return ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child );
}
public void addTreeModelListener(TreeModelListener aListener)
{
listeners.add( aListener );
}
public void removeTreeModelListener(TreeModelListener aListener)
{
listeners.remove( aListener );
}
/**
* Fires a tree nodes changed event to all listeners.
* Provided as a convenience if you need to make manual
* changes to the tree model.
*/
public void fireTreeNodesChanged(Object source,
Object[] path,
int[] childIndices,
Object[] children)
{
willChange(); // queue processRecentChanges
TreeModelEvent event = new TreeModelEvent(
source, path, childIndices, children );
//System.out.println( "fireTreeNodesChanged: " + event );
Enumeration it = listeners.elements();
while ( it.hasMoreElements() )
{
try
{
((TreeModelListener)it.nextElement()).treeNodesChanged( event );
}
catch ( Exception exc )
{
System.out.println( "TreeModelAssociation.fireTreeNodesChanged: caught: " + exc );
System.out.println( "Source:" + source );
System.out.println( "Path:" );
for ( int i = 0; i < path.length; i++ )
{
System.out.print( path[i] + "-" );
}
System.out.println();
System.out.println( "Indices:" );
for ( int i = 0; i < childIndices.length; i++ )
{
System.out.print( childIndices[i] + "-" );
}
System.out.println();
System.out.println( "Children:" );
for ( int i = 0; i < children.length; i++ )
{
System.out.print( children[i] + "-" );
}
System.out.println();
exc.printStackTrace();
}
}
}
/**
* Fires a tree nodes inserted event to all listeners.
* Provided as a convenience if you need to make manual
* changes to the tree model.
*/
public void fireTreeNodesInserted(Object source,
Object[] path,
int[] childIndices,
Object[] children)
{
willChange(); // queue processRecentChanges
TreeModelEvent event = new TreeModelEvent(
source, path, childIndices, children );
//System.out.println( "fireTreeNodesInserted: " + event );
Enumeration it = listeners.elements();
while ( it.hasMoreElements() )
{
try
{
((TreeModelListener)it.nextElement()).treeNodesInserted( event );
}
catch ( Exception exc )
{
System.out.println( "TreeModelAssociation.fireTreeNodesInserted: caught: " + exc );
}
}
}
/**
* Fires a tree nodes removed event to all listeners.
* Provided as a convenience if you need to make manual
* changes to the tree model.
*/
public void fireTreeNodesRemoved(Object source,
Object[] path,
int[] childIndices,
Object[] children)
{
willChange(); // queue processRecentChanges
TreeModelEvent event = new TreeModelEvent(
source, path, childIndices, children );
//System.out.println( "fireTreeNodesRemoved: " + event );
Enumeration it = listeners.elements();
while ( it.hasMoreElements() )
{
try
{
//NOTE: removing nodes causes tree to fire selection change event
// which confuses us if we're rearranging nodes (when sorting, for example).
boolean wasListening = isListening;
if ( wasListening ) isListening = false;
((TreeModelListener)it.nextElement()).treeNodesRemoved( event );
if ( wasListening ) isListening = true;
}
catch ( Exception exc )
{
System.out.println( "TreeModelAssociation.fireTreeNodesRemoved: caught: " + exc );
}
}
}
/**
* Fires a tree structure changed event to all listeners.
* Provided as a convenience if you need to make manual
* changes to the tree model.
*/
public void fireTreeStructureChanged(Object source,
Object[] path,
int[] childIndices,
Object[] children)
{
willChange(); // queue processRecentChanges
TreeModelEvent event = new TreeModelEvent(
source, path, childIndices, children );
//System.out.println( "fireStructureChanged: " + event );
Enumeration it = listeners.elements();
while ( it.hasMoreElements() )
{
((TreeModelListener)it.nextElement()).treeStructureChanged( event );
}
}
/**
* Creates and returns a new display group node.
*/
public DisplayGroupNode createNode( EODisplayGroup aParentGroup, Object anObject )
{
return new MutableDisplayGroupNode( this, aParentGroup, anObject );
}
/**
* Gets whether new objects programmatically inserted into the children
* display group should be inserted as a child of the first selected node.
* If false, new objects are inserted as siblings of the first selected node.
* Default value is true.
*/
public boolean isInsertingChild()
{
return insertingChild;
}
/**
* Sets whether new objects programmatically inserted into the children
* display group should be inserted as a child of the first selected node.
* If false, new objects are inserted as siblings of the first selected node.
* Default value is true.
*/
public void setInsertingChild( boolean asChild )
{
insertingChild = asChild;
}
/**
* Determines where new objects programmatically inserted into the children
* display group should be inserted, based on the value of insertingChild.
* If insertingChild, isInsertingAfter causes objects to be inserted at
* the end of the selected node's child list; otherwise, objects are inserted
* at the beginning of the list.
* If inserting as a sibling, isInsertingAfter causes objects to be inserted
* before the selected node in the selected node's parent's child list;
* otherwise, objects are inserted after the selected node in the child list.
* Default value is true.
*/
public boolean isInsertingAfter()
{
return insertingAfter;
}
/**
* Determines where new objects programmatically inserted into the children
* display group should be inserted, based on the value of insertingChild.
* If insertingChild, isInsertingAfter causes objects to be inserted at
* the end of the selected node's child list; otherwise, objects are inserted
* at the beginning of the list.
* If inserting as a sibling, isInsertingAfter causes objects to be inserted
* before the selected node in the selected node's parent's child list;
* otherwise, objects are inserted after the selected node in the child list.
* Default value is true.
*/
public void setInsertingAfter( boolean after )
{
insertingAfter = after;
}
/**
* Called to by the children group's data source when it receives
* an insertObject message, usually after an object has been inserted
* into the children display group.
* Return the object that should be passed to the titles display
* group's data source's implementation of insertObject, or return
* null to prevent that method from being called.
* This implementation inserts the specified object into the tree
* as determined by calling isInsertingChild and isInsertingAfter,
* then returns the unmodified object. If there's no selection, or
* no selection model, the root node is assumed to be selected.
* And if the root node is selected, the new node will obviously be
* inserted as a child. Override to customize.
*/
protected Object objectInsertedIntoChildrenGroup( Object anObject )
{
// determine selection
DisplayGroupNode selectedNode = (DisplayGroupNode) getRoot();
if ( selectionModel != null )
{
// get selected path
TreePath path = selectionModel.getSelectionPath();
// get selected node
if ( path != null )
{
selectedNode = (DisplayGroupNode) path.getLastPathComponent();
}
}
// determine location of insertion
int index = 0;
if ( ( isInsertingChild() ) || ( selectedNode == getRoot() ) )
{
if ( isInsertingAfter() )
{
index = selectedNode.getChildCount();
}
}
else // inserting as sibling
{
DisplayGroupNode parentNode = selectedNode.getParentGroup();
index = parentNode.getIndex( selectedNode );
if ( isInsertingAfter() )
{
index++;
}
selectedNode = parentNode;
}
// insert and return
selectedNode.insertObjectAtIndex( anObject, index );
return anObject;
}
/**
* Called to by the children group's data source when it receives
* a deleteObject message, usually after an object has been deleted
* from the children display group.
* Return the object that should be passed to the titles display
* group's data source's implementation of deleteObject, or return
* null to prevent that method from being called.
* This implementation deletes all instances of the selected object
* from the tree nodes that are currently loaded, and returns the
* unmodified object. Override to customize.
*/
protected Object objectDeletedFromChildrenGroup( Object anObject )
{
TreePath[] paths = getPathsForObject( anObject );
if ( paths != null )
{
for ( int i = 0; i < paths.length; i++ )
{
((DisplayGroupNode)paths[i].getLastPathComponent()).removeFromParent();
}
}
return anObject;
}
/**
* Called to by the children group's data source to populate it
* with all selected nodes and their siblings. To customize,
* override this method, or specify a different data source for
* the children display group.
*/
protected NSArray objectsFetchedIntoChildrenGroup()
{
DisplayGroupNode node;
TreePath parentPath;
TreePath[] selectedPaths = selectionModel.getSelectionPaths();
NSMutableArray objectList = new NSMutableArray();
if ( selectedPaths != null )
{
for ( int i = 0; i < selectedPaths.length; i++ )
{
// root node is zero - ignore root node
if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) )
{
// select root in selectFromDisplayGroup()
pleaseSelectRootNode = true;
}
else
{
node = (DisplayGroupNode)
selectedPaths[i].getLastPathComponent();
Object o = node.object();
// add all children of parent to object list - includes self
if ( node.parentGroup != null )
{
Enumeration e =
node.parentGroup.displayedObjects().objectEnumerator();
while ( e.hasMoreElements() )
{
// add only if not already in list
o = e.nextElement();
if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound )
{
objectList.addObject( o );
}
}
}
else // no parent node - add the node by itself
{
// add only if not already in list
if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound )
{
objectList.addObject( o );
}
}
}
}
}
// if no selection
if ( objectList.size() == 0 )
{
// populate with children of root
objectList.addAll( rootNode.displayedObjects() );
}
return objectList;
}
/**
* Queues processRecentChanges to be run in the event queue.
*/
private void willChange()
{
EOObserverCenter.notifyObserversObjectWillChange( this );
}
/**
* Tells the children display group to refetch, so that it reflects
* any changes that were made in the node tree,
* and then updates the selection in the selection model.
* Triggered in response to willChange().
*/
public void processRecentChanges()
{
Runnable update = new Runnable()
{
public void run()
{
removeAsListener(); // prevent data source refetch: see fetchObjects()
childrenDisplayGroup.fetch();
addAsListener();
selectFromDisplayGroup( childrenDisplayGroup );
}
};
if ( isListening )
{
if ( selectionPaintedImmediately )
{
// if painting selection immediately, run even later
// so that AWT's repaint event fires before we do.
SwingUtilities.invokeLater( update );
}
else
{
// otherwise run now
update.run();
}
}
}
/**
* Delegates most behaviors to the specified data source,
* except fetchObjects, which calls fetchObjectsIntoChildrenGroup
* on the tree model association. If delegate is null,
* calls are passed to the superclass which is a PropertyDataSource.
*/
static class DelegatingTreeDataSource extends PropertyDataSource
{
TreeModelAssociation parentAssociation;
EODataSource delegateDataSource;
public DelegatingTreeDataSource(
TreeModelAssociation aTreeModelAssociation, EODataSource aDataSource )
{
parentAssociation = aTreeModelAssociation;
delegateDataSource = aDataSource;
}
/**
* Calls to delegateDataSource if it exists, otherwise
* calls to super.
*/
public Object createObject()
{
if ( delegateDataSource != null )
{
return delegateDataSource.createObject();
}
return super.createObject();
}
/**
* Calls objectInsertedIntoChildrenGroup, and if not null
* calls to delegateDataSource.insertObject if it exists,
* and super.insertObjectAtIndex if not.
*/
public void insertObjectAtIndex( Object anObject, int anIndex )
{
anObject =
parentAssociation.objectInsertedIntoChildrenGroup(
anObject );
if ( anObject != null )
{
if ( delegateDataSource != null )
{
if ( delegateDataSource instanceof OrderedDataSource )
{
((OrderedDataSource)delegateDataSource).insertObjectAtIndex( anObject, anIndex );
}
else
{
delegateDataSource.insertObject( anObject );
}
}
else
{
super.insertObjectAtIndex( anObject, anIndex );
}
}
}
/**
* Calls objectDeletedIntoChildrenGroup, and if not null
* calls to delegateDataSource if it exists.
*/
public void deleteObject( Object anObject )
{
anObject =
parentAssociation.objectDeletedFromChildrenGroup(
anObject );
if ( anObject != null )
{
if ( delegateDataSource != null )
{
delegateDataSource.deleteObject( anObject );
}
super.deleteObject( anObject );
}
}
/**
* Overridden to return the delegate's editing context,
* the titles display group's editing context,
* and failing that calling to super.
*/
public EOEditingContext editingContext ()
{
EOEditingContext result = null;
if ( delegateDataSource != null )
{
result = delegateDataSource.editingContext();
}
if ( result == null )
{
EODataSource parentDataSource =
parentAssociation.titlesDisplayGroup.dataSource();
if ( parentDataSource != this && parentDataSource != null )
{
result = parentAssociation.titlesDisplayGroup.
dataSource().editingContext();
}
}
if ( result == null )
{
result = super.editingContext();
}
return result;
}
/**
* Returns a List containing the objects in this
* data source.
*/
public NSArray fetchObjects ()
{
// if titles group is doing double-duty as children group
if ( parentAssociation.titlesDisplayGroup == parentAssociation.childrenDisplayGroup )
{
// if we're not initiating this fetch
if ( parentAssociation.isListening )
{
// need to call to delegate to see if we should update values
if ( delegateDataSource != null )
{
// System.out.println( "fetching from delegate (slow!)" );
NSArray result = delegateDataSource.fetchObjects();
NSArray rootObjects = parentAssociation.rootNode.displayedObjects();
// if titles data source has different objects, return them
if ( rootObjects.count() != result.count()
|| ! rootObjects.containsAll( result ) )
{
// this will force the root node to repopulate in subjectChanged()
//System.out.println( "fetchObjects: data source" );
return result;
}
}
}
}
// otherwise: just repopulate the titles group
//System.out.println( "fetchObjects: objectsFetchedIntoChildrenGroup" );
return parentAssociation.objectsFetchedIntoChildrenGroup();
}
/**
* Returns a data source that is capable of
* manipulating objects of the type returned by
* applying the specified key to objects
* vended by this data source.
* @see #qualifyWithRelationshipKey
*/
public EODataSource
dataSourceQualifiedByKey ( String aKey )
{
if ( delegateDataSource != null )
{
return delegateDataSource.dataSourceQualifiedByKey( aKey );
}
return null;
}
/**
* Restricts this data source to vend those
* objects that are associated with the specified
* key on the specified object.
*/
public void
qualifyWithRelationshipKey (
String aKey, Object anObject )
{
if ( delegateDataSource != null )
{
delegateDataSource.qualifyWithRelationshipKey( aKey, anObject );
}
}
/**
* Returns the value from the delegateDataSource, if it exists.
* Otherwise calls super.
*/
public EOClassDescription classDescriptionForObjects()
{
if ( delegateDataSource != null )
{
return delegateDataSource.classDescriptionForObjects();
}
return super.classDescriptionForObjects();
}
}
}
/*
* $Log$
* Revision 1.2 2006/02/18 23:19:05 cgruber
* Update imports and maven dependencies.
*
* Revision 1.1 2006/02/16 13:22:22 cgruber
* Check in all sources in eclipse-friendly maven-enabled packages.
*
* Revision 1.20 2003/08/06 23:07:52 chochos
* general code cleanup (mostly, removing unused imports)
*
* Revision 1.19 2002/05/03 21:41:18 mpowers
* No longer clearing the selection model when updating from display group:
* we now only modify if a change needs to be made.
* No longer listening for selection change during firing of delete events:
* delete events cause JTree's to update their selection model.
* Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges()
* must happen after the screen is painted, or the selection is not displayed.
*
* Revision 1.18 2002/04/23 19:12:28 mpowers
* Reimplemented fireEventsForChanges. Fitter and happier.
*
* Revision 1.17 2002/04/19 21:18:46 mpowers
* Removed tree event coalescing, which was causing way too many problems.
* The fireChangeEvent algorithm is way faster than before, so we should
* still be better off than before. At least now, we don't have to track
* whether the view component has encountered a particular node.
*
* Revision 1.16 2002/04/18 20:36:11 mpowers
* TreeModelAssociation now populates children group before selected objects.
* Got rid of the forceOnSync workaround for cancelled selection change.
*
* Revision 1.15 2002/04/15 21:52:50 mpowers
* Tightening up TreeModelAssociation and DisplayGroupNode.
* Now only firing root structure changed once.
* Now disposing of root's children.
* Better event coalescing.
*
* Revision 1.14 2002/04/12 21:05:58 mpowers
* Now distinguishing changes in titles group even better.
*
* Revision 1.11 2002/04/10 21:20:04 mpowers
* Better handling for tree nodes when working with editing contexts.
* Better handling for invalidation. No longer broadcasting events
* when nodes have not been "registered" in the tree.
*
* Revision 1.10 2002/04/03 20:01:24 mpowers
* Removed printlns.
*
* Revision 1.8 2002/03/11 03:16:28 mpowers
* Better handling of change events; coalescing changes to children group.
*
* Revision 1.7 2002/03/08 23:19:57 mpowers
* Refactoring of DelegatingTreeDataSource to facilitate binding of titles
* and children aspects to the same display group.
*
* Revision 1.6 2002/03/07 23:04:36 mpowers
* Refining TreeColumnAssociation.
*
* Revision 1.5 2002/03/06 13:04:16 mpowers
* Implemented cascading qualifiers in tree nodes.
*
* Revision 1.4 2002/03/04 22:47:48 mpowers
* Fixed sort ordering for titles group. Optimization for delegate selection.
*
* Revision 1.3 2002/03/04 12:28:47 mpowers
* Revised case where children and titles are bound to same display group.
*
* Revision 1.2 2002/03/01 23:42:09 mpowers
* Implemented TreeColumnAssociation, and updated documentation.
*
* Revision 1.1 2002/02/27 23:19:17 mpowers
* Refactoring of TreeAssociation to create TreeModelAssociation parent.
*
* Revision 1.38 2002/02/18 03:46:08 mpowers
* Implemented TreeTableCellRenderer.
*
* Revision 1.37 2002/02/13 21:20:15 mpowers
* Updated comments.
*
* Revision 1.36 2001/11/21 15:13:25 mpowers
* Better repainting for selectionPaintedImmediately.
* Better handling for selection with multiple instances of the same
* object in the tree (from yjcheung).
*
* Revision 1.35 2001/11/20 19:13:51 mpowers
* Finished implementation of children group's specialized data source.
*
* Revision 1.34 2001/11/19 16:30:37 mpowers
* Tree repaint strategy is now a preference: selectionPaintedImmediately.
*
* Revision 1.33 2001/11/15 17:56:41 mpowers
* Initial implementation of data source for the children display group.
*
* Revision 1.32 2001/11/14 00:05:54 mpowers
* Eliminated the run later in favor of repainting the component immediately.
* This makes things more predictable for users of the association that
* want to listen to mouse or selection events on the tree.
*
* Revision 1.31 2001/11/02 20:43:15 mpowers
* Fixes for delegate's shouldChangeSelection veto (from yjcheung).
*
* Revision 1.30 2001/10/29 20:42:56 mpowers
* On selection change, repainting tree before notifying display group;
* using NSRunLoop instead of SwingUtilities.
*
* Revision 1.29 2001/10/12 20:12:53 mpowers
* Better handling of selection change vetoing when changing selection
* to a node that is not the sibling of the originally selected node.
*
* Revision 1.28 2001/09/14 13:40:26 mpowers
* User-initiated selection changes are now handled on the next event loop
* so that the component repaints the new selection before any potentially
* lengthy logic is triggered by the selection change.
*
* Revision 1.27 2001/09/10 14:10:03 mpowers
* Tree now handles multiple instances of the same object.
*
* Revision 1.26 2001/07/18 13:03:32 mpowers
* TreeNodes now refetch only on demand. Previously, once a node had
* been fetched, it was always refetched after an invalidate, even if
* the node was not being displayed.
*
* Revision 1.25 2001/05/14 15:25:35 mpowers
* No longer copying titles group's data source to children group.
*
* Revision 1.24 2001/05/08 18:47:34 mpowers
* Minor fixes for d3.
*
* Revision 1.23 2001/05/01 00:52:32 mpowers
* Implemented breadth-first traversal of tree for node.
*
* Revision 1.22 2001/04/26 01:15:19 mpowers
* Major clean-up of DisplayGroupNode: fitter, happier, more productive.
*
* Revision 1.21 2001/04/22 23:13:35 mpowers
* Minor bug.
*
* Revision 1.20 2001/04/22 23:05:33 mpowers
* Totally revised DisplayGroupNode so each object gets its own node
* (so the nodes are no longer fixed by index).
*
* Revision 1.19 2001/04/21 23:06:33 mpowers
* A major revisiting to support the revising of DisplayGroupNode.
*
* Revision 1.18 2001/04/03 20:36:01 mpowers
* Fixed refaulting/reverting/invalidating to be self-consistent.
*
* Revision 1.17 2001/03/29 21:35:08 mpowers
* Now handling circular references in the graph.
*
* Revision 1.16 2001/03/22 21:25:42 mpowers
* Fixed some nasty issues with jtree's internal state and array bounds.
*
* Revision 1.15 2001/03/19 21:37:58 mpowers
* Improved refresh of titles display group.
* Fixed dangling selection problem after refresh.
*
* Revision 1.14 2001/03/09 22:08:57 mpowers
* Trying to handle the dangling reference problem after an update.
*
* Revision 1.13 2001/02/17 17:23:49 mpowers
* More changes to support compiling with jdk1.1 collections.
*
* Revision 1.12 2001/01/25 02:16:25 mpowers
* TreeModelAssociation now returns DisplayGroupNode.getUserObject.
*
* Revision 1.11 2001/01/24 18:14:40 mpowers
* Fixed problem with leaving children aspect unspecified.
*
* Revision 1.10 2001/01/24 17:49:15 mpowers
* Added getObjectForNode and getNodeForObject convenience methods.
*
* Revision 1.9 2001/01/24 17:44:11 mpowers
* Renamed getPathForNode to getPathForObject to be more precise.
* And created a new getPathForNode method.
*
* Revision 1.8 2001/01/24 17:20:29 mpowers
* Children display group now holds siblings of selected objects
* in addition to the selected objects.
*
* Revision 1.5 2001/01/19 23:21:15 mpowers
* Fine tuning events broadcast from TreeModelAssociation.
*
* Revision 1.4 2001/01/18 21:27:29 mpowers
* Major rework of TreeModelAssociation.
*
* Revision 1.2 2001/01/11 20:29:19 mpowers
* Expanded access to tree event firing methods.
*
* Revision 1.1.1.1 2000/12/21 15:49:18 mpowers
* Contributing wotonomy.
*
* Revision 1.20 2000/12/20 16:25:42 michael
* Added log to all files.
*
*
*/