JTree - JComboBox interaction
I have a program that displays editable information for each node in a tree. I've run into a problem where a user may update the contents of a JComboBox and then click on a new node without saving their changes. If the information was just lost (as it is with a JTextField), I would have no problem, but the information is saved under the node that was just clicked. Below is an oversimplified example of my problem.
1) Click TreeNode 1, type something in the JComboBox, hit enter, and then click back and forth from TreeNode 2. Everything is as it should be.
2) Now restart the example (there is only a problem if the JComboBox is blank to begin with), and do the same thing without hitting enter.
In the actual program I have a custom TreeModel, a custom TreeSelectionListener, a custom TreeRenderer, etc. but nothing I've tried does any good (and I've tried so many different things, including creating a previousNode variable and an isBeingEditted variable, all to no avail).
The JComboBox and the automatic update both serve important purposes, so I'd like to keep that functionality. Any help would be greatly appreciated.
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.event.*;
import java.awt.*;
publicclass TreeComboBoxProblemextends JFrame{
private JTree tree;
private JLabel nodeName;
private JComboBox nodeValue;
public TreeComboBoxProblem(String name){
super(name);
DefaultMutableTreeNode top =new DefaultMutableTreeNode(new TreeNode("TreeNode 0"));
top.add(new DefaultMutableTreeNode(new TreeNode("TreeNode 1")));
top.add(new DefaultMutableTreeNode(new TreeNode("TreeNode 2")));
top.add(new DefaultMutableTreeNode(new TreeNode("TreeNode 3")));
getContentPane().add( tree =new JTree(top), BorderLayout.NORTH );
tree.addTreeSelectionListener(new TreeListener() );
JPanel info =new JPanel();
info.add( nodeName =new JLabel("TreeNode 0"));
info.add( nodeValue =new JComboBox());
nodeValue.setPreferredSize(new Dimension(102, 20));
nodeValue.setEditable(true);
nodeValue.addActionListener(new infoActionListener());
getContentPane().add( info, BorderLayout.SOUTH );
}
class TreeNode{
String name;
String value;
public TreeNode(String name){
this.name = name;
value ="";
}
public String toString(){
return name;
}
}
protectedclass TreeListenerimplements TreeSelectionListener{
publicvoid valueChanged(TreeSelectionEvent e){
showInfo();
}
}
publicvoid showInfo(){
TreeNode node = (TreeNode)((DefaultMutableTreeNode)tree.getSelectionPath().getLastPathComponent()).getUserObject();
nodeName.setText(node.name);
nodeValue.setSelectedItem(node.value);
}
protectedclass infoActionListenerimplements ActionListener{
publicvoid actionPerformed(ActionEvent ae){
updateInfoSource();
}
}
publicvoid updateInfoSource(){
TreeNode node = (TreeNode)((DefaultMutableTreeNode)tree.getSelectionPath().getLastPathComponent()).getUserObject();
node.value = (String)nodeValue.getSelectedItem();
}
publicstaticvoid main(String[] args){
JFrame frame =new TreeComboBoxProblem("TreeComboBoxProblem");
frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
frame.pack();
frame.setVisible(true );
}
}
[6076 byte] By [
MansaSozea] at [2007-11-26 17:41:11]

