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