KeyListener and Linux 'erroneous' behaviour

Hello everyone,

I am trying to develop a pong clone, using pure Java 2D (i.e. no additional libraries) and Swing/AWT.
Everything works fine for now apart from the following:
When I minimize the game’s frame (a JFrame and no full screen exclusive mode yet), and leave it minimized for, say, 20 minutes, after restoring the frame it looks like the JPanel inside the JFrame does not receive key events through KeyListener.
I have also tried to use Frame as the top most window and Canvas instead of a JPanel, but I get the same behaviour.
The ‘erroneous’ (?) behaviour exists only in linux (specifically Linux Mint 8 with XFCE).

The KeyListener (which is added to the panel/canvas) adds key events to a queue (implemented with LinkedList) and my game loop retrieves the first key event in queue and processes it (if exists) in each game update cycle.
I added a FocusListener, a WindowFocusListener and a WindowListener to the frame so as when it gets the focus or gets activated (or deiconified), I transfer the focus to the panel/canvas (with requestFocusInWindow()).
I also added a FocusListener on the panel/canvas to see if it does gets the focus.

What I have seen as an ‘erroneous’ behaviour is that, after the frame has been minimized for 20 minutes and then restored, the focus seems to be transferred to the panel/canvas but the KeyListener does not seem to get any key events.

I would really appreciate if you can help me to solve this problem.
Also, I would really like not to use any game engine as of now.

Here is some code that might help you understand a little bit more of how my game works.


public class Pong extends Frame implements FocusListener, WindowListener, WindowFocusListener {
	private final PongPanel pp;
	
	public Pong() {
		addFocusListener(this);
		addWindowListener(this);
		addWindowFocusListener(this);
		
		pp = new PongPanel(640, 480, 300, 5);
		
		setBounds(0, 0, 640, 480);
		setResizable(false);
		setLocationRelativeTo(null);
		
		add(pp);
		
		pack();
		setVisible(true);
		
		pp.requestFocusInWindow();
	}
	
	@Override
	public void focusGained(FocusEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				pp.requestFocusInWindow();
			}
		});
	}

	@Override
	public void focusLost(FocusEvent e) {}

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

	@Override
	public void windowGainedFocus(WindowEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				pp.requestFocusInWindow();
			}
		});
	}

	@Override
	public void windowLostFocus(WindowEvent e) {}

	@Override
	public void windowActivated(WindowEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				pp.requestFocusInWindow();
			}
		});
	}

	@Override
	public void windowClosed(WindowEvent e) {}

	@Override
	public void windowClosing(WindowEvent e) {}

	@Override
	public void windowDeactivated(WindowEvent e) {}

	@Override
	public void windowDeiconified(WindowEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				pp.requestFocusInWindow();
			}
		});
	}

	@Override
	public void windowIconified(WindowEvent e) {}

	@Override
	public void windowOpened(WindowEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				pp.requestFocusInWindow();
			}
		});
	}
}


public class PongPanel extends BasePanel implements FocusListener {
	public PongPanel(int width, int height, int updatesPerSecond, int maxFrameSkip) {
		super(width, height, updatesPerSecond, maxFrameSkip);
		addFocusListener(this);
	}

	@Override
	public void focusGained(FocusEvent e) {
		System.out.println("I have the focus");
	}

	@Override
	public void focusLost(FocusEvent e) {
		System.out.println("I lost the focus");
	}
}


public class BasePanel extends Canvas implements KeyListener, Runnable {	
	protected final int MAX_FRAMESKIP;
	protected final int PHEIGHT;
	protected final int PWIDTH;
	protected final int SKIP_TICKS;
	protected final int TICKS_PER_SECOND;

	protected boolean running;
	protected int loops;
	protected LinkedList<KeyEvent> keyEvents = new LinkedList<KeyEvent>();
	protected long nextGameTick = nextGameTick();
	protected Thread animator;
	
	public BasePanel(int width, int height, int updatesPerSecond, int maxFrameSkips) {
		super();
		
		TICKS_PER_SECOND = updatesPerSecond;
		SKIP_TICKS = 1000 / TICKS_PER_SECOND;
		MAX_FRAMESKIP = maxFrameSkips;
		PWIDTH = width;
		PHEIGHT = height;
		
		super.setPreferredSize(new Dimension(PWIDTH, PHEIGHT));
		super.setBounds(0, 0, PWIDTH, PHEIGHT);
		super.setIgnoreRepaint(true);
		super.setFocusable(true);
		
		super.addKeyListener(this);
	}
	
	public void addNotify() {
		super.addNotify();

		if (animator == null) {
			animator = new Thread(this);
		}
		startAnimator();
	}
	
	private void baseRender(float interpolation) {}
	private void baseUpdate() {}
	
	@Override
	public void keyPressed(final KeyEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				keyEvents.add(e);
			}
		});
	}

	@Override
	public void keyReleased(KeyEvent e) {}

	@Override
	public void keyTyped(KeyEvent e) {}
	
	public static final long nextGameTick() {
		return System.nanoTime() / 1000000L;
	}
	
	protected void processKeyEvents() {}
	protected void render(float interpolation) {}
	
	public void run() {
		while (running) {
			loops = 0;
			while (nextGameTick() > nextGameTick && loops < MAX_FRAMESKIP) {
				if (keyEvents.size() > 0) {
					processKeyEvents();
				}
				baseUpdate();
			}
			
			baseRender(0.0f);

			
			Thread.yield();
		}
	}
	
	private void startAnimator() {
		running = true;
		animator.start();
	}
	
	protected void update() {}
}

Kind Regards,
Charalampos

Looks like if I remove the key listener from the canvas/panel when the top frame is deactivated and then add it again when the top frame is activated then the ‘erroneous’ behaviour disappears.

