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

Last change on this file since 1157 was 1157, checked in by stoecker, 15 years ago

fixed shortcut loading. Close #1769

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