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;
021import org.openstreetmap.josm.gui.util.GuiHelper;
022
023/**
024 * Global shortcut class.
025 *
026 * Note: This class represents a single shortcut, contains the factory to obtain
027 *       shortcut objects from, manages shortcuts and shortcut collisions, and
028 *       finally manages loading and saving shortcuts to/from the preferences.
029 *
030 * Action authors: You only need the {@link #registerShortcut} factory. Ignore everything 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 * @since 1084
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 && !"core:none".equals(shortText);
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<>(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<>();
235
236    // and here our modifier groups
237    private static Map<Integer, Integer> groups= new HashMap<>();
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<>();
255        for(Shortcut c : shortcuts.values())
256        {
257            if(!"core:none".equals(c.shortText)) {
258                l.add(c);
259            }
260        }
261        return l;
262    }
263
264    /** None group: used with KeyEvent.CHAR_UNDEFINED if no shortcut is defined */
265    public static final int NONE = 5000;
266    public static final int MNEMONIC = 5001;
267    /** Reserved group: for system shortcuts only */
268    public static final int RESERVED = 5002;
269    /** Direct group: no modifier */
270    public static final int DIRECT = 5003;
271    /** Alt group */
272    public static final int ALT = 5004;
273    /** Shift group */
274    public static final int SHIFT = 5005;
275    /** Command group. Matches CTRL modifier on Windows/Linux but META modifier on OS X */
276    public static final int CTRL = 5006;
277    /** Alt-Shift group */
278    public static final int ALT_SHIFT = 5007;
279    /** Alt-Command group. Matches ALT-CTRL modifier on Windows/Linux but ALT-META modifier on OS X */
280    public static final int ALT_CTRL = 5008;
281    /** Command-Shift group. Matches CTRL-SHIFT modifier on Windows/Linux but META-SHIFT modifier on OS X */
282    public static final int CTRL_SHIFT = 5009;
283    /** Alt-Command-Shift group. Matches ALT-CTRL-SHIFT modifier on Windows/Linux but ALT-META-SHIFT modifier on OS X */
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        int commandDownMask = GuiHelper.getMenuShortcutKeyMaskEx();
298        groups.put(NONE, -1);
299        groups.put(MNEMONIC, KeyEvent.ALT_DOWN_MASK);
300        groups.put(DIRECT, 0);
301        groups.put(ALT, KeyEvent.ALT_DOWN_MASK);
302        groups.put(SHIFT, KeyEvent.SHIFT_DOWN_MASK);
303        groups.put(CTRL, commandDownMask);
304        groups.put(ALT_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
305        groups.put(ALT_CTRL, KeyEvent.ALT_DOWN_MASK|commandDownMask);
306        groups.put(CTRL_SHIFT, commandDownMask|KeyEvent.SHIFT_DOWN_MASK);
307        groups.put(ALT_CTRL_SHIFT, KeyEvent.ALT_DOWN_MASK|commandDownMask|KeyEvent.SHIFT_DOWN_MASK);
308
309        // (1) System reserved shortcuts
310        Main.platform.initSystemShortcuts();
311        // (2) User defined shortcuts
312        LinkedList<Shortcut> newshortcuts = new LinkedList<>();
313        for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
314            newshortcuts.add(new Shortcut(s));
315        }
316
317        for(Shortcut sc : newshortcuts) {
318            if (sc.getAssignedUser()
319            && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
320                shortcuts.put(sc.getShortText(), sc);
321            }
322        }
323        // Shortcuts at their default values
324        for(Shortcut sc : newshortcuts) {
325            if (!sc.getAssignedUser() && sc.getAssignedDefault()
326            && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
327                shortcuts.put(sc.getShortText(), sc);
328            }
329        }
330        // Shortcuts that were automatically moved
331        for(Shortcut sc : newshortcuts) {
332            if (!sc.getAssignedUser() && !sc.getAssignedDefault()
333            && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
334                shortcuts.put(sc.getShortText(), sc);
335            }
336        }
337    }
338
339    private static int getGroupModifier(int group) {
340        Integer m = groups.get(group);
341        if(m == null)
342            m = -1;
343        return m;
344    }
345
346    private static int findModifier(int group, Integer modifier) {
347        if(modifier == null) {
348            modifier = getGroupModifier(group);
349            if (modifier == null) { // garbage in, no shortcut out
350                modifier = getGroupModifier(NONE);
351            }
352        }
353        return modifier;
354    }
355
356    // shutdown handling
357    public static boolean savePrefs() {
358        boolean changed = false;
359        for (Shortcut sc : shortcuts.values()) {
360            changed = changed | sc.save();
361        }
362        return changed;
363    }
364
365    /**
366     * FOR PLATFORMHOOK USE ONLY
367     *
368     * This registers a system shortcut. See PlatformHook for details.
369     */
370    public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
371        if (shortcuts.containsKey(shortText))
372            return shortcuts.get(shortText);
373        Shortcut potentialShortcut = findShortcut(key, modifier);
374        if (potentialShortcut != null) {
375            // this always is a logic error in the hook
376            Main.error("CONFLICT WITH SYSTEM KEY "+shortText);
377            return null;
378        }
379        potentialShortcut = new Shortcut(shortText, longText, key, RESERVED, key, modifier, true, false);
380        shortcuts.put(shortText, potentialShortcut);
381        return potentialShortcut;
382    }
383
384    /**
385     * Register a shortcut.
386     *
387     * Here you get your shortcuts from. The parameters are:
388     *
389     * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
390     * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
391     * actions that are part of JOSM's core. Use something like
392     * {@code <pluginname>+":"+<actionname>}.
393     * @param longText this will be displayed in the shortcut preferences dialog. Better
394     * use something the user will recognize...
395     * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
396     * @param requestedGroup the group this shortcut fits best. This will determine the
397     * modifiers your shortcut will get assigned. Use the constants defined above.
398     */
399    public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
400        return registerShortcut(shortText, longText, requestedKey, requestedGroup, null);
401    }
402
403    // and now the workhorse. same parameters as above, just one more
404    private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier) {
405        doInit();
406        Integer defaultModifier = findModifier(requestedGroup, modifier);
407        if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
408            Shortcut sc = shortcuts.get(shortText);
409            sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
410            sc.saveDefault();
411            return sc;
412        }
413        Shortcut conflict = findShortcut(requestedKey, defaultModifier);
414        if (conflict != null) {
415            if (Main.isPlatformOsx()) {
416                // Try to reassign Meta to Ctrl
417                int newmodifier = findNewOsxModifier(requestedGroup);
418                if ( findShortcut(requestedKey, newmodifier) == null ) {
419                    return reassignShortcut(shortText, longText, requestedKey, conflict, requestedGroup, requestedKey, newmodifier);
420                }
421            }
422            for (int m : mods) {
423                for (int k : keys) {
424                    int newmodifier = getGroupModifier(m);
425                    if ( findShortcut(k, newmodifier) == null ) {
426                        return reassignShortcut(shortText, longText, requestedKey, conflict, m, k, newmodifier);
427                    }
428                }
429            }
430        } else {
431            Shortcut newsc = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
432            newsc.saveDefault();
433            shortcuts.put(shortText, newsc);
434            return newsc;
435        }
436
437        return null;
438    }
439
440    private static int findNewOsxModifier(int requestedGroup) {
441        switch (requestedGroup) {
442            case CTRL: return KeyEvent.CTRL_DOWN_MASK;
443            case ALT_CTRL: return KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK;
444            case CTRL_SHIFT: return KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK;
445            case ALT_CTRL_SHIFT: return KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK;
446            default: return 0;
447        }
448    }
449
450    private static Shortcut reassignShortcut(String shortText, String longText, int requestedKey, Shortcut conflict,
451            int m, int k, int newmodifier) {
452        Shortcut newsc = new Shortcut(shortText, longText, requestedKey, m, k, newmodifier, false, false);
453        Main.info(tr("Silent shortcut conflict: ''{0}'' moved by ''{1}'' to ''{2}''.",
454            shortText, conflict.getShortText(), newsc.getKeyText()));
455        newsc.saveDefault();
456        shortcuts.put(shortText, newsc);
457        return newsc;
458    }
459
460    /**
461     * Replies the platform specific key stroke for the 'Copy' command, i.e.
462     * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
463     * copy command isn't known.
464     *
465     * @return the platform specific key stroke for the  'Copy' command
466     */
467    public static KeyStroke getCopyKeyStroke() {
468        Shortcut sc = shortcuts.get("system:copy");
469        if (sc == null) return null;
470        return sc.getKeyStroke();
471    }
472
473    /**
474     * Replies the platform specific key stroke for the 'Paste' command, i.e.
475     * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
476     * paste command isn't known.
477     *
478     * @return the platform specific key stroke for the 'Paste' command
479     */
480    public static KeyStroke getPasteKeyStroke() {
481        Shortcut sc = shortcuts.get("system:paste");
482        if (sc == null) return null;
483        return sc.getKeyStroke();
484    }
485
486    /**
487     * Replies the platform specific key stroke for the 'Cut' command, i.e.
488     * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
489     * 'Cut' command isn't known.
490     *
491     * @return the platform specific key stroke for the 'Cut' command
492     */
493    public static KeyStroke getCutKeyStroke() {
494        Shortcut sc = shortcuts.get("system:cut");
495        if (sc == null) return null;
496        return sc.getKeyStroke();
497    }
498}