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