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