# 1
Add a FocusListener to the combo box editor. When it gets focus save the currently selected node in the tree. Then use that node in the update method.
# 2
well it seems to me that the easiest solution is1) in "showInfo" you just save a reference to the node as a member variable2) in "updateInfoSource" you check that the "selected node" == "saved node" before updating the value.
# 3
Jaspre:Thanks for your reply. Part of the problem here is the timing of when the comboBoxChanged is thrown. It's after the "showInfo" has been executed; so the reference has already been updated (i.e. the "saved node" will always be the "selected node").
# 4
camickr:
I appreciate your suggestion. It's a putative workaround that I've explored in the past, but I wasn't able to get it to work then or now. For example, the following modifications produce the same results. This might also be related to the aforementioned timing issue - when the comboBoxChanged is fired or when the focus is gained/lost.
private TreeNode lastNode;
...
nodeValue.getEditor().getEditorComponent().addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent fe) {
lastNode = (TreeNode)((DefaultMutableTreeNode)tree.getSelectionPath().getLastPathComponent()).getUserObject();
}
});
...
public void updateInfoSource() {
if (lastNode != null) {
lastNode.value = (String) nodeValue.getSelectedItem();
lastNode = null;
}
}
# 5
> Jaspre:
>
> Thanks for your reply. Part of the problem here is
> the timing of when the comboBoxChanged is thrown.
> It's after the "showInfo" has been executed; so the
> reference has already been updated (i.e. the "saved
> node" will always be the "selected node").
oh, well that's because the loss of focus is causing the JComboBox to stop editing and therefore fire an ActionEvent.Just wrap your showInfo code in an invokeLater so that the ActionListener is notified before your code executes.
public void showInfo() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
sel = (TreeNode)((DefaultMutableTreeNode)tree.getSelectionPath().getLastPathComponent()).getUserObject();
nodeName.setText(sel.name);
nodeValue.setSelectedItem(sel.value);
}
});
}
public void updateInfoSource() {
TreeNode node = (TreeNode)((DefaultMutableTreeNode)tree.getSelectionPath().getLastPathComponent()).getUserObject();
if (node == sel)
node.value = (String)nodeValue.getSelectedItem();
}
# 6
Use this to disable the actionPerformed on focusLost:
info.add( nodeValue = new JComboBox() {
public void actionPerformed(ActionEvent e) {
if (e.getID() == 0)
addItem(getEditor().getItem());
else
super.actionPerformed(e);
}
});
# 7
Jaspre and Rodney:
Thank you guys so much. Both solutions work well for ignoring the unsaved input. I'd tried a variation on Rodney's method before, but I had checked the ID in the super, where all the IDs are identical for some reason. Jaspre's method addresses the timing issue I mentioned and has the added bonus of being able to replace with the following:
public void updateInfoSource() {
sel.value = (String) nodeValue.getSelectedItem();
}
...which allows me to save that unsaved input -- something that I'd actually prefer. Unfortunately, when I adapt my actual program, it hangs on the invokeLater. I end up having to click twice on the tree before it will switch to that node. I have no idea why. I'll investigate further tomorrow, unless this sounds familiar to anyone (or if someone can see a way of modifying Rodney's method to save the data).
Thanks again.
# 8
This is another solution for the problem:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
public class TreeComboBoxProblem extends JFrame {
private JTree tree;
private JLabel nodeName;
private JComboBox nodeValue;
private TreeNode selectedNode;
public TreeComboBoxProblem(String name) {
super(name);
DefaultMutableTreeNode top = new DefaultMutableTreeNode(new TreeNode("TreeNode 0"));
top.add(new DefaultMutableTreeNode(new TreeNode("TreeNode 1")));
top.add(new DefaultMutableTreeNode(new TreeNode("TreeNode 2")));
top.add(new DefaultMutableTreeNode(new TreeNode("TreeNode 3")));
getContentPane().add( tree = new JTree(top), BorderLayout.NORTH );
tree.addTreeSelectionListener( new TreeListener() );
JPanel info = new JPanel();
info.add( nodeName = new JLabel("TreeNode 0"));
info.add( nodeValue = new JComboBox(new String[] {}));
nodeValue.setPreferredSize(new Dimension(102, 20));
nodeValue.setEditable(true);
nodeValue.addActionListener(new infoActionListener());
getContentPane().add( info, BorderLayout.SOUTH );
}
class TreeNode {
String name;
String value;
public TreeNode(String name) {
this.name = name;
value = "";
}
public String toString() {
return name;
}
}
protected class TreeListener implements TreeSelectionListener {
public void valueChanged(TreeSelectionEvent e) {
showInfo();
}
}
public void showInfo() {
if (selectedNode != null)
selectedNode.value = nodeValue.getEditor().getItem().toString();
selectedNode = (TreeNode)((DefaultMutableTreeNode)tree.getSelectionPath().getLastPathComponent()).getUserObject();
nodeName.setText(selectedNode.name);
nodeValue.getEditor().setItem(selectedNode.value);
nodeValue.setSelectedItem(selectedNode.value);
}
protected class infoActionListener implements ActionListener {
public void actionPerformed(ActionEvent ae) {
updateInfoSource();
}
}
public void updateInfoSource() {
selectedNode.value = (String)nodeValue.getSelectedItem();
}
public static void main(String[] args) {
JFrame frame = new TreeComboBoxProblem("TreeComboBoxProblem");
frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
frame.pack();
frame.setVisible( true );
}
}
# 9
nice, but I like my more complicated solution... ya know, job security and all... ;-)
there are some flaws in the code, but I think they existed in everyone's code:
1) selectedNode is not initialized and the initial label is set to "TreeNode 0" which is actually not true. If you type text into the combobox without selecting a node, it doesn't get saved
2) NPE if you clear your selection via CTRL+click.
Easily fixed, just an FYI.
# 10
I just wanted to show the idea.I'm not going to debug it for the OP (-;When an object is not initialized it gets null value by default which is exactly what I wanted...
# 11
Thanks for the second method, Rodney. I was so frustrated as to why the showinfo method was called before the JComoboBox's focusLost or the JTree's focusGained or anything else was fired, that I ignored the obvious solution of just embracing it and adding an updateInfoSource at the beginning of showInfo.
Like I said before, the code here was just a quick and dirty example to illustrate my problem (hence the NPE and the dummy label to set the preferred size to something reasonable). I've now successfully incorporated that last approach into the actual program. The actual methods are fairly complicated, so I needed to find a few workarounds -- for example, the info panel (represented in the example by the JComboBox) can affect the tree structure, which in turn has to be able to re-select multiple selections that may have moved, which in turn fires showInfo again and creates a loop. But that and others were easily patched -- with the patch not being quite as parsimonious as I like, but fully functional nonetheless.
Your sample code also showed me why I was having so much trouble with my previous attempts: I was interacting with the selected item instead of with both that and the editor.
A question about your first, elegant solution: Does overriding the actionPerformed method ever have any unintended consequences? I ask because this is one of the many many methods that the javadoc says "This method is public as an implementation side effect. do not call or override." Am I closing too many doors by following their advice, or sparing myself future headaches?
# 12
> A question about your first, elegant solution: Does
> overriding the actionPerformed method ever have any
> unintended consequences? I ask because this is one of
> the many many methods that the javadoc says "This
> method is public as an implementation side effect. do
> not call or override." Am I closing too many doors by
> following their advice, or sparing myself future
> headaches?
I know this is for Rodney, but I'll give my 2 cents too. Overriding actionPerformed makes a few assumptions about the architecture of JComboBox and it's event model. It may be unlikely to change, but it could change in future implementations, rendering your code useless or even uncompilable. For example, what happens if they move the actionPerformed method to an inner class, where IMO, it belongs in the first place. Also, it's very imprecise about what it's doing and why - it's basing it's decision off of the event ID which could be changed to be anything or the reason for the event could be completely different than what is expected (a focus change in this case).
# 13
And my 2 cents are:
I based my solution on the source code of java 1.5.
It will probably not change, but you might have to change the code if it does.
But this is always true when extending classes that might change, or even using them.
Functions get depricated, and implementation change over time.
So I don't see a big risk doing this.