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}