/* * 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 Bigagli * @author 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 { 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) { 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()]); } 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. (Strategy Pattern was * used). */ 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 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 cheking 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 path. * * @param path the treepath root of the tree */ 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 with root 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 with root path. * * @param path root of the tree to be checked */ public void ungreySubTree(TreePath path) { removeFromGreyedPathsSet(path); for (TreePath childPath : getChildrenPath(path)) { ungreySubTree(childPath); } } /** * Update 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. TODO: test this method, never used * * @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())); } } }