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.Comparator;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013import java.util.Optional;
014import java.util.concurrent.CopyOnWriteArrayList;
015import java.util.function.Predicate;
016import java.util.stream.Collectors;
017
018import javax.swing.AbstractAction;
019import javax.swing.AbstractButton;
020import javax.swing.Action;
021import javax.swing.JMenu;
022import javax.swing.KeyStroke;
023import javax.swing.text.JTextComponent;
024
025import org.openstreetmap.josm.data.Preferences;
026import org.openstreetmap.josm.spi.preferences.Config;
027
028/**
029 * Global shortcut class.
030 *
031 * Note: This class represents a single shortcut, contains the factory to obtain
032 *       shortcut objects from, manages shortcuts and shortcut collisions, and
033 *       finally manages loading and saving shortcuts to/from the preferences.
034 *
035 * Action authors: You only need the {@link #registerShortcut} factory. Ignore everything else.
036 *
037 * All: Use only public methods that are also marked to be used. The others are
038 *      public so the shortcut preferences can use them.
039 * @since 1084
040 */
041public final class Shortcut {
042    /** the unique ID of the shortcut */
043    private final String shortText;
044    /** a human readable description that will be shown in the preferences */
045    private String longText;
046    /** the key, the caller requested */
047    private final int requestedKey;
048    /** the group, the caller requested */
049    private final int requestedGroup;
050    /** the key that actually is used */
051    private int assignedKey;
052    /** the modifiers that are used */
053    private int assignedModifier;
054    /** true if it got assigned what was requested.
055     * (Note: modifiers will be ignored in favour of group when loading it from the preferences then.) */
056    private boolean assignedDefault;
057    /** true if the user changed this shortcut */
058    private boolean assignedUser;
059    /** true if the user cannot change this shortcut (Note: it also will not be saved into the preferences) */
060    private boolean automatic;
061    /** true if the user requested this shortcut to be set to its default value
062     * (will happen on next restart, as this shortcut will not be saved to the preferences) */
063    private boolean reset;
064
065    // simple constructor
066    private Shortcut(String shortText, String longText, int requestedKey, int requestedGroup, int assignedKey, int assignedModifier,
067            boolean assignedDefault, boolean assignedUser) {
068        this.shortText = shortText;
069        this.longText = longText;
070        this.requestedKey = requestedKey;
071        this.requestedGroup = requestedGroup;
072        this.assignedKey = assignedKey;
073        this.assignedModifier = assignedModifier;
074        this.assignedDefault = assignedDefault;
075        this.assignedUser = assignedUser;
076        this.automatic = false;
077        this.reset = false;
078    }
079
080    public String getShortText() {
081        return shortText;
082    }
083
084    public String getLongText() {
085        return longText;
086    }
087
088    // a shortcut will be renamed when it is handed out again, because the original name may be a dummy
089    private void setLongText(String longText) {
090        this.longText = longText;
091    }
092
093    public int getAssignedKey() {
094        return assignedKey;
095    }
096
097    public int getAssignedModifier() {
098        return assignedModifier;
099    }
100
101    public boolean isAssignedDefault() {
102        return assignedDefault;
103    }
104
105    public boolean isAssignedUser() {
106        return assignedUser;
107    }
108
109    public boolean isAutomatic() {
110        return automatic;
111    }
112
113    public boolean isChangeable() {
114        return !automatic && !"core:none".equals(shortText);
115    }
116
117    private boolean isReset() {
118        return reset;
119    }
120
121    /**
122     * FOR PREF PANE ONLY
123     */
124    public void setAutomatic() {
125        automatic = true;
126    }
127
128    /**
129     * FOR PREF PANE ONLY.<p>
130     * Sets the modifiers that are used.
131     * @param assignedModifier assigned modifier
132     */
133    public void setAssignedModifier(int assignedModifier) {
134        this.assignedModifier = assignedModifier;
135    }
136
137    /**
138     * FOR PREF PANE ONLY.<p>
139     * Sets the key that actually is used.
140     * @param assignedKey assigned key
141     */
142    public void setAssignedKey(int assignedKey) {
143        this.assignedKey = assignedKey;
144    }
145
146    /**
147     * FOR PREF PANE ONLY.<p>
148     * Sets whether the user has changed this shortcut.
149     * @param assignedUser {@code true} if the user has changed this shortcut
150     */
151    public void setAssignedUser(boolean assignedUser) {
152        this.reset = (this.assignedUser || reset) && !assignedUser;
153        if (assignedUser) {
154            assignedDefault = false;
155        } else if (reset) {
156            assignedKey = requestedKey;
157            assignedModifier = findModifier(requestedGroup, null);
158        }
159        this.assignedUser = assignedUser;
160    }
161
162    /**
163     * Use this to register the shortcut with Swing
164     * @return the key stroke
165     */
166    public KeyStroke getKeyStroke() {
167        if (assignedModifier != -1)
168            return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
169        return null;
170    }
171
172    // create a shortcut object from an string as saved in the preferences
173    private Shortcut(String prefString) {
174        List<String> s = new ArrayList<>(Config.getPref().getList(prefString));
175        this.shortText = prefString.substring(15);
176        this.longText = s.get(0);
177        this.requestedKey = Integer.parseInt(s.get(1));
178        this.requestedGroup = Integer.parseInt(s.get(2));
179        this.assignedKey = Integer.parseInt(s.get(3));
180        this.assignedModifier = Integer.parseInt(s.get(4));
181        this.assignedDefault = Boolean.parseBoolean(s.get(5));
182        this.assignedUser = Boolean.parseBoolean(s.get(6));
183    }
184
185    private void saveDefault() {
186        Config.getPref().getList("shortcut.entry."+shortText, Arrays.asList(longText,
187            String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(requestedKey),
188            String.valueOf(getGroupModifier(requestedGroup)), String.valueOf(true), String.valueOf(false)));
189    }
190
191    // get a string that can be put into the preferences
192    private boolean save() {
193        if (isAutomatic() || isReset() || !isAssignedUser()) {
194            return Config.getPref().putList("shortcut.entry."+shortText, null);
195        } else {
196            return Config.getPref().putList("shortcut.entry."+shortText, Arrays.asList(longText,
197                String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(assignedKey),
198                String.valueOf(assignedModifier), String.valueOf(assignedDefault), String.valueOf(assignedUser)));
199        }
200    }
201
202    private boolean isSame(int isKey, int isModifier) {
203        // an unassigned shortcut is different from any other shortcut
204        return isKey == assignedKey && isModifier == assignedModifier && assignedModifier != getGroupModifier(NONE);
205    }
206
207    public boolean isEvent(KeyEvent e) {
208        KeyStroke ks = getKeyStroke();
209        return ks != null && ks.equals(KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiersEx()));
210    }
211
212    /**
213     * use this to set a menu's mnemonic
214     * @param menu menu
215     */
216    public void setMnemonic(JMenu menu) {
217        if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
218            menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
219        }
220    }
221
222    /**
223     * use this to set a buttons's mnemonic
224     * @param button button
225     */
226    public void setMnemonic(AbstractButton button) {
227        if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
228            button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
229        }
230    }
231
232    /**
233     * Sets the mnemonic key on a text component.
234     * @param component component
235     */
236    public void setFocusAccelerator(JTextComponent component) {
237        if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
238            component.setFocusAccelerator(KeyEvent.getKeyText(assignedKey).charAt(0));
239        }
240    }
241
242    /**
243     * use this to set a actions's accelerator
244     * @param action action
245     */
246    public void setAccelerator(AbstractAction action) {
247        if (getKeyStroke() != null) {
248            action.putValue(AbstractAction.ACCELERATOR_KEY, getKeyStroke());
249        }
250    }
251
252    /**
253     * Returns a human readable text for the shortcut.
254     * @return a human readable text for the shortcut
255     */
256    public String getKeyText() {
257        return getKeyText(getKeyStroke());
258    }
259
260    /**
261     * Returns a human readable text for the key stroke.
262     * @param keyStroke key stroke to convert to human readable text
263     * @return a human readable text for the key stroke
264     * @since 12520
265     */
266    public static String getKeyText(KeyStroke keyStroke) {
267        if (keyStroke == null) return "";
268        String modifText = KeyEvent.getModifiersExText(keyStroke.getModifiers());
269        if ("".equals(modifText)) return KeyEvent.getKeyText(keyStroke.getKeyCode());
270        return modifText + '+' + KeyEvent.getKeyText(keyStroke.getKeyCode());
271    }
272
273    /**
274     * Sets the action tooltip to the tooltip text plus the {@linkplain #getKeyText(KeyStroke) key stroke text}
275     * this shortcut represents.
276     *
277     * @param action action
278     * @param tooltip Tooltip text to display
279     * @since 14689
280     */
281    public void setTooltip(Action action, String tooltip) {
282        setTooltip(action, tooltip, getKeyStroke());
283    }
284
285    /**
286     * Sets the action tooltip to the tooltip text plus the {@linkplain #getKeyText(KeyStroke) key stroke text}.
287     *
288     * @param action action
289     * @param tooltip Tooltip text to display
290     * @param keyStroke Key stroke associated (to display accelerator between parenthesis)
291     * @since 14689
292     */
293    public static void setTooltip(Action action, String tooltip, KeyStroke keyStroke) {
294        action.putValue(Action.SHORT_DESCRIPTION, makeTooltip(tooltip, keyStroke));
295    }
296
297    @Override
298    public String toString() {
299        return getKeyText();
300    }
301
302    ///////////////////////////////
303    // everything's static below //
304    ///////////////////////////////
305
306    // here we store our shortcuts
307    private static final ShortcutCollection shortcuts = new ShortcutCollection();
308
309    private static class ShortcutCollection extends CopyOnWriteArrayList<Shortcut> {
310        private static final long serialVersionUID = 1L;
311        @Override
312        public boolean add(Shortcut shortcut) {
313            // expensive consistency check only in debug mode
314            if (Logging.isDebugEnabled()
315                    && stream().map(Shortcut::getShortText).anyMatch(shortcut.getShortText()::equals)) {
316                Logging.warn(new AssertionError(shortcut.getShortText() + " already added"));
317            }
318            return super.add(shortcut);
319        }
320
321        void replace(Shortcut newShortcut) {
322            final Optional<Shortcut> existing = findShortcutByKeyOrShortText(-1, NONE, newShortcut.shortText);
323            if (existing.isPresent()) {
324                replaceAll(sc -> existing.get() == sc ? newShortcut : sc);
325            } else {
326                add(newShortcut);
327            }
328        }
329    }
330
331    // and here our modifier groups
332    private static final Map<Integer, Integer> groups = new HashMap<>();
333
334    // check if something collides with an existing shortcut
335
336    /**
337     * Returns the registered shortcut fot the key and modifier
338     * @param requestedKey the requested key
339     * @param modifier the modifier
340     * @return an {@link Optional} registered shortcut, never {@code null}
341     */
342    public static Optional<Shortcut> findShortcut(int requestedKey, int modifier) {
343        return findShortcutByKeyOrShortText(requestedKey, modifier, null);
344    }
345
346    private static Optional<Shortcut> findShortcutByKeyOrShortText(int requestedKey, int modifier, String shortText) {
347        final Predicate<Shortcut> sameKey = sc -> modifier != getGroupModifier(NONE) && sc.isSame(requestedKey, modifier);
348        final Predicate<Shortcut> sameShortText = sc -> sc.getShortText().equals(shortText);
349        return shortcuts.stream()
350                .filter(sameKey.or(sameShortText))
351                .sorted(Comparator.comparingInt(sc -> sameShortText.test(sc) ? 0 : 1))
352                .findAny();
353    }
354
355    /**
356     * Returns a list of all shortcuts.
357     * @return a list of all shortcuts
358     */
359    public static List<Shortcut> listAll() {
360        return shortcuts.stream()
361                .filter(c -> !"core:none".equals(c.shortText))
362                .collect(Collectors.toList());
363    }
364
365    /** None group: used with KeyEvent.CHAR_UNDEFINED if no shortcut is defined */
366    public static final int NONE = 5000;
367    public static final int MNEMONIC = 5001;
368    /** Reserved group: for system shortcuts only */
369    public static final int RESERVED = 5002;
370    /** Direct group: no modifier */
371    public static final int DIRECT = 5003;
372    /** Alt group */
373    public static final int ALT = 5004;
374    /** Shift group */
375    public static final int SHIFT = 5005;
376    /** Command group. Matches CTRL modifier on Windows/Linux but META modifier on OS X */
377    public static final int CTRL = 5006;
378    /** Alt-Shift group */
379    public static final int ALT_SHIFT = 5007;
380    /** Alt-Command group. Matches ALT-CTRL modifier on Windows/Linux but ALT-META modifier on OS X */
381    public static final int ALT_CTRL = 5008;
382    /** Command-Shift group. Matches CTRL-SHIFT modifier on Windows/Linux but META-SHIFT modifier on OS X */
383    public static final int CTRL_SHIFT = 5009;
384    /** Alt-Command-Shift group. Matches ALT-CTRL-SHIFT modifier on Windows/Linux but ALT-META-SHIFT modifier on OS X */
385    public static final int ALT_CTRL_SHIFT = 5010;
386
387    /* for reassignment */
388    private static final int[] mods = {ALT_CTRL, ALT_SHIFT, CTRL_SHIFT, ALT_CTRL_SHIFT};
389    private static final int[] keys = {KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4,
390                                 KeyEvent.VK_F5, KeyEvent.VK_F6, KeyEvent.VK_F7, KeyEvent.VK_F8,
391                                 KeyEvent.VK_F9, KeyEvent.VK_F10, KeyEvent.VK_F11, KeyEvent.VK_F12};
392
393    // bootstrap
394    private static boolean initdone;
395    private static void doInit() {
396        if (initdone) return;
397        initdone = true;
398        int commandDownMask = PlatformManager.getPlatform().getMenuShortcutKeyMaskEx();
399        groups.put(NONE, -1);
400        groups.put(MNEMONIC, KeyEvent.ALT_DOWN_MASK);
401        groups.put(DIRECT, 0);
402        groups.put(ALT, KeyEvent.ALT_DOWN_MASK);
403        groups.put(SHIFT, KeyEvent.SHIFT_DOWN_MASK);
404        groups.put(CTRL, commandDownMask);
405        groups.put(ALT_SHIFT, KeyEvent.ALT_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
406        groups.put(ALT_CTRL, KeyEvent.ALT_DOWN_MASK | commandDownMask);
407        groups.put(CTRL_SHIFT, commandDownMask | KeyEvent.SHIFT_DOWN_MASK);
408        groups.put(ALT_CTRL_SHIFT, KeyEvent.ALT_DOWN_MASK | commandDownMask | KeyEvent.SHIFT_DOWN_MASK);
409
410        // (1) System reserved shortcuts
411        PlatformManager.getPlatform().initSystemShortcuts();
412        // (2) User defined shortcuts
413        Preferences.main().getAllPrefixCollectionKeys("shortcut.entry.").stream()
414                .map(Shortcut::new)
415                .filter(sc -> !findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()).isPresent())
416                .sorted(Comparator.comparing(sc -> sc.isAssignedUser() ? 1 : sc.isAssignedDefault() ? 2 : 3))
417                .forEachOrdered(shortcuts::replace);
418    }
419
420    private static int getGroupModifier(int group) {
421        return Optional.ofNullable(groups.get(group)).orElse(-1);
422    }
423
424    private static int findModifier(int group, Integer modifier) {
425        if (modifier == null) {
426            modifier = getGroupModifier(group);
427            if (modifier == null) { // garbage in, no shortcut out
428                modifier = getGroupModifier(NONE);
429            }
430        }
431        return modifier;
432    }
433
434    // shutdown handling
435    public static boolean savePrefs() {
436        return shortcuts.stream()
437                .map(Shortcut::save)
438                .reduce(Boolean.FALSE, Boolean::logicalOr); // has changed
439    }
440
441    /**
442     * FOR PLATFORMHOOK USE ONLY.
443     * <p>
444     * This registers a system shortcut. See PlatformHook for details.
445     * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
446     * @param longText this will be displayed in the shortcut preferences dialog. Better
447     * use something the user will recognize...
448     * @param key the key. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
449     * @param modifier the modifier. Use a {@link KeyEvent KeyEvent.*_MASK} constant here.
450     * @return the system shortcut
451     */
452    public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
453        final Optional<Shortcut> existing = findShortcutByKeyOrShortText(key, modifier, shortText);
454        if (existing.isPresent() && shortText.equals(existing.get().getShortText())) {
455            return existing.get();
456        } else if (existing.isPresent()) {
457            // this always is a logic error in the hook
458            Logging.error("CONFLICT WITH SYSTEM KEY " + shortText + ": " + existing.get());
459            return null;
460        }
461        final Shortcut shortcut = new Shortcut(shortText, longText, key, RESERVED, key, modifier, true, false);
462        shortcuts.add(shortcut);
463        return shortcut;
464    }
465
466    /**
467     * Register a shortcut linked to several characters.
468     *
469     * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
470     * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
471     * actions that are part of JOSM's core. Use something like
472     * {@code <pluginname>+":"+<actionname>}.
473     * @param longText this will be displayed in the shortcut preferences dialog. Better
474     * use something the user will recognize...
475     * @param characters the characters you'd prefer
476     * @param requestedGroup the group this shortcut fits best. This will determine the
477     * modifiers your shortcut will get assigned. Use the constants defined above.
478     * @return the shortcut
479     */
480    public static List<Shortcut> registerMultiShortcuts(String shortText, String longText, List<Character> characters, int requestedGroup) {
481        List<Shortcut> result = new ArrayList<>();
482        int i = 1;
483        Map<Integer, Integer> regularKeyCodes = KeyboardUtils.getRegularKeyCodesMap();
484        for (Character c : characters) {
485            Integer code = (int) c;
486            result.add(registerShortcut(
487                    new StringBuilder(shortText).append(" (").append(i).append(')').toString(), longText,
488                    // Add extended keyCode if not a regular one
489                    regularKeyCodes.containsKey(code) ? regularKeyCodes.get(code) :
490                        isDeadKey(code) ? code : c | KeyboardUtils.EXTENDED_KEYCODE_FLAG,
491                    requestedGroup));
492            i++;
493        }
494        return result;
495    }
496
497    static boolean isDeadKey(int keyCode) {
498        return KeyEvent.VK_DEAD_GRAVE <= keyCode && keyCode <= KeyEvent.VK_DEAD_SEMIVOICED_SOUND;
499    }
500
501    /**
502     * Register a shortcut.
503     *
504     * Here you get your shortcuts from. The parameters are:
505     *
506     * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
507     * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
508     * actions that are part of JOSM's core. Use something like
509     * {@code <pluginname>+":"+<actionname>}.
510     * @param longText this will be displayed in the shortcut preferences dialog. Better
511     * use something the user will recognize...
512     * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
513     * @param requestedGroup the group this shortcut fits best. This will determine the
514     * modifiers your shortcut will get assigned. Use the constants defined above.
515     * @return the shortcut
516     */
517    public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
518        return registerShortcut(shortText, longText, requestedKey, requestedGroup, null);
519    }
520
521    // and now the workhorse. same parameters as above, just one more
522    private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier) {
523        doInit();
524        Integer defaultModifier = findModifier(requestedGroup, modifier);
525        final Optional<Shortcut> existing = findShortcutByKeyOrShortText(requestedKey, defaultModifier, shortText);
526        if (existing.isPresent() && shortText.equals(existing.get().getShortText())) {
527            // a re-register? maybe a sc already read from the preferences?
528            final Shortcut sc = existing.get();
529            sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
530            sc.saveDefault();
531            return sc;
532        } else if (existing.isPresent()) {
533            final Shortcut conflict = existing.get();
534            if (PlatformManager.isPlatformOsx()) {
535                // Try to reassign Meta to Ctrl
536                int newmodifier = findNewOsxModifier(requestedGroup);
537                if (!findShortcut(requestedKey, newmodifier).isPresent()) {
538                    Logging.info("Reassigning macOS shortcut '" + shortText + "' from Meta to Ctrl because of conflict with " + conflict);
539                    return reassignShortcut(shortText, longText, requestedKey, conflict, requestedGroup, requestedKey, newmodifier);
540                }
541            }
542            for (int m : mods) {
543                for (int k : keys) {
544                    int newmodifier = getGroupModifier(m);
545                    if (!findShortcut(k, newmodifier).isPresent()) {
546                        Logging.info("Reassigning shortcut '" + shortText + "' from " + modifier + " to " + newmodifier +
547                                " because of conflict with " + conflict);
548                        return reassignShortcut(shortText, longText, requestedKey, conflict, m, k, newmodifier);
549                    }
550                }
551            }
552        } else {
553            Shortcut newsc = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
554            newsc.saveDefault();
555            shortcuts.add(newsc);
556            return newsc;
557        }
558
559        return null;
560    }
561
562    private static int findNewOsxModifier(int requestedGroup) {
563        switch (requestedGroup) {
564            case CTRL: return KeyEvent.CTRL_DOWN_MASK;
565            case ALT_CTRL: return KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK;
566            case CTRL_SHIFT: return KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK;
567            case ALT_CTRL_SHIFT: return KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK;
568            default: return 0;
569        }
570    }
571
572    private static Shortcut reassignShortcut(String shortText, String longText, int requestedKey, Shortcut conflict,
573            int m, int k, int newmodifier) {
574        Shortcut newsc = new Shortcut(shortText, longText, requestedKey, m, k, newmodifier, false, false);
575        Logging.info(tr("Silent shortcut conflict: ''{0}'' moved by ''{1}'' to ''{2}''.",
576            shortText, conflict.getShortText(), newsc.getKeyText()));
577        newsc.saveDefault();
578        shortcuts.add(newsc);
579        return newsc;
580    }
581
582    /**
583     * Replies the platform specific key stroke for the 'Copy' command, i.e.
584     * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
585     * copy command isn't known.
586     *
587     * @return the platform specific key stroke for the  'Copy' command
588     */
589    public static KeyStroke getCopyKeyStroke() {
590        return getKeyStrokeForShortKey("system:copy");
591    }
592
593    /**
594     * Replies the platform specific key stroke for the 'Paste' command, i.e.
595     * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
596     * paste command isn't known.
597     *
598     * @return the platform specific key stroke for the 'Paste' command
599     */
600    public static KeyStroke getPasteKeyStroke() {
601        return getKeyStrokeForShortKey("system:paste");
602    }
603
604    /**
605     * Replies the platform specific key stroke for the 'Cut' command, i.e.
606     * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
607     * 'Cut' command isn't known.
608     *
609     * @return the platform specific key stroke for the 'Cut' command
610     */
611    public static KeyStroke getCutKeyStroke() {
612        return getKeyStrokeForShortKey("system:cut");
613    }
614
615    private static KeyStroke getKeyStrokeForShortKey(String shortKey) {
616        return shortcuts.stream()
617                .filter(sc -> shortKey.equals(sc.getShortText()))
618                .findAny()
619                .map(Shortcut::getKeyStroke)
620                .orElse(null);
621    }
622
623    /**
624     * Returns the tooltip text plus the {@linkplain #getKeyText(KeyStroke) key stroke text}.
625     *
626     * Tooltips are usually not system dependent, unless the
627     * JVM is too dumb to provide correct names for all the keys.
628     *
629     * Some LAFs don't understand HTML, such as the OSX LAFs.
630     *
631     * @param tooltip Tooltip text to display
632     * @param keyStroke Key stroke associated (to display accelerator between parenthesis)
633     * @return Full tooltip text (tooltip + accelerator)
634     * @since 14689
635     */
636    public static String makeTooltip(String tooltip, KeyStroke keyStroke) {
637        final Optional<String> keyStrokeText = Optional.ofNullable(keyStroke)
638                .map(Shortcut::getKeyText)
639                .filter(text -> !text.isEmpty());
640
641        final boolean canHtml = PlatformManager.getPlatform().isHtmlSupportedInMenuTooltips();
642
643        StringBuilder result = new StringBuilder(48);
644        if (canHtml) {
645            result.append("<html>");
646        }
647        result.append(tooltip);
648        if (keyStrokeText.isPresent()) {
649            result.append(' ');
650            if (canHtml) {
651                result.append("<font size='-2'>");
652            }
653            result.append('(').append(keyStrokeText.get()).append(')');
654            if (canHtml) {
655                result.append("</font>");
656            }
657        }
658        if (canHtml) {
659            result.append("&nbsp;</html>");
660        }
661        return result.toString();
662    }
663}