How to edit a JXTreeTable cell in only one mouse click?

1.5k views Asked by At

I want to use a JComboBox as a cell editor in a JXTreeTable. It works fine with a standard DefaultCellEditor (i.e. with a click count to start equal to 2).

Now I want the column to be editable on only one click. So I added a cellEditor.setClickCountToStart(1); statement to my code.

Here is my SSCCE:

import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultTreeTableModel;

public class TestCellEditorForJXTreeTable {

    /** The JXTreeTable */
    JXTreeTable treeTable;
    /** The model */
    DefaultTreeTableModel treeTableModel;

    /** Constructor */
    public TestCellEditorForJXTreeTable() {

        treeTable = new JXTreeTable();
        treeTableModel = new DefaultTreeTableModel() {
            @Override
            public String getColumnName(int column) {
                switch (column) {
                    case 0:
                        return "A";
                    case 1:
                        return "B";
                }
                return null;
            }
            @Override
            public Object getValueAt(Object node, int column) {
                switch (column) {
                    case 0:
                        return ((DefaultMutableTreeTableNode) node).getUserObject();
                    case 1:
                        return "Value in B";
                }
                return null;
            }
            @Override
            public int getColumnCount() {
                return 2;
            }
            @Override
            public boolean isCellEditable(Object node, int column) {
                return column == 1;
            }
        };
        treeTable.setTreeTableModel(treeTableModel);

    }

    public static void main(String[] args) {
        TestCellEditorForJXTreeTable test = new TestCellEditorForJXTreeTable();

        // Root node
        DefaultMutableTreeTableNode root = new DefaultMutableTreeTableNode("root");
        test.treeTableModel.setRoot(root);

        // New nodes/rows
        DefaultMutableTreeTableNode node = new DefaultMutableTreeTableNode("child_node");
        test.treeTableModel.insertNodeInto(node, root, 0);
        DefaultMutableTreeTableNode node2 = new DefaultMutableTreeTableNode("child_node2");
        test.treeTableModel.insertNodeInto(node2, root, 1);

        // Showing the frame
        showTable(test.treeTable);

        // Setting the cell editor
        DefaultCellEditor cellEditor = new DefaultCellEditor(new JComboBox(new String[]{"1", "2", "3"}));
        cellEditor.setClickCountToStart(1);
        test.treeTable.getColumn(1).setCellEditor(cellEditor);

    }

