blob: 248cdf808e934f23cb0153c1a29864e36edf719d [file] [log] [blame]
package net.sf.taverna.biocatalogue.ui.tristatetree;
import java.util.Enumeration;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
/**
* This class models tri-state nodes in the tree. Effectively
* it associates a tri-state checkbox with each tree node.
*
* Useful for partial selections of hierarchical data -
* partial selection of a node indicates that some of the
* children of that node are selected.
*
* @author Sergejs Aleksejevs
*/
@SuppressWarnings("serial")
public class TriStateTreeNode extends DefaultMutableTreeNode
{
private TriStateCheckBox.State state;
/**
* Creates a regular tree node; associated tri-state checkbox state is set to UNCHECKED.
*
* @param userObject The object this tree node will represent.
*/
public TriStateTreeNode(Object userObject) {
this(userObject, TriStateCheckBox.State.UNCHECKED);
}
/**
* Creates a regular tree node; associated tri-state checkbox state is set to the provided <code>initialState</code> value.
*
* @param userObject The object this tree node will represent.
* @param initialState One of the enum values of <code>TriStateCheckBox.State</code>.
*/
public TriStateTreeNode(Object userObject, TriStateCheckBox.State initialState) {
super(userObject);
this.state = initialState;
}
/**
* Compares based on the user object, not the state of this node.
*/
public boolean equals(Object other) {
if (other instanceof TriStateTreeNode) {
return (this.userObject.equals(((TriStateTreeNode)other).userObject));
}
else {
return (false);
}
}
/**
* Sets the state of the current node and (optionally) propagates
* those changes through the tree.
*
* @param state The new state to set - value from <code>TriStateCheckBox.State</code> enum.
* @param propagateChangesToRelatedNodes True - to use the tree checking model to
* propagate changes of the state of the current tree node to the
* other related tree nodes (e.g. all descendants and ancestors) -
* up and down the tree hierarchy. False - to only update the current
* node and make no changes to the rest of the tree.
*/
public void setState(TriStateCheckBox.State state, boolean propagateChangesToRelatedNodes)
{
this.state = state;
// check if the tree checking model should be activated
if (propagateChangesToRelatedNodes) {
updateStateOfRelatedNodes();
}
}
/**
* Sets the state of the current node.
*
* @param state The new state to set - value from <code>TriStateCheckBox.State</code> enum.
*/
public void setState(TriStateCheckBox.State state) {
setState(state, false);
}
public TriStateCheckBox.State getState() {
return state;
}
/**
* Toggles the state of the associated tri-state checkbox.
* State transitions are as follows:</br>
* <code>
* TriStateCheckBox.State.CHECKED -> TriStateCheckBox.State.UNCHECKED
* TriStateCheckBox.State.PARTIAL -> TriStateCheckBox.State.UNCHECKED
* TriStateCheckBox.State.UNCHECKED -> TriStateCheckBox.State.CHECKED
* </code>
*
* @param propagateChangesToRelatedNodes True - to use the tree checking model to
* propagate changes of the state of the current tree node to the
* other related tree nodes (e.g. all descendants and ancestors) -
* up and down the tree hierarchy. False - to only update the current
* node and make no changes to the rest of the tree.
* @return The value of the new state.
*/
public TriStateCheckBox.State toggleState(boolean propagateChangesToRelatedNodes)
{
if (state.equals(TriStateCheckBox.State.CHECKED) || state.equals(TriStateCheckBox.State.PARTIAL)) {
state = TriStateCheckBox.State.UNCHECKED;
}
else if (state.equals(TriStateCheckBox.State.UNCHECKED)) {
state = TriStateCheckBox.State.CHECKED;
}
// check if the tree checking model should be activated
if (propagateChangesToRelatedNodes) {
updateStateOfRelatedNodes();
}
return (state);
}
/*
* === The tree CHECKING MODEL ===
*
* Effectively, this defines the way the tree reacts to it's nodes
* being checked / unchecked. Only one model is implemented at the
* moment, therefore it's not extracted into a separate class, but
* remains to be a part of the TriStateTreeNode.
*
* Could possibly be better placed within the JTriStateTree, rather
* than TriStateTreeNode.
*/
/**
* The entry point - must be invoked to traverse the tree and make
* changes to checking states of related tree nodes.
*/
public void updateStateOfRelatedNodes()
{
updateStateOfAncestors(this.getParent());
updateStateOfDescendants(this);
}
/**
* Recursively visits all ancestors of the <code>parentNode</code>
* and decides on their checking states based on the states of their
* children nodes.
*
* @param parentNode Initially - parent node of the current node (i.e. the one,
* for which a state update has been made); then updated for
* recursive calls.
*/
private void updateStateOfAncestors(TreeNode parentNode)
{
// reached root of the tree, do nothing - return
if (parentNode == null) {
return;
}
if (parentNode instanceof TriStateTreeNode) {
TriStateTreeNode parentTriStateNode = (TriStateTreeNode)parentNode;
// explicitly fetch children into a new enumeration - this is
// to make sure that we work with the same enumeration, rather
// than obtaining a fresh one with every reference to 'parentTriStateNode.children()'
Enumeration childNodes = parentTriStateNode.children();
// go through all the children and count the number of selected ones
int iChildrenCount = 0;
int iPartiallySelectedChildren = 0;
int iSelectedChildren = 0;
while(childNodes.hasMoreElements()) {
Object node = childNodes.nextElement();
if (node instanceof TriStateTreeNode) {
TriStateTreeNode currentNode = (TriStateTreeNode)node;
iChildrenCount++;
if (currentNode.getState().equals(TriStateCheckBox.State.CHECKED)) {
iSelectedChildren++;
}
else if (currentNode.getState().equals(TriStateCheckBox.State.PARTIAL)) {
iPartiallySelectedChildren++;
}
}
}
// decide on the state of the 'parentNode' based on the checking state of its children
if (iSelectedChildren == 0 && iPartiallySelectedChildren == 0) {
// no children are selected
parentTriStateNode.setState(TriStateCheckBox.State.UNCHECKED);
}
else if ((iSelectedChildren + iPartiallySelectedChildren) > 0 && iSelectedChildren < iChildrenCount) {
// some children are selected (either partially or fully)
parentTriStateNode.setState(TriStateCheckBox.State.PARTIAL);
}
else if (iSelectedChildren > 0 && iSelectedChildren == iChildrenCount) {
// all children are selected
parentTriStateNode.setState(TriStateCheckBox.State.CHECKED);
}
// repeat the same recursively up the hierarchy
updateStateOfAncestors(parentTriStateNode.getParent());
}
}
/**
* Recursively traverses all descendants of the <code>parentNode</code>
* to set their checking state to the value of the state of the <code>parentNode</code>.
*
* @param parentNode Initially - the tree node for which the state
* change was made; then updated for recursive calls.
*/
private void updateStateOfDescendants(TriStateTreeNode parentNode)
{
// explicitly fetch children into a new enumeration - this is
// to make sure that we work with the same enumeration, rather
// than obtaining a fresh one with every reference to 'parentNode.children()'
Enumeration childNodes = parentNode.children();
// for all child nodes do 2 things:
// - set their state as that of the parent;
// - repeat the same recursively with their children
while(childNodes.hasMoreElements()) {
Object node = childNodes.nextElement();
if (node instanceof TriStateTreeNode) {
TriStateTreeNode currentNode = (TriStateTreeNode) node;
currentNode.setState(parentNode.getState());
currentNode.updateStateOfDescendants(currentNode);
}
}
}
/*
* === End of CHECKING MODEL implementation.
*/
}