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