001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.KeyEventDispatcher;
007import java.awt.KeyboardFocusManager;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.awt.event.KeyEvent;
011import java.util.HashMap;
012import java.util.Map;
013import java.util.Timer;
014import java.util.TimerTask;
015
016import javax.swing.AbstractAction;
017import javax.swing.Action;
018import javax.swing.JMenuItem;
019import javax.swing.JPanel;
020import javax.swing.JPopupMenu;
021import javax.swing.KeyStroke;
022import javax.swing.SwingUtilities;
023import javax.swing.event.PopupMenuEvent;
024import javax.swing.event.PopupMenuListener;
025
026import org.openstreetmap.josm.Main;
027import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo;
028
029public final class MultikeyActionsHandler {
030
031    private static final long DIALOG_DELAY = 1000;
032    private static final String STATUS_BAR_ID = "multikeyShortcut";
033
034    private final Map<MultikeyShortcutAction, MyAction> myActions = new HashMap<>();
035
036    private static final class ShowLayersPopupWorker implements Runnable {
037        private final MyAction action;
038
039        private ShowLayersPopupWorker(MyAction action) {
040            this.action = action;
041        }
042
043        @Override
044        public void run() {
045            JPopupMenu layers = new JPopupMenu();
046
047            JMenuItem lbTitle = new JMenuItem((String) action.action.getValue(Action.SHORT_DESCRIPTION));
048            lbTitle.setEnabled(false);
049            JPanel pnTitle = new JPanel();
050            pnTitle.add(lbTitle);
051            layers.add(pnTitle);
052
053            char repeatKey = (char) action.shortcut.getKeyStroke().getKeyCode();
054            boolean repeatKeyUsed = false;
055
056            for (final MultikeyInfo info: action.action.getMultikeyCombinations()) {
057
058                if (info.getShortcut() == repeatKey) {
059                    repeatKeyUsed = true;
060                }
061
062                JMenuItem item = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(),
063                        String.valueOf(info.getShortcut()), info.getDescription()));
064                item.setMnemonic(info.getShortcut());
065                item.addActionListener(new ActionListener() {
066                    @Override
067                    public void actionPerformed(ActionEvent e) {
068                        action.action.executeMultikeyAction(info.getIndex(), false);
069                    }
070                });
071                layers.add(item);
072            }
073
074            if (!repeatKeyUsed) {
075                MultikeyInfo lastLayer = action.action.getLastMultikeyAction();
076                if (lastLayer != null) {
077                    JMenuItem repeateItem = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(),
078                            KeyEvent.getKeyText(action.shortcut.getKeyStroke().getKeyCode()),
079                            "Repeat " + lastLayer.getDescription()));
080                    repeateItem.setMnemonic(action.shortcut.getKeyStroke().getKeyCode());
081                    repeateItem.addActionListener(new ActionListener() {
082                        @Override
083                        public void actionPerformed(ActionEvent e) {
084                            action.action.executeMultikeyAction(-1, true);
085                        }
086                    });
087                    layers.add(repeateItem);
088                }
089            }
090            layers.addPopupMenuListener(new PopupMenuListener() {
091
092                @Override
093                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
094                    // Do nothing
095                }
096
097                @Override
098                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
099                    Main.map.statusLine.resetHelpText(STATUS_BAR_ID);
100                }
101
102                @Override
103                public void popupMenuCanceled(PopupMenuEvent e) {
104                    // Do nothing
105                }
106            });
107
108            layers.show(Main.parent, Integer.MAX_VALUE, Integer.MAX_VALUE);
109            layers.setLocation(Main.parent.getX() + Main.parent.getWidth() - layers.getWidth(),
110                    Main.parent.getY() + Main.parent.getHeight() - layers.getHeight());
111        }
112    }
113
114    private class MyKeyEventDispatcher implements KeyEventDispatcher {
115        @Override
116        public boolean dispatchKeyEvent(KeyEvent e) {
117
118            if (e.getWhen() == lastTimestamp)
119                return false;
120
121            if (lastAction != null && e.getID() == KeyEvent.KEY_PRESSED) {
122                int index = getIndex(e.getKeyCode());
123                if (index >= 0) {
124                    lastAction.action.executeMultikeyAction(index, e.getKeyCode() == lastAction.shortcut.getKeyStroke().getKeyCode());
125                }
126                lastAction = null;
127                Main.map.statusLine.resetHelpText(STATUS_BAR_ID);
128                return true;
129            }
130            return false;
131        }
132
133        private int getIndex(int lastKey) {
134            if (lastKey >= KeyEvent.VK_1 && lastKey <= KeyEvent.VK_9)
135                return lastKey - KeyEvent.VK_1;
136            else if (lastKey == KeyEvent.VK_0)
137                return 9;
138            else if (lastKey >= KeyEvent.VK_A && lastKey <= KeyEvent.VK_Z)
139                return lastKey - KeyEvent.VK_A + 10;
140            else
141                return -1;
142        }
143    }
144
145    private class MyAction extends AbstractAction {
146
147        private final transient MultikeyShortcutAction action;
148        private final transient Shortcut shortcut;
149
150        MyAction(MultikeyShortcutAction action) {
151            this.action = action;
152            this.shortcut = action.getMultikeyShortcut();
153        }
154
155        @Override
156        public void actionPerformed(ActionEvent e) {
157            lastTimestamp = e.getWhen();
158            lastAction = this;
159            timer.schedule(new MyTimerTask(lastTimestamp, lastAction), DIALOG_DELAY);
160            Main.map.statusLine.setHelpText(STATUS_BAR_ID, tr("{0}... [please type its number]", (String) action.getValue(SHORT_DESCRIPTION)));
161        }
162
163        @Override
164        public String toString() {
165            return "MultikeyAction" + action;
166        }
167    }
168
169    private class MyTimerTask extends TimerTask {
170        private final long lastTimestamp;
171        private final MyAction lastAction;
172
173        MyTimerTask(long lastTimestamp, MyAction lastAction) {
174            this.lastTimestamp = lastTimestamp;
175            this.lastAction = lastAction;
176        }
177
178        @Override
179        public void run() {
180            if (lastTimestamp == MultikeyActionsHandler.this.lastTimestamp &&
181                    lastAction == MultikeyActionsHandler.this.lastAction) {
182                SwingUtilities.invokeLater(new ShowLayersPopupWorker(lastAction));
183                MultikeyActionsHandler.this.lastAction = null;
184            }
185        }
186    }
187
188    private long lastTimestamp;
189    private MyAction lastAction;
190    private final Timer timer;
191
192    private MultikeyActionsHandler() {
193        KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new MyKeyEventDispatcher());
194        timer = new Timer();
195    }
196
197    private static MultikeyActionsHandler instance;
198
199    /**
200     * Replies the unique instance of this class.
201     * @return The unique instance of this class
202     */
203    public static synchronized MultikeyActionsHandler getInstance() {
204        if (instance == null) {
205            instance = new MultikeyActionsHandler();
206        }
207        return instance;
208    }
209
210    private static String formatMenuText(KeyStroke keyStroke, String index, String description) {
211        String shortcutText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers()) + '+'
212                + KeyEvent.getKeyText(keyStroke.getKeyCode()) + ',' + index;
213
214        return "<html><i>" + shortcutText + "</i>&nbsp;&nbsp;&nbsp;&nbsp;" + description;
215    }
216
217    /**
218     * Registers an action and its shortcut
219     * @param action The action to add
220     */
221    public void addAction(MultikeyShortcutAction action) {
222        if (action.getMultikeyShortcut() != null) {
223            MyAction myAction = new MyAction(action);
224            myActions.put(action, myAction);
225            Main.registerActionShortcut(myAction, myAction.shortcut);
226        }
227    }
228
229    /**
230     * Unregisters an action and its shortcut completely
231     * @param action The action to remove
232     */
233    public void removeAction(MultikeyShortcutAction action) {
234        MyAction a = myActions.get(action);
235        if (a != null) {
236            Main.unregisterActionShortcut(a, a.shortcut);
237            myActions.remove(action);
238        }
239    }
240}