001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.util; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.AWTEvent; 007import java.awt.Component; 008import java.awt.KeyboardFocusManager; 009import java.awt.Toolkit; 010import java.awt.event.AWTEventListener; 011import java.awt.event.KeyEvent; 012import java.util.List; 013import java.util.Set; 014import java.util.TreeSet; 015import java.util.concurrent.CopyOnWriteArrayList; 016 017import javax.swing.JFrame; 018import javax.swing.SwingUtilities; 019import javax.swing.Timer; 020 021import org.openstreetmap.josm.tools.ListenerList; 022import org.openstreetmap.josm.tools.Logging; 023 024/** 025 * Helper object that allows cross-platform detection of key press and release events 026 * instance is available globally as {@code Main.map.keyDetector}. 027 * @since 7217 028 */ 029public class AdvancedKeyPressDetector implements AWTEventListener { 030 031 // events for crossplatform key holding processing 032 // thanks to http://www.arco.in-berlin.de/keyevent.html 033 private final Set<Integer> set = new TreeSet<>(); 034 private KeyEvent releaseEvent; 035 private Timer timer; 036 037 private final List<KeyPressReleaseListener> keyListeners = new CopyOnWriteArrayList<>(); 038 private final ListenerList<ModifierExListener> modifierExListeners = ListenerList.create(); 039 private int previousModifiersEx; 040 041 private boolean enabled = true; 042 043 /** 044 * Adds an object that wants to receive key press and release events. 045 * @param l listener to add 046 */ 047 public void addKeyListener(KeyPressReleaseListener l) { 048 keyListeners.add(l); 049 } 050 051 /** 052 * Adds an object that wants to receive extended key modifier changed events. 053 * @param l listener to add 054 * @since 12517 055 */ 056 public void addModifierExListener(ModifierExListener l) { 057 modifierExListeners.addListener(l); 058 } 059 060 /** 061 * Removes the listener. 062 * @param l listener to remove 063 */ 064 public void removeKeyListener(KeyPressReleaseListener l) { 065 keyListeners.remove(l); 066 } 067 068 /** 069 * Removes the extended key modifier listener. 070 * @param l listener to remove 071 * @since 12517 072 */ 073 public void removeModifierExListener(ModifierExListener l) { 074 modifierExListeners.removeListener(l); 075 } 076 077 /** 078 * Register this object as AWTEventListener 079 */ 080 public void register() { 081 try { 082 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 083 } catch (SecurityException ex) { 084 Logging.log(Logging.LEVEL_WARN, "Unable to add AWT event listener", ex); 085 } 086 timer = new Timer(0, e -> { 087 timer.stop(); 088 if (set.remove(releaseEvent.getKeyCode()) && enabled && isFocusInMainWindow()) { 089 for (KeyPressReleaseListener q: keyListeners) { 090 q.doKeyReleased(releaseEvent); 091 } 092 } 093 }); 094 } 095 096 /** 097 * Unregister this object as AWTEventListener 098 * lists of listeners are not cleared! 099 */ 100 public void unregister() { 101 if (timer != null) { 102 timer.stop(); 103 } 104 set.clear(); 105 releaseEvent = null; 106 if (!keyListeners.isEmpty()) { 107 Logging.warn(tr("Some of the key listeners forgot to remove themselves: {0}"), keyListeners.toString()); 108 } 109 if (modifierExListeners.hasListeners()) { 110 Logging.warn(tr("Some of the key modifier listeners forgot to remove themselves: {0}"), modifierExListeners.toString()); 111 } 112 try { 113 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 114 } catch (SecurityException ex) { 115 Logging.log(Logging.LEVEL_WARN, "Unable to remove AWT event listener", ex); 116 } 117 } 118 119 private void processKeyEvent(KeyEvent e) { 120 if (Logging.isTraceEnabled()) { 121 Logging.trace("AdvancedKeyPressDetector enabled={0} => processKeyEvent({1}) from {2}", 122 enabled, e, new Exception().getStackTrace()[2]); 123 } 124 if (e.getID() == KeyEvent.KEY_PRESSED) { 125 if (timer.isRunning()) { 126 timer.stop(); 127 } else if (set.add(e.getKeyCode()) && enabled && isFocusInMainWindow()) { 128 for (KeyPressReleaseListener q: keyListeners) { 129 Logging.trace("{0} => doKeyPressed({1})", q, e); 130 q.doKeyPressed(e); 131 } 132 } 133 } else if (e.getID() == KeyEvent.KEY_RELEASED) { 134 if (timer.isRunning()) { 135 timer.stop(); 136 if (set.remove(e.getKeyCode()) && enabled && isFocusInMainWindow()) { 137 for (KeyPressReleaseListener q: keyListeners) { 138 Logging.trace("{0} => doKeyReleased({1})", q, e); 139 q.doKeyReleased(e); 140 } 141 } 142 } else { 143 releaseEvent = e; 144 timer.restart(); 145 } 146 } 147 } 148 149 @Override 150 public void eventDispatched(AWTEvent e) { 151 if (!(e instanceof KeyEvent)) { 152 return; 153 } 154 KeyEvent ke = (KeyEvent) e; 155 156 // check if ctrl, alt, shift extended modifiers are changed 157 int modifEx = ke.getModifiersEx(); 158 if (previousModifiersEx != modifEx) { 159 previousModifiersEx = modifEx; 160 modifierExListeners.fireEvent(m -> m.modifiersExChanged(modifEx)); 161 } 162 163 processKeyEvent(ke); 164 } 165 166 /** 167 * Allows to determine if the key with specific code is pressed now 168 * @param keyCode the key code, for example KeyEvent.VK_ENTER 169 * @return true if the key is pressed now 170 */ 171 public boolean isKeyPressed(int keyCode) { 172 return set.contains(keyCode); 173 } 174 175 /** 176 * Sets the enabled state of the key detector. We need to disable it when text fields that disable 177 * shortcuts gain focus. 178 * @param enabled if {@code true}, enables this key detector. If {@code false}, disables it 179 * @since 7539 180 */ 181 public final void setEnabled(boolean enabled) { 182 this.enabled = enabled; 183 if (Logging.isTraceEnabled()) { 184 Logging.trace("AdvancedKeyPressDetector enabled={0} from {1}", enabled, new Exception().getStackTrace()[1]); 185 } 186 } 187 188 private static boolean isFocusInMainWindow() { 189 Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); 190 return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame; 191 } 192}