/* * Copyright 2007-2010 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 it.cnr.imaa.essi.lablib.gui.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() { @Override public void addTreeModelListener(TreeModelListener l) { // nothing to do (cf. the Null Object pattern) } @Override public Object getChild(Object parent, int index) { return null; } @Override public int getChildCount(Object parent) { return 0; } @Override public int getIndexOfChild(Object parent, Object child) { return 0; } @Override public Object getRoot() { return null; } @Override public boolean isLeaf(Object node) { return false; } @Override public void removeTreeModelListener(TreeModelListener l) { // nothing to do (cf. the Null Object pattern) } @Override 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. */ @Override public void treeNodesChanged(TreeModelEvent e) { TreePath path = e.getTreePath(); updateSubTreeGreyness(path); updateAncestorsGreyness(path); } /** * Updates the check of the just inserted nodes. */ @Override public void treeNodesInserted(TreeModelEvent e) { TreePath path = e.getTreePath(); DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterChildrenInserted(path); } /** * Nothing to do if nodes were removed. */ @Override public void treeNodesRemoved(TreeModelEvent e) { TreePath path = e.getTreePath(); DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterChildrenRemoved(path); } /** * Updates the tree greyness in case of structure changes. */ @Override public void treeStructureChanged(TreeModelEvent e) { TreePath path = e.getTreePath(); DefaultTreeCheckingModel.this.checkingMode.updateCheckAfterStructureChanged(path); } } private HashSet<TreePath> checkedPathsSet; protected TreeCheckingMode checkingMode; private HashSet<TreePath> disabledPathsSet; private HashSet<TreePath> 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<TreePath>(); this.greyedPathsSet = new HashSet<TreePath>(); this.disabledPathsSet = new HashSet<TreePath>(); this.propagateCheckingListener = new PropagateCheckingListener(); this.setCheckingMode(CheckingMode.PROPAGATE); } /** * Adds a path to the checked paths set. * @param path the path to be added. */ @Override 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. */ @Override 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 */ @Override 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. */ @Override public void clearChecking() { this.checkedPathsSet.clear(); this.greyedPathsSet.clear(); if (this.model != null && this.model.getRoot() != null) fireValueChanged(new TreeCheckingEvent(this, new TreePath(this.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. */ @Override 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. */ @Override public TreePath[] getCheckingPaths() { return this.checkedPathsSet.toArray(new TreePath[this.checkedPathsSet.size()]); } /** * @return Returns the path of any root of a subtree in the checking. */ @Override public TreePath[] getCheckingRoots() { TreePath[] retVal = new TreePath[] {}; if (this.model.getRoot() != null) { Vector<TreePath> roots = getCheckingRoots(new TreePath(this.model.getRoot())); retVal = roots.toArray(retVal); } return retVal; } /** * @param path * @return */ private Vector<TreePath> getCheckingRoots(TreePath path) { Object node = path.getLastPathComponent(); Vector<TreePath> roots = new Vector<TreePath>(); 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. */ @Override public TreePath[] getGreyingPaths() { return this.greyedPathsSet.toArray(new TreePath[this.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)); } @Override public boolean isPathChecked(TreePath path) { return this.checkedPathsSet.contains(path); } @Override public boolean isPathEnabled(TreePath path) { return !this.disabledPathsSet.contains(path); } @Override 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 */ @Override 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 */ @Override 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 */ @Override 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. */ @Override 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. */ @Override public void setCheckingPath(TreePath path) { clearChecking(); addCheckingPath(path); } /** * Sets the checking to the specified paths. */ @Override 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 */ @Override 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 */ @Override 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. */ @Override 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 (this.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"; final StringBuilder description = new StringBuilder(); description .append("Path checked: ").append(checkString) .append(" greyed: ") .append(greyString) .append(" enabled: ") .append(enableString) .append(" Name: ") .append(path.toString()) .append("\n"); for (TreePath childPath : getChildrenPath(path)) { description.append(toString(childPath)); } return description.toString(); } /** * 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 (this.model.getRoot() != null) updateSubTreeCheckingConsistency(new TreePath(this.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 (this.model.getRoot() != null) updateSubTreeGreyness(new TreePath(this.model.getRoot())); } }