I\'ve created a tree cell renderer/editor framework that is admittedly a little bit hacky, but it works perfectly on Windows and Linux. The image below illustrates a sample set
A quick outline of how to implement complex cellEditors. The assumption is that all interactive elements of the editor do indeed edit a property of the node's userObject - as is the case in the OP's full code.
The collaborators
Some code (obviously not usable for real-world usage, just to get the idea :)
public static class ViewProvider extends AbstractCellEditor
implements TreeCellEditor, TreeCellRenderer {
private JCheckBox firstBox;
private JButton colorButton;
private JComponent nodePanel;
private JButton nameButton;
private Data data;
private boolean ignore;
public ViewProvider(boolean asEditor) {
initComponents();
if (asEditor)
installListeners();
}
protected void initComponents() {
nodePanel = new JPanel();
nodePanel.setOpaque(false);
firstBox = new JCheckBox();
colorButton = new JButton();
// if we need something clickable use something ... clickable :-)
nameButton = new JButton();
nameButton.setContentAreaFilled(false);
nameButton.setOpaque(true);
nameButton.setBorderPainted(false);
nameButton.setMargin(new Insets(0, 0, 0, 0));
nodePanel.add(firstBox);
nodePanel.add(colorButton);
nodePanel.add(nameButton);
}
protected void installListeners() {
ActionListener cancel = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
cancelCellEditing();
}
};
nameButton.addActionListener(cancel);
ActionListener stop = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
stopCellEditing();
}
};
firstBox.addActionListener(stop);
// Note: code for using a button to trigger opening a dialog
// is in the tutorial, should replace this
colorButton.addActionListener(stop);
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
Data data = (Data) ((DefaultMutableTreeNode) value).getUserObject();
firstBox.setSelected(data.isVisible);
colorButton.setBackground(data.color);
nameButton.setText(data.name);
nameButton.setBackground(selected ? Color.YELLOW : tree.getBackground());
nameButton.setFont(tree.getFont());
return nodePanel;
}
@Override
public Component getTreeCellEditorComponent(JTree tree, Object value,
boolean isSelected, boolean expanded, boolean leaf, int row) {
// copy to not fiddle with the original
data = new Data((Data) ((DefaultMutableTreeNode) value).getUserObject());
ignore = true;
getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, false);
ignore = false;
return nodePanel;
}
@Override
public Object getCellEditorValue() {
return data;
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
// at this point the editing component is added to the tree
// and the mouse coordinates still in tree coordinates
if (anEvent instanceof MouseEvent) {
MouseEvent me = (MouseEvent) anEvent;
Point loc = SwingUtilities.convertPoint(me.getComponent(),
me.getPoint(), nodePanel);
return loc.x >= nameButton.getX();
}
return false;
}
@Override
public boolean stopCellEditing() {
if (ignore) return false;
// real-world data will have setters
data.isVisible = firstBox.isSelected();
return super.stopCellEditing();
}
@Override
public void cancelCellEditing() {
if (ignore) return;
data = null;
super.cancelCellEditing();
}
}
// simple Data - obviously not for production
public static class Data {
boolean isVisible;
Color color;
String name;
public Data(boolean isVisible, Color color,
String name) {
this.isVisible = isVisible;
this.color = color;
this.name = name;
}
/**
* A copy constructor to allow editors to manipulate its
* properties without changing the original.
*
* @param original
*/
public Data(Data original) {
this.isVisible = original.isVisible;
this.color = original.color;
this.name = original.name;
}
}
// usage:
DefaultMutableTreeNode root = new DefaultMutableTreeNode(
new Data(true, Color.RED, "someName"));
root.add(new DefaultMutableTreeNode(new Data(true, Color.GREEN, "other")));
root.add(new DefaultMutableTreeNode(new Data(false, Color.BLUE, "whatagain")));
root.add(new DefaultMutableTreeNode(new Data(false, Color.YELLOW, "dummy")));
JTree tree = new JTree(root);
tree.setCellRenderer(new ViewProvider(false));
tree.setCellEditor(new ViewProvider(true));
tree.setEditable(true);
I've come up with something that creates my desired behavior and does not use a TreeCellEditor
at all. Instead, the tree is uneditable, and is instantiated with a custom extension of JTree
which has processMouseEvent
overridden. I got this idea here.
It seems to work perfect, but it is still a little bit hacky (it does a loop of calculations to determine where the start of the tree cell is, since that can vary based on indentation). Also I pretty much disabled mouseClicked
and mouseReleased
type events, and am controlling the JTreeMod
only with mousePressed
events. Not sure if this will bite me later or is bad practice, but I didn't like my custom code running 3 times in a row for all the events. I also haven't been able to test on a non-Windows OS just yet.
Here is the console output after clicking, in series (1) the image one (2) the text one (3) the image two (4) the text two. Again, this perfectly implements my desired behavior.
you clicked the image for row 1. this was detected, but no selection will happen!
you clicked the text for row 1. this was detected, and selection WILL happen!
SELECTION CHANGED!
you clicked the image for row 2. this was detected, but no selection will happen!
you clicked the text for row 2. this was detected, and selection WILL happen!
SELECTION CHANGED!
And here is the new SSCCE:
package TreeTest;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.MouseEvent;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
@SuppressWarnings("serial")
public class TreeTest2 extends JComponent {
private JFrame frame;
private DefaultTreeModel treeModel;
private DefaultMutableTreeNode root;
private JTreeMod tree;
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Throwable e) {
e.printStackTrace();
}
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
TreeTest2 window = new TreeTest2();
window.frame.setVisible(true);
window.frame.requestFocusInWindow();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public TreeTest2() {
initialize();
}
private void initialize() {
frame = new JFrame("Tree Test");
frame.setBounds(400, 400, 250, 150);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
root = new DefaultMutableTreeNode("root");
treeModel = new DefaultTreeModel(root);
tree = new JTreeMod(treeModel);
tree.setEditable(false);
tree.getSelectionModel().setSelectionMode(
TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setRootVisible(false);
tree.setShowsRootHandles(true);
tree.setCellRenderer(new TreeRenderer());
tree.putClientProperty("JTree.lineStyle", "None");
tree.setBackground(Color.white);
DefaultMutableTreeNode one = new DefaultMutableTreeNode("one");
DefaultMutableTreeNode two = new DefaultMutableTreeNode("two");
treeModel.insertNodeInto(one, root, 0);
treeModel.insertNodeInto(two, one, 0);
TreeNode[] nodes = treeModel.getPathToRoot(root);
tree.expandPath(new TreePath(nodes));
nodes = treeModel.getPathToRoot(one);
tree.expandPath(new TreePath(nodes));
tree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
System.out.println("SELECTION CHANGED!");
}
});
frame.getContentPane().add(tree);
}
public class TreeRenderer implements TreeCellRenderer {
private ImageIcon oneIcon;
private ImageIcon twoIcon;
public TreeRenderer() {
try {
oneIcon = new ImageIcon(ImageIO.read(
new URL("http://i.imgur.com/HtHJkfI.png")));
twoIcon = new ImageIcon(ImageIO.read(
new URL("http://i.imgur.com/w5jAp5c.png")));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
JLabel numIcon = new JLabel();
numIcon.setAlignmentX(JLabel.CENTER_ALIGNMENT);
numIcon.setAlignmentY(JLabel.CENTER_ALIGNMENT);
numIcon.setBorder(new EmptyBorder(0, 0, 0, 4));
JLabel numText = new JLabel();
JPanel comp = new JPanel();
comp.setLayout(new BoxLayout(comp, BoxLayout.X_AXIS));
comp.add(numIcon);
comp.add(numText);
String str = (String) ((DefaultMutableTreeNode) value).getUserObject();
if (str.equals("one")) {
numIcon.setIcon(oneIcon);
numText.setText("one");
} else if (str.equals("two")) {
numIcon.setIcon(twoIcon);
numText.setText("two");
}
numText.setOpaque(true);
if (selected) {
numText.setBackground(new Color(209, 230, 255));
numText.setBorder(new LineBorder(
new Color(132, 172, 221), 1, false));
} else {
numText.setBackground(Color.white);
numText.setBorder(new LineBorder(Color.white, 1, false));
}
comp.setFocusable(false);
comp.setBackground(Color.white);
return comp;
}
}
public class JTreeMod extends JTree {
public JTreeMod(DefaultTreeModel treeModel) {
super(treeModel);
}
@Override
protected void processMouseEvent(MouseEvent e) {
int type = e.getID();
if (type == MouseEvent.MOUSE_CLICKED || type == MouseEvent.MOUSE_RELEASED) {
// do nothing
} else if (type == MouseEvent.MOUSE_PRESSED) {
int x = e.getX();
int y = e.getY();
int row = this.getRowForLocation(x, y);
if (row == -1) {
super.processMouseEvent(e);
return;
}
int xOffset = x;
int row1 = row;
while (row1 == row) {
xOffset--;
row1 = this.getRowForLocation(xOffset, y);
}
xOffset++;
if (x - xOffset <= 16) {
System.out.println("you clicked the image for row " + (row + 1) +
". this was detected, but no selection will happen!");
return;
} else {
System.out.println("you clicked the text for row " + (row + 1) +
". this was detected, and selection WILL happen!");
super.processMouseEvent(e);
}
} else {
super.processMouseEvent(e);
}
}
}
}
I'm not sure I understand the requirement, but the example below adds a convenient key binding and works with either single-click approach shown here. I've chosen to override canEditImmediately()
, as suggested by kleopatra, subject to her caveats concerning usability.
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.EventObject;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
/**
* @see https://stackoverflow.com/a/15738813/230513
* @see https://stackoverflow.com/q/15625424/230513
*/
public class Test {
private static Icon one;
private static Icon two;
private void display() {
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JTree tree = new JTree();
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
final TreeRenderer renderer = new TreeRenderer();
tree.setCellRenderer(renderer);
tree.setCellEditor(new TreeEditor(tree, renderer));
tree.setEditable(true);
tree.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "startEditing");
f.add(new JScrollPane(tree));
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static class TreeRenderer extends DefaultTreeCellRenderer {
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean sel, boolean exp, boolean leaf, int row, boolean hasFocus) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
String s = node.getUserObject().toString();
if ("colors".equals(s)) {
setOpenIcon(one);
setClosedIcon(one);
} else if ("sports".equals(s)) {
setOpenIcon(two);
setClosedIcon(two);
} else {
setOpenIcon(getDefaultOpenIcon());
setClosedIcon(getDefaultClosedIcon());
}
super.getTreeCellRendererComponent(
tree, value, sel, exp, leaf, row, hasFocus);
return this;
}
}
private static class TreeEditor extends DefaultTreeCellEditor {
public TreeEditor(JTree tree, DefaultTreeCellRenderer renderer) {
super(tree, renderer);
}
@Override
public Component getTreeCellEditorComponent(JTree tree, Object value,
boolean isSelected, boolean exp, boolean leaf, int row) {
Component c = super.getTreeCellEditorComponent(
tree, value, isSelected, exp, leaf, row);
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
String s = node.getUserObject().toString();
if ("colors".equals(s)) {
editingIcon = one;
} else if ("sports".equals(s)) {
editingIcon = two;
}
return c;
}
@Override
protected boolean canEditImmediately(EventObject event) {
if ((event instanceof MouseEvent)
&& SwingUtilities.isLeftMouseButton((MouseEvent) event)) {
MouseEvent me = (MouseEvent) event;
return ((me.getClickCount() >= 1)
&& inHitRegion(me.getX(), me.getY()));
}
return (event == null);
}
}
public static void main(String[] args) throws Exception {
one = new ImageIcon(ImageIO.read(
new URL("http://i.imgur.com/HtHJkfI.png")));
two = new ImageIcon(ImageIO.read(
new URL("http://i.imgur.com/w5jAp5c.png")));
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new Test().display();
}
});
}
}