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    /**
042     * Adds an object that wants to receive key press and release events.
043     * @param l listener to add
044     */
045    public synchronized void addKeyListener(KeyPressReleaseListener l) {
046        keyListeners.add(l);
047    }
048
049    /**
050     * Adds an object that wants to receive key modifier changed events.
051     * @param l listener to add
052     */
053    public synchronized void addModifierListener(ModifierListener l) {
054        modifierListeners.add(l);
055    }
056
057    /**
058     * Removes the listener.
059     * @param l listener to remove
060     */
061    public synchronized void removeKeyListener(KeyPressReleaseListener l) {
062        keyListeners.remove(l);
063    }
064
065    /**
066     * Removes the key modifier listener.
067     * @param l listener to remove
068     */
069    public synchronized void removeModifierListener(ModifierListener l) {
070        modifierListeners.remove(l);
071    }
072
073    /**
074     * Register this object as AWTEventListener
075     */
076    public void register() {
077        try {
078            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
079        } catch (SecurityException ex) {
080            Main.warn(ex);
081        }
082        timer = new Timer(0, new ActionListener() {
083            @Override
084            public void actionPerformed(ActionEvent e) {
085                timer.stop();
086                if (set.remove(releaseEvent.getKeyCode())) {
087                    synchronized (this) {
088                        for (KeyPressReleaseListener q: keyListeners) {
089                            q.doKeyReleased(releaseEvent);
090                        }
091                    }
092                }
093            }
094        });
095    }
096
097    /**
098     * Unregister this object as AWTEventListener
099     * lists of listeners are not cleared!
100     */
101    public void unregister() {
102        timer.stop();
103        set.clear();
104        synchronized (this) {
105            if (!keyListeners.isEmpty()) {
106                Main.warn(tr("Some of the key listeners forgot to remove themselves: {0}"), keyListeners.toString());
107            }
108            if (!modifierListeners.isEmpty()) {
109                Main.warn(tr("Some of the key modifier listeners forgot to remove themselves: {0}"), modifierListeners.toString());
110            }
111        }
112        try {
113            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
114        } catch (SecurityException ex) {
115            Main.warn(ex);
116        }
117    }
118
119    private void processKeyEvent(KeyEvent e) {
120        if (e.getID() == KeyEvent.KEY_PRESSED) {
121            if (timer.isRunning()) {
122                timer.stop();
123            } else if (set.add((e.getKeyCode()))) {
124                synchronized (this) {
125                    for (KeyPressReleaseListener q: keyListeners) {
126                        q.doKeyPressed(e);
127                    }
128                }
129            }
130        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
131            if (timer.isRunning()) {
132                timer.stop();
133                if (set.remove(e.getKeyCode())) {
134                    synchronized (this) {
135                        for (KeyPressReleaseListener q: keyListeners) {
136                            q.doKeyReleased(e);
137                        }
138                    }
139                }
140            } else {
141                releaseEvent = e;
142                timer.restart();
143            }
144        }
145    }
146
147    @Override
148    public void eventDispatched(AWTEvent e) {
149        if (!(e instanceof KeyEvent)) {
150            return;
151        }
152        KeyEvent ke = (KeyEvent) e;
153
154        // check if ctrl, alt, shift modifiers are changed
155        int modif = ke.getModifiers();
156        if (previousModifiers != modif) {
157            previousModifiers = modif;
158            synchronized (this) {
159                for (ModifierListener m: modifierListeners) {
160                    m.modifiersChanged(modif);
161                }
162            }
163        }
164
165        // check if key press is done in main window, not in dialogs
166        Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
167        if (SwingUtilities.getWindowAncestor(focused) instanceof JFrame) {
168            processKeyEvent(ke);
169        }
170    }
171
172    /**
173     * Allows to determine if the key with specific code is pressed now
174     * @param keyCode the key code, for example KeyEvent.VK_ENTER
175     * @return true if the key is pressed now
176     */
177    public boolean isKeyPressed(int keyCode) {
178        return set.contains(keyCode);
179    }
180}