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; 021 022/** 023 * Global shortcut class. 024 * 025 * Note: This class represents a single shortcut, contains the factory to obtain 026 * shortcut objects from, manages shortcuts and shortcut collisions, and 027 * finally manages loading and saving shortcuts to/from the preferences. 028 * 029 * Action authors: You only need the {@link #registerShortcut} factory. Ignore everything 030 * 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 * 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 && !shortText.equals("core:none"); 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<String>(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<String, Shortcut>(); 235 236 // and here our modifier groups 237 private static Map<Integer, Integer> groups= new HashMap<Integer, Integer>(); 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<Shortcut>(); 255 for(Shortcut c : shortcuts.values()) 256 { 257 if(!c.shortText.equals("core:none")) { 258 l.add(c); 259 } 260 } 261 return l; 262 } 263 264 public static final int NONE = 5000; 265 public static final int MNEMONIC = 5001; 266 public static final int RESERVED = 5002; 267 public static final int DIRECT = 5003; 268 public static final int ALT = 5004; 269 public static final int SHIFT = 5005; 270 public static final int CTRL = 5006; 271 public static final int ALT_SHIFT = 5007; 272 public static final int ALT_CTRL = 5008; 273 public static final int CTRL_SHIFT = 5009; 274 public static final int ALT_CTRL_SHIFT = 5010; 275 276 /* for reassignment */ 277 private static int[] mods = {ALT_CTRL, ALT_SHIFT, CTRL_SHIFT, ALT_CTRL_SHIFT}; 278 private static int[] keys = {KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4, 279 KeyEvent.VK_F5, KeyEvent.VK_F6, KeyEvent.VK_F7, KeyEvent.VK_F8, 280 KeyEvent.VK_F9, KeyEvent.VK_F10, KeyEvent.VK_F11, KeyEvent.VK_F12}; 281 282 // bootstrap 283 private static boolean initdone = false; 284 private static void doInit() { 285 if (initdone) return; 286 initdone = true; 287 groups.put(NONE, -1); 288 groups.put(MNEMONIC, KeyEvent.ALT_DOWN_MASK); 289 groups.put(DIRECT, 0); 290 groups.put(ALT, KeyEvent.ALT_DOWN_MASK); 291 groups.put(SHIFT, KeyEvent.SHIFT_DOWN_MASK); 292 groups.put(CTRL, KeyEvent.CTRL_DOWN_MASK); 293 groups.put(ALT_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK); 294 groups.put(ALT_CTRL, KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK); 295 groups.put(CTRL_SHIFT, KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK); 296 groups.put(ALT_CTRL_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK); 297 298 // (1) System reserved shortcuts 299 Main.platform.initSystemShortcuts(); 300 // (2) User defined shortcuts 301 LinkedList<Shortcut> newshortcuts = new LinkedList<Shortcut>(); 302 for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) { 303 newshortcuts.add(new Shortcut(s)); 304 } 305 306 for(Shortcut sc : newshortcuts) { 307 if (sc.getAssignedUser() 308 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) { 309 shortcuts.put(sc.getShortText(), sc); 310 } 311 } 312 // Shortcuts at their default values 313 for(Shortcut sc : newshortcuts) { 314 if (!sc.getAssignedUser() && sc.getAssignedDefault() 315 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) { 316 shortcuts.put(sc.getShortText(), sc); 317 } 318 } 319 // Shortcuts that were automatically moved 320 for(Shortcut sc : newshortcuts) { 321 if (!sc.getAssignedUser() && !sc.getAssignedDefault() 322 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) { 323 shortcuts.put(sc.getShortText(), sc); 324 } 325 } 326 } 327 328 private static int getGroupModifier(int group) { 329 Integer m = groups.get(group); 330 if(m == null) 331 m = -1; 332 return m; 333 } 334 335 private static int findModifier(int group, Integer modifier) { 336 if(modifier == null) { 337 modifier = getGroupModifier(group); 338 if (modifier == null) { // garbage in, no shortcut out 339 modifier = getGroupModifier(NONE); 340 } 341 } 342 return modifier; 343 } 344 345 // shutdown handling 346 public static boolean savePrefs() { 347 boolean changed = false; 348 for (Shortcut sc : shortcuts.values()) { 349 changed = changed | sc.save(); 350 } 351 return changed; 352 } 353 354 /** 355 * FOR PLATFORMHOOK USE ONLY 356 * 357 * This registers a system shortcut. See PlatformHook for details. 358 */ 359 public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) { 360 if (shortcuts.containsKey(shortText)) 361 return shortcuts.get(shortText); 362 Shortcut potentialShortcut = findShortcut(key, modifier); 363 if (potentialShortcut != null) { 364 // this always is a logic error in the hook 365 Main.error("CONFLICT WITH SYSTEM KEY "+shortText); 366 return null; 367 } 368 potentialShortcut = new Shortcut(shortText, longText, key, RESERVED, key, modifier, true, false); 369 shortcuts.put(shortText, potentialShortcut); 370 return potentialShortcut; 371 } 372 373 /** 374 * Register a shortcut. 375 * 376 * Here you get your shortcuts from. The parameters are: 377 * 378 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique. 379 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for 380 * actions that are part of JOSM's core. Use something like 381 * {@code <pluginname>+":"+<actionname>}. 382 * @param longText this will be displayed in the shortcut preferences dialog. Better 383 * use something the user will recognize... 384 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here. 385 * @param requestedGroup the group this shortcut fits best. This will determine the 386 * modifiers your shortcut will get assigned. Use the constants defined above. 387 */ 388 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) { 389 return registerShortcut(shortText, longText, requestedKey, requestedGroup, null); 390 } 391 392 // and now the workhorse. same parameters as above, just one more 393 private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier) { 394 doInit(); 395 Integer defaultModifier = findModifier(requestedGroup, modifier); 396 if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences? 397 Shortcut sc = shortcuts.get(shortText); 398 sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action 399 sc.saveDefault(); 400 return sc; 401 } 402 Shortcut conflict = findShortcut(requestedKey, defaultModifier); 403 if (conflict != null) { 404 for (int m : mods) { 405 for (int k : keys) { 406 int newmodifier = getGroupModifier(m); 407 if ( findShortcut(k, newmodifier) == null ) { 408 Shortcut newsc = new Shortcut(shortText, longText, requestedKey, m, k, newmodifier, false, false); 409 Main.info(tr("Silent shortcut conflict: ''{0}'' moved by ''{1}'' to ''{2}''.", 410 shortText, conflict.getShortText(), newsc.getKeyText())); 411 newsc.saveDefault(); 412 shortcuts.put(shortText, newsc); 413 return newsc; 414 } 415 } 416 } 417 } else { 418 Shortcut newsc = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false); 419 newsc.saveDefault(); 420 shortcuts.put(shortText, newsc); 421 return newsc; 422 } 423 424 return null; 425 } 426 427 /** 428 * Replies the platform specific key stroke for the 'Copy' command, i.e. 429 * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific 430 * copy command isn't known. 431 * 432 * @return the platform specific key stroke for the 'Copy' command 433 */ 434 static public KeyStroke getCopyKeyStroke() { 435 Shortcut sc = shortcuts.get("system:copy"); 436 if (sc == null) return null; 437 return sc.getKeyStroke(); 438 } 439 440 /** 441 * Replies the platform specific key stroke for the 'Paste' command, i.e. 442 * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific 443 * paste command isn't known. 444 * 445 * @return the platform specific key stroke for the 'Paste' command 446 */ 447 static public KeyStroke getPasteKeyStroke() { 448 Shortcut sc = shortcuts.get("system:paste"); 449 if (sc == null) return null; 450 return sc.getKeyStroke(); 451 } 452 453 /** 454 * Replies the platform specific key stroke for the 'Cut' command, i.e. 455 * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific 456 * 'Cut' command isn't known. 457 * 458 * @return the platform specific key stroke for the 'Cut' command 459 */ 460 static public KeyStroke getCutKeyStroke() { 461 Shortcut sc = shortcuts.get("system:cut"); 462 if (sc == null) return null; 463 return sc.getKeyStroke(); 464 } 465}