1

I want to show a JButton within a JTable. This is nothing special, and I found a lot of examples doing this. However, I always have issues with pressing the buttons via Keyboard (not via mouse). I expect that I can select a cell and push (also visually) the button by pressing SPACE (no mnemonics).

Two snippets work like a charm, except supporting keys:


http://tips4java.wordpress.com/2009/07/12/table-button-column/

The author claims that keys work. I believe that they did, but not on all my systems I checked. However, supported mnemonics work perfectly.

(posted here: Adding Jbutton to JTable)


http://www.java2s.com/Code/Java/Swing-Components/ButtonTableExample.htm

(posted here: Adding Jbutton to JTable)

In the example, it works perfectly! However, it doesn't work for my table. Just disable row selection (I have to use cell selection), and pressing the button via key doesn't work anymore:

table.setRowSelectionAllowed(false);


I tried hard figuring out what's going wrong or how to fix it, but I failed. My only achievement is to call the action behind the button, but the button is not pressed (I mean the visual behavior).


Some information added:

I used... (in many combinations)

  • Ubuntu 10.04, Windows 7, Windows 8
  • Java 7u21, JDK 1.6.0_33, OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1)
  • WindowsLookAndFeel, Metal (Cross Platform LAF), Nimbus

0% success!


TableTest.java

import java.awt.event.ActionEvent;
import java.util.LinkedList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.AbstractTableModel;

public class TableTest extends JFrame {

    public TableTest() {

        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTable table = new JTable(new TestModel());
        table.setRowSelectionAllowed(false);
        table.getColumnModel().getColumn(1).setPreferredWidth(3);
        table.getColumnModel().getColumn(2).setPreferredWidth(3);
        this.add(new JScrollPane(table));
        Action increase = new AbstractAction("+") {

            @Override
            public void actionPerformed(ActionEvent e) {
                JTable table = (JTable) e.getSource();
                int row = Integer.valueOf(e.getActionCommand());
                TestModel model = (TestModel) table.getModel();
                model.increment(row, 0);
            }
        };
        ButtonColumn inc = new ButtonColumn(table, increase, 1);
        Action decrease = new AbstractAction("-") {

            @Override
            public void actionPerformed(ActionEvent e) {
                JTable table = (JTable) e.getSource();
                int row = Integer.valueOf(e.getActionCommand());
                TestModel model = (TestModel) table.getModel();
                model.decrement(row, 0);
            }
        };
        ButtonColumn dec = new ButtonColumn(table, decrease, 2);
        pack();
    }

    public static void main(String[] args) {
        new TableTest().setVisible(true);
    }
}

class TestModel extends AbstractTableModel {

    List<TestRecord> records = new LinkedList<TestRecord>();

    private static class TestRecord {

        private int val = 0;
    }

    public void increment(int row, int col) {
        records.get(row).val++;
        fireTableCellUpdated(row, 0);
    }

    public void decrement(int row, int col) {
        records.get(row).val--;
        fireTableCellUpdated(row, 0);
    }

    public TestModel() {
        records.add(new TestRecord());
        records.add(new TestRecord());
    }

    @Override
    public Class<?> getColumnClass(int col) {
        if (col == 0) {
            return Integer.class;
        } else {
            return ButtonColumn.class;
        }
    }

    @Override
    public boolean isCellEditable(int row, int col) {
        return true;
    }

    @Override
    public int getColumnCount() {
        return 3;
    }

    @Override
    public int getRowCount() {
        return records.size();
    }

    @Override
    public Object getValueAt(int row, int col) {
        if (col == 0) {
            return records.get(row).val;
        } else if (col == 1) {
            return "+";
        } else {
            return "-";
        }
    }
}

ButtonColumn.java

import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;

/**
 * The ButtonColumn class provides a renderer and an editor that looks like a
 * JButton. The renderer and editor will then be used for a specified column in
 * the table. The TableModel will contain the String to be displayed on the
 * button.
 * 
 * The button can be invoked by a mouse click or by pressing the space bar when
 * the cell has focus. Optionally a mnemonic can be set to invoke the button.
 * When the button is invoked the provided Action is invoked. The source of the
 * Action will be the table. The action command will contain the model row
 * number of the button that was clicked.
 * 
 */
