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

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

fix #7375 - resetting shortcuts not possible

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