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