Best practise for handling VK_TAB in Java Swing to move between components as well as to move between table cells using the keyboard only

大城市里の小女人 提交于 2021-02-08 09:56:56

问题


I'm trying to find a best practise in terms of accessibility on Tables in relation to other components. In an app that's mainly a set of JTables and JTextFields, I tried to make it accessibility with keyboard as well as mouse. My thoughts are on the best way to help the user to navigate around between components using the VK_TAB key.

My first goal was to stop JTables "swallow" the VK_TAB key when the user tries to navigate to a neighbor JTextField using a solution from Coderanch. I tried to put together a minimal compilable and runnable example below.

package TableTest;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;

public class MyFrame extends JFrame {

    private static final long serialVersionUID = 1L;

    public MyFrame() {
        super();
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                MyFrame frame = new MyFrame();
                frame.init();
                frame.setVisible(true);
            }

        });
    }

    private void init() {
        JPanel contentPane = new JPanel(new BorderLayout());// new GridBagLayout()
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        setContentPane(contentPane);

        JTable table = new JTable(new DefaultTableModel(new Object[][] { { 1, 2, 3 }, //
                { 4, 5, 6 }, //
                { 7, 8, 9 }, //
                { "#", 0, "*" }, }, //
                new String[] { "First", "Second", "Third" }));

        // When TAB is hit, go to next Component instead of next cell
        table.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "tabNext");
        table.getActionMap().put("tabNext", new AbstractAction() {
            private static final long serialVersionUID = 1L;

            public void actionPerformed(ActionEvent ae) {
                KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent();
            }
        });

        // When Shift+TAB is hit, go to previous Component instead of previous cell
        table.getInputMap(JComponent.WHEN_FOCUSED)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK), "tabBefore");
        table.getActionMap().put("tabBefore", new AbstractAction() {
            private static final long serialVersionUID = 1L;

            public void actionPerformed(ActionEvent ae) {
                KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
            }
        });

        JTextField jtf = new JTextField("Text here");

        contentPane.add(jtf, BorderLayout.NORTH);
        contentPane.add(table, BorderLayout.CENTER);

        pack();

    }
}

But that's rather radical and frustrating for a user who wants to navigate to a Table cell, e.g. for editing, using only the keyboard. So, my second goal is to give keyboard access to Table Cells as well.

What is a best practise here? I thought of the focussed JTable reacting to VK_ENTER: after that, it would react to VK_TAB by giving focus to the next cell until ... ESC is pressed or whatever.

Thank you!


回答1:


What is a best practise here?

The default implementation is:

  1. Tab - moves to the next component
  2. Ctrl+Tab - moves to the next component

  3. Shift+Tab - moves to the previous component

  4. Ctrl+Shift+Tab - moves to the previoius component

Some components handle the Tab key. For example:

  1. JTable - tab is used to move to the next cell
  2. Text components (JTextArea, JTextPane) - will insert a Tab character into the text

So for components that handle the Tab key the user would use Ctrl+Tab to navigate to the next component when using the keyboard.

Edit:

I thought of the focussed JTable reacting to VK_ENTER

You already know how to assign a different Action to the Tab key.

So now all you need to do is assign the default Tab Action to the Enter key. You can do this by changing the binding in the InputMap of the Table:

InputMap im = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke addedKeyStroke = KeyStroke.getKeyStroke("ENTER");
im.put(addedKeyStroke, "selectNextColumnCell");

Check out Key Bindings for a simple app that display all the default Actions for each Swing component.




回答2:


Thank you, camickr!

So when I change the code following your advice, it works.

That's the complete example:

package TableTest;

import java.awt.BorderLayout;
import java.awt.EventQueue;
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.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;

public class MyFrame extends JFrame {

    private static final long serialVersionUID = 1L;

