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

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

some shortcut fixes

  • Property svn:eol-style set to native
File size: 22.9 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__MAX = 9;
325 public static final int GROUP_RESERVED = 1000;
326 public static final int GROUPS_DEFAULT = 0;
327 public static final int GROUPS_ALT1 = GROUP__MAX;
328 public static final int GROUPS_ALT2 = GROUP__MAX * 2;
329
330 // bootstrap
331 private static boolean initdone = false;
332 private static void doInit() {
333 if (initdone) return;
334 initdone = true;
335 groups = Main.platform.initShortcutGroups(true);
336 // (1) System reserved shortcuts
337 Main.platform.initSystemShortcuts();
338 // (2) User defined shortcuts
339 LinkedList<Shortcut> shortcuts = new LinkedList<Shortcut>();
340 for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
341 shortcuts.add(new Shortcut(s));
342 }
343 for(Shortcut sc : shortcuts) {
344 if (sc.getAssignedUser()) {
345 registerShortcut(sc);
346 }
347 }
348 // Shortcuts at their default values
349 for(Shortcut sc : shortcuts) {
350 if (!sc.getAssignedUser() && sc.getAssignedDefault()) {
351 registerShortcut(sc);
352 }
353 }
354 // Shortcuts that were automatically moved
355 for(Shortcut sc : shortcuts) {
356 if (!sc.getAssignedUser() && !sc.getAssignedDefault()) {
357 registerShortcut(sc);
358 }
359 }
360 }
361
362 private static int getGroupModifier(int group) {
363 Integer m = groups.get(group);
364 if(m == null)
365 m = -1;
366 return m;
367 }
368
369 // shutdown handling
370 public static boolean savePrefs() {
371 boolean changed = false;
372 for (Shortcut sc : shortcuts.values()) {
373 changed = changed | sc.save();
374 }
375 return changed;
376 }
377
378 // this is used to register a shortcut that was read from the preferences
379 private static void registerShortcut(Shortcut sc) {
380 // put a user configured shortcut in as-is -- unless there's a conflict
381 if(sc.getAssignedUser() && findShortcut(sc.getAssignedKey(),
382 sc.getAssignedModifier()) == null) {
383 shortcuts.put(sc.getShortText(), sc);
384 } else {
385 registerShortcut(sc.getShortText(), sc.getLongText(), sc.getRequestedKey(),
386 sc.getRequestedGroup(), sc.getAssignedModifier(), sc);
387 }
388 }
389
390 /**
391 * FOR PLATFORMHOOK USE ONLY
392 *
393 * This registers a system shortcut. See PlatformHook for details.
394 */
395 public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
396 if (shortcuts.containsKey(shortText))
397 return shortcuts.get(shortText);
398 Shortcut potentialShortcut = findShortcut(key, modifier);
399 if (potentialShortcut != null) {
400 // this always is a logic error in the hook
401 System.err.println("CONFLICT WITH SYSTEM KEY "+shortText);
402 return null;
403 }
404 potentialShortcut = new Shortcut(shortText, longText, key, GROUP_RESERVED, key, modifier, true, false);
405 shortcuts.put(shortText, potentialShortcut);
406 return potentialShortcut;
407 }
408
409 /**
410 * Register a shortcut.
411 *
412 * Here you get your shortcuts from. The parameters are:
413 *
414 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
415 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
416 * actions that are part of JOSM's core. Use something like
417 * {@code <pluginname>+":"+<actionname>}.
418 * @param longText this will be displayed in the shortcut preferences dialog. Better
419 * use something the user will recognize...
420 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
421 * @param requestedGroup the group this shortcut fits best. This will determine the
422 * modifiers your shortcut will get assigned. Use the {@code GROUP_*}
423 * constants defined above.
424 * @param modifier to register a {@code ctrl+shift} command, use {@see #SHIFT_DEFAULT}.
425 */
426 @Deprecated
427 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, int modifier) {
428 return registerShortcut(shortText, longText, requestedKey, requestedGroup, modifier, null);
429 }
430
431 /**
432 * Register a shortcut.
433 *
434 * Here you get your shortcuts from. The parameters are:
435 *
436 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
437 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
438 * actions that are part of JOSM's core. Use something like
439 * {@code <pluginname>+":"+<actionname>}.
440 * @param longText this will be displayed in the shortcut preferences dialog. Better
441 * use something the user will recognize...
442 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
443 * @param requestedGroup the group this shortcut fits best. This will determine the
444 * modifiers your shortcut will get assigned. Use the {@code GROUP_*}
445 * constants defined above.
446 */
447 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
448 return registerShortcut(shortText, longText, requestedKey, requestedGroup, null, null);
449 }
450
451 private static int findModifier(int group, Integer modifier) {
452 Integer defaultModifier = getGroupModifier(group + GROUPS_DEFAULT);
453 if(modifier != null) {
454 if(modifier == SHIFT_DEFAULT) {
455 defaultModifier |= KeyEvent.SHIFT_DOWN_MASK;
456 } else {
457 defaultModifier = modifier;
458 }
459 }
460 else if (defaultModifier == null) { // garbage in, no shortcut out
461 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
462 }
463 return defaultModifier;
464 }
465
466 // and now the workhorse. same parameters as above, just one more: if originalShortcut is not null and
467 // is different from the shortcut that will be assigned, a popup warning will be displayed to the user.
468 // This is used when registering shortcuts that have been visible to the user before (read: have been
469 // read from the preferences file). New shortcuts will never warn, even when they land on some funny
470 // random fallback key like Ctrl+Alt+Shift+Z for "File Open..." <g>
471 private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier,
472 Shortcut originalShortcut) {
473 doInit();
474 Integer defaultModifier = findModifier(requestedGroup, modifier);
475 if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
476 Shortcut sc = shortcuts.get(shortText);
477 sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
478 sc.saveDefault(defaultModifier);
479 return sc;
480 }
481 Shortcut conflictsWith = null;
482 Shortcut potentialShortcut = findShortcut(requestedKey, defaultModifier);
483 if (potentialShortcut != null) { // 3 stage conflict handling
484 conflictsWith = potentialShortcut;
485 defaultModifier = getGroupModifier(requestedGroup + GROUPS_ALT1);
486 if (defaultModifier == null) { // garbage in, no shortcurt out
487 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
488 }
489 potentialShortcut = findShortcut(requestedKey, defaultModifier);
490 if (potentialShortcut != null) {
491 defaultModifier = getGroupModifier(requestedGroup + GROUPS_ALT2);
492 if (defaultModifier == null) { // garbage in, no shortcurt out
493 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
494 }
495 potentialShortcut = findShortcut(requestedKey, defaultModifier);
496 if (potentialShortcut != null) { // if all 3 modifiers for a group are used, we give up
497 potentialShortcut = findRandomShortcut(shortText, longText, requestedKey, requestedGroup);
498 } else {
499 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
500 }
501 } else {
502 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
503 }
504 if (originalShortcut != null && !originalShortcut.isSame(potentialShortcut)) {
505 displayWarning(conflictsWith, potentialShortcut, shortText, longText);
506 } else if (originalShortcut == null) {
507 System.out.println("Silent shortcut conflict: '"+shortText+"' moved by '"+conflictsWith.getShortText()+"' to '"+potentialShortcut.getKeyText()+"'.");
508 }
509 } else {
510 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
511 }
512
513 potentialShortcut.saveDefault(defaultModifier);
514 shortcuts.put(shortText, potentialShortcut);
515 return potentialShortcut;
516 }
517
518 // a lengthy warning message
519 private static void displayWarning(Shortcut conflictsWith, Shortcut potentialShortcut, String shortText, String longText) {
520 JOptionPane.showMessageDialog(Main.parent,
521 tr("Setting the keyboard shortcut ''{0}'' for the action ''{1}'' ({2}) failed\n"+
522 "because the shortcut is already taken by the action ''{3}'' ({4}).\n\n",
523 conflictsWith.getKeyText(), longText, shortText,
524 conflictsWith.getLongText(), conflictsWith.getShortText())+
525 (potentialShortcut.getKeyText().equals("") ?
526 tr("This action will have no shortcut.\n\n")
527 :
528 tr("Using the shortcut ''{0}'' instead.\n\n", potentialShortcut.getKeyText())
529 )+
530 tr("(Hint: You can edit the shortcuts in the preferences.)"),
531 tr("Error"),
532 JOptionPane.ERROR_MESSAGE
533 );
534 }
535
536 /**
537 * Replies the platform specific key stroke for the 'Copy' command, i.e.
538 * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
539 * copy command isn't known.
540 *
541 * @return the platform specific key stroke for the 'Copy' command
542 */
543 static public KeyStroke getCopyKeyStroke() {
544 Shortcut sc = shortcuts.get("system:copy");
545 if (sc == null) return null;
546 return sc.getKeyStroke();
547 }
548
549 /**
550 * Replies the platform specific key stroke for the 'Paste' command, i.e.
551 * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
552 * paste command isn't known.
553 *
554 * @return the platform specific key stroke for the 'Paste' command
555 */
556 static public KeyStroke getPasteKeyStroke() {
557 Shortcut sc = shortcuts.get("system:paste");
558 if (sc == null) return null;
559 return sc.getKeyStroke();
560 }
561
562 /**
563 * Replies the platform specific key stroke for the 'Cut' command, i.e.
564 * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
565 * 'Cut' command isn't known.
566 *
567 * @return the platform specific key stroke for the 'Cut' command
568 */
569 static public KeyStroke getCutKeyStroke() {
570 Shortcut sc = shortcuts.get("system:cut");
571 if (sc == null) return null;
572 return sc.getKeyStroke();
573 }
574}
Note: See TracBrowser for help on using the repository browser.