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