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

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

fix #7358 - custom shortcuts broken

  • Property svn:eol-style set to native
File size: 22.0 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 && assignedUser);
141 if (assignedUser) {
142 assignedDefault = false;
143 }
144 this.assignedUser = assignedUser;
145 }
146
147 /**
148 * Use this to register the shortcut with Swing
149 */
150 public KeyStroke getKeyStroke() {
151 if (assignedModifier != -1)
152 return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
153 return null;
154 }
155
156 private boolean isSame(int isKey, int isModifier) {
157 // -1 --- an unassigned shortcut is different from any other shortcut
158 return( isKey == assignedKey && isModifier == assignedModifier && assignedModifier != getGroupModifier(GROUP_NONE));
159 }
160
161 // create a shortcut object from an string as saved in the preferences
162 private Shortcut(String prefString) {
163 ArrayList<String> s = (new ArrayList<String>(Main.pref.getCollection(prefString)));
164 this.shortText = prefString.substring(15);
165 this.longText = s.get(0);
166 this.requestedKey = Integer.parseInt(s.get(1));
167 this.requestedGroup = Integer.parseInt(s.get(2));
168 this.assignedKey = Integer.parseInt(s.get(3));
169 this.assignedModifier = Integer.parseInt(s.get(4));
170 this.assignedDefault = Boolean.parseBoolean(s.get(5));
171 this.assignedUser = Boolean.parseBoolean(s.get(6));
172 }
173
174 private void saveDefault() {
175 Main.pref.getCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
176 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(assignedKey),
177 String.valueOf(assignedModifier), String.valueOf(true), String.valueOf(false)}));
178 }
179
180 // get a string that can be put into the preferences
181 private boolean save() {
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 private boolean isSame(Shortcut other) {
188 return assignedKey == other.assignedKey && assignedModifier == other.assignedModifier;
189 }
190
191 /**
192 * use this to set a menu's mnemonic
193 */
194 public void setMnemonic(JMenu menu) {
195 if (requestedGroup == GROUP_MNEMONIC && assignedModifier == getGroupModifier(requestedGroup + GROUPS_DEFAULT) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
196 menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
197 }
198 }
199 /**
200 * use this to set a buttons's mnemonic
201 */
202 public void setMnemonic(AbstractButton button) {
203 if (requestedGroup == GROUP_MNEMONIC && assignedModifier == getGroupModifier(requestedGroup + GROUPS_DEFAULT) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
204 button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
205 }
206 }
207
208 /**
209 * use this to get a human readable text for your shortcut
210 */
211 public String getKeyText() {
212 KeyStroke keyStroke = getKeyStroke();
213 if (keyStroke == null) return "";
214 String modifText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
215 if ("".equals (modifText)) return KeyEvent.getKeyText (keyStroke.getKeyCode ());
216 return modifText + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode ());
217 }
218
219 @Override
220 public String toString() {
221 return getKeyText();
222 }
223
224 ///////////////////////////////
225 // everything's static below //
226 ///////////////////////////////
227
228 // here we store our shortcuts
229 private static Map<String, Shortcut> shortcuts = new LinkedHashMap<String, Shortcut>();
230
231 // and here our modifier groups
232 private static Map<Integer, Integer> groups;
233
234 // check if something collides with an existing shortcut
235 private static Shortcut findShortcut(int requestedKey, int modifier) {
236 if (modifier == getGroupModifier(GROUP_NONE))
237 return null;
238 for (Shortcut sc : shortcuts.values()) {
239 if (sc.isSame(requestedKey, modifier))
240 return sc;
241 }
242 return null;
243 }
244
245 /**
246 * FOR PREF PANE ONLY
247 */
248 public static List<Shortcut> listAll() {
249 List<Shortcut> l = new ArrayList<Shortcut>();
250 for(Shortcut c : shortcuts.values())
251 {
252 if(!c.shortText.equals("core:none")) {
253 l.add(c);
254 }
255 }
256 return l;
257 }
258
259 // try to find an unused shortcut
260 private static Shortcut findRandomShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
261 int[] mods = {getGroupModifier(requestedGroup + GROUPS_DEFAULT), getGroupModifier(requestedGroup + GROUPS_ALT1), getGroupModifier(requestedGroup + GROUPS_ALT2)};
262 for (int m : mods) {
263 for (int k = KeyEvent.VK_A; k < KeyEvent.VK_Z; k++) { // we'll limit ourself to 100% safe keys
264 if ( findShortcut(k, m) == null )
265 return new Shortcut(shortText, longText, requestedKey, requestedGroup, k, m, false, false);
266 }
267 }
268 return new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, getGroupModifier(GROUP_NONE), false, false);
269 }
270
271 // use these constants to request shortcuts
272 /**
273 * no shortcut.
274 */
275 public static final int GROUP_NONE = 0;
276 /**
277 * a button action, will use another modifier than MENU on system with a meta key.
278 */
279 public static final int GROUP_HOTKEY = 1;
280 /**
281 * a menu action, e.g. "ctrl-e" (export).
282 */
283 public static final int GROUP_MENU = 2;
284 /**
285 * direct edit key, e.g. "a" (add).
286 */
287 public static final int GROUP_EDIT = 3;
288 /**
289 * toggle one of the right-hand-side windows, e.g. "alt-l" (layers).
290 */
291 public static final int GROUP_LAYER = 4;
292 /**
293 * for non-letter keys, preferable without modifier, e.g. F5.
294 */
295 public static final int GROUP_DIRECT = 5;
296 /**
297 * for use with {@see #setMnemonic} only!
298 */
299 public static final int GROUP_MNEMONIC = 6;
300 public static final int GROUP__MAX = 7;
301 public static final int GROUP_RESERVED = 1000;
302 public static final int GROUPS_DEFAULT = 0;
303 public static final int GROUPS_ALT1 = GROUP__MAX;
304 public static final int GROUPS_ALT2 = GROUP__MAX * 2;
305
306 // bootstrap
307 private static boolean initdone = false;
308 private static void doInit() {
309 if (initdone) return;
310 initdone = true;
311 groups = Main.platform.initShortcutGroups(true);
312 // (1) System reserved shortcuts
313 Main.platform.initSystemShortcuts();
314 // (2) User defined shortcuts
315 LinkedList<Shortcut> shortcuts = new LinkedList<Shortcut>();
316 for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
317 shortcuts.add(new Shortcut(s));
318 }
319 for(Shortcut sc : shortcuts) {
320 if (sc.getAssignedUser()) {
321 registerShortcut(sc);
322 }
323 }
324 // Shortcuts at their default values
325 for(Shortcut sc : shortcuts) {
326 if (!sc.getAssignedUser() && sc.getAssignedDefault()) {
327 registerShortcut(sc);
328 }
329 }
330 // Shortcuts that were automatically moved
331 for(Shortcut sc : shortcuts) {
332 if (!sc.getAssignedUser() && !sc.getAssignedDefault()) {
333 registerShortcut(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 // shutdown handling
346 public static boolean savePrefs() {
347 boolean changed = false;
348 for (Shortcut sc : shortcuts.values()) {
349 if (!sc.getAutomatic() && !sc.getReset() && sc.getAssignedUser()) {
350 changed = changed | sc.save();
351 }
352 }
353 return changed;
354 }
355
356 // this is used to register a shortcut that was read from the preferences
357 private static void registerShortcut(Shortcut sc) {
358 // put a user configured shortcut in as-is -- unless there's a conflict
359 if(sc.getAssignedUser() && findShortcut(sc.getAssignedKey(),
360 sc.getAssignedModifier()) == null) {
361 shortcuts.put(sc.getShortText(), sc);
362 } else {
363 registerShortcut(sc.getShortText(), sc.getLongText(), sc.getRequestedKey(),
364 sc.getRequestedGroup(), sc.getAssignedModifier(), sc);
365 }
366 }
367
368 /**
369 * FOR PLATFORMHOOK USE ONLY
370 *
371 * This registers a system shortcut. See PlatformHook for details.
372 */
373 public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
374 if (shortcuts.containsKey(shortText))
375 return shortcuts.get(shortText);
376 Shortcut potentialShortcut = findShortcut(key, modifier);
377 if (potentialShortcut != null) {
378 // this always is a logic error in the hook
379 System.err.println("CONFLICT WITH SYSTEM KEY "+shortText);
380 return null;
381 }
382 potentialShortcut = new Shortcut(shortText, longText, key, GROUP_RESERVED, key, modifier, true, false);
383 shortcuts.put(shortText, potentialShortcut);
384 return potentialShortcut;
385 }
386
387 /**
388 * Register a shortcut.
389 *
390 * Here you get your shortcuts from. The parameters are:
391 *
392 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
393 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
394 * actions that are part of JOSM's core. Use something like
395 * {@code <pluginname>+":"+<actionname>}.
396 * @param longText this will be displayed in the shortcut preferences dialog. Better
397 * use something the user will recognize...
398 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
399 * @param requestedGroup the group this shortcut fits best. This will determine the
400 * modifiers your shortcut will get assigned. Use the {@code GROUP_*}
401 * constants defined above.
402 * @param modifier to register a {@code ctrl+shift} command, use {@see #SHIFT_DEFAULT}.
403 */
404 @Deprecated
405 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, int modifier) {
406 return registerShortcut(shortText, longText, requestedKey, requestedGroup, modifier, null);
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 */
425 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
426 return registerShortcut(shortText, longText, requestedKey, requestedGroup, null, null);
427 }
428
429 // and now the workhorse. same parameters as above, just one more: if originalShortcut is not null and
430 // is different from the shortcut that will be assigned, a popup warning will be displayed to the user.
431 // This is used when registering shortcuts that have been visible to the user before (read: have been
432 // read from the preferences file). New shortcuts will never warn, even when they land on some funny
433 // random fallback key like Ctrl+Alt+Shift+Z for "File Open..." <g>
434 private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier,
435 Shortcut originalShortcut) {
436 doInit();
437 if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
438 Shortcut sc = shortcuts.get(shortText);
439 sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
440 return sc;
441 }
442 Integer defaultModifier = getGroupModifier(requestedGroup + GROUPS_DEFAULT);
443 if(modifier != null) {
444 if(modifier == SHIFT_DEFAULT) {
445 defaultModifier |= KeyEvent.SHIFT_DOWN_MASK;
446 } else {
447 defaultModifier = modifier;
448 }
449 }
450 else if (defaultModifier == null) { // garbage in, no shortcut out
451 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
452 }
453 Shortcut conflictsWith = null;
454 Shortcut potentialShortcut = findShortcut(requestedKey, defaultModifier);
455 if (potentialShortcut != null) { // 3 stage conflict handling
456 conflictsWith = potentialShortcut;
457 defaultModifier = getGroupModifier(requestedGroup + GROUPS_ALT1);
458 if (defaultModifier == null) { // garbage in, no shortcurt out
459 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
460 }
461 potentialShortcut = findShortcut(requestedKey, defaultModifier);
462 if (potentialShortcut != null) {
463 defaultModifier = getGroupModifier(requestedGroup + GROUPS_ALT2);
464 if (defaultModifier == null) { // garbage in, no shortcurt out
465 defaultModifier = getGroupModifier(GROUP_NONE + GROUPS_DEFAULT);
466 }
467 potentialShortcut = findShortcut(requestedKey, defaultModifier);
468 if (potentialShortcut != null) { // if all 3 modifiers for a group are used, we give up
469 potentialShortcut = findRandomShortcut(shortText, longText, requestedKey, requestedGroup);
470 } else {
471 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
472 }
473 } else {
474 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, false, false);
475 }
476 if (originalShortcut != null && !originalShortcut.isSame(potentialShortcut)) {
477 displayWarning(conflictsWith, potentialShortcut, shortText, longText);
478 } else if (originalShortcut == null) {
479 System.out.println("Silent shortcut conflict: '"+shortText+"' moved by '"+conflictsWith.getShortText()+"' to '"+potentialShortcut.getKeyText()+"'.");
480 }
481 } else {
482 potentialShortcut = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
483 }
484
485 potentialShortcut.saveDefault();
486 shortcuts.put(shortText, potentialShortcut);
487 return potentialShortcut;
488 }
489
490 // a lengthy warning message
491 private static void displayWarning(Shortcut conflictsWith, Shortcut potentialShortcut, String shortText, String longText) {
492 JOptionPane.showMessageDialog(Main.parent,
493 tr("Setting the keyboard shortcut ''{0}'' for the action ''{1}'' ({2}) failed\n"+
494 "because the shortcut is already taken by the action ''{3}'' ({4}).\n\n",
495 conflictsWith.getKeyText(), longText, shortText,
496 conflictsWith.getLongText(), conflictsWith.getShortText())+
497 (potentialShortcut.getKeyText().equals("") ?
498 tr("This action will have no shortcut.\n\n")
499 :
500 tr("Using the shortcut ''{0}'' instead.\n\n", potentialShortcut.getKeyText())
501 )+
502 tr("(Hint: You can edit the shortcuts in the preferences.)"),
503 tr("Error"),
504 JOptionPane.ERROR_MESSAGE
505 );
506 }
507
508 /**
509 * Replies the platform specific key stroke for the 'Copy' command, i.e.
510 * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
511 * copy command isn't known.
512 *
513 * @return the platform specific key stroke for the 'Copy' command
514 */
515 static public KeyStroke getCopyKeyStroke() {
516 Shortcut sc = shortcuts.get("system:copy");
517 if (sc == null) return null;
518 return sc.getKeyStroke();
519 }
520
521 /**
522 * Replies the platform specific key stroke for the 'Paste' command, i.e.
523 * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
524 * paste command isn't known.
525 *
526 * @return the platform specific key stroke for the 'Paste' command
527 */
528 static public KeyStroke getPasteKeyStroke() {
529 Shortcut sc = shortcuts.get("system:paste");
530 if (sc == null) return null;
531 return sc.getKeyStroke();
532 }
533
534 /**
535 * Replies the platform specific key stroke for the 'Cut' command, i.e.
536 * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
537 * 'Cut' command isn't known.
538 *
539 * @return the platform specific key stroke for the 'Cut' command
540 */
541 static public KeyStroke getCutKeyStroke() {
542 Shortcut sc = shortcuts.get("system:cut");
543 if (sc == null) return null;
544 return sc.getKeyStroke();
545 }
546}
Note: See TracBrowser for help on using the repository browser.