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.ArrayList; 015import java.util.Set; 016import java.util.TreeSet; 017 018import javax.swing.JFrame; 019import javax.swing.SwingUtilities; 020import javax.swing.Timer; 021 022import org.openstreetmap.josm.Main; 023 024/** 025 * Helper object that allows cross-platform detection of key press and realease events 026 * instance is available globally as 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 ArrayList<KeyPressReleaseListener> keyListeners = new ArrayList<>(); 038 private final ArrayList<ModifierListener> modifierListeners = new ArrayList<>(); 039 private int previousModifiers; 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 synchronized void addKeyListener(KeyPressReleaseListener l) { 048 keyListeners.add(l); 049 } 050 051 /** 052 * Adds an object that wants to receive key modifier changed events. 053 * @param l listener to add 054 */ 055 public synchronized void addModifierListener(ModifierListener l) { 056 modifierListeners.add(l); 057 } 058 059 /** 060 * Removes the listener. 061 * @param l listener to remove 062 */ 063 public synchronized void removeKeyListener(KeyPressReleaseListener l) { 064 keyListeners.remove(l); 065 } 066 067 /** 068 * Removes the key modifier listener. 069 * @param l listener to remove 070 */ 071 public synchronized void removeModifierListener(ModifierListener l) { 072 modifierListeners.remove(l); 073 } 074 075 /** 076 * Register this object as AWTEventListener 077 */ 078 public void register() { 079 try { 080 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 081 } catch (SecurityException ex) { 082 Main.warn(ex); 083 } 084 timer = new Timer(0, new ActionListener() { 085 @Override 086 public void actionPerformed(ActionEvent e) { 087 timer.stop(); 088 if (set.remove(releaseEvent.getKeyCode()) && enabled) { 089 synchronized (AdvancedKeyPressDetector.this) { 090 if (isFocusInMainWindow()) { 091 for (KeyPressReleaseListener q: keyListeners) { 092 q.doKeyReleased(releaseEvent); 093 } 094 } 095 } 096 } 097 } 098 }); 099 } 100 101 /** 102 * Unregister this object as AWTEventListener 103 * lists of listeners are not cleared! 104 */ 105 public void unregister() { 106 timer.stop(); 107 set.clear(); 108 synchronized (this) { 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 } 116 try { 117 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 118 } catch (SecurityException ex) { 119 Main.warn(ex); 120 } 121 } 122 123 private void processKeyEvent(KeyEvent e) { 124 if (e.getID() == KeyEvent.KEY_PRESSED) { 125 if (timer.isRunning()) { 126 timer.stop(); 127 } else if (set.add((e.getKeyCode())) && enabled) { 128 synchronized (this) { 129 if (isFocusInMainWindow()) { 130 for (KeyPressReleaseListener q: keyListeners) { 131 q.doKeyPressed(e); 132 } 133 } 134 } 135 } 136 } else if (e.getID() == KeyEvent.KEY_RELEASED) { 137 if (timer.isRunning()) { 138 timer.stop(); 139 if (set.remove(e.getKeyCode()) && enabled) { 140 synchronized (this) { 141 if (isFocusInMainWindow()) { 142 for (KeyPressReleaseListener q: keyListeners) { 143 q.doKeyReleased(e); 144 } 145 } 146 } 147 } 148 } else { 149 releaseEvent = e; 150 timer.restart(); 151 } 152 } 153 } 154 155 @Override 156 public void eventDispatched(AWTEvent e) { 157 if (!(e instanceof KeyEvent)) { 158 return; 159 } 160 KeyEvent ke = (KeyEvent) e; 161 162 // check if ctrl, alt, shift modifiers are changed 163 int modif = ke.getModifiers(); 164 if (previousModifiers != modif) { 165 previousModifiers = modif; 166 synchronized (this) { 167 for (ModifierListener m: modifierListeners) { 168 m.modifiersChanged(modif); 169 } 170 } 171 } 172 173 processKeyEvent(ke); 174 } 175 176 /** 177 * Allows to determine if the key with specific code is pressed now 178 * @param keyCode the key code, for example KeyEvent.VK_ENTER 179 * @return true if the key is pressed now 180 */ 181 public boolean isKeyPressed(int keyCode) { 182 return set.contains(keyCode); 183 } 184 185 /** 186 * Sets the enabled state of the key detector. We need to disable it when text fields that disable 187 * shortcuts gain focus. 188 * @param enabled if {@code true}, enables this key detector. If {@code false}, disables it 189 * @since 7539 190 */ 191 public final void setEnabled(boolean enabled) { 192 this.enabled = enabled; 193 } 194 195 private boolean isFocusInMainWindow() { 196 Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); 197 return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame; 198 } 199}