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