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.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.gui.MainApplication;
026import org.openstreetmap.josm.gui.MainFrame;
027import org.openstreetmap.josm.gui.util.MultikeyShortcutAction.MultikeyInfo;
028import org.openstreetmap.josm.tools.Shortcut;
029
030/**
031 * Handles the different multikey actions.
032 * The possible actions can be selected through a popup menu.
033 * @since 4595
034 */
035public final class MultikeyActionsHandler {
036
037    private static final long DIALOG_DELAY = 1000;
038    private static final String STATUS_BAR_ID = "multikeyShortcut";
039
040    private final Map<MultikeyShortcutAction, MyAction> myActions = new HashMap<>();
041
042    static final class ShowLayersPopupWorker implements Runnable {
043        static final class StatusLinePopupMenuListener implements PopupMenuListener {
044            @Override
045            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
046                // Do nothing
047            }
048
049            @Override
050            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
051                MainApplication.getMap().statusLine.resetHelpText(STATUS_BAR_ID);
052            }
053
054            @Override
055            public void popupMenuCanceled(PopupMenuEvent e) {
056                // Do nothing
057            }
058        }
059
060        private final MyAction action;
061
062        ShowLayersPopupWorker(MyAction action) {
063            this.action = action;
064        }
065
066        @Override
067        public void run() {
068            JPopupMenu layers = new JPopupMenu();
069
070            JMenuItem lbTitle = new JMenuItem((String) action.action.getValue(Action.SHORT_DESCRIPTION));
071            lbTitle.setEnabled(false);
072            JPanel pnTitle = new JPanel();
073            pnTitle.add(lbTitle);
074            layers.add(pnTitle);
075
076            char repeatKey = (char) action.shortcut.getKeyStroke().getKeyCode();
077            boolean repeatKeyUsed = false;
078
079            for (final MultikeyInfo info: action.action.getMultikeyCombinations()) {
080
081                if (info.getShortcut() == repeatKey) {
082                    repeatKeyUsed = true;
083                }
084
085                JMenuItem item = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(),
086                        String.valueOf(info.getShortcut()), info.getDescription()));
087                item.setMnemonic(info.getShortcut());
088                item.addActionListener(e -> action.action.executeMultikeyAction(info.getIndex(), false));
089                layers.add(item);
090            }
091
092            if (!repeatKeyUsed) {
093                MultikeyInfo lastLayer = action.action.getLastMultikeyAction();
094                if (lastLayer != null) {
095                    JMenuItem repeateItem = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(),
096                            KeyEvent.getKeyText(action.shortcut.getKeyStroke().getKeyCode()),
097                            "Repeat " + lastLayer.getDescription()));
098                    repeateItem.setMnemonic(action.shortcut.getKeyStroke().getKeyCode());
099                    repeateItem.addActionListener(e -> action.action.executeMultikeyAction(-1, true));
100                    layers.add(repeateItem);
101                }
102            }
103            layers.addPopupMenuListener(new StatusLinePopupMenuListener());
104            MainFrame mainFrame = MainApplication.getMainFrame();
105            if (mainFrame.isShowing()) {
106                layers.show(mainFrame, Integer.MAX_VALUE, Integer.MAX_VALUE);
107                layers.setLocation(mainFrame.getX() + mainFrame.getWidth() - layers.getWidth(),
108                                   mainFrame.getY() + mainFrame.getHeight() - layers.getHeight());
109            }
110        }
111    }
112
113    private class MyKeyEventDispatcher implements KeyEventDispatcher {
114        @Override
115        public boolean dispatchKeyEvent(KeyEvent e) {
116
117            if (e.getWhen() == lastTimestamp)
118                return false;
119
120            if (lastAction != null && e.getID() == KeyEvent.KEY_PRESSED) {
121                int index = getIndex(e.getKeyCode());
122                if (index >= 0) {
123                    lastAction.action.executeMultikeyAction(index, e.getKeyCode() == lastAction.shortcut.getKeyStroke().getKeyCode());
124                }
125                lastAction = null;
126                MainApplication.getMap().statusLine.resetHelpText(STATUS_BAR_ID);
127                return true;
128            }
129            return false;
130        }
131
132        private int getIndex(int lastKey) {
133            if (lastKey >= KeyEvent.VK_1 && lastKey <= KeyEvent.VK_9)
134                return lastKey - KeyEvent.VK_1;
135            else if (lastKey == KeyEvent.VK_0)
136                return 9;
137            else if (lastKey >= KeyEvent.VK_A && lastKey <= KeyEvent.VK_Z)
138                return lastKey - KeyEvent.VK_A + 10;
139            else
140                return -1;
141        }
142    }
143
144    private class MyAction extends AbstractAction {
145
146        private final transient MultikeyShortcutAction action;
147        private final transient Shortcut shortcut;
148
149        MyAction(MultikeyShortcutAction action) {
150            this.action = action;
151            this.shortcut = action.getMultikeyShortcut();
152        }
153
154        @Override
155        public void actionPerformed(ActionEvent e) {
156            lastTimestamp = e.getWhen();
157            lastAction = this;
158            timer.schedule(new MyTimerTask(lastTimestamp, lastAction), DIALOG_DELAY);
159            MainApplication.getMap().statusLine.setHelpText(STATUS_BAR_ID,
160                    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 = Shortcut.getKeyText(keyStroke) + ',' + index;
212
213        return "<html><i>" + shortcutText + "</i>&nbsp;&nbsp;&nbsp;&nbsp;" + description;
214    }
215
216    /**
217     * Registers an action and its shortcut
218     * @param action The action to add
219     */
220    public void addAction(MultikeyShortcutAction action) {
221        if (action.getMultikeyShortcut() != null) {
222            MyAction myAction = new MyAction(action);
223            myActions.put(action, myAction);
224            MainApplication.registerActionShortcut(myAction, myAction.shortcut);
225        }
226    }
227
228    /**
229     * Unregisters an action and its shortcut completely
230     * @param action The action to remove
231     */
232    public void removeAction(MultikeyShortcutAction action) {
233        MyAction a = myActions.get(action);
234        if (a != null) {
235            MainApplication.unregisterActionShortcut(a, a.shortcut);
236            myActions.remove(action);
237        }
238    }
239}