    public MyFrame() {
        super();
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                MyFrame frame = new MyFrame();
                frame.init();
                frame.setVisible(true);
            }

        });
    }

    private void init() {
        JPanel contentPane = new JPanel(new BorderLayout());// new GridBagLayout()
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        setContentPane(contentPane);

        JTable table = new JTable(new DefaultTableModel(new Object[][] { { 1, 2, 3 }, //
                { 4, 5, 6 }, //
                { 7, 8, 9 }, //
                { "#", 0, "*" }, }, //
                new String[] { "First", "Second", "Third" }));

        // When TAB is hit, go to next Component instead of next cell
        final KeyStroke tabKey = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
        table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(tabKey, "tabNext");
        final AbstractAction tabNext = new AbstractAction() {
            private static final long serialVersionUID = 1L;

            public void actionPerformed(ActionEvent ae) {
                KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent();
            }
        };
        table.getActionMap().put("tabNext", tabNext);

        // When Shift+TAB is hit, go to previous Component instead of previous cell
        final KeyStroke shiftTabKey = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK);
        table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shiftTabKey, "tabBefore");
        final AbstractAction tabBefore = new AbstractAction() {
            private static final long serialVersionUID = 1L;

            public void actionPerformed(ActionEvent event) {
                KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
            }
        };
        table.getActionMap().put("tabBefore", tabBefore);

        // on VK_ENTER, navigate in JTable only ("edit mode")
        final KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
        final AbstractAction editModeAction = new AbstractAction() {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent event) {
                editMode(table, tabKey, shiftTabKey);
            }
        };
        table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(enterKey, "editModeAction");
        table.getActionMap().put("editModeAction", editModeAction);

        // On VK_ESCAPE or when JTable loses focus, quit the "edit mode"
        final KeyStroke escKey = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
        final AbstractAction quitEditModeAction = new AbstractAction() {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent event) {
                quitEditMode(table, tabKey, shiftTabKey);
            }
        };
        table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(escKey, "quitEditModeAction");
        table.getActionMap().put("quitEditModeAction", quitEditModeAction);
        final FocusListener listener = new FocusListener() {
            @Override
            public void focusGained(FocusEvent event) {
                //do nothing
            }

            @Override
            public void focusLost(FocusEvent event) {
                quitEditMode(table, tabKey, shiftTabKey);
            }
        };
        table.addFocusListener(listener);

        JTextField jtf = new JTextField("Text here");

        contentPane.add(jtf, BorderLayout.NORTH);
        contentPane.add(table, BorderLayout.CENTER);

        pack();

        //printActions(table);
    }

    private void editMode(JTable table, final KeyStroke tabKey, final KeyStroke shiftTabKey) {
        System.out.println("editing activated");
        table.setCellSelectionEnabled(true);
        InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        input.remove(shiftTabKey);
        input.remove(tabKey);
        input.put(shiftTabKey, "selectPreviousColumnCell");
        input.put(tabKey, "selectNextColumnCell");
    }

    private void quitEditMode(JTable table, final KeyStroke tabKey, final KeyStroke shiftTabKey) {
        System.out.println("editing de-activated");
        table.setCellSelectionEnabled(false);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        input.remove(shiftTabKey);
        input.remove(tabKey);
        input.put(shiftTabKey, "tabBefore");
        input.put(tabKey, "tabNext");
    }
}


I got the Key bindings to actions from the JTable's ActionMap and InputMap. I tried adding a small method

    // print a String representation of each KeyStroke from the InputMap
    private void printActions(JTable table) {
        InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        if (input != null && input.allKeys() != null) {
            for (KeyStroke key : input.allKeys()) {
                if (key != null) {
                    printKeyStroke(key);
                    printActionName(input, key);
                }
            }
        }
    }

    // build the String represantation
    private void printKeyStroke(KeyStroke key) {
        StringBuilder tk = new StringBuilder("[");
        int modifiers = key.getModifiers();
        if ((modifiers & InputEvent.SHIFT_DOWN_MASK) != 0)
            tk.append("shift+");
        if ((modifiers & InputEvent.CTRL_DOWN_MASK) != 0)
            tk.append("ctrl+");
        if ((modifiers & InputEvent.META_DOWN_MASK) != 0)
            tk.append("cmd+");
        if ((modifiers & InputEvent.ALT_DOWN_MASK) != 0)
            tk.append("alt+");
        tk.append("'");
        tk.append(KeyEvent.getKeyText(key.getKeyCode()));
        tk.append("'=");
        tk.append("keycode=");
        tk.append(key.getKeyCode());
        tk.append("]");
        System.out.print(tk.toString());
    }

    private void printActionName(InputMap input, KeyStroke key) {
        System.out.print(": ");
        Object string = input.get(key);
        if (string != null && string instanceof String)
            System.out.println(string.toString());
    } 


来源:https://stackoverflow.com/questions/61556057/best-practise-for-handling-vk-tab-in-java-swing-to-move-between-components-as-we

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!