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 && isFocusInMainWindow()) { 123 for (KeyPressReleaseListener q: keyListeners) { 124 if (Main.isTraceEnabled()) { 125 Main.trace(q+" => doKeyPressed("+e+')'); 126 } 127 q.doKeyPressed(e); 128 } 129 } 130 } else if (e.getID() == KeyEvent.KEY_RELEASED) { 131 if (timer.isRunning()) { 132 timer.stop(); 133 if (set.remove(e.getKeyCode()) && enabled && isFocusInMainWindow()) { 134 for (KeyPressReleaseListener q: keyListeners) { 135 if (Main.isTraceEnabled()) { 136 Main.trace(q+" => doKeyReleased("+e+')'); 137 } 138 q.doKeyReleased(e); 139 } 140 } 141 } else { 142 releaseEvent = e; 143 timer.restart(); 144 } 145 } 146 } 147 148 @Override 149 public void eventDispatched(AWTEvent e) { 150 if (!(e instanceof KeyEvent)) { 151 return; 152 } 153 KeyEvent ke = (KeyEvent) e; 154 155 // check if ctrl, alt, shift modifiers are changed 156 int modif = ke.getModifiers(); 157 if (previousModifiers != modif) { 158 previousModifiers = modif; 159 for (ModifierListener m: modifierListeners) { 160 m.modifiersChanged(modif); 161 } 162 } 163 164 processKeyEvent(ke); 165 } 166 167 /** 168 * Allows to determine if the key with specific code is pressed now 169 * @param keyCode the key code, for example KeyEvent.VK_ENTER 170 * @return true if the key is pressed now 171 */ 172 public boolean isKeyPressed(int keyCode) { 173 return set.contains(keyCode); 174 } 175 176 /** 177 * Sets the enabled state of the key detector. We need to disable it when text fields that disable 178 * shortcuts gain focus. 179 * @param enabled if {@code true}, enables this key detector. If {@code false}, disables it 180 * @since 7539 181 */ 182 public final void setEnabled(boolean enabled) { 183 this.enabled = enabled; 184 if (Main.isTraceEnabled()) { 185 Main.trace("AdvancedKeyPressDetector enabled="+enabled+" from "+new Exception().getStackTrace()[1]); 186 } 187 } 188 189 private static boolean isFocusInMainWindow() { 190 Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); 191 return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame; 192 } 193}