// 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.tools.ListenerList; import org.openstreetmap.josm.tools.Logging; /** * 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 set = new TreeSet<>(); private KeyEvent releaseEvent; private Timer timer; private final List keyListeners = new CopyOnWriteArrayList<>(); /** @deprecated replaced by {@link #modifierExListeners} */ @Deprecated private final List modifierListeners = new CopyOnWriteArrayList<>(); private final ListenerList modifierExListeners = ListenerList.create(); /** @deprecated replaced by {@link #previousModifiersEx} */ @Deprecated private int previousModifiers; private int previousModifiersEx; 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 * @deprecated use {@link #addModifierExListener} instead */ @Deprecated public void addModifierListener(ModifierListener l) { modifierListeners.add(l); } /** * Adds an object that wants to receive extended key modifier changed events. * @param l listener to add * @since 12517 */ public void addModifierExListener(ModifierExListener l) { modifierExListeners.addListener(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 * @deprecated use {@link #removeModifierExListener} instead */ @Deprecated public void removeModifierListener(ModifierListener l) { modifierListeners.remove(l); } /** * Removes the extended key modifier listener. * @param l listener to remove * @since 12517 */ public void removeModifierExListener(ModifierExListener l) { modifierExListeners.removeListener(l); } /** * Register this object as AWTEventListener */ public void register() { try { Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); } catch (SecurityException ex) { Logging.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()) { Logging.warn(tr("Some of the key listeners forgot to remove themselves: {0}"), keyListeners.toString()); } if (!modifierListeners.isEmpty()) { Logging.warn(tr("Some of the key modifier listeners forgot to remove themselves: {0}"), modifierListeners.toString()); } if (modifierExListeners.hasListeners()) { Logging.warn(tr("Some of the key modifier listeners forgot to remove themselves: {0}"), modifierExListeners.toString()); } try { Toolkit.getDefaultToolkit().removeAWTEventListener(this); } catch (SecurityException ex) { Logging.warn(ex); } } private void processKeyEvent(KeyEvent e) { if (Logging.isTraceEnabled()) { Logging.trace("AdvancedKeyPressDetector enabled={0} => processKeyEvent({1}) from {2}", enabled, e, 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) { Logging.trace("{0} => doKeyPressed({1})", q, 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) { Logging.trace("{0} => doKeyReleased({1})", q, e); q.doKeyReleased(e); } } } else { releaseEvent = e; timer.restart(); } } } @Override @SuppressWarnings("deprecation") 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); } } // check if ctrl, alt, shift extended modifiers are changed int modifEx = ke.getModifiersEx(); if (previousModifiersEx != modifEx) { previousModifiersEx = modifEx; modifierExListeners.fireEvent(m -> m.modifiersExChanged(modifEx)); } 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 (Logging.isTraceEnabled()) { Logging.trace("AdvancedKeyPressDetector enabled={0} from {1}", enabled, new Exception().getStackTrace()[1]); } } private static boolean isFocusInMainWindow() { Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame; } }