Regards,
Charalampos

Interesting. You might want to file a bug-report to the AWT team.

http://bugreport.sun.com/bugreport/

Does it really sound like a Java bug?
I’m not 100% sure.
It might be my code.

Trying to create a test case to report the bug I came up with… well… a working version… :smiley:

This was the test case I created, which works fine!


import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.awt.event.WindowListener;

public class TestCaseFrame extends Frame implements FocusListener, WindowFocusListener, WindowListener {
	TestCaseCanvas canvas;
	
	public TestCaseFrame() {
		addWindowListener(this);
		
		canvas = new TestCaseCanvas();
		
		setTitle("TestCaseFrame");
		setBounds(0, 0, 640, 480);
		setResizable(false);
		setLocationRelativeTo(null);
		
		add(canvas);
		
		pack();
		setVisible(true);
	}

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

	@Override
	public void focusGained(FocusEvent e) {
		canvas.requestFocusInWindow();
	}

	@Override
	public void focusLost(FocusEvent e) {}

	@Override
	public void windowGainedFocus(WindowEvent e) {
		canvas.requestFocusInWindow();
	}

	@Override
	public void windowLostFocus(WindowEvent e) {}

	@Override
	public void windowActivated(WindowEvent e) {
		canvas.requestFocusInWindow();
	}

	@Override
	public void windowClosed(WindowEvent e) {}

	@Override
	public void windowClosing(WindowEvent e) {}

	@Override
	public void windowDeactivated(WindowEvent e) {}

	@Override
	public void windowDeiconified(WindowEvent e) {
		canvas.requestFocusInWindow();
	}

	@Override
	public void windowIconified(WindowEvent e) {}

	@Override
	public void windowOpened(WindowEvent e) {}

}


import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.LinkedList;

public class TestCaseCanvas extends Canvas implements FocusListener, KeyListener, Runnable {
	final int MAX_FRAMESKIP;
	final int PHEIGHT;
	final int PWIDTH;
	final int SKIP_TICKS;
	final int TICKS_PER_SECOND;
	
	boolean running;
	Graphics dbg;
	Image dbImg;
	int loops;
	LinkedList<KeyEvent> keyEvents = new LinkedList<KeyEvent>();
	long nextGameTick = nextGameTick();
	Thread animator;
	
	public TestCaseCanvas() {
		TICKS_PER_SECOND = 300;
		SKIP_TICKS = 1000 / TICKS_PER_SECOND;
		MAX_FRAMESKIP = 5;
		PWIDTH = 640;
		PHEIGHT = 480;
		
		setPreferredSize(new Dimension(PWIDTH, PHEIGHT));
		setBounds(0, 0, PWIDTH, PHEIGHT);
		setIgnoreRepaint(true);
		setFocusable(true);
		
		addFocusListener(this);
		addKeyListener(this);
	}
	
	public void addNotify() {
		super.addNotify();

		if (animator == null) {
			animator = new Thread(this);
		}
		startAnimator();
	}
	
	@Override
	public void focusGained(FocusEvent e) {
		System.out.println("I received focus");
	}

	@Override
	public void focusLost(FocusEvent e) {}

	@Override
	public void keyPressed(final KeyEvent e) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				keyEvents.add(e);
			}
		});
	}

	@Override
	public void keyReleased(KeyEvent e) {}

	@Override
	public void keyTyped(KeyEvent e) {}
	
	static long nextGameTick() {
		return System.nanoTime() / 1000000L;
	}
	
	void processKeyEvents() {
		KeyEvent e = keyEvents.poll();
		if (e != null) {
			System.out.println("Processing key event: " + e);
		}
	}
	
	void render() {
		try {
			if (dbImg == null) {
				dbImg = getGraphicsConfiguration().createCompatibleImage(PWIDTH, PHEIGHT);
			}
			
			if (dbg == null) {
				dbg = dbImg.getGraphics();
			}
			
			dbg.setColor(Color.BLACK);
			dbg.fillRect(0, 0, PWIDTH, PHEIGHT);
			dbg.setColor(Color.WHITE);
			dbg.drawString("This is a test case", 20, 20);
			
			Graphics g = getGraphics();
			g.drawImage(dbImg, 0, 0, PWIDTH, PHEIGHT, this);
			Toolkit.getDefaultToolkit().sync();
			g.dispose();
		}
		catch (Exception e) {
			System.out.println("Graphics error: " + e.getLocalizedMessage());
		}
	}
	
	public void run() {
		while (running) {
			loops = 0;
			while (nextGameTick() > nextGameTick && loops < MAX_FRAMESKIP) {
				if (keyEvents.size() > 0) {
					processKeyEvents();
				}
				nextGameTick += SKIP_TICKS;
				loops++;
			}
			
			render();
			
			Thread.yield();
		}
		
		animator = null;
	}
	
	void startAnimator() {
		running = true;
		animator.start();
	}
}

Going through the previous test case, I had a thought that the FocusListener was not working as it should, hence the KeyListener was never getting the KeyEvents.
Changing the PongPanel class with the following code, it fixes the ‘erroneous’ behaviour


public class PongPanel extends BasePanel implements FocusListener {
	public PongPanel(int width, int height, int updatesPerSecond, int maxFrameSkip) {
		super(width, height, updatesPerSecond, maxFrameSkip);
		addFocusListener(this);
	}

	@Override
	public void focusGained(FocusEvent e) {
		System.out.println("I have the focus");
		super.requestFocusInWindow();
	}

	@Override
	public void focusLost(FocusEvent e) {
		System.out.println("I lost the focus");
	}
}

Do I feel a little tiny bit wiser or do I? :stuck_out_tongue:

Onwards to make Java play sounds in Linux :persecutioncomplex:

Regards,
Charalampos