
eu.essilab.lablib.checkboxtree.CheckboxTree 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.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.swing.JTree;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
/**
* A tree whose nodes may be checked (e.g. the widget usually found in software
* installers, that allows to select which features to install/uninstall). If a
* node has some child of different checking status is greyed. You can use the
* same constructors of JTree to instantiate a new CheckboxTree Example from a
* TreeNode:
*
*
* DefaultMutableTreeNode root = new DefaultMutableTreeNode("root");
* root.add(new DefaultMutableTreeNode("child A"));
* root.add(new DefaultMutableTreeNode("child B"));
* CheckboxTree CheckboxTree = new CheckboxTree(root);
*
*
* Example from a TreeModel:
*
*
* DefaultTreeModel dtm = new DefaultTreeModel(root);
*
* CheckboxTree CheckboxTree = new CheckboxTree(root);
*
*
* Default constructor (useful for gui builders):
*
*
* CheckboxTree CheckboxTree = new CheckboxTree();
*
*
* Then you can set the checking propagation style:
*
*
* CheckboxTree.getCheckingModel().setCheckingMode(TreeCheckingModel.CheckingMode.SIMPLE);
* CheckboxTree.getCheckingModel().setCheckingMode(TreeCheckingModel.CheckingMode.PROPAGATE);
* CheckboxTree.getCheckingModel().setCheckingMode(TreeCheckingModel.CheckingMode.PROPAGATE_PRESERVING_CHECK);
* CheckboxTree.getCheckingModel().setCheckingMode(TreeCheckingModel.CheckingMode.PROPAGATE_PRESERVING_UNCHECK);
*
*
* You can also set the model at a later time using:
*
*
* CheckboxTree.setModel(aTreeModel);
*
*
* There are two methods that return the paths that are in the checking set:
*
*
* TreePath[] tp = CheckboxTree.getCheckingPaths();
*
* TreePath[] tp = CheckboxTree.getCheckingRoots();
*
*
* You can also add/remove a listener of a TreeCheckingEvent in this way:
*
*
* CheckboxTree.addTreeCheckingListener(new TreeCheckingListener() {
* public void valueChanged(TreeCheckingEvent e) {
* System.out.println("Checked paths changed: user clicked on " + (e.getLeadingPath().getLastPathComponent()));
* }
* });
*
*
* @author Enrico Boldrini
* @author Lorenzo Bigagli
*/
/**
* @author bigagli
*/
/**
* @author bigagli
*/
public class CheckboxTree extends JTree {
/**
* The mouse listener taking care of node checking/unchecking.
*/
/*
* this inner class (commented out) is actually the Swing way, but since
* JTree is still based on the AWT mechanism for event handling (what causes
* the last added listener to be invoked last) we could not override the
* JTree event handler, so we would _always_ have that checking a node
* causes it to be selected. We had to work around it (see initialize() and
* processMouseEvent()).
*/
// public class NodeCheckListener extends MouseAdapter {
//
// @Override
// public void mousePressed(MouseEvent e) {
// if (e.isConsumed() || !CheckboxTree.this.isEnabled()) {
// return;
// }
// // we use mousePressed instead of mouseClicked for performance
// int x = e.getX();
// int y = e.getY();
// int row = getRowForLocation(x, y);
// if (row == -1) {
// // click outside any node
// return;
// }
// Rectangle rect = getRowBounds(row);
// if (rect == null) {
// // click on an invalid node
// return;
// }
// if ((getCellRenderer()).isOnHotspot(x - rect.x, y - rect.y)) {
// getCheckingModel().toggleCheckingPath(getPathForRow(row));
// e.consume();
// }
// }
// };
/*
* temporary solution for enabling spacebar checking. Should make use of
* InputMaps?
*/
private class SpaceListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (!isEnabled()) {
return;
}
TreePath path = CheckboxTree.this.getSelectionPath();
if (e.getKeyCode() == KeyEvent.VK_SPACE) {
if (path != null) {
TreeCheckingModel cm = CheckboxTree.this.getCheckingModel();
cm.toggleCheckingPath(path);
}
}
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
}
/*
* The checking model of this CheckboxTree. On the contrary of JTree and its
* TreeModel, we enforce that this field never be null (cf. the Null Object
* pattern). Due to subtleties in the sequence of initialization calls
* (JTree.setModel gets called in the constructor, causing nodes rendering
* before field initialization and hence a null pointer exception in the
* default cell renderer) we make it private and initialize it in the
* getter.
*/
private TreeCheckingModel checkingModel;
/**
* Whether checking a node causes it to be selected, too.
*/
private boolean selectsByChecking;
/**
* For GUI builders. It returns a CheckboxTree with a default tree model to
* show something interesting. Creates a CheckboxTree with visible handles,
* a default CheckboxTreeCellRenderer and a default TreeCheckingModel.
*/
public CheckboxTree() {
super(getDefaultTreeModel());
initialize();
}
/**
* Create a CheckboxTree with visible handles, a default
* CheckboxTreeCellRenderer and a default TreeCheckingModel. The tree is
* based on the specified tree model.
*
* @param treemodel the model of this tree
*/
public CheckboxTree(TreeModel treemodel) {
super(treemodel);
initialize();
}
/**
* Create a CheckboxTree with visible handles, a default
* CheckboxTreeCellRenderer and a default TreeCheckingModel. The tree root
* is the specified tree node.
*
* @param root
* the root of the tree
*/
public CheckboxTree(TreeNode root) {
super(root);
initialize();
}
/**
* Add a path in the checking.
*
* @param path the path to add
*/
public void addCheckingPath(TreePath path) {
getCheckingModel().addCheckingPath(path);
}
/**
* Add paths in the checking.
*/
public void addCheckingPaths(TreePath[] paths) {
getCheckingModel().addCheckingPaths(paths);
}
/**
* Add a listener for TreeChecking
events.
*
* @param tsl
* the TreeCheckingListener
that will be notified
* when a node is checked
*/
public void addTreeCheckingListener(TreeCheckingListener tsl) {
this.checkingModel.addTreeCheckingListener(tsl);
}
/**
* Clear the checking set.
*/
public void clearChecking() {
getCheckingModel().clearChecking();
}
/**
* Expand the tree completely.
*/
public void expandAll() {
expandSubTree(new TreePath(getModel().getRoot()));
}
private void expandSubTree(TreePath path) {
expandPath(path);
Object node = path.getLastPathComponent();
int childrenNumber = getModel().getChildCount(node);
TreePath[] childrenPath = new TreePath[childrenNumber];
for (int childIndex = 0; childIndex < childrenNumber; childIndex++) {
childrenPath[childIndex] = path.pathByAddingChild(getModel().getChild(node, childIndex));
expandSubTree(childrenPath[childIndex]);
}
}
/**
* Return the TreeCheckingModel of this CheckboxTree. This method never
* returns null (although it may return the NullTreeCheckingModel
* singleton).
*
* @return the TreeCheckingModel of this CheckboxTree.
*/
public TreeCheckingModel getCheckingModel() {
if (checkingModel == null) {
checkingModel = NullTreeCheckingModel.getInstance();
}
return this.checkingModel;
}
/**
* Return paths that are in the checking.
*/
public TreePath[] getCheckingPaths() {
return getCheckingModel().getCheckingPaths();
}
/**
* @return the paths that are in the checking set and are the (upper) roots
* of checked trees.
*/
public TreePath[] getCheckingRoots() {
return getCheckingModel().getCheckingRoots();
}
/**
* @return the paths that are in the greying.
*/
public TreePath[] getGreyingPaths() {
return getCheckingModel().getGreyingPaths();
}
/**
* Convenience initialization method.
*/
private void initialize() {
setCheckingModel(new DefaultTreeCheckingModel(this.treeModel));
setCellRenderer(new DefaultCheckboxTreeCellRenderer());
/*
* the next line (commented out) is actually the Swing way, but since
* JTree is still based on the AWT mechanism for event handling (what
* causes the last added listener to be invoked last) we could not
* override the JTree event handler, so we would _always_ have that
* checking a node causes it to be selected. We had to work around it
* (see processMouseEvent()).
*/
// addMouseListener(new NodeCheckListener());
setSelectsByChecking(true);
addKeyListener(new SpaceListener());
this.selectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
setShowsRootHandles(true);
putClientProperty("JTree.lineStyle", "Angled");// for Metal L&F
}
/**
* Return true if the item identified by the path is currently checked.
*
* @param path
* a TreePath
identifying a node
* @return true if the node is checked
*/
public boolean isPathChecked(TreePath path) {
return getCheckingModel().isPathChecked(path);
}
/**
* Return whether checking a node causes it to be selected, too.
*
* @return the intended behavior of checking with respect to selection.
*/
public boolean isSelectsByChecking() {
return selectsByChecking;
}
/*
* This is overridden to work around an AWT limitation (see the comment
* inside initialize()). Basically, if a mouse_pressed event insists on a
* checkbox control _and_ we don't want the node to be selected, we stop
* processing the event. Simply consuming the event wouldn't work, because
* the BasicTreeUI would select the node on the mouse_released event!
*
* @see javax.swing.JComponent#processMouseEvent(java.awt.event.MouseEvent)
*/
@Override
protected void processMouseEvent(MouseEvent e) {
if (e.getID() == MouseEvent.MOUSE_PRESSED) {
// we use mousePressed instead of mouseClicked for performance
if (!e.isConsumed() && this.isEnabled()) {
int x = e.getX();
int y = e.getY();
int row = getRowForLocation(x, y);
if (row != -1) {
// click inside some node
Rectangle rect = getRowBounds(row);
if (rect != null) {
// click on a valid node
if ((getCellRenderer()).isOnHotspot(x - rect.x, y - rect.y)) {
getCheckingModel().toggleCheckingPath(getPathForRow(row));
if (!isSelectsByChecking())
return;
}
}
}
}
}
super.processMouseEvent(e);
}
/**
* Remove a path from the checking.
*/
public void removeCheckingPath(TreePath path) {
getCheckingModel().removeCheckingPath(path);
}
/**
* Remove paths from the checking.
*/
public void removeCheckingPaths(TreePath[] paths) {
getCheckingModel().removeCheckingPaths(paths);
}
/**
* Remove a TreeChecking
listener.
*
* @param tcl
* the TreeCheckingListener
to remove
*/
public void removeTreeCheckingListener(TreeCheckingListener tcl) {
this.checkingModel.removeTreeCheckingListener(tcl);
}
/**
* Set the CheckboxTreeCellRenderer
that will be used to draw
* each cell.
*
* @param tcl
* the TreeCellRenderer
that is to render each cell
* @throws IllegalArgumentException
* if the argument is not a
* CheckboxTreeCellRenderer
.
*/
@Override
public void setCellRenderer(TreeCellRenderer tcl) {
if (!(tcl instanceof CheckboxTreeCellRenderer)) {
throw new IllegalArgumentException("The argument does not implement the CheckboxTreeCellRenderer interface: " + tcl);
}
super.setCellRenderer(tcl);
}
/**
* Co-variant method for retrieving the
* CheckboxTreeCellRenderer
of this tree.
*/
@Override
public CheckboxTreeCellRenderer getCellRenderer() {
if (cellRenderer == null) {
cellRenderer = new DefaultCheckboxTreeCellRenderer();
}
return (CheckboxTreeCellRenderer) cellRenderer;
}
/**
* Set the checking model of this CheckboxTree. If the parameter is null,
* the checking model is set to the NullTreeCheckingModel singleton.
*
* @param newCheckingModel
* the new TreeCheckingModel of this CheckboxTree.
*/
public void setCheckingModel(TreeCheckingModel newCheckingModel) {
/*
* in case we are dealing with DefaultTreeCheckingModel, we link/unlink
* it from the model of this tree
*/
TreeCheckingModel oldCheckingModel = this.checkingModel;
if (oldCheckingModel instanceof DefaultTreeCheckingModel) {
// null the model to avoid dangling pointers
((DefaultTreeCheckingModel) oldCheckingModel).setTreeModel(null);
}
if (newCheckingModel != null) {
this.checkingModel = newCheckingModel;
if (newCheckingModel instanceof DefaultTreeCheckingModel) {
((DefaultTreeCheckingModel) newCheckingModel).setTreeModel(getModel());
}
// add a treeCheckingListener to repaint upon checking modifications
newCheckingModel.addTreeCheckingListener(new TreeCheckingListener() {
public void valueChanged(TreeCheckingEvent e) {
repaint();
}
});
} else {
this.checkingModel = NullTreeCheckingModel.getInstance();
}
}
/**
* Set path in the checking.
*/
public void setCheckingPath(TreePath path) {
getCheckingModel().setCheckingPath(path);
}
/**
* Set paths that are in the checking.
*/
public void setCheckingPaths(TreePath[] paths) {
getCheckingModel().setCheckingPaths(paths);
}
/**
* Set the TreeModel and links it to the existing checkingModel.
*/
@Override
public void setModel(TreeModel newModel) {
super.setModel(newModel);
if (checkingModel instanceof DefaultTreeCheckingModel) {
((DefaultTreeCheckingModel) checkingModel).setTreeModel(newModel);
}
}
/**
* Specify whether checking a node causes it to be selected, too, or else
* the selection is not affected. The default behavior is the former.
*
* @param selectsByChecking
* the intended behavior of checking with respect to selection.
*/
public void setSelectsByChecking(boolean selectsByChecking) {
this.selectsByChecking = selectsByChecking;
}
/**
* @return a string representation of the tree, including the checking,
* enabling and greying sets.
*/
@Override
public String toString() {
String retVal = super.toString();
TreeCheckingModel tcm = getCheckingModel();
if (tcm != null) {
return retVal + "\n" + tcm.toString();
}
return retVal;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy