/* * 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.awt.Rectangle; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; 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: * *
* 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
*/
public class CheckboxTree extends JTree {
/**
* The mouse listener taking care of node checking/unchecking.
*/
public class NodeCheckListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (e.isConsumed()) {
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) {
}
}
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();
}
/**
* Creates a CheckboxTree with visible handles, a default
* CheckboxTreeCellRenderer and a default TreeCheckingModel. The tree is
* based on the specified tree model.
*/
public CheckboxTree(TreeModel treemodel) {
super(treemodel);
initialize();
}
/**
* Creates 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.
*/
public void addCheckingPath(TreePath path) {
getCheckingModel().addCheckingPath(path);
}
/**
* Add paths in the checking.
*/
public void addCheckingPaths(TreePath[] paths) {
getCheckingModel().addCheckingPaths(paths);
}
/**
* Adds 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);
}
/**
* Clears the checking.
*/
public void clearChecking() {
getCheckingModel().clearChecking();
}
/**
* Expand completely a tree
*/
public void expandAll() {
expandSubTree(getPathForRow(0));
}
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 Returns the TreeCheckingModel.
*/
public TreeCheckingModel getCheckingModel() {
return this.checkingModel;
}
/**
* Return paths that are in the checking.
*/
public TreePath[] getCheckingPaths() {
return getCheckingModel().getCheckingPaths();
}
/**
* @return Returns the paths that are in the checking set and are the
* (upper) roots of checked trees.
*/
public TreePath[] getCheckingRoots() {
return getCheckingModel().getCheckingRoots();
}
/**
* @return Returns 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 is commented out: it 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
}
/**
* Returns 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);
}
/**
* Returns whether checking a node causes it to be selected, too.
*
* @return the intended behaviour of checking wrt 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) {
if (!e.isConsumed()) {
// 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 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);
}
/**
* Removes a TreeChecking listener.
*
* @param tcl the TreeCheckingListener to remove
*/
public void removeTreeCheckingListener(TreeCheckingListener tcl) {
this.checkingModel.removeTreeCheckingListener(tcl);
}
/**
* Sets 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 CheckboxTreeCellRenderer: " + tcl);
}
super.setCellRenderer(tcl);
}
/**
* Co-variant method for retrieving the
* CheckboxTreeCellRenderer of this tree.
*/
@Override
public CheckboxTreeCellRenderer getCellRenderer() {
return (CheckboxTreeCellRenderer) cellRenderer;
}
/**
* Set the checking model of this CheckboxTree.
*
* @param newCheckingModel The new TreeCheckingModel.
*/
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 != null && oldCheckingModel instanceof DefaultTreeCheckingModel) {
// null the model to avoid dangling pointers
((DefaultTreeCheckingModel) oldCheckingModel).setTreeModel(null);
}
// TODO: what if newCheckingModel == null ?
this.checkingModel = newCheckingModel;
if (newCheckingModel != null) {
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();
}
});
}
}
/**
* 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);
}
/**
* Sets the TreeModel and links it to the existing checkingModel.
*/
@Override
public void setModel(TreeModel newModel) {
super.setModel(newModel);
if (checkingModel != null && checkingModel instanceof DefaultTreeCheckingModel) {
((DefaultTreeCheckingModel) checkingModel).setTreeModel(newModel);
}
}
/**
* Specifies whether checking a node causes it to be selected, too, or else
* the selection is not affected. The default behaviour is the former.
*
* @param selectsByChecking the intended behaviour of checking wrt
* 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;
}
}