Tab and back-tab out of a JTable
Hi There,
I have been working for some time on improving the default behaviour of the tab key within editable JTables. It largely works as required now, but I'm stuck on getting back-tab to leave the table if on row 0, col 0. I register tab and back-tab action in the table's ActionMap that looks like this:
class TabActionextends AbstractAction{
publicvoid actionPerformed(ActionEvent e){
if (getRowCount() > 0){
int row = getSelectedRow();
int column = getSelectedColumn();
if (row < 0 || column < 0){
row = 0;
column = 0;
}
do{
if (++column >= getColumnCount()){
row++;
column = 0;
}
}while (row < getRowCount() && ! isCellTabbable(row, column));
if (row < getRowCount()){
changeSelection(row, column, false,false);
}else{
clearSelection();
transferFocus();
}
}else{
transferFocus();
}
}
}
class BackTabActionextends AbstractAction{
publicvoid actionPerformed(ActionEvent e){
if (getRowCount() > 0){
int row = getSelectedRow();
int column = getSelectedColumn();
if (row < 0 || column < 0){
row = getRowCount() - 1;
column = getColumnCount() - 1;
}
do{
if (--column < 0){
row--;
column = getColumnCount() - 1;
}
}while (row >= 0 && ! isCellTabbable(row, column));
if (row >= 0){
changeSelection(row, column, false,false);
}else{
clearSelection();
transferFocusBackward();
KeyboardFocusManager fm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
// fm.upFocusCycle(BTTTable_Editable.this);
// fm.focusPreviousComponent(BTTTable_Editable.this);
}
}else{
// transferFocusBackward();
// KeyboardFocusManager fm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
// fm.upFocusCycle(BTTTable_Editable.this);
// fm.focusPreviousComponent(BTTTable_Editable.this);
}
}
}
transferFocus() to go to the next screen component works fine - as long as I callsetFocusTraversalPolicyProvider(true);
when I create the JTable.
But the backward traversal just doesn't work - instead it does nothing the first time you press back-tab on row 0 col 0, then if you press it again, focus goes onto the last row/col of the table.
As an alternative, I thought maybe I could call the Ctrl-back-tab action, but I can't find that registered in the action map - can anyone tell me how ctrl-back-tab is called and whether I can call that code directly?
Any ideas?
Thanks,
Tim
[4895 byte] By [
TimRyanNZa] at [2007-10-2 5:03:14]

