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.tools.ListenerList;
022import org.openstreetmap.josm.tools.Logging;
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 List<KeyPressReleaseListener> keyListeners = new CopyOnWriteArrayList<>();
038    private final ListenerList<ModifierExListener> modifierExListeners = ListenerList.create();
039    private int previousModifiersEx;
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 void addKeyListener(KeyPressReleaseListener l) {
048        keyListeners.add(l);
049    }
050
051    /**
052     * Adds an object that wants to receive extended key modifier changed events.
053     * @param l listener to add
054     * @since 12517
055     */
056    public void addModifierExListener(ModifierExListener l) {
057        modifierExListeners.addListener(l);
058    }
059
060    /**
061     * Removes the listener.
062     * @param l listener to remove
063     */
064    public void removeKeyListener(KeyPressReleaseListener l) {
065        keyListeners.remove(l);
066    }
067
068    /**
069     * Removes the extended key modifier listener.
070     * @param l listener to remove
071     * @since 12517
072     */
073    public void removeModifierExListener(ModifierExListener l) {
074        modifierExListeners.removeListener(l);
075    }
076
077    /**
078     * Register this object as AWTEventListener
079     */
080    public void register() {
081        try {
082            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
083        } catch (SecurityException ex) {
084            Logging.warn(ex);
085        }
086        timer = new Timer(0, e -> {
087            timer.stop();
088            if (set.remove(releaseEvent.getKeyCode()) && enabled && isFocusInMainWindow()) {
089                for (KeyPressReleaseListener q: keyListeners) {
090                    q.doKeyReleased(releaseEvent);
091                }
092            }
093        });
094    }
095
096    /**
097     * Unregister this object as AWTEventListener
098     * lists of listeners are not cleared!
099     */
100    public void unregister() {
101        if (timer != null) {
102            timer.stop();
103        }
104        set.clear();
105        releaseEvent = null;
106        if (!keyListeners.isEmpty()) {
107            Logging.warn(tr("Some of the key listeners forgot to remove themselves: {0}"), keyListeners.toString());
108        }
109        if (modifierExListeners.hasListeners()) {
110            Logging.warn(tr("Some of the key modifier listeners forgot to remove themselves: {0}"), modifierExListeners.toString());
111        }
112        try {
113            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
114        } catch (SecurityException ex) {
115            Logging.warn(ex);
116        }
117    }
118
119    private void processKeyEvent(KeyEvent e) {
120        if (Logging.isTraceEnabled()) {
121            Logging.trace("AdvancedKeyPressDetector enabled={0} => processKeyEvent({1}) from {2}",
122                    enabled, e, new Exception().getStackTrace()[2]);
123        }
124        if (e.getID() == KeyEvent.KEY_PRESSED) {
125            if (timer.isRunning()) {
126                timer.stop();
127            } else if (set.add(e.getKeyCode()) && enabled && isFocusInMainWindow()) {
128                for (KeyPressReleaseListener q: keyListeners) {
129                    Logging.trace("{0} => doKeyPressed({1})", q, e);
130                    q.doKeyPressed(e);
131                }
132            }
133        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
134            if (timer.isRunning()) {
135                timer.stop();
136                if (set.remove(e.getKeyCode()) && enabled && isFocusInMainWindow()) {
137                    for (KeyPressReleaseListener q: keyListeners) {
138                        Logging.trace("{0} => doKeyReleased({1})", q, e);
139                        q.doKeyReleased(e);
140                    }
141                }
142            } else {
143                releaseEvent = e;
144                timer.restart();
145            }
146        }
147    }
148
149    @Override
150    public void eventDispatched(AWTEvent e) {
151        if (!(e instanceof KeyEvent)) {
152            return;
153        }
154        KeyEvent ke = (KeyEvent) e;
155
156        // check if ctrl, alt, shift extended modifiers are changed
157        int modifEx = ke.getModifiersEx();
158        if (previousModifiersEx != modifEx) {
159            previousModifiersEx = modifEx;
160            modifierExListeners.fireEvent(m -> m.modifiersExChanged(modifEx));
161        }
162
163        processKeyEvent(ke);
164    }
165
166    /**
167     * Allows to determine if the key with specific code is pressed now
168     * @param keyCode the key code, for example KeyEvent.VK_ENTER
169     * @return true if the key is pressed now
170     */
171    public boolean isKeyPressed(int keyCode) {
172        return set.contains(keyCode);
173    }
174
175    /**
176     * Sets the enabled state of the key detector. We need to disable it when text fields that disable
177     * shortcuts gain focus.
178     * @param enabled if {@code true}, enables this key detector. If {@code false}, disables it
179     * @since 7539
180     */
181    public final void setEnabled(boolean enabled) {
182        this.enabled = enabled;
183        if (Logging.isTraceEnabled()) {
184            Logging.trace("AdvancedKeyPressDetector enabled={0} from {1}", enabled, new Exception().getStackTrace()[1]);
185        }
186    }
187
188    private static boolean isFocusInMainWindow() {
189        Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
190        return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame;
191    }
192}