001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.tools;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.KeyEventDispatcher;
007    import java.awt.KeyboardFocusManager;
008    import java.awt.event.ActionEvent;
009    import java.awt.event.ActionListener;
010    import java.awt.event.KeyEvent;
011    import java.util.HashMap;
012    import java.util.Map;
013    import java.util.Timer;
014    import java.util.TimerTask;
015    
016    import javax.swing.AbstractAction;
017    import javax.swing.Action;
018    import javax.swing.JMenuItem;
019    import javax.swing.JPanel;
020    import javax.swing.JPopupMenu;
021    import javax.swing.KeyStroke;
022    import javax.swing.SwingUtilities;
023    import javax.swing.event.PopupMenuEvent;
024    import javax.swing.event.PopupMenuListener;
025    
026    import org.openstreetmap.josm.Main;
027    import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo;
028    
029    public class MultikeyActionsHandler {
030    
031        private static final long DIALOG_DELAY = 1000;
032        private static final String STATUS_BAR_ID = new String("multikeyShortcut");
033    
034        private Map<MultikeyShortcutAction, MyAction> myActions = new HashMap<MultikeyShortcutAction,MyAction>();
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        public static MultikeyActionsHandler getInstance() {
123            if (instance == null) {
124                instance = new MultikeyActionsHandler();
125            }
126            return instance;
127        }
128    
129        private String formatMenuText(KeyStroke keyStroke, String index, String description) {
130            String shortcutText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers()) + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode()) + "," + index;
131    
132            return "<html><i>" + shortcutText + "</i>&nbsp;&nbsp;&nbsp;&nbsp;" + description;
133    
134        }
135    
136        private void showLayersPopup(final MyAction action) {
137            SwingUtilities.invokeLater(new Runnable() {
138                @Override
139                public void run() {
140                    JPopupMenu layers = new JPopupMenu();
141    
142                    JMenuItem lbTitle = new JMenuItem((String) action.action.getValue(Action.SHORT_DESCRIPTION));
143                    lbTitle.setEnabled(false);
144                    JPanel pnTitle = new JPanel();
145                    pnTitle.add(lbTitle);
146                    layers.add(pnTitle);
147    
148                    char repeatKey = (char) action.shortcut.getKeyStroke().getKeyCode();
149                    boolean repeatKeyUsed = false;
150    
151    
152                    for (final MultikeyInfo info: action.action.getMultikeyCombinations()) {
153    
154                        if (info.getShortcut() == repeatKey) {
155                            repeatKeyUsed = true;
156                        }
157    
158                        JMenuItem item = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(), String.valueOf(info.getShortcut()), info.getDescription()));
159                        item.setMnemonic(info.getShortcut());
160                        item.addActionListener(new ActionListener() {
161                            @Override
162                            public void actionPerformed(ActionEvent e) {
163                                action.action.executeMultikeyAction(info.getIndex(), false);
164                            }
165                        });
166                        layers.add(item);
167                    }
168    
169                    if (!repeatKeyUsed) {
170                        MultikeyInfo lastLayer = action.action.getLastMultikeyAction();
171                        if (lastLayer != null) {
172                            JMenuItem repeateItem = new JMenuItem(formatMenuText(action.shortcut.getKeyStroke(),
173                                    KeyEvent.getKeyText(action.shortcut.getKeyStroke().getKeyCode()),
174                                    "Repeat " + lastLayer.getDescription()));
175                            repeateItem.setMnemonic(action.shortcut.getKeyStroke().getKeyCode());
176                            repeateItem.addActionListener(new ActionListener() {
177                                @Override
178                                public void actionPerformed(ActionEvent e) {
179                                    action.action.executeMultikeyAction(-1, true);
180                                }
181                            });
182                            layers.add(repeateItem);
183                        }
184                    }
185                    layers.addPopupMenuListener(new PopupMenuListener() {
186    
187                        @Override
188                        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {}
189    
190                        @Override
191                        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
192                            Main.map.statusLine.resetHelpText(STATUS_BAR_ID);
193                        }
194    
195                        @Override
196                        public void popupMenuCanceled(PopupMenuEvent e) {}
197                    });
198    
199                    layers.show(Main.parent, Integer.MAX_VALUE, Integer.MAX_VALUE);
200                    layers.setLocation(Main.parent.getX() + Main.parent.getWidth() - layers.getWidth(), Main.parent.getY() + Main.parent.getHeight() - layers.getHeight());
201                }
202            });
203        }
204    
205        public void addAction(MultikeyShortcutAction action) {
206            if(action.getMultikeyShortcut() != null) {
207                MyAction myAction = new MyAction(action);
208                myActions.put(action, myAction);
209                Main.registerActionShortcut(myAction, myAction.shortcut);
210            }
211        }
212    
213        // unregister action and its shortcut completely
214        public void removeAction(MultikeyShortcutAction action) {
215            MyAction a = myActions.get(action);
216            if (a!=null) {
217                Main.unregisterActionShortcut(a, a.shortcut);
218                myActions.remove(action);
219            }
220        }
221    }