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