Where's the rest of your code? What good does posting the Actions do if we can't execute your code and see how you've installed the Actions on the table? Who knows where the mistake is. It may be in the posted code or it may be in some other code.
Here's some code I use to override the forward tab Action:
http://forum.java.sun.com/thread.jspa?forumID=57&threadID=657819
And here is the code I would use to go to the previous component:
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
[nobr]> Where's the rest of your code? What good does posting
> the Actions do if we can't execute your code and see
> how you've installed the Actions on the table? Who
> knows where the mistake is. It may be in the posted
> code or it may be in some other code.
Well I assume the Action is registered okay because back-tab within the table works fine, and in fact using a debugger I can see that the focus request code (transferFocusBackward) is being called at the right time. I followed it into that code and it appears that it is picking up the tableCellEditor as the "previous" component which is "why" it isn't working right - not that that helps me to fix it.
Anyway, just to confirm, here is a complete standalone testcase:import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
/**
*
* <br>
* <br>
* Created on 14/11/2005 by Tim Ryan
*/
public class TabTable extends JTable implements FocusListener {
protected KeyStroke TAB = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
protected KeyStroke BACK_TAB = KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
InputEvent.SHIFT_DOWN_MASK);
protected KeyStroke LEFT_ARROW = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0);
protected KeyStroke RIGHT_ARROW = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
public TabTable(Object[][] rowData, Object[] columnNames) {
super(rowData, columnNames);
initialise();
}
public TabTable() {
super();
initialise();
}
private void initialise() {
addFocusListener(this);
InputMap inMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap actionMap = getActionMap();
inMap.put(LEFT_ARROW, "None");
inMap.put(RIGHT_ARROW, "None");
actionMap.put(inMap.get(TAB), new TabAction());
actionMap.put(inMap.get(BACK_TAB), new BackTabAction());
setCellSelectionEnabled(true);
putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
setFocusTraversalPolicyProvider(true);
}
public void focusGained(FocusEvent e) {
if (getRowCount() > 0 && getSelectedRow() < 0) {
editCellAt(0, 0);
getEditorComponent().requestFocusInWindow();
}
}
public void focusLost(FocusEvent e) {
}
public void changeSelection(int row, int column, boolean toggle, boolean extend) {
super.changeSelection(row, column, toggle, false);
if (editCellAt(row, column)) {
getEditorComponent().requestFocusInWindow();
}
}
/**
* This class handles the back-tab operation on the table.
* It repeatedly steps the selection backwards until the focus
* ends up on a cell that is allowable (see <code>isCellTabbable()</code>).
* If already at the end of the table then focus is transferred out
* to the previous component on the screen.
*/
class BackTabAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
if (getRowCount() > 0) {
int row = getSelectedRow();
int column = getSelectedColumn();
if (row < 0 || column < 0) {
row = getRowCount() - 1;
column = getColumnCount() - 1;
}
do {
if (--column < 0) {
row--;
column = getColumnCount() - 1;
}
} while (row >= 0 && ! isCellTabbable(row, column));
if (row >= 0) {
changeSelection(row, column, false, false);
} else {
clearSelection();
// transferFocusBackward();
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
}
} else {
// transferFocusBackward();
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
}
}
}
/**
* This class handles the tab operation on the table.
* It repeatedly steps the selection forwards until the focus ends
* up on a cell that is allowable (see <code>isCellTabbable()</code>).
* If already at the end of the table then focus is transferred out
* to the next component on the screen.
*/
class TabAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
if (getRowCount() > 0) {
int row = getSelectedRow();
int column = getSelectedColumn();
if (row < 0 || column < 0) {
row = 0;
column = 0;
}
do {
if (++column >= getColumnCount()) {
row++;
column = 0;
}
} while (row < getRowCount() && ! isCellTabbable(row, column));
if (row < getRowCount()) {
changeSelection(row, column, false, false);
} else {
clearSelection();
transferFocus();
}
} else {
transferFocus();
}
}
}
/**
* Some cells can be tabbed to, but are not actually editable.
* @param row
* @param column
* @return
*/
private boolean isCellTabbable(int row, int column) {
return (column % 2 == 0);
}
public boolean isCellEditable(int row, int column) {
return (column == 1 || column == 3);
}
/**
* @param args
*/
public static void main(String[] args) {
JFrame frame = new JFrame("Tables test");
frame.setName("TablesTest");
frame.setSize(600, 400);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panelMain = new JPanel(new GridBagLayout());
frame.add(panelMain);
Object[][] tableData = new Object[2][6];
Object[] columnHeadings = new Object[] {"H0", "H1", "H2", "H3", "H4", "H5"};
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.NORTH;
gbc.insets = new Insets(10, 10, 10, 10);
JTextField field1 = new JTextField("left", 8);
field1.setName("left");
panelMain.add(field1, gbc);
Dimension tableSize = new Dimension(300, 300);
BTTTable table3 = new BTTTable_Editable(tableData, columnHeadings);
table3.setName("Editable");
JScrollPane scroll3 = new JScrollPane(table3);
scroll3.setPreferredSize(tableSize);
panelMain.add(scroll3, gbc);
JTextField field2 = new JTextField("right", 8);
field2.setName("right");
gbc.gridwidth = GridBagConstraints.REMAINDER;
panelMain.add(field2, gbc);
frame.setVisible(true);
}
}
I thought it might be the focusGained() code, but commenting that out makes no difference.
> And here is the code I would use to go to the
> previous component:
>
> KeyboardFocusManager.getCurrentKeyboardFocusManager().
> focusPreviousComponent();
I tried that too, as you can see from my original post. It gives the same answer.
So the big question is why does the FocusManager think that the "previous" field to the table is an editor component within the table?
Regards,
Tim[/nobr]
If you can't solve the problem using a debugger, why do you think we automatically can solve the problem just by looking at some random code?
Solving problems is always much easier when we have the complete picture which is why I've asked you in the past for demo code. It should always be provided because by definition you have a problem and you don't know where the problem is.
For example, I doubt anybody reading this posting would have guessed that you have customized the table by overriding the changeSelection(...) method to always place the cell in editing mode. This in fact turns out to be the problem. I solved it by adding the following before attempting to change the focus:
if (isEditing()) getCellEditor().cancelCellEditing();
The only good think about this posting is that I learned about the transferFocus() and transferFocusBackward() methods, which, by the way work better than what I suggested.
Thanks for the solution, and the spanking. I'll try to do better next time. It was hardly "random" code but I take your point.
I (foolishly) assumed that putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
would release editing in this sort of situation. I guess that is incorrect.
Regards,
Tim
P.S. Why then does transferFocus() from the tab key work, but not transferFocusBackward() from the backtab key. That is what had me confused. I wonder if the transferFocusBackward() handler converts a negative row to zero - that would explain it I guess.
> Thanks for the solutionActually it doesn't fix it properly, but I can see that the problem is definitely in that area, so thanks for that.
> putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
This would only apply if you click on another component. The TabAction is invoked while the table still has focus, so the editor would still be active.
> // setFocusTraversalPolicyProvider(true);
I commented out that line since I don't use JDK1.5
> Actually it doesn't fix it properly
Oops, I had also comment out your focusGained() code. When I uncommented it then I also had problems. I then removed the clearSelection() method and it seemed to work, but tabbing doesn't cycle through the table, it will just tab out directly.
You may also want to try wrapping the "transferFocus" method in a SwingUtilities.invokeLater(...).
> Oops, I had also comment out your focusGained() code.
> When I uncommented it then I also had problems. I
> then removed the clearSelection() method and it
> seemed to work, but tabbing doesn't cycle through the
> table, it will just tab out directly.
Yeah unfortunately there are still several funnies - e.g. I want the focus to go to the last cell if you back-tab into the table, and that requires me to determine the tab direction in focusGained. I also tried removing the clearSelection, but then the field remains selected (which they don't want) and when I tab in again it doesn't go directly into edit mode. I just seem to go round in circles with this stuff, it's like a hydra, you fix one bug and two more appear.
>
> You may also want to try wrapping the "transferFocus"
> method in a SwingUtilities.invokeLater(...).
I don't believe I need to because all events that run this code will be running in the AWT thread won't they (tab, back-tab, mouse click, etc)?
[nobr]Well I've got a lot closer to a working version now. I can tab through the table in either direction. The only real problem now is how to determine whether the user tabbed or backtabbed into the table so that I start the traversal at the correct end of the table. Does anyone know how to determine if the "opposite" component in the focusGained event is just before or just after the table? Or is there a way to determine which key out of tab and backtab was pressed? Or some other sneaky solution?
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
/**
*
* <br>
* <br>
* Created on 14/11/2005 by Tim Ryan
*/
public class TabTable extends JTable implements FocusListener {
protected KeyStroke TAB = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
protected KeyStroke BACK_TAB = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK);
protected KeyStroke LEFT_ARROW = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0);
protected KeyStroke RIGHT_ARROW = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
public TabTable(Object[][] rowData, Object[] columnNames) {
super(rowData, columnNames);
initialise();
}
public TabTable() {
super();
initialise();
}
private void initialise() {
addFocusListener(this);
InputMap inMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap actionMap = getActionMap();
inMap.put(LEFT_ARROW, "None");
inMap.put(RIGHT_ARROW, "None");
actionMap.put(inMap.get(TAB), new TabAction());
actionMap.put(inMap.get(BACK_TAB), new BackTabAction());
setCellSelectionEnabled(true);
putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
setFocusTraversalPolicyProvider(true);
}
public void focusGained(FocusEvent e) {
// If I am receiving focus from outside of the table (and not one of the editors)
// Put the focus onto the appropriate cell and go into edit mode.
Component c = e.getOppositeComponent();
if (getRowCount() > 0 && getSelectedRow() < 0 && c != null && c.getParent() != null) {
getNextCell();
}
}
public void focusLost(FocusEvent e) {
}
public void changeSelection(int row, int column, boolean toggle, boolean extend) {
super.changeSelection(row, column, toggle, false);
if (editCellAt(row, column)) {
getEditorComponent().requestFocusInWindow();
}
}
/**
* This class handles the back-tab operation on the table.
* It repeatedly steps the selection backwards until the focus
* ends up on a cell that is allowable (see <code>isCellTabbable()</code>).
* If already at the end of the table then focus is transferred out
* to the previous component on the screen.
*/
class BackTabAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
getPreviousCell();
}
}
private void getPreviousCell() {
if (getRowCount() > 0) {
int row = getSelectedRow();
int column = getSelectedColumn();
if (row < 0 || column < 0) {
row = getRowCount() - 1;
column = getColumnCount() - 1;
}
do {
if (--column < 0) {
row--;
column = getColumnCount() - 1;
}
} while (row >= 0 && ! isCellTabbable(row, column));
if (row >= 0) {
changeSelection(row, column, false, false);
} else {
if (isEditing()) {
getCellEditor().stopCellEditing();
}
clearSelection();
transferFocusBackward();
}
} else {
if (isEditing()) {
getCellEditor().stopCellEditing();
}
clearSelection();
transferFocusBackward();
}
}
/**
* This class handles the tab operation on the table.
* It repeatedly steps the selection forwards until the focus ends
* up on a cell that is allowable (see <code>isCellTabbable()</code>).
* If already at the end of the table then focus is transferred out
* to the next component on the screen.
*/
class TabAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
getNextCell();
}
}
private void getNextCell() {
if (getRowCount() > 0) {
int row = getSelectedRow();
int column = getSelectedColumn();
if (row < 0 || column < 0) {
row = 0;
column = - 1;
}
do {
if (++column >= getColumnCount()) {
row++;
column = 0;
}
} while (row < getRowCount() && ! isCellTabbable(row, column));
if (row < getRowCount()) {
changeSelection(row, column, false, false);
} else {
if (isEditing()) {
getCellEditor().stopCellEditing();
}
clearSelection();
transferFocus();
}
} else {
if (isEditing()) {
getCellEditor().stopCellEditing();
}
clearSelection();
transferFocus();
}
}
/**
* Some cells can be tabbed to, but are not actually editable.
*
* @param row
* @param column
* @return
*/
private boolean isCellTabbable(int row, int column) {
return (column % 2 == 0);
}
public boolean isCellEditable(int row, int column) {
return (column == 0 || column == 2);
}
/**
* @param args
*/
public static void main(String[] args) {
JFrame frame = new JFrame("Tables test");
frame.setName("TablesTest");
frame.setSize(600, 400);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panelMain = new JPanel(new GridBagLayout());
frame.add(panelMain);
Object[][] tableData = new Object[2][6];
Object[] columnHeadings = new Object[] {"H0", "H1", "H2", "H3", "H4", "H5"};
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.NORTH;
gbc.insets = new Insets(10, 10, 10, 10);
JTextField field1 = new JTextField("left", 8);
field1.setName("left");
panelMain.add(field1, gbc);
Dimension tableSize = new Dimension(300, 300);
TabTable table3 = new TabTable(tableData, columnHeadings);
table3.setName("Editable");
JScrollPane scroll3 = new JScrollPane(table3);
scroll3.setPreferredSize(tableSize);
panelMain.add(scroll3, gbc);
JTextField field2 = new JTextField("right", 8);
field2.setName("right");
gbc.gridwidth = GridBagConstraints.REMAINDER;
panelMain.add(field2, gbc);
frame.setVisible(true);
}
}
Regards,
Tim[/nobr]
Here's a slight modification and addition to your code that takes care of it.
public void focusGained(FocusEvent e) {
// If I am receiving focus from outside of the table (and not one of the editors)
// Put the focus onto the appropriate cell and go into edit mode.
Component c = e.getOppositeComponent();
if (getRowCount() > 0 && getSelectedRow() < 0 && c != null && c.getParent() != null) {
if ( c == getTransferFocusBackwardComponent() )
getNextCell();
else
getPreviousCell();
}
}
/**
* This is simply {@link Component#transferFocusBackward()} modified to
* return the component that would get the focus rather than actually
* transferring the focus.
*
* @return The component that would get the focus if transferred backwards
*/
public Component getTransferFocusBackwardComponent() {
Component toFocus = null;
Container rootAncestor = getFocusCycleRootAncestor();
Component comp = this;
while (rootAncestor != null &&
!(rootAncestor.isShowing() &&
rootAncestor.isFocusable() &&
rootAncestor.isEnabled()))
{
comp = rootAncestor;
rootAncestor = comp.getFocusCycleRootAncestor();
}
if (rootAncestor != null) {
FocusTraversalPolicy policy =
rootAncestor.getFocusTraversalPolicy();
toFocus = policy.getComponentBefore(rootAncestor, comp);
if (toFocus == null) {
toFocus = policy.getDefaultComponent(rootAncestor);
}
}
return toFocus;
}
: jay
> Does anyone know how to determine if the "opposite" component in > the focusGained event is just before or just after the table?FocusTraversalPolicy.getComponentAfter(....);FocusTraversalPolicy.getComponentBefore(....);
Perfect! Thanks guys. Hey I'm running out of requirements to implement for tables. Let's just hope I can get all this stuff to work in the real code!Regards,Tim