1

I have been trying to use the arrow keys as part of my application. Following the best practice I have stuck with using key bindings. I gound that the arow keys do not produce a key typed event so I employed this answer.

However, my application has a number of components and I found that if I have a JToolbar in my JFrame the method in the previous link no longer works. Why is this and how do I have a JToolbar and use key bindings?

Here is a SSCCE

public class ArrowTest extends JFrame {

    public static void main(final String[] args){
        final ArrowTest at = new ArrowTest();
        at.setSize(100,200);

        final JPanel jp = new JPanel();
        jp.setBackground(Color.BLUE);
        at.getContentPane().add(jp);
        final JToolBar toolbar = new JToolBar();
        toolbar.add(new JButton());
        //at.add(toolbar);
        at.setVisible(true);
    }

    public ArrowTest() {
        super();    
        this.getContentPane().setLayout(new GridBagLayout());    
        this.getContentPane().setBackground(Color.BLACK);    
        this.setKeyBindings();
    }

    public void setKeyBindings() {

        final int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;    
        final ActionMap actionMap = this.getRootPane().getActionMap();
        final InputMap inputMap = this.getRootPane().getInputMap(condition);

        for (final Direction direction : Direction.values()) {
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V,0), direction.getText());
            inputMap.put(direction.getKeyStroke(), direction.getText());
            actionMap.put(direction.getText(), new MyArrowBinding(direction.getText()));
        }

    }

    enum Direction {
        UP("Up", KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0)),
        DOWN("Down", KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0)),
        LEFT("Left", KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)),
        RIGHT("Right", KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0));

        Direction(final String text, final KeyStroke keyStroke) {
            this.text = text;
            this.keyStroke = keyStroke;
        }
        private String text;
        private KeyStroke keyStroke;

        public String getText() {
            return text;
        }

        public KeyStroke getKeyStroke() {
            return keyStroke;
        }

        @Override
        public String toString() {
            return text;
        }
    }

    private class MyArrowBinding extends AbstractAction {

        private static final long serialVersionUID = -6904517741228319299L;

        public MyArrowBinding(final String text) {
            super(text);
            putValue(ACTION_COMMAND_KEY, text);
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final String actionCommand = e.getActionCommand();
            System.out.println("Key Binding: " + actionCommand);
        }
    }    
}
Community
  • 1
  • 1
Codey McCodeface
  • 2,988
  • 6
  • 30
  • 55

2 Answers2

4

By default, JToolBar registers an action for the KeyStroke's UP/DOWN/LEFT/RIGHT, and the JToolBar you add to the JFrame automatically grabs the focus, hence you don't see anything when binding to the rootpane (the toolbar catches the events before you).

One solution is to make the JToolBar and the JButton not focusable and allow your JPanel to be focusable (actually you could leave the JToolBar and the JButton and request the focus on your panel but this also means that you have to handle the focus management of your panel):

import java.awt.Color;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;

public class ArrowTest extends JFrame {

    public static void main(final String[] args) {
        final ArrowTest at = new ArrowTest();
        at.setSize(100, 200);

        final JPanel jp = new JPanel();
        jp.setBackground(Color.BLUE);
        jp.setFocusable(true);
        at.getContentPane().add(jp);
        final JToolBar toolbar = new JToolBar();
        JButton comp = new JButton();
        toolbar.add(comp);
        at.add(toolbar);
        at.setVisible(true);
    }

    public ArrowTest() {
        super();
        this.getContentPane().setLayout(new GridBagLayout());
        this.getContentPane().setBackground(Color.BLACK);
        this.setKeyBindings();
    }

    public void setKeyBindings() {
        for (final Direction direction : Direction.values()) {
            MyArrowBinding binding = new MyArrowBinding(direction.getText());
            getRootPane().registerKeyboardAction(binding, direction.getText(), direction.getKeyStroke(),
                    JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        }
    }

    enum Direction {
        UP("Up", KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0)), DOWN("Down", KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0)), LEFT("Left",
                KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)), RIGHT("Right", KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0));

        Direction(final String text, final KeyStroke keyStroke) {
            this.text = text;
            this.keyStroke = keyStroke;
        }

        private String text;
        private KeyStroke keyStroke;

        public String getText() {
            return text;
        }

        public KeyStroke getKeyStroke() {
            return keyStroke;
        }

        @Override
        public String toString() {
            return text;
        }
    }

    private class MyArrowBinding extends AbstractAction {

        private static final long serialVersionUID = -6904517741228319299L;

        public MyArrowBinding(final String text) {
            super(text);
            putValue(ACTION_COMMAND_KEY, text);
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final String actionCommand = e.getActionCommand();
            System.out.println("Key Binding: " + actionCommand);
        }
    }
}

I also replaced your calls to getActionMap/getInputMap by a single call to javax.swing.JComponent.registerKeyboardAction(ActionListener, String, KeyStroke, int)

Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117
  • Many thanks for you answer. I will be using the `javax.swing.JComponent.registerKeyboardAction(ActionListener, String, KeyStroke, int)`. Looking at your solution it seems like I would need to `setFocusable(false)` on every component I add to the toolbar. I think it would be preferable to replace the action on the toolbar once. – Codey McCodeface Jun 11 '13 at 19:25
  • @medPhys-pl Actually no. You can even let the `JToolbar` being focusable but then you will also need to somehow make your component focusable and handle mouse click to request the focus on it. Your solution is not ideal at all because you kill the default actions of the `JToolBar` and if some other component (not under the JToolBar) grabs the focus, your actions won't work anymore. – Guillaume Polet Jun 11 '13 at 19:28
  • I have included more of the code of my solution. I think this shows that your second point on another component grabbing focus wouldn't be an issue. It was not clear from my previous post. You are correct I do kill the default actions. Is there something wrong with overwriting the default actions though? – Codey McCodeface Jun 11 '13 at 19:47
  • @medPhys-pl I usually try to avoid this, for 2 reasons: 1) If L&F was to be changed at runtime, your fix would be killed. 2) Default key bindings/actions are usually expected to work, so killing them may seem weird for some users. Yet, if the API is there, nothing prevents you from doing so. My opinion is to try to avoid this kind of code as it does not smell good. – Guillaume Polet Jun 11 '13 at 20:27
  • @medPhys-pl "Look and Feel". It defines how components look (a progress bar on Windows is very different from a progress bar on Mac). It also defines how components "feel/work/behave". For example, on Windows, you use right-click to show a popup-menu. On Mac you use CTRL+LEFT-Click. On Windows you will use CTRL+C for copy, while on Mac you will use "Apple"+C. All these differences are handles by L&F. Try once to add this line (as the first line of your `main()`): `try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(); }` – Guillaume Polet Jun 11 '13 at 22:08
1

Following the advice in this answer I have solved the issue using

public void setKeyBindings() {

    final int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;    
    final ActionMap actionMap = this.getRootPane().getActionMap();
    final InputMap inputMap = this.getRootPane().getInputMap(condition);

    for (final Direction direction : Direction.values()) {
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V,0), direction.getText());
        inputMap.put(direction.getKeyStroke(), direction.getText());
        actionMap.put(direction.getText(), new MyArrowBinding(direction.getText()));
    }

    condition = JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;

    actionMap = toolbar.getActionMap();
    inputMap = toolbar.getInputMap(condition);


    for (final Direction direction : Direction.values()) {
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V,0), direction.getText());
        inputMap.put(direction.getKeyStroke(), direction.getText());
        actionMap.put(direction.getText(), new MyArrowBinding(direction.getText()));
    } 


}
Community
  • 1
  • 1
Codey McCodeface
  • 2,988
  • 6
  • 30
  • 55