
eu.essilab.lablib.checkboxtree.DefaultTreeCheckingModel Maven / Gradle / Ivy
/*
* Copyright 2007-2022 Enrico Boldrini, Lorenzo Bigagli This file is part of
* CheckboxTree. CheckboxTree is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. CheckboxTree 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 General
* Public License for more details. You should have received a copy of the GNU
* General Public License along with CheckboxTree; if not, write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA
*/
package eu.essilab.lablib.checkboxtree;
import java.util.HashSet;
import java.util.Vector;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
/**
* The default checking model, providing methods for storing and retrieving the
* checked TreePaths. Adding/removing paths is delegated to a CheckingMode
* instance. This implementation is based on TreePath only and does not take
* advantage of TreeNode convenience methods (this is left to future/alternative
* implementations).
*
* @author Lorenzo Bigagli
* @author Enrico Boldrini
*/
// TODO: DefaultTreeCheckingModel allows to set null tree models, which will
// however
// cause nullPointerException (setTreeModel calls clearChecking...)
// Fix, either allowing or banning null tree models.
public class DefaultTreeCheckingModel implements TreeCheckingModel {
private final static TreeModel NULL_TREE_MODEL = new TreeModel() {
public void addTreeModelListener(TreeModelListener l) {
// nothing to do (cf. the Null Object pattern)
}
public Object getChild(Object parent, int index) {
return null;
}
public int getChildCount(Object parent) {
return 0;
}
public int getIndexOfChild(Object parent, Object child) {
return 0;
}
public Object getRoot() {
return null;
}
public boolean isLeaf(Object node) {
return false;
}
public void removeTreeModelListener(TreeModelListener l) {
// nothing to do (cf. the Null Object pattern)
}
public void valueForPathChanged(TreePath path, Object newValue) {
// nothing to do (cf. the Null Object pattern)
}
};
public enum ChildrenChecking {
ALL_CHECKED, ALL_UNCHECKED, HALF_CHECKED, NO_CHILDREN
}
private class PropagateCheckingListener implements TreeModelListener {
/**
* Updates the tree greyness in case of nodes changes.
*/
public void treeNodesChanged(TreeModelEvent e) {
TreePath path = e.getTreePath();
updateSubTreeGreyness(path);
updateAncestorsGreyness(path);
}
/**
* Updates the check of the just inserted nodes.
*/
public void treeNodesInserted(TreeModelEvent e) {
TreePath path = e.getTreePath();
DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterChildrenInserted(path);
}
/**
* Nothing to do if nodes were removed.
*/
public void treeNodesRemoved(TreeModelEvent e) {
TreePath path = e.getTreePath();
DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterChildrenRemoved(path);
}
/**
* Updates the tree greyness in case of structure changes.
*/
public void treeStructureChanged(TreeModelEvent e) {
TreePath path = e.getTreePath();
DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterStructureChanged(path);
}
}
private HashSet checkedPathsSet;
protected TreeCheckingMode checkingMode;
private HashSet disabledPathsSet;
private HashSet greyedPathsSet;
/** Event listener list. */
protected EventListenerList listenerList = new EventListenerList();
protected TreeModel model;
private PropagateCheckingListener propagateCheckingListener;
/**
* Creates a DefaultTreeCheckingModel with PropagateTreeCheckingMode.
*/
public DefaultTreeCheckingModel(TreeModel model) {
if (model == null) {
this.model = NULL_TREE_MODEL;
} else {
this.model = model;
}
this.checkedPathsSet = new HashSet();
this.greyedPathsSet = new HashSet();
this.disabledPathsSet = new HashSet();
this.propagateCheckingListener = new PropagateCheckingListener();
this.setCheckingMode(CheckingMode.PROPAGATE);
}
/**
* Adds a path to the checked paths set.
*
* @param path
* the path to be added.
*/
public void addCheckingPath(TreePath path) {
this.checkingMode.checkPath(path);
TreeCheckingEvent event = new TreeCheckingEvent(this, path, true);
fireValueChanged(event);
}
/**
* Adds the paths to the checked paths set.
*
* @param paths
* the paths to be added.
*/
public void addCheckingPaths(TreePath[] paths) {
for (TreePath path : paths) {
addCheckingPath(path);
}
}
void addToCheckedPathsSet(TreePath path) {
this.checkedPathsSet.add(path);
}
void addToGreyedPathsSet(TreePath path) {
this.greyedPathsSet.add(path);
}
/**
* Adds x to the list of listeners that are notified each time the set of
* checking TreePaths changes.
*
* @param x
* the new listener to be added
*/
public void addTreeCheckingListener(TreeCheckingListener x) {
this.listenerList.add(TreeCheckingListener.class, x);
}
/**
* Checks the subtree with root path.
*
* @param path
* root of the tree to be checked
*/
public void checkSubTree(final TreePath path) {
addToCheckedPathsSet(path);
removeFromGreyedPathsSet(path);
Object node = path.getLastPathComponent();
int childrenNumber = this.model.getChildCount(node);
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
checkSubTree(childPath);
}
}
/**
* Clears the checking.
*/
public void clearChecking() {
this.checkedPathsSet.clear();
this.greyedPathsSet.clear();
if (model != null && model.getRoot() != null) {
fireValueChanged(new TreeCheckingEvent(this, new TreePath(model.getRoot()), false));
}
}
/**
* Notifies all listeners that are registered for tree selection events on
* this object.
*
* @see #addTreeCheckingListener
* @see EventListenerList
*/
protected void fireValueChanged(TreeCheckingEvent e) {
// Guaranteed to return a non-null array
Object[] listeners = this.listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TreeCheckingListener.class) {
((TreeCheckingListener) listeners[i + 1]).valueChanged(e);
}
}
}
/**
* @return The CheckingMode.
*/
public CheckingMode getCheckingMode() {
if (this.checkingMode instanceof SimpleTreeCheckingMode) {
return CheckingMode.SIMPLE;
}
if (this.checkingMode instanceof PropagateTreeCheckingMode) {
return CheckingMode.PROPAGATE;
}
if (this.checkingMode instanceof PropagatePreservingCheckTreeCheckingMode) {
return CheckingMode.PROPAGATE_PRESERVING_CHECK;
}
if (this.checkingMode instanceof PropagatePreservingUncheckTreeCheckingMode) {
return CheckingMode.PROPAGATE_PRESERVING_UNCHECK;
}
if (this.checkingMode instanceof PropagateUpWhiteTreeCheckingMode) {
return CheckingMode.PROPAGATE_UP_UNCHECK;
}
return null;
}
/**
* @return Returns the paths that are in the checking.
*/
public TreePath[] getCheckingPaths() {
return checkedPathsSet.toArray(new TreePath[checkedPathsSet.size()]);
}
/**
* @return Returns the path of any root of a subtree in the checking.
*/
public TreePath[] getCheckingRoots() {
TreePath[] retVal = new TreePath[] {};
if (model.getRoot() != null) {
Vector roots = getCheckingRoots(new TreePath(this.model.getRoot()));
retVal = roots.toArray(retVal);
}
return retVal;
}
/**
* @param path
* @return
*/
private Vector getCheckingRoots(TreePath path) {
Object node = path.getLastPathComponent();
Vector roots = new Vector();
if (!isPathGreyed(path)) {
if (isPathChecked(path)) {
roots.add(path);
}
return roots;
}
// path is greyed
int childrenNumber = this.model.getChildCount(node);
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
roots.addAll(getCheckingRoots(childPath));
}
return roots;
}
public ChildrenChecking getChildrenChecking(TreePath path) {
Object node = path.getLastPathComponent();
int childrenNumber = this.model.getChildCount(node);
boolean someChecked = false;
boolean someUnchecked = false;
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
if (isPathGreyed(childPath)) {
return ChildrenChecking.HALF_CHECKED;
}
// not greyed
if (isPathChecked(childPath)) {
if (someUnchecked) {
return ChildrenChecking.HALF_CHECKED;
}
someChecked = true;
} else {
if (someChecked) {
return ChildrenChecking.HALF_CHECKED;
}
someUnchecked = true;
}
}
if (someChecked) {
return ChildrenChecking.ALL_CHECKED;
}
if (someUnchecked) {
return ChildrenChecking.ALL_UNCHECKED;
}
return ChildrenChecking.NO_CHILDREN;
}
/**
* Return the paths that are children of path, using methods of TreeModel.
* Nodes don't have to be of type TreeNode.
*
* @param path
* the parent path
* @return the array of children path
*/
protected TreePath[] getChildrenPath(TreePath path) {
Object node = path.getLastPathComponent();
int childrenNumber = this.model.getChildCount(node);
TreePath[] childrenPath = new TreePath[childrenNumber];
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
childrenPath[childIndex] = path.pathByAddingChild(this.model.getChild(node, childIndex));
}
return childrenPath;
}
/**
* @return The paths that are in the greying.
*/
public TreePath[] getGreyingPaths() {
return greyedPathsSet.toArray(new TreePath[greyedPathsSet.size()]);
}
/*
* I'm commenting out this out, since the TreeModel should be a write-only
* field for this class.
*/
// public TreeModel getTreeModel() {
// return this.model;
// }
/**
* @return true if exists a child of node with a value different from
* itself.
* @param path
* the root path of the tree to be checked.
*/
public boolean hasDifferentChildren(TreePath path) {
return pathHasChildrenWithValue(path, !isPathChecked(path));
}
public boolean isPathChecked(TreePath path) {
return this.checkedPathsSet.contains(path);
}
public boolean isPathEnabled(TreePath path) {
return !this.disabledPathsSet.contains(path);
}
public boolean isPathGreyed(TreePath path) {
return this.greyedPathsSet.contains(path);
}
/**
* @return true if exists a checked node in the subtree of path.
* @param path
* the root of the subtree to be checked.
*/
public boolean pathHasCheckedChildren(TreePath path) {
return pathHasChildrenWithValue(path, true);
}
/**
* @return true if exists a node with checked status value in the subtree of
* path.
* @param path
* the root of the subtree to be searched.
* @param value
* the value to be found.
*/
protected boolean pathHasChildrenWithValue(TreePath path, boolean value) {
Object node = path.getLastPathComponent();
int childrenNumber = this.model.getChildCount(node);
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
if (isPathChecked(childPath) == value) {
return true;
}
}
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
if (pathHasChildrenWithValue(childPath, value)) {
return true;
}
}
return false;
}
/**
* Note: The checking and the greyness of children MUST be consistent to
* work properly.
*
* @return true if exists an unchecked node in the subtree of path.
* @param path
* the root of the subtree to be checked.
*/
public boolean pathHasUncheckedChildren(TreePath path) {
Object node = path.getLastPathComponent();
int childrenNumber = this.model.getChildCount(node);
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
if (isPathGreyed(childPath) | !isPathChecked(childPath)) {
return true;
}
}
return false;
}
/**
* Removes a path from the checked paths set
*
* @param path
* the path to be removed
*/
public void removeCheckingPath(TreePath path) {
this.checkingMode.uncheckPath(path);
TreeCheckingEvent event = new TreeCheckingEvent(this, path, false);
fireValueChanged(event);
}
/**
* Removes the paths from the checked paths set
*
* @param paths
* the paths to be removed
*/
public void removeCheckingPaths(TreePath[] paths) {
for (TreePath path : paths) {
removeCheckingPath(path);
}
}
void removeFromCheckedPathsSet(TreePath path) {
this.checkedPathsSet.remove(path);
}
void removeFromGreyedPathsSet(TreePath path) {
this.greyedPathsSet.remove(path);
}
/**
* Removes x from the list of listeners that are notified each time the set
* of checking TreePaths changes.
*
* @param x
* the listener to remove
*/
public void removeTreeCheckingListener(TreeCheckingListener x) {
this.listenerList.remove(TreeCheckingListener.class, x);
}
/**
* Sets the specified checking mode. The consistence of the existing
* checking is not enforced nor controlled.
*/
public void setCheckingMode(CheckingMode mode) {
/*
* CheckingMode implements togglePath method (cf. the Strategy Pattern).
*/
switch (mode) {
case SIMPLE:
this.checkingMode = new SimpleTreeCheckingMode(this);
break;
case SINGLE:
this.checkingMode = new SingleTreeCheckingMode(this);
break;
case PROPAGATE:
this.checkingMode = new PropagateTreeCheckingMode(this);
break;
case PROPAGATE_PRESERVING_CHECK:
this.checkingMode = new PropagatePreservingCheckTreeCheckingMode(this);
break;
case PROPAGATE_PRESERVING_UNCHECK:
this.checkingMode = new PropagatePreservingUncheckTreeCheckingMode(this);
break;
case PROPAGATE_UP_UNCHECK:
this.checkingMode = new PropagateUpWhiteTreeCheckingMode(this);
break;
default:
break;
}
// // TODO: safe to delete???
// updateTreeGreyness();
}
/**
* Sets the specified checking mode. The consistence of the existing
* checking is not enforced nor controlled.
*/
public void setCheckingMode(TreeCheckingMode mode) {
this.checkingMode = mode;
}
/**
* Sets the checking to path.
*/
public void setCheckingPath(TreePath path) {
clearChecking();
addCheckingPath(path);
}
/**
* Sets the checking to the specified paths.
*/
public void setCheckingPaths(TreePath[] paths) {
clearChecking();
for (TreePath path : paths) {
addCheckingPath(path);
}
}
/**
* Sets whether or not the path is enabled.
*
* @param path
* the path to enable/disable
*/
public void setPathEnabled(TreePath path, boolean enable) {
if (enable) {
this.disabledPathsSet.remove(path);
} else {
this.disabledPathsSet.add(path);
}
}
/**
* Sets whether or not the paths are enabled.
*
* @param paths
* the paths to enable/disable
*/
public void setPathsEnabled(TreePath[] paths, boolean enable) {
for (TreePath path : paths) {
setPathEnabled(path, enable);
}
}
/**
* Sets the specified tree model. The current checking set is cleared.
*/
public void setTreeModel(TreeModel newModel) {
TreeModel oldModel = this.model;
if (oldModel != null) {
oldModel.removeTreeModelListener(this.propagateCheckingListener);
}
this.model = newModel;
if (newModel != null) {
newModel.addTreeModelListener(this.propagateCheckingListener);
}
clearChecking();
}
/**
* Delegates to the current checkingMode the toggling style, using the
* Strategy Pattern.
*/
public void toggleCheckingPath(TreePath path) {
if (!isPathEnabled(path)) {
return;
}
if (isPathChecked(path)) {
removeCheckingPath(path);
} else {
addCheckingPath(path);
}
}
/**
* Return a string that describes the tree model including the values of
* checking, enabling, greying.
*/
@Override
public String toString() {
if (model.getRoot() != null) {
return toString(new TreePath(this.model.getRoot()));
} else
return null;
}
/**
* Convenience method for getting a string that describes the tree starting
* at the specified path.
*
* @param path
* the root of the subtree to describe.
*/
private String toString(TreePath path) {
String checkString = "n";
String greyString = "n";
String enableString = "n";
if (isPathChecked(path)) {
checkString = "y";
}
if (isPathEnabled(path)) {
enableString = "y";
}
if (isPathGreyed(path)) {
greyString = "y";
}
String description = "Path checked: " + checkString + " greyed: " + greyString + " enabled: " + enableString + " Name: "
+ path.toString() + "\n";
for (TreePath childPath : getChildrenPath(path)) {
description += toString(childPath);
}
return description;
}
/**
* Unchecks the subtree rooted at path.
*
* @param path
* root of the tree to be unchecked
*/
public void uncheckSubTree(TreePath path) {
removeFromCheckedPathsSet(path);
removeFromGreyedPathsSet(path);
Object node = path.getLastPathComponent();
int childrenNumber = this.model.getChildCount(node);
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
TreePath childPath = path.pathByAddingChild(this.model.getChild(node, childIndex));
uncheckSubTree(childPath);
}
}
/**
* Ungreys the subtree rooted at path.
*
* @param path
* the root of the tree to be checked
*/
public void ungreySubTree(TreePath path) {
removeFromGreyedPathsSet(path);
for (TreePath childPath : getChildrenPath(path)) {
ungreySubTree(childPath);
}
}
/**
* Updates the grayness value of the parents of path. Note: the greyness and
* checking of the other nodes (not ancestors) MUST BE consistent.
*
* @param path
* the treepath containing the ancestors to be grey-updated
*/
public void updateAncestorsGreyness(TreePath path) {
TreePath[] parents = new TreePath[path.getPathCount()];
parents[0] = path;
boolean greyAll = isPathGreyed(path);
for (int i = 1; i < parents.length; i++) {
parents[i] = parents[i - 1].getParentPath();
if (greyAll) {
addToGreyedPathsSet(parents[i]);
} else {
updatePathGreyness(parents[i]);
greyAll = isPathGreyed(parents[i]);
}
}
}
/**
* Updates consistency of the checking, by invoking
* updateSubTreeCheckingConsistency on the root node.
*/
public void updateCheckingConsistency() {
if (model.getRoot() != null) {
updateSubTreeCheckingConsistency(new TreePath(model.getRoot()));
}
}
/**
* Updates the greyness value value for the given path if there are children
* with different values. Note: the greyness and cheking of children MUST BE
* consistent.
*
* @param ancestor
* the path to be grey-updated.
*/
protected void updatePathGreyness(TreePath ancestor) {
boolean value = isPathChecked(ancestor);
Object ancestorNode = ancestor.getLastPathComponent();
int childrenNumber = this.model.getChildCount(ancestorNode);
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
Object childNode = this.model.getChild(ancestorNode, childIndex);
TreePath childPath = ancestor.pathByAddingChild(childNode);
if (isPathGreyed(childPath)) {
addToGreyedPathsSet(ancestor);
return;
}
if (isPathChecked(childPath) != value) {
addToGreyedPathsSet(ancestor);
return;
}
}
removeFromGreyedPathsSet(ancestor);
}
/**
* Updates consistency of the checking of sub-tree starting at path. It's
* based on paths greyness.
*
* @param path
* the root of the sub-tree to be grey-updated
*/
public void updateSubTreeCheckingConsistency(TreePath path) {
if (isPathGreyed(path)) {
// greyed
for (TreePath childPath : getChildrenPath(path)) {
updateSubTreeCheckingConsistency(childPath);
}
updatePathGreyness(path);
} else {
// not greyed
if (isPathChecked(path)) {
checkSubTree(path);
} else {
uncheckSubTree(path);
}
return;
}
}
/**
* Updates the greyness of sub-tree starting at path.
*
* @param path
* the root of the sub-tree to be grey-updated
*/
public void updateSubTreeGreyness(TreePath path) {
if (pathHasChildrenWithValue(path, !isPathChecked(path))) {
addToGreyedPathsSet(path);
} else {
removeFromGreyedPathsSet(path);
}
if (isPathGreyed(path)) {
for (TreePath childPath : getChildrenPath(path)) {
updateSubTreeGreyness(childPath);
}
return;
} else {
ungreySubTree(path);
}
}
/**
* Updates the greyness state of the entire tree.
*/
public void updateTreeGreyness() {
if (model.getRoot() != null) {
updateSubTreeGreyness(new TreePath(this.model.getRoot()));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy