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