source: josm/trunk/src/org/openstreetmap/josm/tools/Shortcut.java@ 4928

Last change on this file since 4928 was 4928, checked in by stoecker, 12 years ago

fix group numbers, so they stay unique also after adding new groups

  • Property svn:eol-style set to native
File size: 22.8 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.KeyEvent;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.HashMap;
11import java.util.LinkedHashMap;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Map;
15
16import javax.swing.AbstractAction;
17import javax.swing.AbstractButton;
18import javax.swing.JMenu;
19import javax.swing.JOptionPane;
20import javax.swing.KeyStroke;
21
22import org.openstreetmap.josm.Main;
23
24/**
25 * Global shortcut class.
26 *
27 * Note: This class represents a single shortcut, contains the factory to obtain
28 * shortcut objects from, manages shortcuts and shortcut collisions, and
29 * finally manages loading and saving shortcuts to/from the preferences.
30 *
31 * Action authors: You only need the {@see #registerShortcut} factory. Ignore everything
32 * else.
33 *
34 * All: Use only public methods that are also marked to be used. The others are
35 * public so the shortcut preferences can use them.
36 *
37 */
38public class Shortcut {
39 @Deprecated
40 public static final int SHIFT_DEFAULT = 1;
41 private String shortText; // the unique ID of the shortcut
42 private String longText; // a human readable description that will be shown in the preferences
43 private int requestedKey; // the key, the caller requested
44 private int requestedGroup; // the group, the caller requested
45 private int assignedKey; // the key that actually is used
46 private int assignedModifier; // the modifiers that are used
47 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.)
48 private boolean assignedUser; // true if the user changed this shortcut
49 private boolean automatic; // true if the user cannot change this shortcut (Note: it also will not be saved into the preferences)
50 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)
51
52 // simple constructor
53 private Shortcut(String shortText, String longText, int requestedKey, int requestedGroup, int assignedKey, int assignedModifier, boolean assignedDefault, boolean assignedUser) {
54 this.shortText = shortText;
55 this.longText = longText;
56 this.requestedKey = requestedKey;
57 this.requestedGroup = requestedGroup;
58 this.assignedKey = assignedKey;
59 this.assignedModifier = assignedModifier;
60 this.assignedDefault = assignedDefault;
61 this.assignedUser = assignedUser;
62 this.automatic = false;
63 this.reset = false;
64 }
65
66 public String getShortText() {
67 return shortText;
68 }
69
70 public String getLongText() {
71 return longText;
72 }
73
74 // a shortcut will be renamed when it is handed out again, because the original name
75 // may be a dummy
76 private void setLongText(String longText) {
77 this.longText = longText;
78 }
79
80 private int getRequestedKey() {
81 return requestedKey;
82 }
83
84 public int getRequestedGroup() {
85 return requestedGroup;
86 }
87
88 public int getAssignedKey() {
89 return assignedKey;
90 }
91
92 public int getAssignedModifier() {
93 return assignedModifier;
94 }
95
96 public boolean getAssignedDefault() {
97 return assignedDefault;
98 }
99
100 public boolean getAssignedUser() {
101 return assignedUser;
102 }
103
104 public boolean getAutomatic() {
105 return automatic;
106 }
107
108 public boolean isChangeable() {
109 return !automatic && !shortText.equals("core:none");
110 }
111
112 private boolean getReset() {
113 return reset;
114 }
115
116 /**
117 * FOR PREF PANE ONLY
118 */
119 public void setAutomatic() {
120 automatic = true;
121 }
122
123 /**
124 * FOR PREF PANE ONLY
125 */
126 public void setAssignedModifier(int assignedModifier) {
127 this.assignedModifier = assignedModifier;
128 }
129
130 /**
131 * FOR PREF PANE ONLY
132 */
133 public void setAssignedKey(int assignedKey) {
134 this.assignedKey = assignedKey;
135 }
136
137 /**
138 * FOR PREF PANE ONLY
139 */
140 public void setAssignedUser(boolean assignedUser) {
141 this.reset = (this.assignedUser || reset) && !assignedUser;
142 if (assignedUser) {
143 assignedDefault = false;
144 } else if (reset) {
145 assignedKey = requestedKey;
146 assignedModifier = findModifier(requestedGroup, null);
147 }
148 this.assignedUser = assignedUser;
149 }
150
151 /**
152 * Use this to register the shortcut with Swing
153 */
154 public KeyStroke getKeyStroke() {
155 if (assignedModifier != -1)
156 return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
157 return null;
158 }
159
160 private boolean isSame(int isKey, int isModifier) {
161 // -1 --- an unassigned shortcut is different from any other shortcut
162 return( isKey == assignedKey && isModifier == assignedModifier && assignedModifier != getGroupModifier(GROUP_NONE));
163 }
164
165 // create a shortcut object from an string as saved in the preferences
166 private Shortcut(String prefString) {
167 ArrayList<String> s = (new ArrayList<String>(Main.pref.getCollection(prefString)));
168 this.shortText = prefString.substring(15);
169 this.longText = s.get(0);
170 this.requestedKey = Integer.parseInt(s.get(1));
171 this.requestedGroup = Integer.parseInt(s.get(2));
172 this.assignedKey = Integer.parseInt(s.get(3));
173 this.assignedModifier = Integer.parseInt(s.get(4));
174 this.assignedDefault = Boolean.parseBoolean(s.get(5));
175 this.assignedUser = Boolean.parseBoolean(s.get(6));
176 }
177
178 private void saveDefault(int modifier) {
179 Main.pref.getCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
180 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(requestedKey),
181 String.valueOf(modifier), String.valueOf(true), String.valueOf(false)}));
182 }
183
184 // get a string that can be put into the preferences
185 private boolean save() {
186 if (getAutomatic() || getReset() || !getAssignedUser()) {
187 return Main.pref.putCollection("shortcut.entry."+shortText, null);
188 } else {
189 return Main.pref.putCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
190 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(assignedKey),
191 String.valueOf(assignedModifier), String.valueOf(assignedDefault), String.valueOf(assignedUser)}));
192 }
193 }
194
195 private boolean isSame(Shortcut other) {
196 return assignedKey == other.assignedKey && assignedModifier == other.assignedModifier;
197 }
198
199 /**
200 * use this to set a menu's mnemonic
201 */
202 public void setMnemonic(JMenu menu) {
203 if (requestedGroup == GROUP_MNEMONIC && assignedModifier == getGroupModifier(requestedGroup + GROUPS_DEFAULT) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
204 menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
205 }
206 }
207 /**
208 * use this to set a buttons's mnemonic
209 */
210 public void setMnemonic(AbstractButton button) {
211 if (requestedGroup == GROUP_MNEMONIC && assignedModifier == getGroupModifier(requestedGroup + GROUPS_DEFAULT) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
212 button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
213 }
214 }
215 /**
216 * use this to set a actions's accelerator
217 */
218 public void setAccelerator(AbstractAction action) {
219 if (getKeyStroke() != null) {
220 action.putValue(AbstractAction.ACCELERATOR_KEY, getKeyStroke());
221 }
222 }
223
224 /**
225 * use this to get a human readable text for your shortcut
226 */
227 public String getKeyText() {
228 KeyStroke keyStroke = getKeyStroke();
229 if (keyStroke == null) return "";
230 String modifText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
231 if ("".equals (modifText)) return KeyEvent.getKeyText (keyStroke.getKeyCode ());
232 return modifText + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode ());
233 }
234
235 @Override
236 public String toString() {
237 return getKeyText();
238 }
239
240 ///////////////////////////////
241 // everything's static below //
242 ///////////////////////////////
243
244 // here we store our shortcuts
245 private static Map<String, Shortcut> shortcuts = new LinkedHashMap<String, Shortcut>();
246
247 // and here our modifier groups
248 private static Map<Integer, Integer> groups;
249
250 // check if something collides with an existing shortcut
251 private static Shortcut findShortcut(int requestedKey, int modifier) {
252 if (modifier == getGroupModifier(GROUP_NONE))
253 return null;
254 for (Shortcut sc : shortcuts.values()) {
255 if (sc.isSame(requestedKey, modifier))
256 return sc;
257 }
258 return null;
259 }
260
261 /**
262 * FOR PREF PANE ONLY
263 */
264 public static List<Shortcut> listAll() {
265 List<Shortcut> l = new ArrayList<Shortcut>();
266 for(Shortcut c : shortcuts.values())
267 {
268 if(!c.shortText.equals("core:none")) {
269 l.add(c);
270 }
271 }
272 return l;
273 }
274
275 // try to find an unused shortcut
276 private static Shortcut findRandomShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
277 int[] mods = {getGroupModifier(requestedGroup + GROUPS_DEFAULT), getGroupModifier(requestedGroup + GROUPS_ALT1), getGroupModifier(requestedGroup + GROUPS_ALT2)};
278 for (int m : mods) {
279 for (int k = KeyEvent.VK_A; k < KeyEvent.VK_Z; k++) { // we'll limit ourself to 100% safe keys
280 if ( findShortcut(k, m) == null )
281 return new Shortcut(shortText, longText, requestedKey, requestedGroup, k, m, false, false);
282 }
283 }
284 return new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, getGroupModifier(GROUP_NONE), false, false);
285 }
286
287 // use these constants to request shortcuts
288 /**
289 * no shortcut.
290 */
291 public static final int GROUP_NONE = 0;
292 /**
293 * a button action, will use another modifier than MENU on system with a meta key.
294 */
295 public static final int GROUP_HOTKEY = 1;
296 /**
297 * a menu action, e.g. "ctrl-e" (export).
298 */
299 public static final int GROUP_MENU = 2;
300 /**
301 * direct edit key, e.g. "a" (add).
302 */
303 public static final int GROUP_EDIT = 3;
304 /**
305 * toggle one of the right-hand-side windows, e.g. "alt-l" (layers).
306 */
307 public static final int GROUP_LAYER = 4;
308 /**
309 * for non-letter keys, preferable without modifier, e.g. F5.
310 */
311 public static final int GROUP_DIRECT = 5;
312 /**
313 * for use with {@see #setMnemonic} only!
314 */
315 public static final int GROUP_MNEMONIC = 6;
316 /**
317 * for direct access, with alt modifier.
318 */
319 public static final int GROUP_DIRECT2 = 7;
320 /**
321 * for direct access, remaining modifiers.
322 */
323 public static final int GROUP_DIRECT3 = 8;
324 public static final int GROUP_RESERVED = 1000;
325 public static final int GROUPS_DEFAULT = 0;
326 public static final int GROUPS_ALT1 = 100;
327 public static final int GROUPS_ALT2 = 200;
328
329 // bootstrap
330 private static boolean initdone = false;
331 private static void doInit() {
332 if (initdone) return;
333 initdone = true;
334 groups = Main.platform.initShortcutGroups(true);
335 // (1) System reserved shortcuts
336 Main.platform.initSystemShortcuts();
337 // (2) User defined shortcuts
338 LinkedList<Shortcut> shortcuts = new LinkedList<Shortcut>();
339 for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
340 shortcuts.add(new Shortcut(s));
341 }
342 for(Shortcut sc : shortcuts) {
343 if (sc.getAssignedUser()) {
344 registerShortcut(sc);
345 }
346 }
347 // Shortcuts at their default values
348 for(Shortcut sc : shortcuts) {
349 if (!sc.getAssignedUser() && sc.getAssignedDefault()) {
350 registerShortcut(sc);
351 }
352 }
353 // Shortcuts that were automatically moved
354 for(Shortcut sc : shortcuts) {
355 if (!sc.getAssignedUser() && !sc.getAssignedDefault()) {
356 registerShortcut(sc);
357 }
358 }
359 }
360
361 private static int getGroupModifier(int group) {
362 Integer m = groups.get(group);
363 if(m == null)
364 m = -1;
365 return m;
366 }
367
368 // shutdown handling
369 public static boolean savePrefs() {
370 boolean changed = false;
371 for (Shortcut sc : shortcuts.values()) {
372 changed = changed | sc.save();
373 }
374 return changed;
375 }
376
377 // this is used to register a shortcut that was read from the preferences
378 private static void registerShortcut(Shortcut sc) {
379 // put a user configured shortcut in as-is -- unless there's a conflict
380 if(sc.getAssignedUser() && findShortcut(sc.getAssignedKey(),
381 sc.getAssignedModifier()) == null) {
382 shortcuts.put(sc.getShortText(), sc);
383 } else {
384 registerShortcut(sc.getShortText(), sc.getLongText(), sc.getRequestedKey(),
385 sc.getRequestedGroup(), sc.getAssignedModifier(), sc);
386 }
387 }
388
389 /**
390 * FOR PLATFORMHOOK USE ONLY
391 *
392 * This registers a system shortcut. See PlatformHook for details.
393 */
394 public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
395 if (shortcuts.containsKey(shortText))
396 return shortcuts.get(shortText);
397 Shortcut potentialShortcut = findShortcut(key, modifier);
398 if (potentialShortcut != null) {
399 // this always is a logic error in the hook
400 System.err.println("CONFLICT WITH SYSTEM KEY "+shortText);
401 return null;
402 }
403 potentialShortcut = new Shortcut(shortText, longText, key, GROUP_RESERVED, key, modifier, true, false);
404 shortcuts.put(shortText, potentialShortcut);
405 return potentialShortcut;
406 }
407
408 /**
409 * Register a shortcut.
410 *
411 * Here you get your shortcuts from. The parameters are:
412 *
413 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
414 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
415 * actions that are part of JOSM's core. Use something like
416 * {@code <pluginname>+":"+<actionname>}.
417 * @param longText this will be displayed in the shortcut preferences dialog. Better
418 * use something the user will recognize...
419 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
420 * @param requestedGroup the group this shortcut fits best. This will determine the
421 * modifiers your shortcut will get assigned. Use the {@code GROUP_*}
422 * constants defined above.
423 * @param modifier to register a {@code ctrl+shift} command, use {@see #SHIFT_DEFAULT}.
424 */
425 @Deprecated
426 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, int modifier) {
427 return registerShortcut(shortText, longText, requestedKey, requestedGroup, modifier, null);
428 }
429
430 /**
431 * Register a shortcut.
432 *
433 * Here you get your shortcuts from. The parameters are:
434 *
435 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
436 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
437 * actions that are part of JOSM's core. Use something like
438 * {@code <pluginname>+":"+<actionname>}.
439 * @param longText this will be displayed in the shortcut preferences dialog. Better
440 * use something the user will recognize...
441 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
442 * @param requestedGroup the group this shortcut fits best. This will determine the
443 * modifiers your shortcut will get assigned. Use the {@code GROUP_*}
444 * constants defined above.
445 */
446 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
447 return registerShortcut(shortText, longText, requestedKey, requestedGroup, null, null);
448 }
449
450 private static int findModifier(int group, Integer modifier) {
451 Integer defaultModifier = getGroupModifier(group);
452 if(modifier != null) {
453 if(modifier == SHIFT_DEFAULT) {
454 defaultModifier |= KeyEvent.SHIFT_DOWN_MASK;
455 } else {
456 defaultModifier = modifier;
457 }
458 }
459 else if (defaultModifier == null) { // garbage in, no shortcut out
460 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
461 }
462 return defaultModifier;
463 }
464
465 // and now the workhorse. same parameters as above, just one more: if originalShortcut is not null and
466 // is different from the shortcut that will be assigned, a popup warning will be displayed to the user.
467 // This is used when registering shortcuts that have been visible to the user before (read: have been
468 // read from the preferences file). New shortcuts will never warn, even when they land on some funny
469 // random fallback key like Ctrl+Alt+Shift+Z for "File Open..." <g>
470 private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier,
471 Shortcut originalShortcut) {
472 doInit();
473 Integer defaultModifier = findModifier(requestedGroup, modifier);
474 if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
475 Shortcut sc = shortcuts.get(shortText);
476 sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
477 sc.saveDefault(defaultModifier);
478 return sc;
479 }
480 Shortcut conflictsWith = null;
481 Shortcut potentialShortcut = findShortcut(requestedKey, defaultModifier);
482 if (potentialShortcut != null) { // 3 stage conflict handling
483 conflictsWith = potentialShortcut;
484 defaultModifier = getGroupModifier(requestedGroup + GROUPS_ALT1);
485 if (defaultModifier == null) { // garbage in, no shortcut out
486 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
487 }
488 potentialShortcut = findShortcut(requestedKey, defaultModifier);
489 if (potentialShortcut != null) {
490 defaultModifier = getGroupModifier(requestedGroup + GROUPS_ALT2);
491 if (defaultModifier == null) { // garbage in, no shortcut out
492 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
493 }
494 potentialShortcut = findShortcut(requestedKey, defaultModifier);
495 if (potentialShortcut != null) { // if all 3 modifiers for a group are used, we give up
496 potentialShortcut = findRandomShortcut(shortText, longText, requestedKey, requestedGroup);
497 } else {
498 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
499 }
500 } else {
501 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
502 }
503 if (originalShortcut != null && !originalShortcut.isSame(potentialShortcut)) {
504 displayWarning(conflictsWith, potentialShortcut, shortText, longText);
505 } else if (originalShortcut == null) {
506 System.out.println("Silent shortcut conflict: '"+shortText+"' moved by '"+conflictsWith.getShortText()+"' to '"+potentialShortcut.getKeyText()+"'.");
507 }
508 } else {
509 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
510 }
511
512 potentialShortcut.saveDefault(defaultModifier);
513 shortcuts.put(shortText, potentialShortcut);
514 return potentialShortcut;
515 }
516
517 // a lengthy warning message
518 private static void displayWarning(Shortcut conflictsWith, Shortcut potentialShortcut, String shortText, String longText) {
519 JOptionPane.showMessageDialog(Main.parent,
520 tr("Setting the keyboard shortcut ''{0}'' for the action ''{1}'' ({2}) failed\n"+
521 "because the shortcut is already taken by the action ''{3}'' ({4}).\n\n",
522 conflictsWith.getKeyText(), longText, shortText,
523 conflictsWith.getLongText(), conflictsWith.getShortText())+
524 (potentialShortcut.getKeyText().equals("") ?
525 tr("This action will have no shortcut.\n\n")
526 :
527 tr("Using the shortcut ''{0}'' instead.\n\n", potentialShortcut.getKeyText())
528 )+
529 tr("(Hint: You can edit the shortcuts in the preferences.)"),
530 tr("Error"),
531 JOptionPane.ERROR_MESSAGE
532 );
533 }
534
535 /**
536 * Replies the platform specific key stroke for the 'Copy' command, i.e.
537 * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
538 * copy command isn't known.
539 *
540 * @return the platform specific key stroke for the 'Copy' command
541 */
542 static public KeyStroke getCopyKeyStroke() {
543 Shortcut sc = shortcuts.get("system:copy");
544 if (sc == null) return null;
545 return sc.getKeyStroke();
546 }
547
548 /**
549 * Replies the platform specific key stroke for the 'Paste' command, i.e.
550 * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
551 * paste command isn't known.
552 *
553 * @return the platform specific key stroke for the 'Paste' command
554 */
555 static public KeyStroke getPasteKeyStroke() {
556 Shortcut sc = shortcuts.get("system:paste");
557 if (sc == null) return null;
558 return sc.getKeyStroke();
559 }
560
561 /**
562 * Replies the platform specific key stroke for the 'Cut' command, i.e.
563 * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
564 * 'Cut' command isn't known.
565 *
566 * @return the platform specific key stroke for the 'Cut' command
567 */
568 static public KeyStroke getCutKeyStroke() {
569 Shortcut sc = shortcuts.get("system:cut");
570 if (sc == null) return null;
571 return sc.getKeyStroke();
572 }
573}
Note: See TracBrowser for help on using the repository browser.