This is a followup to
9609185 wherein I ask for pointers to help me understand the source of focus-loss behaviour in my JTable's custom cell editor.
I have done some more investigations and it turns out that the focus loss is a more general problem with custom cell editors which call other windows. Even the color-picker demo in the JTable tutorial at http://download.oracle.com/javase/tutorial/uiswing/examples/components/index.html#TableDialogEditDemo has this problem, IF you add a text field or two to the layout BEFORE the table. The only reason the table in the demo doesn't lose the focus when the color-picker comes out is because the table is the only thing in the window!
Here is the demo code, augmented with two text fields, which are admittedly ugly here but which serve the desired purpose:
/*
* Copyright (c) 1995, 2008, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Oracle or the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class TableDialogEditDemo extends JPanel {
public class ColorEditor extends AbstractCellEditor
implements TableCellEditor,
ActionListener {
Color currentColor;
JButton button;
JColorChooser colorChooser;
JDialog dialog;
protected static final String EDIT = "edit";
public ColorEditor() {
//Set up the editor (from the table's point of view), which is a button.
//This button brings up the color chooser dialog, which is the editor from the user's point of view.
button = new JButton();
button.setActionCommand(EDIT);
button.addActionListener(this);
button.setBorderPainted(false);
//Set up the dialog that the button brings up.
colorChooser = new JColorChooser();
dialog = JColorChooser.createDialog(button, "Pick a Color", true, //modal
colorChooser, this, //OK button handler
null); //no CANCEL button handler
}
/**
* Handles events from the editor button and from the dialog's OK button.
*/
public void actionPerformed(ActionEvent e) {
if (EDIT.equals(e.getActionCommand())) {
//The user has clicked the cell, so bring up the dialog.
button.setBackground(currentColor);
colorChooser.setColor(currentColor);
dialog.setVisible(true);
//Make the renderer reappear.
fireEditingStopped();
} else { //User pressed dialog's "OK" button
currentColor = colorChooser.getColor();
}
}
public Object getCellEditorValue() {
return currentColor;
}
public Component getTableCellEditorComponent(JTable table,
Object value,
boolean isSelected,
int row,
int column) {
currentColor = (Color) value;
return button;
}
}
public class ColorRenderer extends JLabel
implements TableCellRenderer {
Border unselectedBorder = null;
Border selectedBorder = null;
boolean isBordered = true;
public ColorRenderer(boolean isBordered) {
this.isBordered = isBordered;
setOpaque(true);
}
public Component getTableCellRendererComponent(
JTable table, Object color,
boolean isSelected, boolean hasFocus,
int row, int column) {
Color newColor = (Color) color;
setBackground(newColor);
if (isBordered) {
if (isSelected) {
if (selectedBorder == null) {
selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
table.getSelectionBackground());
}
setBorder(selectedBorder);
} else {
if (unselectedBorder == null) {
unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
table.getBackground());
}
setBorder(unselectedBorder);
}
}
return this;
}
}
public TableDialogEditDemo() {
super(new GridLayout());
JTextField tf1 = new JTextField("tf1");
add(tf1);
JTextField tf2 = new JTextField("tf2");
add(tf2);
JTable table = new JTable(new MyTableModel());
table.setPreferredScrollableViewportSize(new Dimension(500, 70));
table.setFillsViewportHeight(true);
JScrollPane scrollPane = new JScrollPane(table);
table.setDefaultRenderer(Color.class,
new ColorRenderer(true));
table.setDefaultEditor(Color.class,
new ColorEditor());
add(scrollPane);
}
class MyTableModel extends AbstractTableModel {
private String[] columnNames = {"First Name",
"Favorite Color",
"Sport",
"# of Years",
"Vegetarian"};
private Object[][] data = {
{"Mary", new Color(153, 0, 153),
"Snowboarding", new Integer(5), new Boolean(false)},
{"Alison", new Color(51, 51, 153),
"Rowing", new Integer(3), new Boolean(true)},
{"Kathy", new Color(51, 102, 51),
"Knitting", new Integer(2), new Boolean(false)},
{"Sharon", Color.red,
"Speed reading", new Integer(20), new Boolean(true)},
{"Philip", Color.pink,
"Pool", new Integer(10), new Boolean(false)}
};
public int getColumnCount() {
return columnNames.length;
}
public int getRowCount() {
return data.length;
}
public String getColumnName(int col) {
return columnNames[col];
}
public Object getValueAt(int row, int col) {
return data[row][col];
}
public Class getColumnClass(int c) {
return getValueAt(0, c).getClass();
}
public boolean isCellEditable(int row, int col) {
if (col < 1) {
return false;
} else {
return true;
}
}
public void setValueAt(Object value, int row, int col) {
data[row][col] = value;
fireTableCellUpdated(row, col);
}
}
private static void createAndShowGUI() {
JFrame frame = new JFrame("TableDialogEditDemo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JComponent newContentPane = new TableDialogEditDemo();
newContentPane.setOpaque(true);
frame.setContentPane(newContentPane);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
}
When you come back from choosing a color, tf1 is given the focus, instead of the table. This is because bringing the color picker window to the front causes a focus lost event for the cell editor component; it's temporary, as it should be, so why on earth is the system losing track of who has focus in the window??
I see the following in Window#getMostRecentFocusOwner():
public Component getMostRecentFocusOwner()
{
if (isFocused())
{
return getFocusOwner();
}
else
{
Component mostRecent =
KeyboardFocusManager.getMostRecentFocusOwner(this);
if (mostRecent != null)
{
return mostRecent;
}
else
{
return (isFocusableWindow())
? getFocusTraversalPolicy().getInitialComponent(this)
: null;
}
}
}
My app has a custom focus traversal policy, so I'm able to see who is being called, and indeed, getInitialComponent() is being called. Clearly, the KeyboardFocusManager is actually losing track of the fact that the table was focussed at the point where control was transferred to the color picker! This strikes me as completely unreasonable, especially since, as noted, this is a
temporary focus loss event, not a permanent one.
I'd be grateful for any wisdom in solving this, since similar behaviour to this little demo -- without focus loss, naturally -- is an essential part of my application.