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