Ticket #1622: ShortCut.java

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