문제

In my main application, the JTable is losing focus when a dialog is shown from a cell editor component.

Below is a simple SSCCE I made for you to see the problem.

Do these simples experiments:

  • Press F2 in the first table column to start editing. Then change to column contents to the number 2 and press ENTER key. The table will lose focus and the first field in the form with get focus.
  • Press F2 in the first table column to start editing. Then change to column contents to the number 2 and press TAB key. The table will lose focus and the first field in the form with get focus.

The first field in the form is also a SearchField component. Because it is not in the JTable, it behaves properly when you change its contente to the number 2 and commit the edit (with ENTER or TAB).

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Objects;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultCellEditor;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.NumberFormatter;

public class SSCCE extends JPanel
{
    private SSCCE()
    {
        setLayout(new BorderLayout());
        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

        JPanel pnlFields = new JPanel();
        pnlFields.setLayout(new BoxLayout(pnlFields, BoxLayout.PAGE_AXIS));
        pnlFields.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));

        SearchField field1 = new SearchField();
        configureField(field1);
        pnlFields.add(field1);

        pnlFields.add(Box.createRigidArea(new Dimension(0, 3)));

        JTextField field2 = new JTextField();
        configureField(field2);
        pnlFields.add(field2);

        add(pnlFields, BorderLayout.PAGE_START);
        add(new JScrollPane(createTable()), BorderLayout.CENTER);
    }

    private void configureField(JTextField field)
    {
        field.setPreferredSize(new Dimension(150, field.getPreferredSize().height));
        field.setMaximumSize(field.getPreferredSize());
        field.setAlignmentX(LEFT_ALIGNMENT);
    }

    private JTable createTable()
    {
        JTable table = new JTable(new CustomTableModel());

        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        table.setCellSelectionEnabled(true);
        table.getTableHeader().setReorderingAllowed(false);
        table.setPreferredScrollableViewportSize(new Dimension(500, 170));

        table.setDefaultEditor(Integer.class, new SearchFieldCellEditor(new SearchField()));

        return table;
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new JFrame("SSCCE (JTable Loses Focus)");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new SSCCE());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(
            new Runnable()
            {
                @Override
                public void run()
                {
                    createAndShowGUI();
                }
            }
        );
    }
}

class CustomTableModel extends AbstractTableModel
{
    private String[] columnNames = {"Column1 (Search Field)", "Column 2"};
    private Class<?>[] columnTypes = {Integer.class, String.class};
    private Object[][] data = {{1, ""}, {3, ""}, {4, ""}, {5, ""}, {6, ""}};

    @Override
    public int getColumnCount()
    {
        return columnNames.length;
    }

    @Override
    public int getRowCount()
    {
        return data.length;
    }

    @Override
    public String getColumnName(int col)
    {
        return columnNames[col];
    }

    @Override
    public Object getValueAt(int row, int col)
    {
        return data[row][col];
    }

    @Override
    public Class<?> getColumnClass(int c)
    {
        return columnTypes[c];
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex)
    {
        return true;
    }

    @Override
    public void setValueAt(Object value, int row, int col)
    {
        data[row][col] = value;
        fireTableCellUpdated(row, col);
    }
}

class SearchFieldCellEditor extends DefaultCellEditor
{
    SearchFieldCellEditor(final SearchField searchField)
    {
        super(searchField);
        searchField.removeActionListener(delegate);
        delegate = new EditorDelegate()
        {
            @Override
            public void setValue(Object value)
            {
                searchField.setValue(value);
            }

            @Override
            public Object getCellEditorValue()
            {
                return searchField.getValue();
            }
        };
        searchField.addActionListener(delegate);
    }

    @Override
    public boolean stopCellEditing()
    {
        try
        {
            ((SearchField) getComponent()).commitEdit();
        }
        catch (ParseException ex)
        {
            ex.printStackTrace();
        }
        return super.stopCellEditing();
    }
}

class SearchField extends JFormattedTextField implements PropertyChangeListener
{
    private Object _oldValue;

    SearchField()
    {
        setupFormatter();
        addPropertyChangeListener("value", this);
    }

    private void setupFormatter()
    {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        integerFormat.setGroupingUsed(false);

        NumberFormatter integerFormatter =
            new NumberFormatter(integerFormat)
            {
                @Override
                public Object stringToValue(String text) throws ParseException
                {
                    return text.isEmpty() ? null : super.stringToValue(text);
                }
            };
        integerFormatter.setValueClass(Integer.class);
        integerFormatter.setMinimum(Integer.MIN_VALUE);
        integerFormatter.setMaximum(Integer.MAX_VALUE);

        setFormatterFactory(new DefaultFormatterFactory(integerFormatter));
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        Object newValue = evt.getNewValue();
        if (!Objects.equals(newValue, _oldValue))
        {
            _oldValue = newValue;
            // Suppose that a value of 2 means that the data wasn't found.
            // So we display a message to the user.
            if (new Integer(2).equals(newValue))
            {
                JOptionPane.showMessageDialog(
                    null, "Not found: " + newValue + ".", "Warning",
                    JOptionPane.WARNING_MESSAGE);
            }
        }
    }
}