public class ButtonColumn extends AbstractCellEditor implements
        TableCellRenderer, TableCellEditor, ActionListener, MouseListener {
    private JTable table;
    private Action action;
    private int mnemonic;
    private Border originalBorder;
    private Border focusBorder;

    private JButton renderButton;
    private JButton editButton;
    private Object editorValue;
    private boolean isButtonColumnEditor;

    /**
     * Create the ButtonColumn to be used as a renderer and editor. The renderer
     * and editor will automatically be installed on the TableColumn of the
     * specified column.
     * 
     * @param table
     *            the table containing the button renderer/editor
     * @param action
     *            the Action to be invoked when the button is invoked
     * @param column
     *            the column to which the button renderer/editor is added
     */
    public ButtonColumn(JTable table, Action action, int column) {
        this.table = table;
        this.action = action;

        renderButton = new JButton();
        editButton = new JButton();
        editButton.setFocusPainted(false);
        editButton.addActionListener(this);
        originalBorder = editButton.getBorder();
        setFocusBorder(new LineBorder(Color.BLUE));

        TableColumnModel columnModel = table.getColumnModel();
        columnModel.getColumn(column).setCellRenderer(this);
        columnModel.getColumn(column).setCellEditor(this);
        table.addMouseListener(this);
    }

    /**
     * Get foreground color of the button when the cell has focus
     * 
     * @return the foreground color
     */
    public Border getFocusBorder() {
        return focusBorder;
    }

    /**
     * The foreground color of the button when the cell has focus
     * 
     * @param focusBorder
     *            the foreground color
     */
    public void setFocusBorder(Border focusBorder) {
        this.focusBorder = focusBorder;
        editButton.setBorder(focusBorder);
    }

    public int getMnemonic() {
        return mnemonic;
    }

    /**
     * The mnemonic to activate the button when the cell has focus
     * 
     * @param mnemonic
     *            the mnemonic
     */
    public void setMnemonic(int mnemonic) {
        this.mnemonic = mnemonic;
        renderButton.setMnemonic(mnemonic);
        editButton.setMnemonic(mnemonic);
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value,
            boolean isSelected, int row, int column) {
        if (value == null) {
            editButton.setText("");
            editButton.setIcon(null);
        } else if (value instanceof Icon) {
            editButton.setText("");
            editButton.setIcon((Icon) value);
        } else {
            editButton.setText(value.toString());
            editButton.setIcon(null);
        }

        this.editorValue = value;
        return editButton;
    }

    @Override
    public Object getCellEditorValue() {
        return editorValue;
    }

    //
    // Implement TableCellRenderer interface
    //
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
        if (isSelected) {
            renderButton.setForeground(table.getSelectionForeground());
            renderButton.setBackground(table.getSelectionBackground());
        } else {
            renderButton.setForeground(table.getForeground());
            renderButton.setBackground(UIManager.getColor("Button.background"));
        }

        if (hasFocus) {
            renderButton.setBorder(focusBorder);
        } else {
            renderButton.setBorder(originalBorder);
        }

        // renderButton.setText( (value == null) ? "" : value.toString() );
        if (value == null) {
            renderButton.setText("");
            renderButton.setIcon(null);
        } else if (value instanceof Icon) {
            renderButton.setText("");
            renderButton.setIcon((Icon) value);
        } else {
            renderButton.setText(value.toString());
            renderButton.setIcon(null);
        }

        return renderButton;
    }

    //
    // Implement ActionListener interface
    //
    /*
     * The button has been pressed. Stop editing and invoke the custom Action
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        int row = table.convertRowIndexToModel(table.getEditingRow());
        fireEditingStopped();

        // Invoke the Action

        ActionEvent event = new ActionEvent(table,
                ActionEvent.ACTION_PERFORMED, "" + row);
        action.actionPerformed(event);
    }

    //
    // Implement MouseListener interface
    //
    /*
     * When the mouse is pressed the editor is invoked. If you then then drag
     * the mouse to another cell before releasing it, the editor is still
     * active. Make sure editing is stopped when the mouse is released.
     */
    @Override
    public void mousePressed(MouseEvent e) {
        if (table.isEditing() && table.getCellEditor() == this)
            isButtonColumnEditor = true;
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        if (isButtonColumnEditor && table.isEditing())
            table.getCellEditor().stopCellEditing();

        isButtonColumnEditor = false;
    }

    @Override
    public void mouseClicked(MouseEvent e) {
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }
}
Community
  • 1
  • 1
