001    //License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.tools;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.event.KeyEvent;
007    import java.util.ArrayList;
008    import java.util.Arrays;
009    import java.util.Collection;
010    import java.util.HashMap;
011    import java.util.LinkedHashMap;
012    import java.util.LinkedList;
013    import java.util.List;
014    import java.util.Map;
015    
016    import javax.swing.AbstractAction;
017    import javax.swing.AbstractButton;
018    import javax.swing.JMenu;
019    import javax.swing.JOptionPane;
020    import javax.swing.KeyStroke;
021    
022    import org.openstreetmap.josm.Main;
023    
024    /**
025     * Global shortcut class.
026     *
027     * Note: This class represents a single shortcut, contains the factory to obtain
028     *       shortcut objects from, manages shortcuts and shortcut collisions, and
029     *       finally manages loading and saving shortcuts to/from the preferences.
030     *
031     * Action authors: You only need the {@link #registerShortcut} factory. Ignore everything
032     *                 else.
033     *
034     * All: Use only public methods that are also marked to be used. The others are
035     *      public so the shortcut preferences can use them.
036     *
037     */
038    public class Shortcut {
039        private String shortText;        // the unique ID of the shortcut
040        private String longText;         // a human readable description that will be shown in the preferences
041        private int requestedKey;        // the key, the caller requested
042        private int requestedGroup;      // the group, the caller requested
043        private int assignedKey;         // the key that actually is used
044        private int assignedModifier;    // the modifiers that are used
045        private boolean assignedDefault; // true if it got assigned what was requested. (Note: modifiers will be ignored in favour of group when loading it from the preferences then.)
046        private boolean assignedUser;    // true if the user changed this shortcut
047        private boolean automatic;       // true if the user cannot change this shortcut (Note: it also will not be saved into the preferences)
048        private boolean reset;           // true if the user requested this shortcut to be set to its default value (will happen on next restart, as this shortcut will not be saved to the preferences)
049    
050        // simple constructor
051        private Shortcut(String shortText, String longText, int requestedKey, int requestedGroup, int assignedKey, int assignedModifier, boolean assignedDefault, boolean assignedUser) {
052            this.shortText = shortText;
053            this.longText = longText;
054            this.requestedKey = requestedKey;
055            this.requestedGroup = requestedGroup;
056            this.assignedKey = assignedKey;
057            this.assignedModifier = assignedModifier;
058            this.assignedDefault = assignedDefault;
059            this.assignedUser = assignedUser;
060            this.automatic = false;
061            this.reset = false;
062        }
063    
064        public String getShortText() {
065            return shortText;
066        }
067    
068        public String getLongText() {
069            return longText;
070        }
071    
072        // a shortcut will be renamed when it is handed out again, because the original name
073        // may be a dummy
074        private void setLongText(String longText) {
075            this.longText = longText;
076        }
077    
078        private int getRequestedKey() {
079            return requestedKey;
080        }
081    
082        public int getRequestedGroup() {
083            return requestedGroup;
084        }
085    
086        public int getAssignedKey() {
087            return assignedKey;
088        }
089    
090        public int getAssignedModifier() {
091            return assignedModifier;
092        }
093    
094        public boolean getAssignedDefault() {
095            return assignedDefault;
096        }
097    
098        public boolean getAssignedUser() {
099            return assignedUser;
100        }
101    
102        public boolean getAutomatic() {
103            return automatic;
104        }
105    
106        public boolean isChangeable() {
107            return !automatic && !shortText.equals("core:none");
108        }
109    
110        private boolean getReset() {
111            return reset;
112        }
113    
114        /**
115         * FOR PREF PANE ONLY
116         */
117        public void setAutomatic() {
118            automatic = true;
119        }
120    
121        /**
122         * FOR PREF PANE ONLY
123         */
124        public void setAssignedModifier(int assignedModifier) {
125            this.assignedModifier = assignedModifier;
126        }
127    
128        /**
129         * FOR PREF PANE ONLY
130         */
131        public void setAssignedKey(int assignedKey) {
132            this.assignedKey = assignedKey;
133        }
134    
135        /**
136         * FOR PREF PANE ONLY
137         */
138        public void setAssignedUser(boolean assignedUser) {
139            this.reset = (this.assignedUser || reset) && !assignedUser;
140            if (assignedUser) {
141                assignedDefault = false;
142            } else if (reset) {
143                assignedKey = requestedKey;
144                assignedModifier = findModifier(requestedGroup, null);
145            }
146            this.assignedUser = assignedUser;
147        }
148    
149        /**
150         * Use this to register the shortcut with Swing
151         */
152        public KeyStroke getKeyStroke() {
153            if (assignedModifier != -1)
154                return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
155            return null;
156        }
157    
158        // create a shortcut object from an string as saved in the preferences
159        private Shortcut(String prefString) {
160            ArrayList<String> s = (new ArrayList<String>(Main.pref.getCollection(prefString)));
161            this.shortText = prefString.substring(15);
162            this.longText = s.get(0);
163            this.requestedKey = Integer.parseInt(s.get(1));
164            this.requestedGroup = Integer.parseInt(s.get(2));
165            this.assignedKey = Integer.parseInt(s.get(3));
166            this.assignedModifier = Integer.parseInt(s.get(4));
167            this.assignedDefault = Boolean.parseBoolean(s.get(5));
168            this.assignedUser = Boolean.parseBoolean(s.get(6));
169        }
170    
171        private void saveDefault() {
172            Main.pref.getCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
173            String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(requestedKey),
174            String.valueOf(getGroupModifier(requestedGroup)), String.valueOf(true), String.valueOf(false)}));
175        }
176    
177        // get a string that can be put into the preferences
178        private boolean save() {
179            if (getAutomatic() || getReset() || !getAssignedUser()) {
180                return Main.pref.putCollection("shortcut.entry."+shortText, null);
181            } else {
182                return Main.pref.putCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
183                String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(assignedKey),
184                String.valueOf(assignedModifier), String.valueOf(assignedDefault), String.valueOf(assignedUser)}));
185            }
186        }
187    
188        private boolean isSame(int isKey, int isModifier) {
189            // an unassigned shortcut is different from any other shortcut
190            return isKey == assignedKey && isModifier == assignedModifier && assignedModifier != getGroupModifier(NONE);
191        }
192    
193        public boolean isEvent(KeyEvent e) {
194            return getKeyStroke() != null && getKeyStroke().equals(
195            KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()));
196        }
197    
198        /**
199         * use this to set a menu's mnemonic
200         */
201        public void setMnemonic(JMenu menu) {
202            if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
203                menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
204            }
205        }
206        /**
207         * use this to set a buttons's mnemonic
208         */
209        public void setMnemonic(AbstractButton button) {
210            if (assignedModifier == getGroupModifier(MNEMONIC)  && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
211                button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
212            }
213        }
214        /**
215         * use this to set a actions's accelerator
216         */
217        public void setAccelerator(AbstractAction action) {
218            if (getKeyStroke() != null) {
219                action.putValue(AbstractAction.ACCELERATOR_KEY, getKeyStroke());
220            }
221        }
222    
223        /**
224         * use this to get a human readable text for your shortcut
225         */
226        public String getKeyText() {
227            KeyStroke keyStroke = getKeyStroke();
228            if (keyStroke == null) return "";
229            String modifText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
230            if ("".equals (modifText)) return KeyEvent.getKeyText (keyStroke.getKeyCode ());
231            return modifText + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode ());
232        }
233    
234        @Override
235        public String toString() {
236            return getKeyText();
237        }
238    
239        ///////////////////////////////
240        // everything's static below //
241        ///////////////////////////////
242    
243        // here we store our shortcuts
244        private static Map<String, Shortcut> shortcuts = new LinkedHashMap<String, Shortcut>();
245    
246        // and here our modifier groups
247        private static Map<Integer, Integer> groups= new HashMap<Integer, Integer>();
248    
249        // check if something collides with an existing shortcut
250        private static Shortcut findShortcut(int requestedKey, int modifier) {
251            if (modifier == getGroupModifier(NONE))
252                return null;
253            for (Shortcut sc : shortcuts.values()) {
254                if (sc.isSame(requestedKey, modifier))
255                    return sc;
256            }
257            return null;
258        }
259    
260        /**
261         * FOR PREF PANE ONLY
262         */
263        public static List<Shortcut> listAll() {
264            List<Shortcut> l = new ArrayList<Shortcut>();
265            for(Shortcut c : shortcuts.values())
266            {
267                if(!c.shortText.equals("core:none")) {
268                    l.add(c);
269                }
270            }
271            return l;
272        }
273    
274        public static final int NONE = 5000;
275        public static final int MNEMONIC = 5001;
276        public static final int RESERVED = 5002;
277        public static final int DIRECT = 5003;
278        public static final int ALT = 5004;
279        public static final int SHIFT = 5005;
280        public static final int CTRL = 5006;
281        public static final int ALT_SHIFT = 5007;
282        public static final int ALT_CTRL = 5008;
283        public static final int CTRL_SHIFT = 5009;
284        public static final int ALT_CTRL_SHIFT = 5010;
285    
286        /* for reassignment */
287        private static int[] mods = {ALT_CTRL, ALT_SHIFT, CTRL_SHIFT, ALT_CTRL_SHIFT};
288        private static int[] keys = {KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4,
289                                     KeyEvent.VK_F5, KeyEvent.VK_F6, KeyEvent.VK_F7, KeyEvent.VK_F8,
290                                     KeyEvent.VK_F9, KeyEvent.VK_F10, KeyEvent.VK_F11, KeyEvent.VK_F12};
291    
292        // bootstrap
293        private static boolean initdone = false;
294        private static void doInit() {
295            if (initdone) return;
296            initdone = true;
297            groups.put(NONE, -1);
298            groups.put(MNEMONIC, KeyEvent.ALT_DOWN_MASK);
299            groups.put(DIRECT, 0);
300            groups.put(ALT, KeyEvent.ALT_DOWN_MASK);
301            groups.put(SHIFT, KeyEvent.SHIFT_DOWN_MASK);
302            groups.put(CTRL, KeyEvent.CTRL_DOWN_MASK);
303            groups.put(ALT_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
304            groups.put(ALT_CTRL, KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK);
305            groups.put(CTRL_SHIFT, KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
306            groups.put(ALT_CTRL_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
307    
308            // (1) System reserved shortcuts
309            Main.platform.initSystemShortcuts();
310            // (2) User defined shortcuts
311            LinkedList<Shortcut> newshortcuts = new LinkedList<Shortcut>();
312            for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
313                newshortcuts.add(new Shortcut(s));
314            }
315    
316            for(Shortcut sc : newshortcuts) {
317                if (sc.getAssignedUser()
318                && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
319                    shortcuts.put(sc.getShortText(), sc);
320                }
321            }
322            // Shortcuts at their default values
323            for(Shortcut sc : newshortcuts) {
324                if (!sc.getAssignedUser() && sc.getAssignedDefault()
325                && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
326                    shortcuts.put(sc.getShortText(), sc);
327                }
328            }
329            // Shortcuts that were automatically moved
330            for(Shortcut sc : newshortcuts) {
331                if (!sc.getAssignedUser() && !sc.getAssignedDefault()
332                && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
333                    shortcuts.put(sc.getShortText(), sc);
334                }
335            }
336        }
337    
338        private static int getGroupModifier(int group) {
339            Integer m = groups.get(group);
340            if(m == null)
341                m = -1;
342            return m;
343        }
344    
345        private static int findModifier(int group, Integer modifier) {
346            if(modifier == null) {
347                modifier = getGroupModifier(group);
348                if (modifier == null) { // garbage in, no shortcut out
349                    modifier = getGroupModifier(NONE);
350                }
351            }
352            return modifier;
353        }
354    
355        // shutdown handling
356        public static boolean savePrefs() {
357            boolean changed = false;
358            for (Shortcut sc : shortcuts.values()) {
359                changed = changed | sc.save();
360            }
361            return changed;
362        }
363    
364        /**
365         * FOR PLATFORMHOOK USE ONLY
366         *
367         * This registers a system shortcut. See PlatformHook for details.
368         */
369        public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
370            if (shortcuts.containsKey(shortText))
371                return shortcuts.get(shortText);
372            Shortcut potentialShortcut = findShortcut(key, modifier);
373            if (potentialShortcut != null) {
374                // this always is a logic error in the hook
375                System.err.println("CONFLICT WITH SYSTEM KEY "+shortText);
376                return null;
377            }
378            potentialShortcut = new Shortcut(shortText, longText, key, RESERVED, key, modifier, true, false);
379            shortcuts.put(shortText, potentialShortcut);
380            return potentialShortcut;
381        }
382    
383        /**
384         * Register a shortcut.
385         *
386         * Here you get your shortcuts from. The parameters are:
387         *
388         * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
389         * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
390         * actions that are part of JOSM's core. Use something like
391         * {@code <pluginname>+":"+<actionname>}.
392         * @param longText this will be displayed in the shortcut preferences dialog. Better
393         * use something the user will recognize...
394         * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
395         * @param requestedGroup the group this shortcut fits best. This will determine the
396         * modifiers your shortcut will get assigned. Use the constants defined above.
397         */
398        public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
399            return registerShortcut(shortText, longText, requestedKey, requestedGroup, null);
400        }
401    
402        // and now the workhorse. same parameters as above, just one more
403        private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier) {
404            doInit();
405            Integer defaultModifier = findModifier(requestedGroup, modifier);
406            if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
407                Shortcut sc = shortcuts.get(shortText);
408                sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
409                sc.saveDefault();
410                return sc;
411            }
412            Shortcut conflict = findShortcut(requestedKey, defaultModifier);
413            if (conflict != null) {
414                for (int m : mods) {
415                    for (int k : keys) {
416                        int newmodifier = getGroupModifier(m);
417                        if ( findShortcut(k, newmodifier) == null ) {
418                            Shortcut newsc = new Shortcut(shortText, longText, requestedKey, m, k, newmodifier, false, false);
419                            System.out.println(tr("Silent shortcut conflict: ''{0}'' moved by ''{1}'' to ''{2}''.",
420                                shortText, conflict.getShortText(), newsc.getKeyText()));
421                            newsc.saveDefault();
422                            shortcuts.put(shortText, newsc);
423                            return newsc;
424                        }
425                    }
426                }
427            } else {
428                Shortcut newsc = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
429                newsc.saveDefault();
430                shortcuts.put(shortText, newsc);
431                return newsc;
432            }
433    
434            return null;
435        }
436    
437        /**
438         * Replies the platform specific key stroke for the 'Copy' command, i.e.
439         * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
440         * copy command isn't known.
441         *
442         * @return the platform specific key stroke for the  'Copy' command
443         */
444        static public KeyStroke getCopyKeyStroke() {
445            Shortcut sc = shortcuts.get("system:copy");
446            if (sc == null) return null;
447            return sc.getKeyStroke();
448        }
449    
450        /**
451         * Replies the platform specific key stroke for the 'Paste' command, i.e.
452         * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
453         * paste command isn't known.
454         *
455         * @return the platform specific key stroke for the 'Paste' command
456         */
457        static public KeyStroke getPasteKeyStroke() {
458            Shortcut sc = shortcuts.get("system:paste");
459            if (sc == null) return null;
460            return sc.getKeyStroke();
461        }
462    
463        /**
464         * Replies the platform specific key stroke for the 'Cut' command, i.e.
465         * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
466         * 'Cut' command isn't known.
467         *
468         * @return the platform specific key stroke for the 'Cut' command
469         */
470        static public KeyStroke getCutKeyStroke() {
471            Shortcut sc = shortcuts.get("system:cut");
472            if (sc == null) return null;
473            return sc.getKeyStroke();
474        }
475    }