I'm trying to make a 'delete' button that deletes either a) a single character in a text-area if pressed and released in quick succession, or b) all of the text if pressed and held down for more than 2 seconds without release.
Is this possible in Java?
In order to be able to detect long key presses from the keyboard input, you need to understand and use 2 concepts:
1. KeyListener.
2. How to get current time.
Once you understand both, just compare the times between keyPressed and keyReleased and call the proper delete action.
Alternativly to a Swing-Timer (watch here for example) you could use a simple SwingWorker to realize the delay. In general you should not execute a delay, i.e. by Thread.sleep(1000), on the Swing EDT, since this would block the gui (for further information ...). Furthermore you should use a MouseListener to capture other informations that you need (stop the timer when mouse is released or exits the buttona area). Here is a very short example:
public class JButtonTest extends JFrame {
public static void main(String[] args) {
JButtonTest x = new JButtonTest();
JButton button = new JButton("Delete");
button.addMouseListener(new MouseAdapter() {
private static final long DELTA = 2000;
private SwingWorker<Void, Void> waitingWorker;
private Long timer;
#Override
public void mousePressed(MouseEvent e) {
timer = System.currentTimeMillis();
System.out.println("delete single char");//DO single delete here
if (waitingWorker != null && !waitingWorker.isDone())
waitingWorker.cancel(true);
waitingWorker = new SwingWorker<Void, Void>() {
#Override
protected Void doInBackground() throws Exception {
Thread.sleep(DELTA);
return null;
}
#Override
protected void done() {
if (timer != null && System.currentTimeMillis() >= timer + DELTA)
System.out.println("delete all text");//DO text delete here
}
};
waitingWorker.execute();
}
#Override
public void mouseReleased(MouseEvent e) {
timer = null;
}
#Override
public void mouseExited(MouseEvent e) {
timer = null;
}
});
x.add(button);
x.setSize(100, 100);
x.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
x.setVisible(true);
}
}
Related
I understand that it is important to use the Event Dispatch Thread for any changes to the interface in Java. However, I have no idea how I can manipulate these events to stop/continue/start. I want to refrain from moving on to the next line of main() (after the ones which put the Runnable in the EventQueue) until a certain key is pressed.
I put together an example for clarity. What I'd like to do here is spawn the JFrame, allow the user to move the box around with the arrow keys and then press Enter to cease the box-shifting operations, and ONLY then make the calculation at the end of main() and cause the answer to appear. I should be able to get 400, 500, 600, etc. As it is, the calculation is made immediately after the JFrame appears, so the answer is always 300.
I carved out a spot for whatever action should be bound to Enter; it's underneath the declarations for the actions bound to the arrow keys.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class EndTheShifter extends JFrame
{
private Color ourRectColor = new Color(28,222,144);
private int ourRectWidth = 50;
private int ourRectHeight = 50;
protected static Point ourRecLocation = new Point(100,100);
// Rectangle object can paint itself
public class Rectangle
{
protected void paint(Graphics2D g2d)
{
g2d.setColor(ourRectColor);
g2d.fillRect(ourRecLocation.x, ourRecLocation.y, ourRectWidth, ourRectHeight);
}
} // Rectangle class
// OurRectangle can create a Rectangle and call paint() on it
public class OurRectangle extends JPanel
{
private Rectangle capableRectangle;
public OurRectangle()
{
capableRectangle = new Rectangle();
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2d = (Graphics2D)g.create();
capableRectangle.paint(g2d);
g2d.dispose();
}
} // OurRectangle class
KeyStroke pressRight = KeyStroke.getKeyStroke("RIGHT");
KeyStroke pressLeft = KeyStroke.getKeyStroke("LEFT");
KeyStroke pressUp = KeyStroke.getKeyStroke("UP");
KeyStroke pressDown = KeyStroke.getKeyStroke("DOWN");
KeyStroke pressEnter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0);
OurRectangle recToWorkWith = new OurRectangle();
// Create InputMap and ActionMap
InputMap inputMap = recToWorkWith.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = recToWorkWith.getActionMap();
// Mapping Shortcut
protected void setTheAction(KeyStroke a, String b, Action c)
{
inputMap.put(a,b);
actionMap.put(b,c);
}
// Constructor!!!
public EndTheShifter()
{
add(recToWorkWith);
Action rightAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
if(ourRecLocation.x != 600)
ourRecLocation.x += 50;
else
ourRecLocation.x = 100;
recToWorkWith.repaint();
}
};
Action leftAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
if(ourRecLocation.x != 100)
ourRecLocation.x -= 50;
else
ourRecLocation.x = 600;
recToWorkWith.repaint();
}
};
Action downAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
if(ourRecLocation.y != 600)
ourRecLocation.y += 50;
else
ourRecLocation.y = 100;
recToWorkWith.repaint();
}
};
Action upAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
if(ourRecLocation.y != 100)
ourRecLocation.y -= 50;
else
ourRecLocation.y = 600;
recToWorkWith.repaint();
}
};
/*
Action enterAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
}
}
setTheAction(pressEnter,"enterAction",enterAction);
*/
setTheAction(pressRight,"rightAction",rightAction);
setTheAction(pressLeft,"leftAction",leftAction);
setTheAction(pressDown,"downAction",downAction);
setTheAction(pressUp,"upAction",upAction);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(800,800);
setVisible(true);
}
// Main kicks things off by putting all of the above
// in the Event Dispatch thread
// On an enter press, I want the last line of main() to run
public static void main(String[] argv)
{
EventQueue.invokeLater(
new Runnable()
{
#Override
public void run()
{
new EndTheShifter();
}
});
// What I want to trigger only on Enter
System.out.println(ourRecLocation.x + 2*ourRecLocation.y);
}
} // EndTheShifter, our outermost class
and ONLY then make the calculation at the end of main()
That is not the way a GUI works.
The main() method is only used to display the frame.
Once the frame is visible the EDT is started and the frame sits there waiting for user events to be generated.
Your application code then responds to these user events.
I understand that it is important to use the Event Dispatch Thread for any changes to the interface in Java.
All code invoked in a listener does execute on the EDT. So the code in your Action does execute on the EDT. You don't need to do anything special.
What I want to trigger only on Enter
Then that logic should be contained in the Enter Action.
I would like to support what camickr said; there is likely a better way to achieve what you are trying to do. That said, if you really want to make your main method wait until the enter key is pressed, here's how:
First, at the top of your file, define an object to use as a synchronization lock like so:
public static final Object LOCK = new Object();
Then, in your main method, before your println statement, put the following code:
synchronized (LOCK) {
LOCK.wait();
}
What this does is it waits until the LOCK object's monitor lock is not being used by any thread (very simplified explanation, read more here), and then it makes the current thread (in this case, the thread that started your main method) wait indefinitely.
Next, add a throws declaration to the method header on your main method:
public static void main(String[] argv) throws InterruptedException
This tells the compiler that your code could throw an InterruptedException, which would happen if your thread was interrupted while it was waiting.
Finally, anywhere in your EndTheShifter constructor, put the following code:
synchronized (LOCK) {
LOCK.notify();
}
This again waits until the LOCK object's monitor lock becomes available, and it then "notifies" all threads waiting on the LOCK object that they may continue. In this case, it will make our main thread continue and execute the println.
I have my GUI set up so that if the button b1 is pressed:
public class CubeCalc {
static int next = 0;
public static void MakeTitlePage()
{
final JFrame window = new JFrame("Cubic Feet Calculator"); //Creates Frame
JButton b1 = new JButton("Start");
b1.setBackground(Color.decode("#5A20DF"));
b1.setForeground(Color.WHITE);
/*b1.setLayout(new GridBagLayout());*/
b1.setPreferredSize(new Dimension(150,50));
b1.addActionListener(new ActionListener() { // action when button is pressed
int pressCount=0;
#Override
public void actionPerformed(ActionEvent e) {
window.dispose();
next = 1;
}
});
then it will dispose of the title page and and next will equal one, and on the Event Dispatch Thread, it creates a new page that does other things:
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() { // launch frame on the Event Dispatch Thread
#Override
public void run() {
MakeTitlePage();
if (next==1)
{
MakeCalcPage();
}
System.out.println(next);
}
});
}
The problem is that the variable next remains equal to zero even though I have changed it in the method MakeTitlePage(). How do I change the variable across all the methods, and not just that one?
I think you might have misunderstood how the event dispatch thread works. When you add a listener to a component then you are telling Swing to listen for certain events and invoke the associated listener on the event dispatch thread. If you are using the static variable next to communicate between threads then, firstly, that's not the way to do it and, secondly, you are communicating to the same thread anyway.
If you want the button to close the current window and open a new one then you should do that directly in the actionPerformed method:
public void actionPerformed(ActionEvent e) {
window.setVisible(false);
showCalculationFrame();
}
I'm trying to write a game in java3d on Linux and for that I need a proper KeyListener.
Did anyone of you know how to do it? I'm currently using following code, I found somewhere on the net. It's working pretty good, holding down just one key, but as soon, as I press more than one (like space and w) it will do unexpected things...
public class RepeatingReleasedEventsFixer implements AWTEventListener {
private final HashMap<Integer, ReleasedAction> _map = new HashMap<Integer, ReleasedAction>();
public void install() {
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
}
public void remove() {
Toolkit.getDefaultToolkit().removeAWTEventListener(this);
}
#Override
public void eventDispatched(AWTEvent event) {
assert event instanceof KeyEvent : "Shall only listen to KeyEvents, so no other events shall come here";
assert assertEDT(); // REMEMBER THAT THIS IS SINGLE THREADED, so no need for synch.
// ?: Is this one of our synthetic RELEASED events?
if (event instanceof Reposted) {
// -> Yes, so we shalln't process it again.
return;
}
// ?: KEY_TYPED event? (We're only interested in KEY_PRESSED and KEY_RELEASED).
if (event.getID() == KeyEvent.KEY_TYPED) {
// -> Yes, TYPED, don't process.
return;
}
final KeyEvent keyEvent = (KeyEvent) event;
// ?: Is this already consumed?
// (Note how events are passed on to all AWTEventListeners even though a previous one consumed it)
if (keyEvent.isConsumed()) {
return;
}
// ?: Is this RELEASED? (the problem we're trying to fix!)
if (keyEvent.getID() == KeyEvent.KEY_RELEASED) {
// -> Yes, so stick in wait
/**
* Really just wait until "immediately", as the point is that the subsequent PRESSED shall already have been
* posted on the event queue, and shall thus be the direct next event no matter which events are posted
* afterwards. The code with the ReleasedAction handles if the Timer thread actually fires the action due to
* lags, by cancelling the action itself upon the PRESSED.
*/
final Timer timer = new Timer(2, null);
ReleasedAction action = new ReleasedAction(keyEvent, timer);
timer.addActionListener(action);
timer.start();
_map.put(Integer.valueOf(keyEvent.getKeyCode()), action);
// Consume the original
keyEvent.consume();
}
else if (keyEvent.getID() == KeyEvent.KEY_PRESSED) {
// Remember that this is single threaded (EDT), so we can't have races.
ReleasedAction action = _map.remove(Integer.valueOf(keyEvent.getKeyCode()));
// ?: Do we have a corresponding RELEASED waiting?
if (action != null) {
// -> Yes, so dump it
action.cancel();
}
// System.out.println("PRESSED: [" + keyEvent + "]");
}
else {
throw new AssertionError("All IDs should be covered.");
}
}
/**
* The ActionListener that posts the RELEASED {#link RepostedKeyEvent} if the {#link Timer} times out (and hence the
* repeat-action was over).
*/
private class ReleasedAction implements ActionListener {
private final KeyEvent _originalKeyEvent;
private Timer _timer;
ReleasedAction(KeyEvent originalReleased, Timer timer) {
_timer = timer;
_originalKeyEvent = originalReleased;
}
void cancel() {
assert assertEDT();
_timer.stop();
_timer = null;
_map.remove(Integer.valueOf(_originalKeyEvent.getKeyCode()));
}
#Override
public void actionPerformed(#SuppressWarnings ("unused") ActionEvent e) {
assert assertEDT();
// ?: Are we already cancelled?
// (Judging by Timer and TimerQueue code, we can theoretically be raced to be posted onto EDT by TimerQueue,
// due to some lag, unfair scheduling)
if (_timer == null) {
// -> Yes, so don't post the new RELEASED event.
return;
}
// Stop Timer and clean.
cancel();
// Creating new KeyEvent (we've consumed the original).
KeyEvent newEvent = new RepostedKeyEvent((Component) _originalKeyEvent.getSource(),
_originalKeyEvent.getID(), _originalKeyEvent.getWhen(), _originalKeyEvent.getModifiers(),
_originalKeyEvent.getKeyCode(), _originalKeyEvent.getKeyChar(), _originalKeyEvent.getKeyLocation());
// Posting to EventQueue.
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(newEvent);
// System.out.println("Posted synthetic RELEASED [" + newEvent + "].");
}
}
/**
* Marker interface that denotes that the {#link KeyEvent} in question is reposted from some
* {#link AWTEventListener}, including this. It denotes that the event shall not be "hack processed" by this class
* again. (The problem is that it is not possible to state "inject this event from this point in the pipeline" - one
* have to inject it to the event queue directly, thus it will come through this {#link AWTEventListener} too.
*/
public interface Reposted {
// marker
}
/**
* Dead simple extension of {#link KeyEvent} that implements {#link Reposted}.
*/
public static class RepostedKeyEvent extends KeyEvent implements Reposted {
public RepostedKeyEvent(#SuppressWarnings ("hiding") Component source, #SuppressWarnings ("hiding") int id,
long when, int modifiers, int keyCode, char keyChar, int keyLocation) {
super(source, id, when, modifiers, keyCode, keyChar, keyLocation);
}
}
private static boolean assertEDT() {
if (!EventQueue.isDispatchThread()) {
throw new AssertionError("Not EDT, but [" + Thread.currentThread() + "].");
}
return true;
}
}
I can't be the only one who still runs into this - meanwhile 15 y.o. - problem and don't want to use timers...
EDIT: What this code is doing is fix the known problem on any Linux distri, where you add a simple KeyListener, which handles keyDowns, but invokes keyReleased Event repeatedly. To clearify my problem here a simple example
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
public class Test5 extends JFrame{
public Test5() {
addKeyListener(new KeyListener() {
boolean keydown = false;
#Override
public void keyTyped(KeyEvent arg0) {
// TODO Auto-generated method stub
}
#Override
public void keyReleased(KeyEvent arg0) {
keydown = false;
System.out.println("keyup");
}
#Override
public void keyPressed(KeyEvent arg0) {
if (keydown){
System.out.println("key is down");
} else {
System.out.println("key not down");
}
keydown = true;
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 400);
setVisible(true);
//new RepeatingReleasedEventsFixer().install(); // This line will fix it for one key pressed
}
public static void main(String[] args) {
new Test5();
}
}
The output without the line being commented out:
key not down
keyup
key not down
keyup
key not down
keyup
key not down
keyup
key not down
keyup
otherwise:
key not down
key is down
key is down
key is down
key is down
key is down
key is down
key is down
key is down
key is down
keyup
Btw. How come, that it's not beeing fixed by now?
EDIT:
I tried the KeyBindings, as suggested, where it comes to these problems:
public class Test5 extends JFrame{
long timestamp = 0;
public Test5() {
((JComponent)getComponent(0)).getInputMap().put(KeyStroke.getKeyStroke('a'), "a");
((JComponent)getComponent(0)).getActionMap().put("a", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("time: "+(System.currentTimeMillis()-timestamp));
timestamp = System.currentTimeMillis();
}
});
((JComponent)getComponent(0)).getInputMap().put(KeyStroke.getKeyStroke('s'), "s");
((JComponent)getComponent(0)).getActionMap().put("s", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent arg0) {
System.out.println("s");
}
});
((JComponent)getComponent(0)).getInputMap().put(KeyStroke.getKeyStroke('d'), "d");
((JComponent)getComponent(0)).getActionMap().put("d", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent arg0) {
System.out.println("d");
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 400);
setVisible(true);
new RepeatingReleasedEventsFixer().install(); // This line will fix it for one key pressed
}
/**
* #param args
*/
public static void main(String[] args) {
new Test5();
}
Holding down "a" will give me following output:
time: 4171
time: 501
time: 30
time: 30
time: 30
Where the second time is the actual problem. It takes about 470ms too long.
Holding down "s" and then somewhne pressing "d" will give me that output:
s
s
s
s
d
d
d
d
d
So I can't process two actions as the same time, so I can't use KeyBindings
This is not an answer, it is a long comment with a picture and some explanations.
I used your Test5 (without RepeatingReleasedEventsFixer) to hold down a and measure the time responses. The output is of the form
time: t1
time: t2
time: t3
time: t3
time: t3
...
t1 is meaningless since it depends on the current time and has nothing to do with response time (you also seem to ignore it).
t2 is the time it takes for the OS to realize that you're holding the key for repeated input.
t3 is the "sample time" of the held key, or a discretization of the input.
I'm using Windows where I have the following control panel options:
Repeat delay allows me to set t2 between ~257 (short) and ~1050 (long).
Repeat rate allows me to set t3 between ~407 (slow) and ~37 (fast).
For Linux, you'll have to consult someone / somewhere on how to change these values if you don't already know how to.
As for using multiple keys, see this question and answer and the excellent link within (especially the "Motion With Multiple Keys Pressed" section). It's a short tutorial and analysis of key bindings and key listeners, similar to the one I sent you to on this site.
Key bindings will always be preferred over key listeners unless maybe there is some very low level thing you want to do.
After days of researching and putting stuff together, I ended up writing my own Listener combined with a KeyEventDispatcher, here is the code for someone running into the same problem. It can and should be optimized, but is working for now:
Klass to test if a specific key is pressed:
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.util.HashMap;
public class IsKeyPressed {
private static boolean wPressed = false;
private HashMap<Integer, Boolean> keys = new HashMap<Integer, Boolean>();
public IsKeyPressed() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
#Override
public boolean dispatchKeyEvent(KeyEvent ke) {
synchronized (IsKeyPressed.class) {
switch (ke.getID()) {
case KeyEvent.KEY_PRESSED:
keys.put(ke.getKeyCode(), true);
break;
case KeyEvent.KEY_RELEASED:
keys.put(ke.getKeyCode(), false);
break;
}
return false;
}
}
});
}
public static boolean isWPressed() {
synchronized (IsKeyPressed.class) {
return wPressed;
}
}
public boolean isPressed(int keyCode){
synchronized (IsKeyPressed.class) {
if (keys == null)
return false;
if (keys.get(keyCode) == null)
return false;
return keys.get(keyCode);
}
}
}
Abstract class, thats beeing used for the actions.
public abstract class KeyActionListener {
protected int keyCode;
public KeyActionListener(int keyCode) {
this.keyCode = keyCode;
}
public void setKeyCode(int keyCode){
this.keyCode = keyCode;
}
public int getKeyCode(){
return this.keyCode;
}
public abstract void onKeyDown();
public abstract void onKeyUp();
public abstract void onKeyHolding();
}
Start listening to the keys and run the actions.
import java.util.ArrayList;
import java.util.HashMap;
public class KeyThread extends Thread{
private int sleep = 3;
ArrayList<KeyActionListener> listener = new ArrayList<KeyActionListener>();
IsKeyPressed isPressed = new IsKeyPressed();
HashMap<KeyActionListener, Boolean> pressed = new HashMap<KeyActionListener, Boolean>();
public KeyThread() {
this.start();
}
public void run() {
while (true){
for (int i = 0; i < listener.size(); i++) {
KeyActionListener curListener = listener.get(i);
if (isPressed.isPressed(curListener.getKeyCode()) && !pressed.get(curListener)){
curListener.onKeyDown();
pressed.put(curListener, true);
} else if(!isPressed.isPressed(curListener.getKeyCode()) && pressed.get(curListener)) {
curListener.onKeyUp();
pressed.put(curListener, false);
}
if(isPressed.isPressed(curListener.getKeyCode())){
curListener.onKeyHolding();
}
try{
Thread.sleep(sleep);
} catch(InterruptedException e){
}
}
}
}
public void addKeyActionListener(KeyActionListener l){
listener.add(l);
pressed.put(l, false);
}
}
On JTable, when down arrow key is pressed repeatedly, multiple KeyEvents are fired in quick succession. My requirement is that I need to act only on last KeyEvent. I am trying to use TimerTask in KeySelectionListener, but it is giving inconclusive results.
Any idea what changes can be done in KeyListener ?
Sample code -
addKeyListener(new KeyListener() {
Timer t = new Timer();
TimerTask tt;
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyReleased(KeyEvent e) {
tt.cancel();
tt = null;
}
#Override
public void keyPressed(KeyEvent e) {
if (tt != null)
return;
tt = new TimerTask() {
#Override
public void run() {
System.out.println("Selected-- "+getModel().getValueAt(getSelectedRow(), 2));
}
};
// t.scheduleAtFixedRate(tt, 0, 500);
t.schedule(tt, 0, 200);
}
});
Thanks
The idea with a Timer (and I strongly suggest to use the javax.swing.Timer class since you interact with Swing components) should work.
Just set the repeats to false, start the timer when you receive a keystroke if the timer is not running, or restart the timer when the timer is already running. This will cause a slight delay in the handling of the last key (delay = the delay you set on the timer), but avoid that you react on each key stroke
I used Timer only. I maintained reference of KeyEvent in listener and if another KeyEvent is received in pre-defined delay (for testing kept it 1 sec), new event is referneced. Thus when there is no new keyEvent for 1 sec, last referred event is used for further processing. its working for now. Feel free to suggest your opinions.
addKeyListener(new KeyListener() {
KeyEvent eventToProcess;
Timer t = new Timer();
TimerTask tt;
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyPressed(KeyEvent e) {
if(tt != null){
tt.cancel();
tt = null;
}
}
#Override
public void keyReleased(KeyEvent e) {
eventToProcess = e;
tt = new TimerTask() {
#Override
public void run() {
System.out.println("Selected-- "+getModel().getValueAt(getSelectedRow(), 2)+" Key Event > "+eventToProcess.getKeyChar());
}
};
t.schedule(tt, 1000);
}
});
I have hit another wall. After getting my key input working, I have been racking my brains for hours, i want to create a pause function, so that if the same key is pressed again the timertask stops running (i.e the game is paused)
JPanel component = (JPanel)frame.getContentPane();
component.getInputMap().put(KeyStroke.getKeyStroke("SPACE"), "space");
component.getActionMap().put("space", (new AbstractAction(){
public void actionPerformed(ActionEvent e){
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask(){
public void run(){
grid.stepGame();
}
},250, 250);
}}));
}
The problem is i cant use a global boolean isRunning var and switch it each time the key is pressed because the timerTask method in a nested class (so the boolean isRunning would have to be declared final to be accessed...). Any ideas on how to detect if the key is pressed again or if the game is already running so i can pause/cancel my timerTask.
Many Thanks Sam
Since this is a Swing game, you should be using a javax.swing.Timer or Swing Timer and not a java.util.Timer. By using a Swing Timer, you guarantee that the code being called intermittently is called on the EDT, a key issue for Swing apps, and it also has a stop method that pauses the Timer. You can also give your anonymous AbstractAction class a private boolean field to check if the key is being pressed for the first time or not.
Also, kudos and 1+ for using Key Bindings instead of a KeyListener.
e.g.,
JPanel component = (JPanel) frame.getContentPane();
component.getInputMap().put(KeyStroke.getKeyStroke("SPACE"), "space");
component.getActionMap().put("space", (new AbstractAction() {
private boolean firstPress = true;
private int timerDelay = 250;
private javax.swing.Timer keyTimer = new javax.swing.Timer(timerDelay , new ActionListener() {
// Swing Timer's actionPerformed
public void actionPerformed(ActionEvent e) {
grid.stepGame();
}
});
// key binding AbstractAction's actionPerformed
public void actionPerformed(ActionEvent e) {
if (firstPress) {
keyTimer.start();
} else {
keyTimer.stop();
}
firstPress = !firstPress;
}
}));
Another useful option is to perform a repeating task on key press and stop it on key release, and this can be done easily by getting the keystrokes for on press and on release:
KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true) // for key release
KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false) // for key press
For example:
import java.awt.event.*;
import javax.swing.*;
public class SwingTimerEg2 {
private JFrame frame;
private Grid2 grid = new Grid2(this);
private JTextArea textarea = new JTextArea(20, 20);
private int stepCount = 0;
public SwingTimerEg2() {
frame = new JFrame();
textarea.setEditable(false);
frame.add(new JScrollPane(textarea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
setUpKeyBinding();
}
void setUpKeyBinding() {
final int timerDelay = 250;
final Timer keyTimer = new Timer(timerDelay, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
grid.stepGame();
}
});
JPanel component = (JPanel) frame.getContentPane();
final int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;
final String spaceDown = "space down";
final String spaceUp = "space up";
component.getInputMap(condition).put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), spaceDown);
component.getActionMap().put(spaceDown, (new AbstractAction() {
public void actionPerformed(ActionEvent e) {
keyTimer.start();
}
}));
component.getInputMap(condition).put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true), spaceUp);
component.getActionMap().put(spaceUp, (new AbstractAction() {
public void actionPerformed(ActionEvent e) {
keyTimer.stop();
}
}));
}
public void doSomething() {
textarea.append(String.format("Zap %d!!!%n", stepCount));
stepCount ++;
}
private static void createAndShowGui() {
new SwingTimerEg2();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class Grid2 {
private SwingTimerEg2 stEg;
public Grid2(SwingTimerEg2 stEg) {
this.stEg = stEg;
}
void stepGame() {
stEg.doSomething();
}
}
Easiest and dirty solution:
final boolean[] isRunning = new boolean[1];
You don't want to do that—but it works assuming proper synchronization around.
What would be better is
final AtomicBoolean isRunning = new AtomicBoolean();
What would be even better is to review the design once again: global state usually means, "global problems"
The final qualifier requirement can easily be avoided -- replace your inner method (which has the final requirement) with a call to a class method.
No you got the wrong idea about WHY you need final for anonymous classes! Final is only needed for local variables (well more exactly any variable that might have a live time shorter than the given object).
Hence a static variable in a class is perfectly fine and will work perfectly!
Edit: example:
public class Main {
interface Test {
void call();
}
public static volatile boolean running = true;
public static void main(String[] args) {
Test t = new Test() {
#Override
public void call() {
System.out.println(Main.running);
}
};
t.call();
running = false;
t.call();
}
}
Keep a reference to the Timer somewhere, say in your game class.
When the game is paused cancel the Timer.
This will cancel any currently scheduled tasks.
Then when the game is unpaused schedule the timer again as you have done above.
public class Game {
private Timer timer;
public void pause() {
if (timer != null) {
timer.pause();
}
}
public void startOrResumeGame() {
if (timer == null) {
timer = new Timer();
} else {
// Just in case the game was already running.
timer.cancel();
}
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
grid.stepGame();
}
}, 250, 250);
}
}