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

Last change on this file since 3488 was 3488, checked in by stoecker, 14 years ago

fix #5212 - No shortcut displayed in shortcut list

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