package abbot.util;
import java.awt.*;
import java.awt.event.*;
import javax.swing.text.*;
import java.text.*;
import java.util.*;
import javax.swing.KeyStroke;
import abbot.Log;
import abbot.Platform;
import abbot.tester.Robot;
/** Provide an AWTEventListener which normalizes the event stream.
<ul>
<li>removes modifier key repeats on w32
<li>sends a single WINDOW_CLOSED
<li>removes some spurious key events on OSX
</ul>
*/
public class EventNormalizer implements AWTEventListener {
// Normally we want to ignore these (w32 generates them)
private static boolean captureModifierRepeats =
Boolean.getBoolean("abbot.capture_modifier_repeats");
private AWTEventListener listener;
private WeakAWTEventListener weakListener;
private long modifiers;
private Map disposedWindows = new WeakHashMap();
public void startListening(AWTEventListener listener, long mask) {
fnKeyDown = false;
lastKeyPress = lastKeyRelease = KeyEvent.VK_UNDEFINED;
lastKeyStroke = null;
lastKeyChar = KeyEvent.VK_UNDEFINED;
lastKeyComponent = null;
modifiers = 0;
this.listener = listener;
weakListener = new WeakAWTEventListener(this, mask);
}
public void stopListening() {
if (weakListener != null) {
weakListener.dispose();
weakListener = null;
}
listener = null;
modifiers = 0;
}
/** For OSX pre-1.4 laptops... */
private boolean fnKeyDown;
/** These aid in culling duplicate key events, pre-1.4. */
private int lastKeyPress = KeyEvent.VK_UNDEFINED;
private int lastKeyRelease = KeyEvent.VK_UNDEFINED;
private KeyStroke lastKeyStroke;
private char lastKeyChar = KeyEvent.VK_UNDEFINED;
private Component lastKeyComponent;
/** Returns whether the event is spurious and should be discarded. */
private boolean isSpuriousEvent(AWTEvent event) {
return isDuplicateKeyEvent(event)
|| isOSXFunctionKey(event)
|| isDuplicateDispose(event);
}
// TODO: maybe make this an AWT event listener instead, so we can use one
// instance instead of one per window.
private class DisposalWatcher extends ComponentAdapter {
private Map map;
public DisposalWatcher(Map map) {
this.map = map;
}
public void componentShown(ComponentEvent e) {
e.getComponent().removeComponentListener(this);
map.remove(e.getComponent());
}
}
// We want to ignore consecutive event indicating window disposal; there
// needs to be an intervening SHOWN/OPEN before we're interested again.
private boolean isDuplicateDispose(AWTEvent event) {
if (event instanceof WindowEvent) {
WindowEvent we = (WindowEvent)event;
switch(we.getID()) {
case WindowEvent.WINDOW_CLOSED:
Window w = we.getWindow();
if (disposedWindows.containsKey(w)) {
return true;
}
disposedWindows.put(w, Boolean.TRUE);
w.addComponentListener(new DisposalWatcher(disposedWindows));
break;
case WindowEvent.WINDOW_CLOSING:
break;
default:
disposedWindows.remove(we.getWindow());
break;
}
}
return false;
}
/** Flag duplicate key events on pre-1.4 VMs, and repeated modifiers. */
private boolean isDuplicateKeyEvent(AWTEvent event) {
int id = event.getID();
if (id == KeyEvent.KEY_PRESSED) {
KeyEvent ke = (KeyEvent)event;
lastKeyRelease = KeyEvent.VK_UNDEFINED;
int code = ke.getKeyCode();
if (code == lastKeyPress) {
// Discard duplicate key events; they don't add any
// information.
// A duplicate key event is sent to the parent frame on
// components that don't otherwise consume it (JButton)
if (event.getSource() != lastKeyComponent) {
lastKeyPress = KeyEvent.VK_UNDEFINED;
lastKeyComponent = null;
return true;
}
}
lastKeyPress = code;
lastKeyComponent = ke.getComponent();
// Don't pass on key repeats for modifier keys (w32)
if (AWT.isModifier(code)) {
int mask = AWT.keyCodeToMask(code);
if ((mask & modifiers) != 0
&& !captureModifierRepeats) {
return true;
}
}
modifiers = ke.getModifiers();
}
else if (id == KeyEvent.KEY_RELEASED) {
KeyEvent ke = (KeyEvent)event;
lastKeyPress = KeyEvent.VK_UNDEFINED;
int code = ke.getKeyCode();
if (code == lastKeyRelease) {
if (event.getSource() != lastKeyComponent) {
lastKeyRelease = KeyEvent.VK_UNDEFINED;
lastKeyComponent = null;
return true;
}
}
lastKeyRelease = code;
lastKeyComponent = ke.getComponent();
modifiers = ke.getModifiers();
}
else if (id == KeyEvent.KEY_TYPED) {
KeyStroke ks = KeyStroke.getKeyStrokeForEvent((KeyEvent)event);
char ch = ((KeyEvent)event).getKeyChar();
if (ks.equals(lastKeyStroke) || ch == lastKeyChar) {
if (event.getSource() != lastKeyComponent) {
lastKeyStroke = null;
lastKeyChar = KeyEvent.VK_UNDEFINED;
lastKeyComponent = null;
return true;
}
}
lastKeyStroke = ks;
lastKeyChar = ch;
lastKeyComponent = ((KeyEvent)event).getComponent();
}
else {
lastKeyPress = lastKeyRelease = KeyEvent.VK_UNDEFINED;
lastKeyComponent = null;
}
return false;
}
/** Discard function key press/release on 1.3.1 OSX laptops. */
// FIXME fn pressed after arrow keys results in a RELEASE event
private boolean isOSXFunctionKey(AWTEvent event) {
if (event.getID() == KeyEvent.KEY_RELEASED) {
if (((KeyEvent)event).getKeyCode() == KeyEvent.VK_CONTROL
&& fnKeyDown) {
fnKeyDown = false;
return true;
}
}
else if (event.getID() == KeyEvent.KEY_PRESSED) {
if (((KeyEvent)event).getKeyCode() == KeyEvent.VK_CONTROL) {
int mods = ((KeyEvent)event).getModifiers();
if ((mods & KeyEvent.CTRL_MASK) == 0) {
fnKeyDown = true;
return true;
}
}
}
return false;
}
protected void delegate(AWTEvent e) {
if (Bugs.hasInputMethodInsteadOfKeyTyped()) {
if (e.getSource() instanceof JTextComponent
&& e.getID() == InputMethodEvent.INPUT_METHOD_TEXT_CHANGED) {
InputMethodEvent im = (InputMethodEvent)e;
if (im.getCommittedCharacterCount() > 0) {
AttributedCharacterIterator iter = im.getText();
for (char ch = iter.first();
ch != CharacterIterator.DONE;
ch = iter.next()) {
e = new KeyEvent((Component)e.getSource(),
KeyEvent.KEY_TYPED,
System.currentTimeMillis(),
(int)modifiers,
KeyEvent.VK_UNDEFINED, ch);
listener.eventDispatched(e);
}
return;
}
}
}
listener.eventDispatched(e);
}
/** Event reception callback. */
public void eventDispatched(AWTEvent event) {
boolean discard = isSpuriousEvent(event);
if (!discard && listener != null) {
delegate(event);
}
}
}