| /******************************************************************************* |
| * Copyright (c) 2007 Oracle. All rights reserved. |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v1.0, which accompanies this distribution |
| * and is available at http://www.eclipse.org/legal/epl-v10.html. |
| * |
| * Contributors: |
| * Oracle - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.jpt.utility.internal.model.value.swing; |
| |
| import java.util.ArrayList; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import javax.swing.event.TreeModelListener; |
| import javax.swing.tree.TreePath; |
| |
| import org.eclipse.jpt.utility.internal.StringTools; |
| import org.eclipse.jpt.utility.internal.model.event.ListChangeEvent; |
| import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent; |
| import org.eclipse.jpt.utility.internal.model.event.StateChangeEvent; |
| import org.eclipse.jpt.utility.internal.model.listener.ListChangeListener; |
| import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener; |
| import org.eclipse.jpt.utility.internal.model.listener.StateChangeListener; |
| import org.eclipse.jpt.utility.internal.model.value.ListValueModel; |
| import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel; |
| import org.eclipse.jpt.utility.internal.model.value.ReadOnlyPropertyValueModel; |
| import org.eclipse.jpt.utility.internal.model.value.TreeNodeValueModel; |
| import org.eclipse.jpt.utility.internal.model.value.ValueModel; |
| |
| /** |
| * This javax.swing.tree.TreeModel can be used to keep a TreeModelListener |
| * (e.g. a JTree) in synch with a tree of TreeNodeValueModel objects. Unlike |
| * javax.swing.tree.DefaultTreeModel, you do not add and remove nodes with |
| * methods implemented here. You can add and remove nodes by adding and |
| * removing them directly to/from the nodes (or, more typically, the domain |
| * objects the nodes are wrapping and listening to). |
| * |
| * Due to limitations in JTree, the root of the tree can never be null, |
| * which, typically, should not be a problem. (If you want to display an empty |
| * tree you can set the JTree's treeModel to null.) |
| */ |
| public class TreeModelAdapter |
| extends AbstractTreeModel |
| { |
| /** |
| * A value model on the underlying tree's root node and its |
| * corresponding listener. This allows clients to swap out |
| * the entire tree. Due to limitations in JTree, the root should |
| * never be set to null while we have listeners. |
| */ |
| private final PropertyValueModel rootHolder; |
| private final PropertyChangeListener rootListener; |
| |
| /** |
| * A listener that notifies us when a node's internal |
| * "state" changes (as opposed to the node's value or list of |
| * children), allowing us to forward notification to our listeners. |
| */ |
| private final StateChangeListener nodeStateListener; |
| |
| /** |
| * A listener that notifies us when a node's "value" |
| * changes (as opposed to the node's state or list of |
| * children), allowing us to forward notification to our listeners. |
| * Typically, this will only happen with nodes that hold |
| * primitive data. |
| */ |
| private final PropertyChangeListener nodeValueListener; |
| |
| /** |
| * A listener that notifies us when an underlying node's |
| * "list" of children changes, allowing us to keep our |
| * internal tree in synch with the underlying tree model. |
| */ |
| private final ListChangeListener childrenListener; |
| |
| /* these attributes make up our internal tree */ |
| /** |
| * The root cannot be null while we have listeners, which is |
| * most of the time. The root is cached so we can disengage |
| * from it when it has been swapped out. |
| */ |
| private TreeNodeValueModel root; |
| |
| /** |
| * Map the nodes to their lists of children. |
| * We cache these so we can swap out the entire list of children |
| * when we receive a #listChanged() event (which does not include |
| * the items that were affected). |
| * @see EventChangePolicy#rebuildChildren() |
| */ |
| final IdentityHashMap<TreeNodeValueModel, List<TreeNodeValueModel>> childrenLists; |
| |
| /** |
| * Map the children models to their parents. |
| * We cache these so we can figure out the "real" source of the |
| * list change events (the parent). |
| * @see EventChangePolicy#parent() |
| */ |
| final IdentityHashMap<ListValueModel, TreeNodeValueModel> parents; |
| |
| |
| // ********** constructors ********** |
| |
| /** |
| * Construct a tree model for the specified root. |
| */ |
| public TreeModelAdapter(PropertyValueModel rootHolder) { |
| super(); |
| if (rootHolder == null) { |
| throw new NullPointerException(); |
| } |
| this.rootHolder = rootHolder; |
| this.rootListener = this.buildRootListener(); |
| this.nodeStateListener = this.buildNodeStateListener(); |
| this.nodeValueListener = this.buildNodeValueListener(); |
| this.childrenListener = this.buildChildrenListener(); |
| this.childrenLists = new IdentityHashMap<TreeNodeValueModel, List<TreeNodeValueModel>>(); |
| this.parents = new IdentityHashMap<ListValueModel, TreeNodeValueModel>(); |
| } |
| |
| /** |
| * Construct a tree model for the specified root. |
| */ |
| public TreeModelAdapter(TreeNodeValueModel root) { |
| this(new ReadOnlyPropertyValueModel(root)); |
| } |
| |
| |
| // ********** initialization ********** |
| |
| private PropertyChangeListener buildRootListener() { |
| return new PropertyChangeListener() { |
| public void propertyChanged(PropertyChangeEvent e) { |
| TreeModelAdapter.this.rootChanged(); |
| } |
| @Override |
| public String toString() { |
| return "root listener"; |
| } |
| }; |
| } |
| |
| private PropertyChangeListener buildNodeValueListener() { |
| return new PropertyChangeListener() { |
| public void propertyChanged(PropertyChangeEvent e) { |
| TreeModelAdapter.this.nodeChanged((TreeNodeValueModel) e.getSource()); |
| } |
| @Override |
| public String toString() { |
| return "node value listener"; |
| } |
| }; |
| } |
| |
| private StateChangeListener buildNodeStateListener() { |
| return new StateChangeListener() { |
| public void stateChanged(StateChangeEvent e) { |
| TreeModelAdapter.this.nodeChanged((TreeNodeValueModel) e.getSource()); |
| } |
| @Override |
| public String toString() { |
| return "node state listener"; |
| } |
| }; |
| } |
| |
| private ListChangeListener buildChildrenListener() { |
| return new ListChangeListener() { |
| public void itemsAdded(ListChangeEvent e) { |
| new EventChangePolicy(e).addChildren(); |
| } |
| public void itemsRemoved(ListChangeEvent e) { |
| new EventChangePolicy(e).removeChildren(); |
| } |
| public void itemsReplaced(ListChangeEvent e) { |
| new EventChangePolicy(e).replaceChildren(); |
| } |
| public void itemsMoved(ListChangeEvent e) { |
| new EventChangePolicy(e).moveChildren(); |
| } |
| public void listCleared(ListChangeEvent e) { |
| new EventChangePolicy(e).clearChildren(); |
| } |
| public void listChanged(ListChangeEvent e) { |
| new EventChangePolicy(e).rebuildChildren(); |
| } |
| @Override |
| public String toString() { |
| return "children listener"; |
| } |
| }; |
| } |
| |
| |
| // ********** TreeModel implementation ********** |
| |
| public Object getRoot() { |
| return this.root; |
| } |
| |
| public Object getChild(Object parent, int index) { |
| return ((TreeNodeValueModel) parent).child(index); |
| } |
| |
| public int getChildCount(Object parent) { |
| return ((TreeNodeValueModel) parent).childrenSize(); |
| } |
| |
| public boolean isLeaf(Object node) { |
| return ((TreeNodeValueModel) node).isLeaf(); |
| } |
| |
| public void valueForPathChanged(TreePath path, Object newValue) { |
| ((TreeNodeValueModel) path.getLastPathComponent()).setValue(newValue); |
| } |
| |
| public int getIndexOfChild(Object parent, Object child) { |
| return ((TreeNodeValueModel) parent).indexOfChild((TreeNodeValueModel) child); |
| } |
| |
| /** |
| * Extend to start listening to the underlying model if necessary. |
| */ |
| @Override |
| public void addTreeModelListener(TreeModelListener l) { |
| if (this.hasNoTreeModelListeners()) { |
| this.engageModel(); |
| } |
| super.addTreeModelListener(l); |
| } |
| |
| /** |
| * Extend to stop listening to the underlying model if appropriate. |
| */ |
| @Override |
| public void removeTreeModelListener(TreeModelListener l) { |
| super.removeTreeModelListener(l); |
| if (this.hasNoTreeModelListeners()) { |
| this.disengageModel(); |
| } |
| } |
| |
| |
| // ********** behavior ********** |
| |
| /** |
| * Listen to the root and all the other nodes |
| * in the underlying tree model. |
| */ |
| private void engageModel() { |
| this.rootHolder.addPropertyChangeListener(ValueModel.VALUE, this.rootListener); |
| this.root = (TreeNodeValueModel) this.rootHolder.value(); |
| if (this.root == null) { |
| throw new NullPointerException(); // the root cannot be null while we have listeners |
| } |
| this.engageNode(this.root); |
| this.addRoot(); |
| } |
| |
| /** |
| * Add the root and all of the nodes to the underlying tree. |
| */ |
| private void addRoot() { |
| this.addNode(0, this.root); |
| } |
| |
| /** |
| * Stop listening to the root and all the other |
| * nodes in the underlying tree model. |
| */ |
| private void disengageModel() { |
| this.removeRoot(); |
| this.disengageNode(this.root); |
| this.root = null; |
| this.rootHolder.removePropertyChangeListener(ValueModel.VALUE, this.rootListener); |
| } |
| |
| /** |
| * Remove the root and all of the nodes from the underlying tree. |
| */ |
| private void removeRoot() { |
| this.removeNode(0, this.root); |
| } |
| |
| /** |
| * The root has been swapped. |
| * This method is a bit gnarly because the API for notifying listeners |
| * that the root has changed is a bit inconsistent with that used for |
| * non-root nodes. |
| */ |
| void rootChanged() { |
| TreeNodeValueModel newRoot = (TreeNodeValueModel) this.rootHolder.value(); |
| if (newRoot == null) { |
| throw new NullPointerException(); // the root cannot be null while we have listeners |
| } |
| // remove all the current root's children from the tree |
| // and remove the it from the internal tree |
| this.removeRoot(); |
| |
| // save the old root and swap in the new root |
| TreeNodeValueModel oldRoot = this.root; |
| this.root = newRoot; |
| |
| // we must be listening to both the old and new roots when we fire the event |
| // because their values can be affected by whether they have listeners |
| this.engageNode(this.root); |
| this.fireTreeRootReplaced(this.root); |
| // now we can stop listening to the old root |
| this.disengageNode(oldRoot); |
| |
| // add the new root to the internal tree and |
| // add all its children to the tree also |
| this.addRoot(); |
| } |
| |
| /** |
| * Either the "value" or the "state" of the specified node has changed, |
| * forward notification to our listeners. |
| */ |
| void nodeChanged(TreeNodeValueModel node) { |
| TreeNodeValueModel parent = node.parent(); |
| if (parent == null) { |
| this.fireTreeRootChanged(node); |
| } else { |
| this.fireTreeNodeChanged(parent.path(), parent.indexOfChild(node), node); |
| } |
| } |
| |
| /** |
| * Listen to the nodes, notify our listeners that the nodes were added, |
| * and then add the nodes to our internal tree. |
| * We must listen to the nodes before notifying anybody, because |
| * adding a listener can change the value of a node. |
| */ |
| void addChildren(Object[] path, int[] childIndices, Object[] children) { |
| int len = childIndices.length; |
| for (int i = 0; i < len; i++) { |
| this.engageNode((TreeNodeValueModel) children[i]); |
| } |
| this.fireTreeNodesInserted(path, childIndices, children); |
| for (int i = 0; i < len; i++) { |
| this.addNode(childIndices[i], (TreeNodeValueModel) children[i]); |
| } |
| } |
| |
| /** |
| * Listen to the node and its children model. |
| */ |
| private void engageNode(TreeNodeValueModel node) { |
| node.addStateChangeListener(this.nodeStateListener); |
| node.addPropertyChangeListener(ValueModel.VALUE, this.nodeValueListener); |
| node.childrenModel().addListChangeListener(ListValueModel.LIST_VALUES, this.childrenListener); |
| } |
| |
| /** |
| * Add the node to our internal tree; |
| * then recurse down through the node's children, |
| * adding them to the internal tree also. |
| */ |
| private void addNode(int index, TreeNodeValueModel node) { |
| this.addNodeToInternalTree(node.parent(), index, node, node.childrenModel()); |
| new NodeChangePolicy(node).addChildren(); |
| } |
| |
| /** |
| * Add the specified node to our internal tree. |
| */ |
| private void addNodeToInternalTree(TreeNodeValueModel parent, int index, TreeNodeValueModel node, ListValueModel childrenModel) { |
| List<TreeNodeValueModel> siblings = this.childrenLists.get(parent); |
| if (siblings == null) { |
| siblings = new ArrayList<TreeNodeValueModel>(); |
| this.childrenLists.put(parent, siblings); |
| } |
| siblings.add(index, node); |
| |
| this.parents.put(childrenModel, node); |
| } |
| |
| /** |
| * Remove nodes from our internal tree, notify our listeners that the |
| * nodes were removed, then stop listening to the nodes. |
| * We must listen to the nodes until after notifying anybody, because |
| * removing a listener can change the value of a node. |
| */ |
| void removeChildren(Object[] path, int[] childIndices, Object[] children) { |
| int len = childIndices.length; |
| for (int i = 0; i < len; i++) { |
| // the indices slide down a notch each time we remove a child |
| this.removeNode(childIndices[i] - i, (TreeNodeValueModel) children[i]); |
| } |
| this.fireTreeNodesRemoved(path, childIndices, children); |
| for (int i = 0; i < len; i++) { |
| this.disengageNode((TreeNodeValueModel) children[i]); |
| } |
| } |
| |
| /** |
| * First, recurse down through the node's children, |
| * removing them from our internal tree; |
| * then remove the node itself from our internal tree. |
| */ |
| private void removeNode(int index, TreeNodeValueModel node) { |
| new NodeChangePolicy(node).removeChildren(); |
| this.removeNodeFromInternalTree(node.parent(), index, node, node.childrenModel()); |
| } |
| |
| /** |
| * Remove the specified node from our internal tree. |
| */ |
| private void removeNodeFromInternalTree(TreeNodeValueModel parent, int index, TreeNodeValueModel node, ListValueModel childrenModel) { |
| this.parents.remove(childrenModel); |
| |
| List<TreeNodeValueModel> siblings = this.childrenLists.get(parent); |
| siblings.remove(index); |
| if (siblings.isEmpty()) { |
| this.childrenLists.remove(parent); |
| } |
| } |
| |
| /** |
| * Stop listening to the node and its children model. |
| */ |
| private void disengageNode(TreeNodeValueModel node) { |
| node.childrenModel().removeListChangeListener(ListValueModel.LIST_VALUES, this.childrenListener); |
| node.removePropertyChangeListener(ValueModel.VALUE, this.nodeValueListener); |
| node.removeStateChangeListener(this.nodeStateListener); |
| } |
| |
| void moveChildren(TreeNodeValueModel parent, int targetIndex, int sourceIndex, int length) { |
| List<TreeNodeValueModel> childrenList = this.childrenLists.get(parent); |
| ArrayList<TreeNodeValueModel> temp = new ArrayList<TreeNodeValueModel>(length); |
| for (int i = 0; i < length; i++) { |
| temp.add(childrenList.remove(sourceIndex)); |
| } |
| childrenList.addAll(targetIndex, temp); |
| |
| this.fireTreeStructureChanged(parent.path()); |
| } |
| |
| |
| // ********** standard methods ********** |
| |
| @Override |
| public String toString() { |
| return StringTools.buildToStringFor(this, this.root); |
| } |
| |
| |
| // ********** inner classes ********** |
| |
| /** |
| * Coalesce some of the common change policy behavior. |
| */ |
| private abstract class ChangePolicy { |
| |
| ChangePolicy() { |
| super(); |
| } |
| |
| /** |
| * Add the current set of children. |
| */ |
| void addChildren() { |
| TreeModelAdapter.this.addChildren(this.parent().path(), this.childIndices(), this.childArray()); |
| } |
| |
| /** |
| * Remove the current set of children. |
| */ |
| void removeChildren() { |
| TreeModelAdapter.this.removeChildren(this.parent().path(), this.childIndices(), this.childArray()); |
| } |
| |
| /** |
| * Return an array of the indices of the current set of children, |
| * which should be contiguous. |
| */ |
| int[] childIndices() { |
| return this.buildIndices(this.childrenStartIndex(), this.childrenSize()); |
| } |
| |
| /** |
| * Return an array of the current set of children. |
| */ |
| Object[] childArray() { |
| return this.buildArray(this.children(), this.childrenSize()); |
| } |
| |
| /** |
| * Build an array to hold the elements in the specified iterator. |
| * If they are different sizes, something is screwed up... |
| */ |
| Object[] buildArray(Iterator<?> stream, int size) { |
| Object[] array = new Object[size]; |
| for (int i = 0; stream.hasNext(); i++) { |
| array[i] = stream.next(); |
| } |
| return array; |
| } |
| |
| /** |
| * Return a set of indices, starting at zero and |
| * continuing for the specified size. |
| */ |
| int[] buildIndices(int size) { |
| return buildIndices(0, size); |
| } |
| |
| /** |
| * Return a set of indices, starting at the specified index and |
| * continuing for the specified size. |
| */ |
| int[] buildIndices(int start, int size) { |
| int[] indices = new int[size]; |
| int index = start; |
| for (int i = 0; i < size; i++) { |
| indices[i] = index++; |
| } |
| return indices; |
| } |
| |
| /** |
| * Return the parent of the current set of children. |
| */ |
| abstract TreeNodeValueModel parent(); |
| |
| /** |
| * Return the starting index for the current set of children. |
| */ |
| abstract int childrenStartIndex(); |
| |
| /** |
| * Return the size of the current set of children. |
| */ |
| abstract int childrenSize(); |
| |
| /** |
| * Return an interator on the current set of children. |
| */ |
| abstract Iterator children(); |
| |
| } |
| |
| |
| /** |
| * Wraps a ListChangeEvent for adding, removing, replacing, |
| * and changing children. |
| */ |
| private class EventChangePolicy extends ChangePolicy { |
| private ListChangeEvent event; |
| |
| EventChangePolicy(ListChangeEvent event) { |
| this.event = event; |
| } |
| |
| /** |
| * Map the ListChangeEvent's source to the corresponding parent. |
| */ |
| @Override |
| TreeNodeValueModel parent() { |
| return TreeModelAdapter.this.parents.get(this.event.getSource()); |
| } |
| |
| /** |
| * The ListChangeEvent's item index is the children start index. |
| */ |
| @Override |
| int childrenStartIndex() { |
| return this.event.index(); |
| } |
| |
| /** |
| * The ListChangeEvent's size is the children size. |
| */ |
| @Override |
| int childrenSize() { |
| return this.event.itemsSize(); |
| } |
| |
| /** |
| * The ListChangeEvent's items are the children. |
| */ |
| @Override |
| Iterator children() { |
| return this.event.items(); |
| } |
| |
| /** |
| * Remove the old nodes and add the new ones. |
| */ |
| void replaceChildren() { |
| Object[] parentPath = this.parent().path(); |
| int[] childIndices = this.childIndices(); |
| TreeModelAdapter.this.removeChildren(parentPath, childIndices, this.replacedChildren()); |
| TreeModelAdapter.this.addChildren(parentPath, childIndices, this.childArray()); |
| } |
| |
| /** |
| * Remove the old nodes and add the new ones. |
| */ |
| void moveChildren() { |
| TreeModelAdapter.this.moveChildren(this.parent(), this.event.targetIndex(), this.event.sourceIndex(), this.event.moveLength()); |
| } |
| |
| /** |
| * Clear all the nodes. |
| */ |
| void clearChildren() { |
| TreeNodeValueModel parent = this.parent(); |
| Object[] parentPath = parent.path(); |
| List<TreeNodeValueModel> childrenList = TreeModelAdapter.this.childrenLists.get(parent); |
| int[] childIndices = this.buildIndices(childrenList.size()); |
| Object[] childArray = this.buildArray(childrenList.iterator(), childrenList.size()); |
| TreeModelAdapter.this.removeChildren(parentPath, childIndices, childArray); |
| } |
| |
| /** |
| * Remove all the old nodes and add all the new nodes. |
| */ |
| void rebuildChildren() { |
| TreeNodeValueModel parent = this.parent(); |
| Object[] parentPath = parent.path(); |
| List<TreeNodeValueModel> childrenList = TreeModelAdapter.this.childrenLists.get(parent); |
| int[] childIndices = this.buildIndices(childrenList.size()); |
| Object[] childArray = this.buildArray(childrenList.iterator(), childrenList.size()); |
| TreeModelAdapter.this.removeChildren(parentPath, childIndices, childArray); |
| |
| childIndices = this.buildIndices(parent.childrenModel().size()); |
| childArray = this.buildArray((Iterator) parent.childrenModel().values(), parent.childrenSize()); |
| TreeModelAdapter.this.addChildren(parentPath, childIndices, childArray); |
| } |
| |
| /** |
| * The ListChangeEvent's replaced items are the replaced children. |
| */ |
| Object[] replacedChildren() { |
| return this.buildArray(this.event.replacedItems(), this.event.itemsSize()); |
| } |
| |
| } |
| |
| |
| /** |
| * Wraps a TreeNodeValueModel for adding and removing its children. |
| */ |
| private class NodeChangePolicy extends ChangePolicy { |
| private TreeNodeValueModel node; |
| |
| NodeChangePolicy(TreeNodeValueModel node) { |
| this.node = node; |
| } |
| |
| /** |
| * The node itself is the parent. |
| */ |
| @Override |
| TreeNodeValueModel parent() { |
| return this.node; |
| } |
| |
| /** |
| * Since we will always be dealing with all of the node's |
| * children, the children start index is always zero. |
| */ |
| @Override |
| int childrenStartIndex() { |
| return 0; |
| } |
| |
| /** |
| * Since we will always be dealing with all of the node's |
| * children, the children size is always equal to the size |
| * of the children model. |
| */ |
| @Override |
| int childrenSize() { |
| return this.node.childrenModel().size(); |
| } |
| |
| /** |
| * Since we will always be dealing with all of the node's |
| * children, the children are all the objects held by |
| * the children model. |
| */ |
| @Override |
| Iterator children() { |
| return (Iterator) this.node.childrenModel().values(); |
| } |
| |
| } |
| |
| } |