// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.util;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.KeyboardFocusManager;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.openstreetmap.josm.Main;
/**
* Helper object that allows cross-platform detection of key press and release events
* instance is available globally as {@code Main.map.keyDetector}.
* @since 7217
*/
public class AdvancedKeyPressDetector implements AWTEventListener {
// events for crossplatform key holding processing
// thanks to http://www.arco.in-berlin.de/keyevent.html
private final Set<Integer> set = new TreeSet<>();
private KeyEvent releaseEvent;
private Timer timer;
private final List<KeyPressReleaseListener> keyListeners = new CopyOnWriteArrayList<>();
private final List<ModifierListener> modifierListeners = new CopyOnWriteArrayList<>();
private int previousModifiers;
private boolean enabled = true;
/**
* Adds an object that wants to receive key press and release events.
* @param l listener to add
*/
public void addKeyListener(KeyPressReleaseListener l) {
keyListeners.add(l);
}
/**
* Adds an object that wants to receive key modifier changed events.
* @param l listener to add
*/
public void addModifierListener(ModifierListener l) {
modifierListeners.add(l);
}
/**
* Removes the listener.
* @param l listener to remove
*/
public void removeKeyListener(KeyPressReleaseListener l) {
keyListeners.remove(l);
}
/**
* Removes the key modifier listener.
* @param l listener to remove
*/
public void removeModifierListener(ModifierListener l) {
modifierListeners.remove(l);
}
/**
* Register this object as AWTEventListener
*/
public void register() {
try {
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
} catch (SecurityException ex) {
Main.warn(ex);
}
timer = new Timer(0, e -> {
timer.stop();
if (set.remove(releaseEvent.getKeyCode()) && enabled && isFocusInMainWindow()) {
for (KeyPressReleaseListener q: keyListeners) {
q.doKeyReleased(releaseEvent);
}
}
});
}
/**
* Unregister this object as AWTEventListener
* lists of listeners are not cleared!
*/
public void unregister() {
if (timer != null) {
timer.stop();
}
set.clear();
if (!keyListeners.isEmpty()) {
Main.warn(tr("Some of the key listeners forgot to remove themselves: {0}"), keyListeners.toString());
}
if (!modifierListeners.isEmpty()) {
Main.warn(tr("Some of the key modifier listeners forgot to remove themselves: {0}"), modifierListeners.toString());
}
try {
Toolkit.getDefaultToolkit().removeAWTEventListener(this);
} catch (SecurityException ex) {
Main.warn(ex);
}
}
private void processKeyEvent(KeyEvent e) {
if (Main.isTraceEnabled()) {
Main.trace("AdvancedKeyPressDetector enabled="+enabled+" => processKeyEvent("+e+") from "+new Exception().getStackTrace()[2]);
}
if (e.getID() == KeyEvent.KEY_PRESSED) {
if (timer.isRunning()) {
timer.stop();
} else if (set.add(e.getKeyCode()) && enabled && isFocusInMainWindow()) {
for (KeyPressReleaseListener q: keyListeners) {
if (Main.isTraceEnabled()) {
Main.trace(q+" => doKeyPressed("+e+')');
}
q.doKeyPressed(e);
}
}
} else if (e.getID() == KeyEvent.KEY_RELEASED) {
if (timer.isRunning()) {
timer.stop();
if (set.remove(e.getKeyCode()) && enabled && isFocusInMainWindow()) {
for (KeyPressReleaseListener q: keyListeners) {
if (Main.isTraceEnabled()) {
Main.trace(q+" => doKeyReleased("+e+')');
}
q.doKeyReleased(e);
}
}
} else {
releaseEvent = e;
timer.restart();
}
}
}
@Override
public void eventDispatched(AWTEvent e) {
if (!(e instanceof KeyEvent)) {
return;
}
KeyEvent ke = (KeyEvent) e;
// check if ctrl, alt, shift modifiers are changed
int modif = ke.getModifiers();
if (previousModifiers != modif) {
previousModifiers = modif;
for (ModifierListener m: modifierListeners) {
m.modifiersChanged(modif);
}
}
processKeyEvent(ke);
}
/**
* Allows to determine if the key with specific code is pressed now
* @param keyCode the key code, for example KeyEvent.VK_ENTER
* @return true if the key is pressed now
*/
public boolean isKeyPressed(int keyCode) {
return set.contains(keyCode);
}
/**
* Sets the enabled state of the key detector. We need to disable it when text fields that disable
* shortcuts gain focus.
* @param enabled if {@code true}, enables this key detector. If {@code false}, disables it
* @since 7539
*/
public final void setEnabled(boolean enabled) {
this.enabled = enabled;
if (Main.isTraceEnabled()) {
Main.trace("AdvancedKeyPressDetector enabled="+enabled+" from "+new Exception().getStackTrace()[1]);
}
}
private static boolean isFocusInMainWindow() {
Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame;
}
}