Question

I have a program where the user can press a key to perform an action. That one event takes a small amount of time. The user can also hold down that key and perform the action many times in a row. The issues is that the keyPress() events are queued up faster than the events can be processed. This means that after the user releases the key, events keep getting processed that were queued up from the user previously holding down the key. I also noticed that the keyRelease event doesn't occur until after the final keyPress event is processed regardless of when the key was actually released. I'd like to be able to either 1. Detect the key release event and ignore future keyPress events until the user actually presses the key again. 2. Not perform a subsequent keyPress event until the first is one finished and then detect when the key is not pressed, and just stop.

Does anyone know how to do this?

Was it helpful?

Solution

Disclaimer: I am not feeling well so this code is horrific, as though.. it too is sick.

What I want to happen: To access DirectInput to obtain a keyboard state, instead of events. That is far beyond the scope of this question though. So we will maintain our own action state.

The problem you are having is that you are executing your action within the UI thread. You need to spawn a worker thread and ignore subsequent events until your action is completed.

In the example I've given I start a new action when the letter 'a' is pressed or held down. It will not spawn another action until the first action has completed. The action updates a label on the form, displaying how many 'cycles' are left before it has completed.

There is also another label that displays how many actions have occurred thus far.

Spawning a new action

The important part is to let all the UI key events to occur, not blocking in the UI thread causing them to queue up.

public void keyPressed(KeyEvent e) {
    char keyChar = e.getKeyChar();
    System.out.println("KeyChar: " + keyChar);
    // Press a to start an Action
    if (keyChar == 'a') {
        if (!mAction.isRunning()) {
            mTotalActions.setText("Ran " + (++mTotalActionsRan) + " actions.");
            System.out.println("Starting new Action");
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    mAction.run();
                }
            });
            thread.start();
        }
    }
}

Updates to the UI Thread

If your action performs any kind of updates to the User Interface, it will need to use the SwingUtilities.invokeLater method. This method will queue your code to run in the UI thread. You cannot modify the user interface in a thread other than the UI thread. Also, only use SwingUtilities to update UI components. Any calculations, processing, etc that does not invoke methods on a Component, can be done outside the scope of SwingUtilities.invokeLater.

Full Code Listing

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package stackoverflow_4589538;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class Main extends JFrame {

    private JLabel mActionLabel;
    private JLabel mTotalActions;
    private int mTotalActionsRan;

    private class MyAction {

        private boolean mIsRunning = false;

        public void run() {
            // Make up a random wait cycle time
            final int cycles = new Random().nextInt(100);
            for (int i = 0; i < cycles; ++i) {
                final int currentCycle = i;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                }
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                            mActionLabel.setText("Cycle " + currentCycle + " of " + cycles);
                    }
                });
            }
            completed();
        }

        public synchronized void start() {
            mIsRunning = true;
        }

        public synchronized void completed() {
            mIsRunning = false;
        }

        public synchronized boolean isRunning() {
            return mIsRunning;
    }
}
    private MyAction mAction = new MyAction();

    public Main() {
        setLayout(null);
        setBounds(40, 40, 800, 600);
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        addKeyListener(new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent e) {
                char keyChar = e.getKeyChar();
                System.out.println("KeyChar: " + keyChar);
                // Press A to start an Action
                if (keyChar == 'a') {
                if (!mAction.isRunning()) {
                        mTotalActions.setText("Ran " + (++mTotalActionsRan) + " actions.");
                        System.out.println("Starting new Action");
                        Thread thread = new Thread(new Runnable() {

                            public void run() {
                                mAction.run();
                            }
                        });
                        // I had this within the run() method before
                        // but realized that it is possible for another UI event
                        // to occur and spawn another Action before, start() had
                        // occured within the thread
                        mAction.start();
                        thread.start();
                    }
                }
            }

        @Override
            public void keyReleased(KeyEvent e) {
            }
        });

        mActionLabel = new JLabel();
        mActionLabel.setBounds(10, 10, 150, 40);

        mTotalActions = new JLabel();
        mTotalActions.setBounds(10, 50, 150, 40);

        add(mActionLabel);
        add(mTotalActions);
    }    

    public static void main(String[] args) {
        new Main().setVisible(true);
    }
}

OTHER TIPS

I also noticed that the keyRelease event doesn't occur until after the final keyPress event is processed regardless of when the key was actually released

This depends on the OS you are using. This is the behaviour on Windows (which makes sense to me). On Unix or Mac I believe you get multiple keyPressed, keyReleased events. So you solution should not be based on keyReleased events.

I have a program where the user can press a key to perform an action.

Then you should be using Key Binding, not a KeyListener. Read the section from the Swing tutorial on How to Use Key Bindings for more information.

When the Action is invoked you can then disable it. I'm not sure if this will prevent the KeyStroke from working again or whether you will still need to check the enabled state of the Action. Then when the Action code is finished executing you can re-enable the Action.

Also, this long running code should not execute on the EDT. Read the section from the Swing tutorial on Concurrency for more information about this and for solutions.

You will have to go with option 1. Once you start your longer process, set a boolean of some time to indicate you are working on it and throw out other incoming identical requests. Once you complete the process set the boolean back and allow additional events.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top