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