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(" </html>"); 660 } 661 return result.toString(); 662 } 663}