package furbelow;
/* Copyright (c) 2006-2007 Timothy Wall, All Rights Reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* This library 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
* Lesser General Public License for more details.
*/
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
/** Provides checkbox-based selection of tree nodes. Override the protected
* methods to adapt this renderer's behavior to your local tree table flavor.
* No change listener notifications are provided.
*/
public class CheckBoxTreeCellRenderer implements TreeCellRenderer {
public static final int UNCHECKABLE = 0;
public static final int FULLCHECKED = 1;
public static final int UNCHECKED = 2;
public static final int PARTIALCHECKED = 3;
private TreeCellRenderer renderer;
private JCheckBox checkBox;
private Point mouseLocation;
private int mouseRow = -1;
private int pressedRow = -1;
private boolean mouseInCheck;
private int state = UNCHECKED;
private Set checkedPaths;
private JTree tree;
private MouseHandler handler;
/** Create a per-tree instance of the checkbox renderer. */
public CheckBoxTreeCellRenderer(JTree tree, TreeCellRenderer original) {
this.tree = tree;
this.renderer = original;
checkedPaths = new HashSet();
checkBox = new JCheckBox();
checkBox.setOpaque(false);
checkBox.setSize(checkBox.getPreferredSize());
}
protected void installMouseHandler() {
if (handler == null) {
handler = new MouseHandler();
addMouseHandler(handler);
}
}
protected void addMouseHandler(MouseHandler handler) {
tree.addMouseListener(handler);
tree.addMouseMotionListener(handler);
}
private void updateMouseLocation(Point newLoc) {
if (mouseRow != -1) {
repaint(mouseRow);
}
mouseLocation = newLoc;
if (mouseLocation != null) {
mouseRow = getRow(newLoc);
repaint(mouseRow);
}
else {
mouseRow = -1;
}
if (mouseRow != -1 && mouseLocation != null) {
Point mouseLoc = new Point(mouseLocation);
Rectangle r = getRowBounds(mouseRow);
if (r != null)
mouseLoc.x -= r.x;
mouseInCheck = isInCheckBox(mouseLoc);
}
else {
mouseInCheck = false;
}
}
protected int getRow(Point p) {
return tree.getRowForLocation(p.x, p.y);
}
protected Rectangle getRowBounds(int row) {
return tree.getRowBounds(row);
}
protected TreePath getPathForRow(int row) {
return tree.getPathForRow(row);
}
protected int getRowForPath(TreePath path) {
return tree.getRowForPath(path);
}
protected void repaint(Rectangle r) {
tree.repaint(r);
}
protected void repaint() {
tree.repaint();
}
private void repaint(int row) {
Rectangle r = getRowBounds(row);
if (r != null)
repaint(r);
}
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
installMouseHandler();
TreePath path = getPathForRow(row);
state = UNCHECKABLE;
if (path != null) {
if (isChecked(path)) {
state = FULLCHECKED;
}
else if (isPartiallyChecked(path)) {
state = PARTIALCHECKED;
}
else if (isSelectable(path)) {
state = UNCHECKED;
}
}
checkBox.setSelected(state == FULLCHECKED);
checkBox.getModel().setArmed(mouseRow == row && pressedRow == row && mouseInCheck);
checkBox.getModel().setPressed(pressedRow == row && mouseInCheck);
checkBox.getModel().setRollover(mouseRow == row && mouseInCheck);
Component c = renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
checkBox.setForeground(c.getForeground());
if (c instanceof JLabel) {
JLabel label = (JLabel)c;
// Augment the icon to include the checkbox
label.setIcon(new CompoundIcon(label.getIcon()));
}
return c;
}
private boolean isInCheckBox(Point where) {
Insets insets = tree.getInsets();
int right = checkBox.getWidth();
int left = 0;
if (insets != null) {
left += insets.left;
right += insets.left;
}
return where.x >= left && where.x < right;
}
public boolean isExplicitlyChecked(TreePath path) {
return checkedPaths.contains(path);
}
/** Returns whether selecting the given path is allowed. The default
* returns true. You should return false if the given path represents
* a placeholder for a node that has not yet loaded, or anything else
* that doesn't represent a normal, operable object in the tree.
*/
public boolean isSelectable(TreePath path) {
return true;
}
/** Returns whether the given path is currently checked. */
public boolean isChecked(TreePath path) {
if (isExplicitlyChecked(path)) {
return true;
}
else {
if (path.getParentPath() != null) {
return isChecked(path.getParentPath());
}
else {
return false;
}
}
}
public boolean isPartiallyChecked(TreePath path) {
Object node = path.getLastPathComponent();
for (int i = 0; i < tree.getModel().getChildCount(node); i++) {
Object child = tree.getModel().getChild(node, i);
TreePath childPath = path.pathByAddingChild(child);
if (isChecked(childPath) || isPartiallyChecked(childPath)) {
return true;
}
}
return false;
}
private boolean isFullyChecked(TreePath parent) {
Object node = parent.getLastPathComponent();
for (int i = 0; i < tree.getModel().getChildCount(node); i++) {
Object child = tree.getModel().getChild(node, i);
TreePath childPath = parent.pathByAddingChild(child);
if (!isExplicitlyChecked(childPath)) {
return false;
}
}
return true;
}
public void toggleChecked(int row) {
TreePath path = getPathForRow(row);
boolean isChecked = isChecked(path);
removeDescendants(path);
if (!isChecked) {
checkedPaths.add(path);
}
setParent(path);
repaint();
}
private void setParent(TreePath path) {
TreePath parent = path.getParentPath();
if (parent != null) {
if (isFullyChecked(parent)) {
removeChildren(parent);
checkedPaths.add(parent);
} else {
if (isChecked(parent)) {
checkedPaths.remove(parent);
addChildren(parent);
checkedPaths.remove(path);
}
}
setParent(parent);
}
}
private void addChildren(TreePath parent) {
Object node = parent.getLastPathComponent();
for (int i = 0; i < tree.getModel().getChildCount(node); i++) {
Object child = tree.getModel().getChild(node, i);
TreePath path = parent.pathByAddingChild(child);
checkedPaths.add(path);
}
}
private void removeChildren(TreePath parent) {
for (Iterator i = checkedPaths.iterator(); i.hasNext();) {
TreePath p = (TreePath) i.next();
if (p.getParentPath() != null && parent.equals(p.getParentPath())) {
i.remove();
}
}
}
private void removeDescendants(TreePath ancestor) {
for (Iterator i = checkedPaths.iterator(); i.hasNext();) {
TreePath path = (TreePath) i.next();
if (ancestor.isDescendant(path)) {
i.remove();
}
}
}
/** Returns all checked rows. */
public int[] getCheckedRows() {
TreePath[] paths = getCheckedPaths();
int[] rows = new int[checkedPaths.size()];
for (int i = 0; i < checkedPaths.size(); i++) {
rows[i] = getRowForPath(paths[i]);
}
Arrays.sort(rows);
return rows;
}
/** Returns all checked paths. */
public TreePath[] getCheckedPaths() {
return (TreePath[]) checkedPaths.toArray(new TreePath[checkedPaths.size()]);
}
protected class MouseHandler extends MouseAdapter implements MouseMotionListener {
public void mouseEntered(MouseEvent e) {
updateMouseLocation(e.getPoint());
}
public void mouseExited(MouseEvent e) {
updateMouseLocation(null);
}
public void mouseMoved(MouseEvent e) {
updateMouseLocation(e.getPoint());
}
public void mouseDragged(MouseEvent e) {
updateMouseLocation(e.getPoint());
}
public void mousePressed(MouseEvent e) {
pressedRow = e.getModifiersEx() == InputEvent.BUTTON1_DOWN_MASK
? getRow(e.getPoint()) : -1;
updateMouseLocation(e.getPoint());
}
public void mouseReleased(MouseEvent e) {
if (pressedRow != -1) {
int row = getRow(e.getPoint());
if (row == pressedRow) {
Point p = e.getPoint();
Rectangle r = getRowBounds(row);
p.x -= r.x;
if (isInCheckBox(p)) {
toggleChecked(row);
}
}
pressedRow = -1;
updateMouseLocation(e.getPoint());
}
}
}
/** Combine a JCheckBox's checkbox with another icon. */
private final class CompoundIcon implements Icon {
private final Icon icon;
private final int w;
private final int h;
private CompoundIcon(Icon icon) {
if (icon == null) {
icon = new Icon() {
public int getIconHeight() { return 0; }
public int getIconWidth() { return 0; }
public void paintIcon(Component c, Graphics g, int x, int y) { }
};
}
this.icon = icon;
this.w = icon.getIconWidth();
this.h = icon.getIconHeight();
}
public int getIconWidth() {
return checkBox.getPreferredSize().width + w;
}
public int getIconHeight() {
return Math.max(checkBox.getPreferredSize().height, h);
}
public void paintIcon(Component c, Graphics g, int x, int y) {
if (c.getComponentOrientation().isLeftToRight()) {
int xoffset = checkBox.getPreferredSize().width;
int yoffset = (getIconHeight()-icon.getIconHeight())/2;
icon.paintIcon(c, g, x + xoffset, y + yoffset);
if (state != UNCHECKABLE) {
paintCheckBox(g, x, y);
}
}
else {
int yoffset = (getIconHeight()-icon.getIconHeight())/2;
icon.paintIcon(c, g, x, y + yoffset);
if (state != UNCHECKABLE) {
paintCheckBox(g, x + icon.getIconWidth(), y);
}
}
}
private void paintCheckBox(Graphics g, int x, int y) {
int yoffset;
boolean db = checkBox.isDoubleBuffered();
checkBox.setDoubleBuffered(false);
try {
yoffset = (getIconHeight()-checkBox.getPreferredSize().height)/2;
g = g.create(x, y+yoffset, getIconWidth(), getIconHeight());
checkBox.paint(g);
if (state == PARTIALCHECKED) {
final int WIDTH = 2;
g.setColor(UIManager.getColor("CheckBox.foreground"));
Graphics2D g2d = (Graphics2D)g;
g2d.setStroke(new BasicStroke(WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = checkBox.getWidth();
int h = checkBox.getHeight();
g.drawLine(w/4+2, h/2-WIDTH/2+1, w/4+w/2-3, h/2-WIDTH/2+1);
}
g.dispose();
}
finally {
checkBox.setDoubleBuffered(db);
}
}
}
private static String createText(TreePath[] paths) {
if (paths.length == 0) {
return "Nothing checked";
}
String checked = "Checked:\n";
for (int i=0;i < paths.length;i++) {
checked += paths[i] + "\n";
}
return checked;
}
public static void main(String[] args) {
try {
final String SWITCH = "toggle-componentOrientation";
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
JFrame frame = new JFrame("Tree with Check Boxes");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JTree tree = new JTree();
final CheckBoxTreeCellRenderer r =
new CheckBoxTreeCellRenderer(tree, tree.getCellRenderer());
tree.setCellRenderer(r);
tree.getActionMap().put(SWITCH, new AbstractAction(SWITCH) {
public void actionPerformed(ActionEvent e) {
ComponentOrientation o = tree.getComponentOrientation();
if (o.isLeftToRight()) {
o = ComponentOrientation.RIGHT_TO_LEFT;
}
else {
o = ComponentOrientation.LEFT_TO_RIGHT;
}
tree.setComponentOrientation(o);
tree.repaint();
}
});
int mask = InputEvent.SHIFT_MASK|Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_O, mask), SWITCH);
final JTextArea text = new JTextArea(createText(r.getCheckedPaths()));
text.setPreferredSize(new Dimension(200, 100));
tree.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
// Invoke later to ensure all mouse handling is completed
SwingUtilities.invokeLater(new Runnable() { public void run() {
text.setText(createText(r.getCheckedPaths()));
}});
}
});
frame.getContentPane().add(new JScrollPane(tree));
frame.getContentPane().add(new JScrollPane(text), BorderLayout.SOUTH);
frame.pack();
frame.setSize(300, 350);
frame.setVisible(true);
}
catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}