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