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