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 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 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}