/*
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.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;
import javax.swing.JTree;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreePath;
import net.wotonomy.foundation.NSArray;
import net.wotonomy.foundation.NSMutableArray;
import net.wotonomy.ui.EODisplayGroup;
/**
* TreeAssociation is a TreeModelAssociation further
* customized for JTrees. It binds a JTree 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. TreeAssociation 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.
* Note that the children aspect requires the bound
* display group to have a DataSource that can vend a
* DataSource appropriate for the bound key That data
* source is then used to create data sources for
* child nodes, and so on.
*
*
*
* - 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 visible nodes 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 visible nodes 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.
*
*
*
* All other usage is as TreeModelAssociation.
*
* @author michael@mpowers.net
* @author $Author: cgruber $
* @version $Revision: 904 $
*/
public class TreeAssociation extends TreeModelAssociation
implements FocusListener, TreeExpansionListener, TreeWillExpandListener, Runnable
{
private boolean isExpanding;
private Vector nodeQueue;
private Vector structureQueue;
private boolean isRunning;
/**
* Constructor expecting a JTree.
*/
public TreeAssociation ( Object anObject )
{
super( anObject );
init();
}
/**
* Constructor expecting a JTree or similar component
* and specifying a label for the root node.
*/
public TreeAssociation( Object anObject, Object aRootLabel )
{
super( anObject );
init();
rootLabel = aRootLabel;
rootNode.setUserObject( aRootLabel );
}
// convenience
private JTree component()
{
return (JTree) object();
}
/**
* Called by both constructors.
*/
protected void init()
{
isExpanding = false;
isRunning = false;
nodeQueue = new Vector();
structureQueue = new Vector();
component().addFocusListener( this );
component().addTreeExpansionListener( this );
component().addTreeWillExpandListener( this );
}
/**
* Returns whether this class can control the specified
* object.
*/
public static boolean isUsableWithObject ( Object anObject )
{
return ( anObject instanceof JTree );
}
/**
* Overridden to not fire events during initial population.
*/
public void establishConnection ()
{
isExpanding = true;
super.establishConnection();
isExpanding = false;
}
// interface TreeSelectionListener
public void valueChanged(TreeSelectionEvent e)
{
if ( ! isListening ) return;
// NOTE: This approach causes focus rectangle to perceptibly trail
// the selection rectangle, presumably because we're called after the
// new selection has been processed but before the focus is processed.
// Users don't like it, so we're going with a preference to use
// invokeLater.
/*
// paint immediately before updating the display group and
// before any potentially lengthy second-order effects happen:
// this improves user-perceived responsiveness of big apps
if ( object() instanceof javax.swing.JComponent )
{
javax.swing.JComponent component = (javax.swing.JComponent)object();
component.paintImmediately( component.getBounds() );
}
selectFromSelectionModel();
*/
// NOTE: This approach uses invoke later to cause the update of
// the display group (which could be lengthly if that in turn
// causes other things to update) to happen after the tree repaints.
// Users like this because it "feels faster", but developers have
// to remember that if they listen to tree selection events they
// will have to do a similar invoke later if they check the display
// group.
Runnable select = new Runnable()
{
public void run()
{
selectFromSelectionModel();
}
};
if ( selectionPaintedImmediately )
{
if ( object() instanceof java.awt.Component )
{
((java.awt.Component)object()).repaint();
}
EventQueue.invokeLater( select );
}
else
{
select.run();
}
}
/**
* Overridden to check whether the node is visible
* in the tree on screen. Offscreen in a scrollpane
* does not count.
*/
public boolean isVisible(Object node)
{
JTree tree = (JTree) object();
TreePath path = ((DisplayGroupNode)node).treePath();
if ( tree.isVisible( path ) )
{
Rectangle rowRect = tree.getPathBounds( path );
if ( rowRect != null )
{
Rectangle visible = tree.getVisibleRect();
if ( visible != null )
{
//System.out.println( "isVisible: intersects: " + visible.intersects( rowRect ) );
return visible.intersects( rowRect );
}
}
}
//System.out.println( "isVisible: false" );
return false;
}
/**
* 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(final Object source,
final Object[] path,
final int[] childIndices,
final Object[] children)
{
if ( !isExpanding )
{
for ( int i = 0; i < children.length; i++ )
{
nodeQueue.add( children[i] );
}
if ( !isRunning )
{
isRunning = true;
EventQueue.invokeLater( this );
}
}
}
/**
* 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)
{
if ( !isExpanding )
{
super.fireTreeNodesInserted( source, path, childIndices, children );
EventQueue.invokeLater( this );
}
}
/**
* 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)
{
if ( !isExpanding )
{
super.fireTreeNodesRemoved( source, path, childIndices, children );
EventQueue.invokeLater( this );
}
}
/**
* 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)
{
if ( !isExpanding )
{
structureQueue.add( path[path.length-1] );
if ( !isRunning )
{
isRunning = true;
EventQueue.invokeLater( this );
}
}
}
/**
* Overridden to return all visible rows in the tree.
*/
public NSArray objectsFetchedIntoChildrenGroup()
{
JTree tree = (JTree) object();
NSMutableArray objectList = new NSMutableArray();
int count = tree.getRowCount();
for ( int i = 0; i < count; i++ )
{
objectList.add(
((DisplayGroupNode) tree.getPathForRow( i ).getLastPathComponent()).object() );
}
//new net.wotonomy.ui.swing.util.StackTraceInspector( Integer.toString( objectList.size() ) );
return objectList;
}
// interface FocusListener
/**
* Notifies of beginning of edit.
*/
public void focusGained(FocusEvent evt)
{
Object o;
EODisplayGroup displayGroup;
Enumeration e = aspects().objectEnumerator();
while ( e.hasMoreElements() )
{
displayGroup =
displayGroupForAspect( e.nextElement().toString() );
if ( displayGroup != null )
{
displayGroup.associationDidBeginEditing( this );
}
}
}
/**
* Updates object on focus lost and notifies of end of edit.
*/
public void focusLost(FocusEvent evt)
{
if ( ! component().isEditing() )
{
Object o;
EODisplayGroup displayGroup;
Enumeration e = aspects().objectEnumerator();
while ( e.hasMoreElements() )
{
displayGroup =
displayGroupForAspect( e.nextElement().toString() );
if ( displayGroup != null )
{
displayGroup.associationDidEndEditing( this );
}
}
}
}
// interface TreeWillExpandListener
public void treeWillExpand(TreeExpansionEvent event)
throws ExpandVetoException
{
isExpanding = true;
}
public void treeWillCollapse(TreeExpansionEvent event)
throws ExpandVetoException
{
// do nothing
}
// interface TreeExpansionListener
/**
* Updates the children display group, if any.
*/
public void treeExpanded(TreeExpansionEvent event)
{ //System.out.println( "treeExpanded: " + event.getPath().getLastPathComponent() );
isExpanding = false;
if ( childrenDisplayGroup != null )
{
removeAsListener(); // prevent data source refetch: see fetchObjects()
childrenDisplayGroup.fetch();
addAsListener();
}
}
/**
* Updates the children display group, if any.
*/
public void treeCollapsed(TreeExpansionEvent event)
{
if ( childrenDisplayGroup != null )
{
removeAsListener(); // prevent data source refetch: see fetchObjects()
childrenDisplayGroup.fetch();
addAsListener();
}
}
// interface Runnable
/**
* Fires any queued node changed and structure changed events.
* Typically invoked on a delayed event loop.
*/
public void run()
{
DisplayGroupNode node;
int index;
Iterator i;
i = nodeQueue.iterator();
while ( i.hasNext() )
{
node = (DisplayGroupNode) i.next();
index = ((DisplayGroupNode)node.parentGroup).getIndex( node );
if ( ( index != -1 )
&& ( node.treePath().getParentPath() != null ) )
{
super.fireTreeNodesChanged(
node,
node.treePath().getParentPath().getPath(),
new int[] { index },
new Object[] { node } );
}
}
nodeQueue.clear();
i = structureQueue.iterator();
while ( i.hasNext() )
{
node = (DisplayGroupNode) i.next();
super.fireTreeStructureChanged(
node,
node.treePath().getPath(),
null,
null );
}
structureQueue.clear();
isRunning = false;
/*
EventQueue.invokeLater( new Runnable() { public void run() {
((JTree)object()).treeDidChange();
((JTree)object()).getParent().invalidate();
((JTree)object()).getParent().validate();
((JTree)object()).getParent().update( ((JTree)object()).getGraphics() );
// ((JTree)object()).getParent().doLayout();
// ((JTree)object()).getParent().repaint();
// ((JTree)object()).repaint();
// ((JTree)object()).updateUI();
} } );
*/
}
}
/*
* $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.55 2004/02/05 02:18:50 mpowers
* Super was calling back into this class before init() was called.
*
* Revision 1.54 2003/08/06 23:07:52 chochos
* general code cleanup (mostly, removing unused imports)
*
* Revision 1.53 2003/06/03 14:49:48 mpowers
* Now correctly calculating isVisible based on the component visible rect.
* Now deferring node changed events to a later event queue to allow repaints
* to happen after all changes have taken effect.
*
* Revision 1.52 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.51 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.49 2002/04/12 21:05:58 mpowers
* Now distinguishing changes in titles group even better.
*
* Revision 1.48 2002/04/12 20:36:31 mpowers
* Now distinguishing between changes made on titles group by tree expansion
* versus external changes which should cause us to repopulate root nodes.
*
* Revision 1.47 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.46 2002/03/27 20:44:53 mpowers
* Added isVisible test for node.
*
* Revision 1.45 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.44 2002/03/07 23:04:36 mpowers
* Refining TreeColumnAssociation.
*
* Revision 1.43 2002/03/06 13:04:15 mpowers
* Implemented cascading qualifiers in tree nodes.
*
* Revision 1.42 2002/03/05 23:18:28 mpowers
* Added documentation.
* Added isSelectionPaintedImmediate and isSelectionTracking attributes
* to TableAssociation.
* Added getTableAssociation to TableColumnAssociation.
*
* Revision 1.41 2002/03/01 23:42:08 mpowers
* Implemented TreeColumnAssociation, and updated documentation.
*
* Revision 1.40 2002/03/01 20:41:39 mpowers
* Now a focus listener and an expansion listener.
*
* Revision 1.39 2002/02/27 23:19:17 mpowers
* Refactoring of TreeAssociation to create TreeModelAssociation parent.
*
*
*/