So, is there a way to solve this problem? The solution of this issue is very important to me.

Thank you.

Marcos

* UPDATE *

I think I've found a solution, but I would like to have your opinion if it is really a trustworthy solution.

Change the stopCellEditing method to this and test the SSCCE again:

@Override
public boolean stopCellEditing()
{
    SearchField searchField = (SearchField) getComponent();

    try
    {
        searchField.commitEdit();
    }
    catch (ParseException ex)
    {
        ex.printStackTrace();
    }

    Component table = searchField.getParent();
    table.requestFocusInWindow();

    return super.stopCellEditing();
}

So, do you think this really solves the problem or is there any flaw?

Marcos

UPDATE 2

I've found a little flaw. It is corrected with these changes:

class SearchFieldCellEditor extends DefaultCellEditor
{
    SearchFieldCellEditor(final SearchField searchField)
    {
        super(searchField);
        searchField.removeActionListener(delegate);
        delegate = new EditorDelegate()
        {
            @Override
            public void setValue(Object value)
            {
                searchField.setValue(value);
            }

            @Override
            public Object getCellEditorValue()
            {
                return searchField.getValue();
            }
        };
        searchField.addActionListener(delegate);
    }

    @Override
    public Component getTableCellEditorComponent(
        JTable table, Object value, boolean isSelected, int row, int column)
    {
        SearchField searchField = (SearchField) getComponent();
        searchField.setPreparingForEdit(true);
        try
        {
            return super.getTableCellEditorComponent(
                table, value, isSelected, row, column);
        }
        finally
        {
            searchField.setPreparingForEdit(false);
        }
    }

    @Override
    public boolean stopCellEditing()
    {
        SearchField searchField = (SearchField) getComponent();

        try
        {
            searchField.commitEdit();
        }
        catch (ParseException ex)
        {
            ex.printStackTrace();
        }

        Component table = searchField.getParent();
        table.requestFocusInWindow();

        return super.stopCellEditing();
    }
}

class SearchField extends JFormattedTextField implements PropertyChangeListener
{
    private boolean _isPreparingForEdit;
    private Object _oldValue;

    SearchField()
    {
        setupFormatter();
        addPropertyChangeListener("value", this);
    }

    void setPreparingForEdit(boolean isPreparingForEdit)
    {
        _isPreparingForEdit = isPreparingForEdit;
    }

    private void setupFormatter()
    {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        integerFormat.setGroupingUsed(false);

        NumberFormatter integerFormatter =
            new NumberFormatter(integerFormat)
            {
                @Override
                public Object stringToValue(String text) throws ParseException
                {
                    return text.isEmpty() ? null : super.stringToValue(text);
                }
            };
        integerFormatter.setValueClass(Integer.class);
        integerFormatter.setMinimum(Integer.MIN_VALUE);
        integerFormatter.setMaximum(Integer.MAX_VALUE);

        setFormatterFactory(new DefaultFormatterFactory(integerFormatter));
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        final Object newValue = evt.getNewValue();
        if (!Objects.equals(newValue, _oldValue))
        {
            _oldValue = newValue;
            // Suppose that a value of 2 means that the data wasn't found.
            // So we display a message to the user.
            if (new Integer(2).equals(newValue) && !_isPreparingForEdit)
            {
                JOptionPane.showMessageDialog(null, "Not found: " + newValue + ".", "Warning",
                    JOptionPane.WARNING_MESSAGE);
            }
        }
    }
}

Have you found any more flaws too? I would like to have your review.

Marcos

UPDATE 3

Another solution after suggestion by kleopatra :

class SearchFieldCellEditor extends DefaultCellEditor
{
    SearchFieldCellEditor(final SearchField searchField)
    {
        super(searchField);
        searchField.setShowMessageAsynchronously(true);
        searchField.removeActionListener(delegate);
        delegate = new EditorDelegate()
        {
            @Override
            public void setValue(Object value)
            {
                searchField.setValue(value);
            }

            @Override
            public Object getCellEditorValue()
            {
                return searchField.getValue();
            }
        };
        searchField.addActionListener(delegate);
    }

    @Override
    public Component getTableCellEditorComponent(
        JTable table, Object value, boolean isSelected, int row, int column)
    {
        SearchField searchField = (SearchField) getComponent();
        searchField.setPreparingForEdit(true);
        try
        {
            return super.getTableCellEditorComponent(
                table, value, isSelected, row, column);
        }
        finally
        {
            searchField.setPreparingForEdit(false);
        }
    }

    @Override
    public boolean stopCellEditing()
    {
        SearchField searchField = (SearchField) getComponent();

        try
        {
            searchField.commitEdit();
        }
        catch (ParseException ex)
        {
            ex.printStackTrace();
        }

        return super.stopCellEditing();
    }
}

