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) {
123                if (isFocusInMainWindow()) {
124                    for (KeyPressReleaseListener q: keyListeners) {
125                        if (Main.isTraceEnabled()) {
126                            Main.trace(q+" => doKeyPressed("+e+')');
127                        }
128                        q.doKeyPressed(e);
129                    }
130                }
131            }
132        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
133            if (timer.isRunning()) {
134                timer.stop();
135                if (set.remove(e.getKeyCode()) && enabled) {
136                    if (isFocusInMainWindow()) {
137                        for (KeyPressReleaseListener q: keyListeners) {
138                            if (Main.isTraceEnabled()) {
139                                Main.trace(q+" => doKeyReleased("+e+')');
140                            }
141                            q.doKeyReleased(e);
142                        }
143                    }
144                }
145            } else {
146                releaseEvent = e;
147                timer.restart();
148            }
149        }
150    }
151
152    @Override
153    public void eventDispatched(AWTEvent e) {
154        if (!(e instanceof KeyEvent)) {
155            return;
156        }
157        KeyEvent ke = (KeyEvent) e;
158
159        // check if ctrl, alt, shift modifiers are changed
160        int modif = ke.getModifiers();
161        if (previousModifiers != modif) {
162            previousModifiers = modif;
163            for (ModifierListener m: modifierListeners) {
164                m.modifiersChanged(modif);
165            }
166        }
167
168        processKeyEvent(ke);
169    }
170
171    /**
172     * Allows to determine if the key with specific code is pressed now
173     * @param keyCode the key code, for example KeyEvent.VK_ENTER
174     * @return true if the key is pressed now
175     */
176    public boolean isKeyPressed(int keyCode) {
177        return set.contains(keyCode);
178    }
179
180    /**
181     * Sets the enabled state of the key detector. We need to disable it when text fields that disable
182     * shortcuts gain focus.
183     * @param enabled if {@code true}, enables this key detector. If {@code false}, disables it
184     * @since 7539
185     */
186    public final void setEnabled(boolean enabled) {
187        this.enabled = enabled;
188        if (Main.isTraceEnabled()) {
189            Main.trace("AdvancedKeyPressDetector enabled="+enabled+" from "+new Exception().getStackTrace()[1]);
190        }
191    }
192
193    private static boolean isFocusInMainWindow() {
194        Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
195        return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame;
196    }
197}