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