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

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

applied patch #2185 by bruce89

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