class SearchField extends JFormattedTextField implements PropertyChangeListener
{
    private boolean _showMessageAsynchronously;
    private boolean _isPreparingForEdit;
    private Object _oldValue;

    SearchField()
    {
        setupFormatter();
        addPropertyChangeListener("value", this);
    }

    public boolean isShowMessageAsynchronously()
    {
        return _showMessageAsynchronously;
    }

    public void setShowMessageAsynchronously(boolean showMessageAsynchronously)
    {
        _showMessageAsynchronously = showMessageAsynchronously;
    }

    void setPreparingForEdit(boolean isPreparingForEdit)
    {
        _isPreparingForEdit = isPreparingForEdit;
    }

    private void setupFormatter()
    {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        integerFormat.setGroupingUsed(false);

        NumberFormatter integerFormatter =
            new NumberFormatter(integerFormat)
            {
                @Override
                public Object stringToValue(String text) throws ParseException
                {
                    return text.isEmpty() ? null : super.stringToValue(text);
                }
            };
        integerFormatter.setValueClass(Integer.class);
        integerFormatter.setMinimum(Integer.MIN_VALUE);
        integerFormatter.setMaximum(Integer.MAX_VALUE);

        setFormatterFactory(new DefaultFormatterFactory(integerFormatter));
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        final Object newValue = evt.getNewValue();
        if (!Objects.equals(newValue, _oldValue))
        {
            _oldValue = newValue;
            // Suppose that a value of 2 means that the data wasn't found.
            // So we display a message to the user.
            if (new Integer(2).equals(newValue) && !_isPreparingForEdit)
            {
                if (_showMessageAsynchronously)
                {
                    SwingUtilities.invokeLater(
                        new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                showMessage(newValue);
                            }
                        }
                    );
                }
                else
                {
                    showMessage(newValue);
                }
            }
        }
    }

    private void showMessage(Object value)
    {
        JOptionPane.showMessageDialog(null, "Not found: " + value + ".",
            "Warning", JOptionPane.WARNING_MESSAGE);
    }
}

Comments and suggestions about this last solution are still appreciated. Is this the ultimate and optimal solution?

Marcos

도움이 되었습니까?

해결책

As I already commented: it's a bit fishy to change the state of the table in the editor, especially if it's related to focus which is brittle even at its best. So I would go to great lengths to avoid it.

The mis-behaviour feels similar to an incorrectly implemented InputVerifier which has side-effects (like grabbing the focus) in its verify vs. in its shouldYieldFocus as would be correct: in such a context the focusManager gets confused, it "forgets" about the natural last-focusOwner-before.

The remedy might be to let the manager do its job first, and show the message only when it's done. In your example code that can be achieved by wrapping into an invokeLater:

if (needsMessage()) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            JOptionPane.showMessageDialog(null, "Not found: " +
                    newValue + ".", "Warning",
                    JOptionPane.WARNING_MESSAGE);

        }
    });
}

다른 팁

Do the editing in the stopCellEditing() method.

In this example you are forced to enter a string of 5 characters:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.table.*;

public class TableEdit extends JFrame
{
    TableEdit()
    {
        JTable table = new JTable(5,5);
        table.setPreferredScrollableViewportSize(table.getPreferredSize());

        JScrollPane scrollpane = new JScrollPane(table);
        add(scrollpane);

        //  Use a custom editor

        TableCellEditor fce = new FiveCharacterEditor();
        table.setDefaultEditor(Object.class, fce);

        add(new JTextField(), BorderLayout.NORTH);
    }

    class FiveCharacterEditor extends DefaultCellEditor
    {
        FiveCharacterEditor()
        {
            super( new JTextField() );
        }

        public boolean stopCellEditing()
        {
            JTable table = (JTable)getComponent().getParent();

            try
            {
                System.out.println(getCellEditorValue().getClass());
                String editingValue = (String)getCellEditorValue();

                if(editingValue.length() != 5)
                {
                    JTextField textField = (JTextField)getComponent();
                    textField.setBorder(new LineBorder(Color.red));
                    textField.selectAll();
                    textField.requestFocusInWindow();

                    JOptionPane.showMessageDialog(
                        null,
                        "Please enter string with 5 letters.",
                        "Alert!",JOptionPane.ERROR_MESSAGE);
                    return false;
                }
            }
            catch(ClassCastException exception)
            {
                return false;
            }

            return super.stopCellEditing();
        }

        public Component getTableCellEditorComponent(
            JTable table, Object value, boolean isSelected, int row, int column)
        {
            Component c = super.getTableCellEditorComponent(
                table, value, isSelected, row, column);
            ((JComponent)c).setBorder(new LineBorder(Color.black));

            return c;
        }

    }

    public static void main(String [] args)
    {
        JFrame frame = new TableEdit();
        frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible(true);
    }
}
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top