<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