Home > Blockchain >  Why is my program not listening to keyEvents?
Why is my program not listening to keyEvents?

Time:12-06

I am trying to develop a main menu for a game in java, but my JMenuItems wouldn't listen to the KeyEvents and I don't understand why. Note that I don't want to set any JMenuBars nor JMenus as this program is intended to be a game used with screen readers, so I don't want accessibility roles to be read. Furthermore, adding a menu complicates the access to the elements with the keyboard and I want the focus to start in the first option.

This is my code:

import java.util.Set;
import java.util.HashSet;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.MenuKeyEvent;
import javax.swing.event.MenuKeyListener;

public class Screen {

    public Screen() {
        // Accept arrow keys as focus traversal keys
        Set<AWTKeyStroke> set = new HashSet<AWTKeyStroke>(KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTrave rsalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
        set.add(KeyStroke.getKeyStroke("DOWN"));
        KeyboardFocusManager.getCurrentKeyboardFocusManager().setDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,set);
        set = new HashSet<AWTKeyStroke>(KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS));
        set.add(KeyStroke.getKeyStroke("UP"));
        KeyboardFocusManager.getCurrentKeyboardFocusManager().setDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,set);
        // definition of Menu Items
        JMenuItem mi=new JMenuItem("play");
        JMenuItem mi2=new JMenuItem("exit");
        mi.setFocusable(true);
        mi2.setFocusable(true);
        // Attempt with MenuKeyListener
        mi.addMenuKeyListener(new MenuKeyListener() {
            public void menuKeyReleased(MenuKeyEvent e) {
                System.out.println("Play released");
            }
            public void menuKeyTyped(MenuKeyEvent e) {}
            public void menuKeyPressed(MenuKeyEvent e) {}
        });
        // Attempt with ActionListener
        mi2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
        mi.setVisible(true);
        mi2.setVisible(true);
        JPanel mp = new JPanel();
        JFrame mf = new JFrame("Game");
        mf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mp.add(mi);
        mp.add(mi2);
        mf.add(mp);
        mf.setVisible(true);
    }
    public static void main(String args[]) {
        new Screen();
    }
}

I've tried with both ActionListener and MenuKeyListener, with and without the JPanel, changing visibilities... I also tried to use KeyEventDispatcher but I didn't know how to send a KeyEvent to the component that returns KeyboardFocusManager.getFocusOwner().

Please help.

CodePudding user response:

Don't use JMenuItem this way, that's not how it's intended to be used

Instead, I'd start with JButton. The following makes use of your focus transversal code, the Action API and the key bindings API

import java.awt.AWTKeyStroke;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridBagLayout());

            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.fill = GridBagConstraints.HORIZONTAL;

            Set<AWTKeyStroke> set = new HashSet<AWTKeyStroke>(KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
            set.add(KeyStroke.getKeyStroke("DOWN"));
            KeyboardFocusManager.getCurrentKeyboardFocusManager().setDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, set);

            set = new HashSet<AWTKeyStroke>(KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS));
            set.add(KeyStroke.getKeyStroke("UP"));
            KeyboardFocusManager.getCurrentKeyboardFocusManager().setDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, set);

            Action playAction = new AbstractAction("Play") {                
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.out.println("play");
                }
            };
            playAction.putValue(Action.MNEMONIC_KEY, (int)'P');
            Action exitAction = new AbstractAction("Exit") {
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.out.println("exit");
                }
            };
            exitAction.putValue(Action.MNEMONIC_KEY, (int)'x');

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap am = getActionMap();
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0), "play");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0), "exit");

            am.put("play", playAction);
            am.put("exit", exitAction);

            JButton playButton = new JButton(playAction);
            JButton exitButton = new JButton(exitAction);

            add(playButton, gbc);
            add(exitButton, gbc);
        }

    }
}

Now, you could do something similar with JLabel, but, JLabel isn't focusable by default, nor does it render any kind of focus indication. Instead, I might be tempted to just strip down a JButton so it didn't paint its border or content/background instead

CodePudding user response:

There's a lot to help with in this case. For starters,

public static void main(String args[]) {
    new Screen();
}

is wrong. It is not appropriate to perform any operation that alters the Swing layout or presentation on the main thread. Instead do

public static void main(String args[]) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            new Screen();
        }
    });
}

if you decide that you want to put swing calls into an object's constructor in this way.

Ideally your Screen would be a JFrame but one that is customized to meet your needs. That means you might want to create a new class GamePanel that extends JPanel

 public class Screen extends JFrame {
     public Screen() {
         super("Game"); // to remind us that the JFrame is created
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         add(new GamePanel());
         pack();
         setVisible(true);
     }
 }

 public class GamePanel extends JPanel {
     public GamePanel() {
         super();
     }
 }

Now, if you want that panel to listen to keys, you need to add a KeyListener interface. This only gives it the capability to listen, it doesn't make it listen.

 public class GamePanel extends JPanel implements KeyListener {

     ...

     public void keyTyped(KeyEvent e) {
         System.out.println("KEY TYPED: "   e);
     }

     public void keyPressed(KeyEvent e) {
         System.out.println("KEY PRESSED: "   e);
     }

     public void keyReleased(KeyEvent e) {
         System.out.println("KEY RELEASED: "   e);
     }

}

Now you can add your key listener to the Screen or to the GamePanel (it will listen to itself). Whenever the focus is in one of these items, and a key is pressed, the even will be routed to the listener.

 public class GamePanel extends JPanel implements KeyListener {

     public GamePanel() {
         super();
         addKeyListener(this);
     }

     ...
 }

Ideally, you might want to not combine your key listener with the panel itself, but make it a "Command Processor" of some sort. In that case, remove the KeyListener code from the JPanel and make a completely new CommmandHandler object that implements KeyListener.

As far as Actions go, they are convenience items that pre-configure menu entries with lots of things (icons, acceleration keys, text, callback functions). If you are avoiding menus for whatever reason, you will find that much of their utility in setting up menus is misdirected for your purpose. Effectively, they are configuration entries that configure the MenuItem objects to handle a key (through the KeyListener interface and then dispatch a swing Event object. This provide "better than key" tracking of items through an application as it converts a keyboard letter k to a class ActionEvent which is passed to the registered "action handler" typically a subclass of AbstractAction.

An example of an Action would be

public class MoveLeft extends AbstractAction {  // which extends ActionListener
    private final GameState gameState;

    public MoveLeft(GameState gameState) {
        super("move left", new ImageIcon(MoveLeft.class.getResource("/images/moveleft.png"));
        putValue("SHORT_DESCRIPTION", "left");
        putValue("MNEMONIC_KEY", "l");
        this.gameState = gameState;
    }

    public void actionPerformed(ActionEvent e) {
        gamestate.getSelected().moveLeft();
    }
}

Assuming you wanted this convenience, you would initialize your CommandHandler with Actions, maybe like so:

public CommandHandler implements KeyListener {

    private int actionId;

    ...

    public void addAction(Action action) {
        handlers.put(action.getValue("MNEMONIC_KEY")., action);
    }

    public void keyTyped(KeyEvent e) {
        Action action = handlers.get(String.valueOf(e.getKeyChar());
        ActionEvent event = new ActionEvent(this, id, action.getValue("NAME"));
        action.actionPerformed( event );
    } 
}

As you can see, the added convenience of having Actions defined for the actions within your game is a balanced by making your components that use them be configured by them (and using them in the KeyListener implementations).

For large projects, the simplicity of having all your actions listed generally makes creating Actions well worth the effort; but, for smaller projects that are mostly using custom components, the added one-time-cost of making the KeyListener components use actions might outweigh the benefits.

  • Related