<Swing Dev> Incorrect / unexpected handling of keybindings using non-standard keyboard layouts
Semyon Sadetsky
semyon.sadetsky at oracle.com
Thu Dec 24 11:54:31 UTC 2015
Hi Matthijs,
Thank you for your e-mail and the analysis. We really appreciate your
contribution.
I ran your test on Windows and Linux. In both cases with Belgian(Dutch)
keyboard layout I got single key combinations only, just keys were
swapped Q->A, W->Z, Z->W. (I don't have the Belgium physical keyboard,
I only switched the OS layout.) It looks like that this is really Mac
specific problem.
--Semyon
On 12/10/2015 12:24 PM, Matthijs Kooijman wrote:
> (Please keep my CC'd, I'm not subscribed to the list)
>
> Hi folks,
>
> while going over the Java keybinding code (trying to solve a different
> problem), I found some strangeness in how KeyboardManager and JComponent
> handle keystrokes. In particular, they try to match any registered
> keybindings to both the normal keycode, as well as the extended keycode
> from the KeyEvent.
>
> According to the documentation, the normal keycode relates to the
> physical position of the key on the keyboard and is not influenced by
> the keyboard layout. The extended keycode relates to the key pressed,
> according to the current keyboard layout. When a US QWERTY layout is
> used, both keycodes will be identical.
>
> Matching the extended keycode makes sense, if a keybinding is CTRL-Q,
> then you want it to work whenever you press the key labeled "Q" on the
> keyboard. Matching the normal keycode makes less sense to me - why would
> the "key-that-is-Q-on-a-QWERTY-keyboard" trigger a Ctrl-Q binding? I
> presume this is a sort of backward compatibility feature, but I wonder
> if the application should be in control of this? In any case, in the
> current implementation, this approach causes a problem, which I will
> describe below.
>
> Handling of keybindings happens by letting each JComponent track a
> number of "input maps", mapping keystrokes to actions. There is a map
> for when the component is focused, when it is the ancestor of a focused
> component, and when the component is in a focused window. The first two
> mappings are resolved by traversing the component hierarchy and letting
> each component check its maps, the latter map is resolved by
> KeyboardManager, which keeps one big map of bindings for each window.
>
> In summary, resolving keypress works like this:
>
> 1. SwingUtilities.processKeyBindings() / JComponent.processKeyBindings():
> Starting with the focused component, going upwards, each component
> checks its keybindings for the current keyEvent. Each component first
> checks against the extended keycode, then the normal keycode.
>
> 2. KeyboardManager.fireKeyboardAction(), via JComponent.processKeyBindingsForAllComponents():
> In the map of WHEN_IN_FOCUSED_WINDOW bindings kept by
> KeyboardManager for the current window, the key event is looked up
> and, if found, forwarded to the right JComponent. Again, this checks
> the extended keycode first, then the normal keycode.
>
> 3. KeyboardManager.fireKeyboardAction() / JMenuBar.processKeyBinding():
> For each JMenuBar in the current window (tracked in a separate map
> by KeyboardManager), JMenuBar.processKeyBinding() is called, which
> recurses through the menu hierarchy to check the
> WHEN_IN_FOCUSED_WINDOW keybindings of each menu item. I assume this
> is needed because children of JMenus are not added to the hierarchy
> directly, but through a (detached) JPopupMenu instance.
>
> Note that in step 1, it actually seems like the component hierarchy is
> traversed in both SwingUtilities.processKeyBindings() as well as in
> JComponent.processKeyBindings(), which seems like it is doing double
> work (and applies the WHEN_FOCUSED maps even to ancestors of the focused
> component). However, this doesn't seem relevant to the subject of this
> mail, so I'll not investigate this further here.
>
> In my examples below, I will be using the Belgian AZERTY layout. For
> these examples, the only relevant changes are that the Q is swapped with
> the A, and the W is swapped with the Z.
>
> This resolution process has, AFAICS, two problems:
>
> 1. A given shortcut can be triggered by two keys. For example, when
> using the Belgian AZERTY layout, a Ctrl-Q shortcut will be triggered
> by both CTRL-Q (matching the extended keycode) as well as by CTRL-A
> (the key in the same place as the Q on a QWERTY keyboard, by
> matching the normal keycode).
>
> I guess this is really intended as a feature, but it might be
> confusing. One unintended side effect seems to be that, on OSX, some
> keyboard shortcuts (such as Ctrl-, to open preferences) are handled
> by the windowing system and passed to the application using a
> seperate notification (e.g. "Open your preferences"), in *addition*
> to passing the original keystroke to the application. This was
> reported in https://bugs.openjdk.java.net/browse/JDK-8019498 where
> a keybinding for Ctrl-M was triggered by pressing Ctrl-, in
> *addition* to opening the preferences. Note that the preferences
> event handling uses some OSX specific code, e.g.
> http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/c021b855f51e/src/java.desktop/macosx/classes/com/apple/eawt/_AppEventHandler.java#l192
>
> 2. The second problem is that a component that is checked earlier in
> the resolution order can "hijack" keystrokes from later components.
> Normally, this is intentional - WHEN_FOCUSED and
> WHEN_ANCESTOR_OF_FOCUSED_COMPONENTS favor the focused component, or
> components close to them, and for WHEN_IN_FOCUSED_WINDOW bindings,
> no duplicate bindings should exist.
>
> However, because each step in the resolution matches both the
> extended and normal keycode, a component can hijack the even using
> the "backward compatible" normal keycode matching, even when a later
> component would have succesfully matched against the extended
> keycode.
>
> For example, when using a Belgian AZERTY layout, a button in the
> window binds Ctrl-Z, and a menu item binds Ctrl-W, you would expect
> both keypresses to trigger the corresponding action. However, in
> practice the button will match the Ctrl-W keypress too (by matching
> the normal keycode), preventing the menu bar (which comes later in
> the resolution procedure) from matching the keypress, so both
> Ctrl-W and Ctrl-Z trigger the button.
>
> The latter problem is of course the bigger one. The obvious solution is
> to run through the entire resolution twice, once matching the extended
> keycode, then once more matching the normal keycode. To fix the first
> problem too, the last step could perhaps be made optional, or perhaps
> KeyStroke can be modified to match the normal keycode, extended keycode,
> or both. Not sure if this can be done in a compatible way, though, since
> some of the methods involved are public or protected.
>
> To confirm my analisis, I wrote a small testcase (inline below, and at
> https://gist.github.com/matthijskooijman/4d016e7a9e3fb07d0699). It shows
> both of the problems outlined above. It contains a menu item bound to
> Ctrl-Q, which triggers on both Ctrl-Q and Ctrl-A when using the Belgian
> AZERTY layout. It also contains a button binding to Ctrl-Z, which
> prevents a menu item bound to Ctrl-W from working when using the Belgian
> AZERTY layout. I have tested this on Linux, by selecting a different
> keyboard layout ("Input source") in the Gnome keyboard settings (without
> actually switching the keyboard).
>
> There seem to be a few related bug reports already. I suspect that most
> or all of these are caused by this problem, though not all of them have
> enough details to be sure.
>
> https://bugs.openjdk.java.net/browse/JDK-8087915
> https://bugs.openjdk.java.net/browse/JDK-8022079
> https://bugs.openjdk.java.net/browse/JDK-8019498
> https://bugs.openjdk.java.net/browse/JDK-8141096
>
> Interestingly, all of these reports are about OSX, but I'm pretty sure
> the problem I'm describing will occur on all platforms, being caused by
> platform-independent code. There might be additional OSX-specific
> problems, of course, but I do not have access to OSX to test.
>
> Gr.
>
> Matthijs
>
>
>
> import javax.swing.*;
> import java.awt.event.*;
> import java.awt.*;
>
> class Item extends JMenuItem {
> public void addNotify() {
> System.out.println("Add\n");
> }
> public Item(String s) { super(s); }
> }
>
> public class Test {
> public static void main(String[] args) {
> SwingUtilities.invokeLater(() -> {
> JFrame frame = new JFrame();
> JMenuBar bar = new JMenuBar();
> JButton button = new JButton("Button Ctrl-Z");
> JTextArea text = new JTextArea("");
> JMenu menu = new JMenu("Menu");
> JMenuItem item1 = new Item( "Item 1" );
> JMenuItem item2 = new JMenuItem( "Item 2" );
>
> item1.addActionListener((ActionEvent e) -> text.append("Menuitem 1\n"));
> item2.addActionListener((ActionEvent e) -> text.append("Menuitem 2\n"));
> button.addActionListener((ActionEvent e) -> text.append("Button click\n"));
>
> item1.setAccelerator(KeyStroke.getKeyStroke('Q', KeyEvent.CTRL_MASK));
> item2.setAccelerator(KeyStroke.getKeyStroke('W', KeyEvent.CTRL_MASK));
> KeyStroke keyStroke = KeyStroke.getKeyStroke('Z', KeyEvent.CTRL_MASK);
> ActionListener action = (ActionEvent) -> text.append("Button key\n");
> button.registerKeyboardAction(action, "action", keyStroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
>
> menu.add(item1);
> menu.add(item2);
> bar.add(menu);
>
> text.setEnabled(false);
> frame.setLayout(new BorderLayout());
> frame.add(button, BorderLayout.SOUTH);
> frame.add(text, BorderLayout.CENTER);
> frame.setJMenuBar(bar);
> frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
> frame.setSize(200,200);
> frame.setVisible(true);
> });
> }
> }
More information about the swing-dev
mailing list