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.List;
015import java.util.Set;
016import java.util.TreeSet;
017import java.util.concurrent.CopyOnWriteArrayList;
018
019import javax.swing.JFrame;
020import javax.swing.SwingUtilities;
021import javax.swing.Timer;
022
023import org.openstreetmap.josm.Main;
024
025/**
026 * Helper object that allows cross-platform detection of key press and release events
027 * instance is available globally as {@code Main.map.keyDetector}.
028 * @since 7217
029 */
030public class AdvancedKeyPressDetector implements AWTEventListener {
031
032    // events for crossplatform key holding processing
033    // thanks to http://www.arco.in-berlin.de/keyevent.html
034    private final Set<Integer> set = new TreeSet<>();
035    private KeyEvent releaseEvent;
036    private Timer timer;
037
038    private final List<KeyPressReleaseListener> keyListeners = new CopyOnWriteArrayList<>();
039    private final List<ModifierListener> modifierListeners = new CopyOnWriteArrayList<>();
040    private int previousModifiers;
041
042    private boolean enabled = true;
043
044    /**
045     * Adds an object that wants to receive key press and release events.
046     * @param l listener to add
047     */
048    public void addKeyListener(KeyPressReleaseListener l) {
049        keyListeners.add(l);
050    }
051
052    /**
053     * Adds an object that wants to receive key modifier changed events.
054     * @param l listener to add
055     */
056    public void addModifierListener(ModifierListener l) {
057        modifierListeners.add(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 key modifier listener.
070     * @param l listener to remove
071     */
072    public void removeModifierListener(ModifierListener l) {
073        modifierListeners.remove(l);
074    }
075
076    /**
077     * Register this object as AWTEventListener
078     */
079    public void register() {
080        try {
081            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
082        } catch (SecurityException ex) {
083            Main.warn(ex);
084        }
085        timer = new Timer(0, new ActionListener() {
086            @Override
087            public void actionPerformed(ActionEvent e) {
088                timer.stop();
089                if (set.remove(releaseEvent.getKeyCode()) && enabled) {
090                    if (isFocusInMainWindow()) {
091                        for (KeyPressReleaseListener q: keyListeners) {
092                            q.doKeyReleased(releaseEvent);
093                        }
094                    }
095                }
096            }
097        });
098    }
099
100    /**
101     * Unregister this object as AWTEventListener
102     * lists of listeners are not cleared!
103     */
104    public void unregister() {
105        if (timer != null) {
106            timer.stop();
107        }
108        set.clear();
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        try {
116            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
117        } catch (SecurityException ex) {
118            Main.warn(ex);
119        }
120    }
121
122    private void processKeyEvent(KeyEvent e) {
123        if (Main.isDebugEnabled()) {
124            Main.debug("AdvancedKeyPressDetector enabled="+enabled+" => processKeyEvent("+e+") from "+new Exception().getStackTrace()[2]);
125        }
126        if (e.getID() == KeyEvent.KEY_PRESSED) {
127            if (timer.isRunning()) {
128                timer.stop();
129            } else if (set.add(e.getKeyCode()) && enabled) {
130                if (isFocusInMainWindow()) {
131                    for (KeyPressReleaseListener q: keyListeners) {
132                        if (Main.isDebugEnabled()) {
133                            Main.debug(q+" => doKeyPressed("+e+')');
134                        }
135                        q.doKeyPressed(e);
136                    }
137                }
138            }
139        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
140            if (timer.isRunning()) {
141                timer.stop();
142                if (set.remove(e.getKeyCode()) && enabled) {
143                    if (isFocusInMainWindow()) {
144                        for (KeyPressReleaseListener q: keyListeners) {
145                            if (Main.isDebugEnabled()) {
146                                Main.debug(q+" => doKeyReleased("+e+')');
147                            }
148                            q.doKeyReleased(e);
149                        }
150                    }
151                }
152            } else {
153                releaseEvent = e;
154                timer.restart();
155            }
156        }
157    }
158
159    @Override
160    public void eventDispatched(AWTEvent e) {
161        if (!(e instanceof KeyEvent)) {
162            return;
163        }
164        KeyEvent ke = (KeyEvent) e;
165
166        // check if ctrl, alt, shift modifiers are changed
167        int modif = ke.getModifiers();
168        if (previousModifiers != modif) {
169            previousModifiers = modif;
170            for (ModifierListener m: modifierListeners) {
171                m.modifiersChanged(modif);
172            }
173        }
174
175        processKeyEvent(ke);
176    }
177
178    /**
179     * Allows to determine if the key with specific code is pressed now
180     * @param keyCode the key code, for example KeyEvent.VK_ENTER
181     * @return true if the key is pressed now
182     */
183    public boolean isKeyPressed(int keyCode) {
184        return set.contains(keyCode);
185    }
186
187    /**
188     * Sets the enabled state of the key detector. We need to disable it when text fields that disable
189     * shortcuts gain focus.
190     * @param enabled if {@code true}, enables this key detector. If {@code false}, disables it
191     * @since 7539
192     */
193    public final void setEnabled(boolean enabled) {
194        this.enabled = enabled;
195        if (Main.isDebugEnabled()) {
196            Main.debug("AdvancedKeyPressDetector enabled="+enabled+" from "+new Exception().getStackTrace()[1]);
197        }
198    }
199
200    private static boolean isFocusInMainWindow() {
201        Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
202        return focused != null && SwingUtilities.getWindowAncestor(focused) instanceof JFrame;
203    }
204}