Stefe Klauou
  • 113
  • 4
  • `but not on all my systems I checked.` - What systems? Maybe other using those systems can check the behaviour. The clicking of a button using the space bar can be LAF dependent. Does clicking a regular JButton not displayed in the table result in the Action being invoked? – camickr Sep 30 '13 at 16:52
  • information added... Of course, clicking (by keyboard) a regular button works. Also clicking (by keyboard) with row selection allowed "true" works in some examples. – Stefe Klauou Sep 30 '13 at 21:58

2 Answers2

2

Both TableTest, which uses ButtonColumn, and TablePopupEditor are complete examples that work correctly when the Space key is pressed in a selected button cell. Neither evinces the typical ButtonModel-defined appearance of a stand-alone button, but you can provide your own visual queues as required. This related example uses a colored border.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Both examples do not work with setRowSelectionAllowed(false) as described in my initial post. – Stefe Klauou Sep 30 '13 at 17:53
  • Both work for me with `setRowSelectionAllowed(false)` on Java 6; please edit your question to include an [sscce](http://sscce.org/) that exhibits the problem. – trashgod Sep 30 '13 at 20:03
  • No, doesn't work. I just used the code you posted including ´setRowSelectionAllowed(false)´. However, I will edit my post including everything. I will also include all systems/versions/LAFs I tried. If it really works on your system please add information about used OS, LAF, exact version, pressed key (steps to reproduce) etc. I really tried many things, but 0% success!! – Stefe Klauou Sep 30 '13 at 21:47
  • Works either way on Mac OS X, Java 6; fails on Ubuntu 10.04, Java 6 with `setRowSelectionAllowed(false)`. – trashgod Sep 30 '13 at 23:32
2

The problem isn't the editor. The SPACE key stroke is not forwarded to the default editor in the first column either.

The problem is that JTable defines an Action for the SPACE key so it is intercepted before it has a chance to be passed to the editor. Search my blog for the Key Bindings entry where you will find a program that lists all the default Key Bindings for JTable. The Action that is invoked is called "addToSelection", so I'm not sure why it works differently depending on the row selection.

Anyway one solution is to remove this Action:

InputMap im = table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke space = KeyStroke.getKeyStroke("SPACE");
im.put(space, "none");
camickr
  • 321,443
  • 19
  • 166
  • 288
  • Aha! Mac OS X has no key binding for `"addToSelection"`. – trashgod Oct 01 '13 at 06:10
  • When I tried fixing, I also wanted to start modifying the key binding for space. However, I totally failed, because I tried calling the standard behavior (without knowing what this is) *and* sending it to the editor. Do you know a working possibility? I have a bad feelding, if I just remove some predefined binding. Otherwise, you are totally right and pressing the button works with your addition. – Stefe Klauou Oct 01 '13 at 06:30
  • @StefeKlauou `I have a bad feelding, if I just remove some predefined binding.` - your table doesn't use selection anyway. So removing an Action related to selection shouldn't cause a problem. – camickr Oct 01 '13 at 16:16
  • What about other columns (no buttons) and their needs? I'd prefer a solution which doesn't remove basic 'functionality'. If only for the button column, ok, but the current code removes the binding for the whole table. – Stefe Klauou Oct 03 '13 at 10:14
  • @StefeKlauou, you have turned of row selection for the entire row so this is not an issue for other columns anyway, since you would assume that "addToSelection" only does something when selection is enabled. Also, as trashgod has already pointed out this feature doesn't even exist in Mac. Anyway you've been given the answer to your original question. – camickr Oct 03 '13 at 14:56