    /** Shows a JXTreeTable in a frame */
    private static void showTable(JXTreeTable table) {
        JFrame frame = new JFrame("Testing cell editor for JXTreeTable");
        frame.setPreferredSize(new Dimension(640, 480));
        frame.setLayout(new BorderLayout());
        frame.add(table, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

But now it looks pretty ugly:

When I click on an editable cell it opens the JComboBox popup menu (Great! It's what I was expecting!), but this popup menu is immediately closed (Erf!). It flashes. I have to click a second time on the selected cell to get it definitively opened.

The problem repeats each time I select another cell in the editable column.

How could I get the JComboBox popup menu really opened after the first click?

Thanks.

Edit 2014-01-24

Here is the same example, but using JTable. The JComboBox popup menu does not flash.

import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

public class TestCellEditorForJTable {

    /** The JTable */
    JTable table;
    /** The model */
    DefaultTableModel tableModel;

    /** Constructor */
    public TestCellEditorForJTable() {
        table = new JTable();
        tableModel = new DefaultTableModel(new String[] {"A", "B"}, 0) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return column == 1;
            }
        };
        table.setModel(tableModel);

    }

    public static void main(String[] args) {
        TestCellEditorForJTable test = new TestCellEditorForJTable();

        // New rows
        test.tableModel.insertRow(0, new String[] {"Value1 in A", "Value1 in B"});
        test.tableModel.insertRow(1, new String[] {"Value2 in A", "Value2 in B"});

        // Showing the frame
        showTable(test.table);

        // Setting the cell editor
        DefaultCellEditor cellEditor = new DefaultCellEditor(new JComboBox(new String[]{"1", "2", "3"}));
        cellEditor.setClickCountToStart(1);
        test.table.getColumnModel().getColumn(1).setCellEditor(cellEditor);

    }

    /** Shows a table in a frame */
    private static void showTable(JTable table) {
        JFrame frame = new JFrame("Testing cell editor for JTable");
        frame.setPreferredSize(new Dimension(640, 480));
        frame.setLayout(new BorderLayout());
        frame.add(table, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

And I forgot to mention that I'm using Java 1.6.

Edit 2014-01-24 (2)

Using the ContainerListener and the FocusListener of the kleopatra's answer, and running the same execution flow, I get the following output with the JXTreeTable SSCCE:

// first click
24.01.2014 13:10:59 my.pkg.TestCellEditorForJXTreeTable$2 componentAdded
INFO: java.awt.event.ContainerEvent[COMPONENT_ADDED...JXTreeTable...
24.01.2014 13:10:59 my.pkg.TestCellEditorForJXTreeTable$3 focusGained
INFO: java.awt.FocusEvent[FOCUS_GAINED...JXTreeTable...
24.01.2014 13:10:59 my.pkg.TestCellEditorForJXTreeTable$2 componentRemoved
INFO: java.awt.event.ContainerEvent[COMPONENT_REMOVED...JXTreeTable...
24.01.2014 13:10:59 my.pkg.TestCellEditorForJXTreeTable$3 focusLost
INFO: java.awt.FocusEvent[FOCUS_LOST...JXTreeTable...

// second click
24.01.2014 13:11:02 my.pkg.TestCellEditorForJXTreeTable$2 componentAdded
INFO: java.awt.event.ContainerEvent[COMPONENT_ADDED...JXTreeTable...
24.01.2014 13:11:02 my.pkg.TestCellEditorForJXTreeTable$3 focusGained
INFO: java.awt.FocusEvent[FOCUS_GAINED...JXTreeTable...
24.01.2014 13:11:02 my.pkg.TestCellEditorForJXTreeTable$2 componentRemoved
INFO: java.awt.event.ContainerEvent[COMPONENT_REMOVED...JXTreeTable...
24.01.2014 13:11:02 my.pkg.TestCellEditorForJXTreeTable$3 focusLost
INFO: java.awt.FocusEvent[FOCUS_LOST...JXTreeTable...
2

There are 2 answers

4
kleopatra On BEST ANSWER

Tricky bugger - and I think it's indeed a core issue.

Let's first define exactly what/when it is happening: take the plain table example (btw: +1 for the nice and concise SSCCE!)

  • run
  • click into cell (1, 1), that is last row, second column: table starts editing, combo's popup is showing
  • while still editing (note that is important to not click anywhere else in between), click into cell (0, 1): table starts editing that cell, combo's popup is hidden

Digging reveals the probable reason: it's an out-of-order focusLost received after the combo was added again as editing component. To see, register a containerListener to the table and a focusListener to the combo and print the events

ContainerListener containerL = new ContainerListener() {

    @Override
    public void componentRemoved(ContainerEvent e) {
        LOG.info("" + e);
    }

    @Override
    public void componentAdded(ContainerEvent e) {
        LOG.info("" + e);
    }
};
table.addContainerListener(containerL);
FocusListener focusL = new FocusListener() {

    @Override
    public void focusGained(FocusEvent e) {
        LOG.info("" + e);
       // following line is a hack around: force the popup open
       // ((JComboBox) cellEditor.getComponent()).setPopupVisible(true);
    }

    @Override
    public void focusLost(FocusEvent e) {
        LOG.info("" + e);
    }

};
cellEditor.getComponent().addFocusListener(focusL);

The output:

// first click
24.01.2014 12:13:44 org.jdesktop.swingx.table.TestCellEditorForJTable$2 componentAdded
INFO: java.awt.event.ContainerEvent[COMPONENT_ADDED,child=null] on javax.swing.JTable...
24.01.2014 12:13:44 org.jdesktop.swingx.table.TestCellEditorForJTable$3 focusGained
INFO: java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=javax.swing.JTable

// second click
24.01.2014 12:13:49 org.jdesktop.swingx.table.TestCellEditorForJTable$2 componentRemoved
INFO: java.awt.event.ContainerEvent[COMPONENT_REMOVED,child=null] on javax.swing.JTable
24.01.2014 12:13:49 org.jdesktop.swingx.table.TestCellEditorForJTable$2 componentAdded
INFO: java.awt.event.ContainerEvent[COMPONENT_ADDED,child=null] on javax.swing.JTable
// here's the problem: focusLost _after_ added again
24.01.2014 12:13:49 org.jdesktop.swingx.table.TestCellEditorForJTable$3 focusLost
INFO: java.awt.FocusEvent[FOCUS_LOST,permanent,opposite=javax.swing.JTable
24.01.2014 12:13:49 org.jdesktop.swingx.table.TestCellEditorForJTable$3 focusGained
INFO: java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=javax.swing.JTable

A quick hack could be to force the popup open in the focusListener. Didn't check for side-effects, though.

3
dic19 On

Interesting fact

If you pick a value from the combobox then the editor works as expected (next click on an editable cell will open the combobox of that cell as you wish), but if you close the combobox without selecting a value then it behaves as you describe. So the issue seems to be the combobox doesn't stop editing until you select a value or focus in some other component. Consequently the first click to another cell makes its own editor request focus instead of start editing.

Explanation

Looking closer at DefaultCellEditor implementation the problem is only an ActionListener is attached to the combobox causing a fireEditingStopped() call through the EditorDelegate when an item is selected but nothing happens when the combobox is closed or cancelled without selecting a value:

public DefaultCellEditor(final JComboBox comboBox) {
    editorComponent = comboBox;
    comboBox.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
    delegate = new EditorDelegate() {...}
    comboBox.addActionListener(delegate); // delegate is the ActionListener
}

protected class EditorDelegate implements ActionListener, ItemListener, Serializable {

    ...

    public void actionPerformed(ActionEvent e) {
        DefaultCellEditor.this.stopCellEditing(); // This will finally call  fireEditingStopped();
    }
}

Solution

Make your own TableCellEditor using a combobox as editor and attach a PopupMenuListener to call fireEditingStopped() or fireEditingCanceled() as needed. For instance:

class ComboBoxEditor extends AbstractCellEditor implements TableCellEditor {

    private JComboBox editor;
    private int clickCountToStart = 2;
    private Object selectedValue;

    public ComboBoxEditor(Object[] selectableValues) {

        editor = new JComboBox(selectableValues);

        editor.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectedValue = editor.getSelectedItem();
                ComboBoxEditor.this.fireEditingStopped();
            }
        });

        editor.addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                // Nothing to do here, it's not relevant to your purpose
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                ComboBoxEditor.this.fireEditingStopped();
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
                ComboBoxEditor.this.fireEditingCanceled();
            }
        });
    }

    public void setClickCountToStart(int clickCountToStart) {
        this.clickCountToStart = clickCountToStart;
    }

    public int getClickCountToStart() {
        return clickCountToStart;
    }

    // Rest of implementation is up to you, look into DefaultCellEditor implementation
}