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

Last change on this file since 11122 was 11122, checked in by simon04, 8 years ago

fix #12030 - ConcurrentModificationException in findShortcut

Use a CopyOnWriteArrayList to store the shortcuts.

  • Property svn:eol-style set to native
File size: 21.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
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.HashMap;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.Map;
13import java.util.Optional;
14import java.util.concurrent.CopyOnWriteArrayList;
15import java.util.stream.Collectors;
16
17import javax.swing.AbstractAction;
18import javax.swing.AbstractButton;
19import javax.swing.JMenu;
20import javax.swing.KeyStroke;
21import javax.swing.text.JTextComponent;
22
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.gui.util.GuiHelper;
25
26/**
27 * Global shortcut class.
28 *
29 * Note: This class represents a single shortcut, contains the factory to obtain
30 * shortcut objects from, manages shortcuts and shortcut collisions, and
31 * finally manages loading and saving shortcuts to/from the preferences.
32 *
33 * Action authors: You only need the {@link #registerShortcut} factory. Ignore everything else.
34 *
35 * All: Use only public methods that are also marked to be used. The others are
36 * public so the shortcut preferences can use them.
37 * @since 1084
38 */
39public final class Shortcut {
40 /** the unique ID of the shortcut */
41 private final String shortText;
42 /** a human readable description that will be shown in the preferences */
43 private String longText;
44 /** the key, the caller requested */
45 private final int requestedKey;
46 /** the group, the caller requested */
47 private final int requestedGroup;
48 /** the key that actually is used */
49 private int assignedKey;
50 /** the modifiers that are used */
51 private int assignedModifier;
52 /** true if it got assigned what was requested.
53 * (Note: modifiers will be ignored in favour of group when loading it from the preferences then.) */
54 private boolean assignedDefault;
55 /** true if the user changed this shortcut */
56 private boolean assignedUser;
57 /** true if the user cannot change this shortcut (Note: it also will not be saved into the preferences) */
58 private boolean automatic;
59 /** true if the user requested this shortcut to be set to its default value
60 * (will happen on next restart, as this shortcut will not be saved to the preferences) */
61 private boolean reset;
62
63 // simple constructor
64 private Shortcut(String shortText, String longText, int requestedKey, int requestedGroup, int assignedKey, int assignedModifier,
65 boolean assignedDefault, boolean assignedUser) {
66 this.shortText = shortText;
67 this.longText = longText;
68 this.requestedKey = requestedKey;
69 this.requestedGroup = requestedGroup;
70 this.assignedKey = assignedKey;
71 this.assignedModifier = assignedModifier;
72 this.assignedDefault = assignedDefault;
73 this.assignedUser = assignedUser;
74 this.automatic = false;
75 this.reset = false;
76 }
77
78 public String getShortText() {
79 return shortText;
80 }
81
82 public String getLongText() {
83 return longText;
84 }
85
86 // a shortcut will be renamed when it is handed out again, because the original name may be a dummy
87 private void setLongText(String longText) {
88 this.longText = longText;
89 }
90
91 public int getAssignedKey() {
92 return assignedKey;
93 }
94
95 public int getAssignedModifier() {
96 return assignedModifier;
97 }
98
99 public boolean isAssignedDefault() {
100 return assignedDefault;
101 }
102
103 public boolean isAssignedUser() {
104 return assignedUser;
105 }
106
107 public boolean isAutomatic() {
108 return automatic;
109 }
110
111 public boolean isChangeable() {
112 return !automatic && !"core:none".equals(shortText);
113 }
114
115 private boolean isReset() {
116 return reset;
117 }
118
119 /**
120 * FOR PREF PANE ONLY
121 */
122 public void setAutomatic() {
123 automatic = true;
124 }
125
126 /**
127 * FOR PREF PANE ONLY.<p>
128 * Sets the modifiers that are used.
129 * @param assignedModifier assigned modifier
130 */
131 public void setAssignedModifier(int assignedModifier) {
132 this.assignedModifier = assignedModifier;
133 }
134
135 /**
136 * FOR PREF PANE ONLY.<p>
137 * Sets the key that actually is used.
138 * @param assignedKey assigned key
139 */
140 public void setAssignedKey(int assignedKey) {
141 this.assignedKey = assignedKey;
142 }
143
144 /**
145 * FOR PREF PANE ONLY.<p>
146 * Sets whether the user has changed this shortcut.
147 * @param assignedUser {@code true} if the user has changed this shortcut
148 */
149 public void setAssignedUser(boolean assignedUser) {
150 this.reset = (this.assignedUser || reset) && !assignedUser;
151 if (assignedUser) {
152 assignedDefault = false;
153 } else if (reset) {
154 assignedKey = requestedKey;
155 assignedModifier = findModifier(requestedGroup, null);
156 }
157 this.assignedUser = assignedUser;
158 }
159
160 /**
161 * Use this to register the shortcut with Swing
162 * @return the key stroke
163 */
164 public KeyStroke getKeyStroke() {
165 if (assignedModifier != -1)
166 return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
167 return null;
168 }
169
170 // create a shortcut object from an string as saved in the preferences
171 private Shortcut(String prefString) {
172 List<String> s = new ArrayList<>(Main.pref.getCollection(prefString));
173 this.shortText = prefString.substring(15);
174 this.longText = s.get(0);
175 this.requestedKey = Integer.parseInt(s.get(1));
176 this.requestedGroup = Integer.parseInt(s.get(2));
177 this.assignedKey = Integer.parseInt(s.get(3));
178 this.assignedModifier = Integer.parseInt(s.get(4));
179 this.assignedDefault = Boolean.parseBoolean(s.get(5));
180 this.assignedUser = Boolean.parseBoolean(s.get(6));
181 }
182
183 private void saveDefault() {
184 Main.pref.getCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
185 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(requestedKey),
186 String.valueOf(getGroupModifier(requestedGroup)), String.valueOf(true), String.valueOf(false)}));
187 }
188
189 // get a string that can be put into the preferences
190 private boolean save() {
191 if (isAutomatic() || isReset() || !isAssignedUser()) {
192 return Main.pref.putCollection("shortcut.entry."+shortText, null);
193 } else {
194 return Main.pref.putCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
195 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(assignedKey),
196 String.valueOf(assignedModifier), String.valueOf(assignedDefault), String.valueOf(assignedUser)}));
197 }
198 }
199
200 private boolean isSame(int isKey, int isModifier) {
201 // an unassigned shortcut is different from any other shortcut
202 return isKey == assignedKey && isModifier == assignedModifier && assignedModifier != getGroupModifier(NONE);
203 }
204
205 public boolean isEvent(KeyEvent e) {
206 return getKeyStroke() != null && getKeyStroke().equals(
207 KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()));
208 }
209
210 /**
211 * use this to set a menu's mnemonic
212 * @param menu menu
213 */
214 public void setMnemonic(JMenu menu) {
215 if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
216 menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
217 }
218 }
219
220 /**
221 * use this to set a buttons's mnemonic
222 * @param button button
223 */
224 public void setMnemonic(AbstractButton button) {
225 if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
226 button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
227 }
228 }
229
230 /**
231 * Sets the mnemonic key on a text component.
232 * @param component component
233 */
234 public void setFocusAccelerator(JTextComponent component) {
235 if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
236 component.setFocusAccelerator(KeyEvent.getKeyText(assignedKey).charAt(0));
237 }
238 }
239
240 /**
241 * use this to set a actions's accelerator
242 * @param action action
243 */
244 public void setAccelerator(AbstractAction action) {
245 if (getKeyStroke() != null) {
246 action.putValue(AbstractAction.ACCELERATOR_KEY, getKeyStroke());
247 }
248 }
249
250 /**
251 * Returns a human readable text for the shortcut.
252 * @return a human readable text for the shortcut
253 */
254 public String getKeyText() {
255 KeyStroke keyStroke = getKeyStroke();
256 if (keyStroke == null) return "";
257 String modifText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
258 if ("".equals(modifText)) return KeyEvent.getKeyText(keyStroke.getKeyCode());
259 return modifText + '+' + KeyEvent.getKeyText(keyStroke.getKeyCode());
260 }
261
262 @Override
263 public String toString() {
264 return getKeyText();
265 }
266
267 ///////////////////////////////
268 // everything's static below //
269 ///////////////////////////////
270
271 // here we store our shortcuts
272 private static List<Shortcut> shortcuts = new CopyOnWriteArrayList<>();
273
274 // and here our modifier groups
275 private static Map<Integer, Integer> groups = new HashMap<>();
276
277 // check if something collides with an existing shortcut
278
279 /**
280 * Returns the registered shortcut fot the key and modifier
281 * @param requestedKey the requested key
282 * @param modifier the modifier
283 * @return the registered shortcut or {@code null}
284 */
285 public static Shortcut findShortcut(int requestedKey, int modifier) {
286 return findShortcutByKeyOrShortText(requestedKey, modifier, null)
287 .orElse(null);
288 }
289
290 private static Optional<Shortcut> findShortcutByKeyOrShortText(int requestedKey, int modifier, String shortText) {
291 if (modifier == getGroupModifier(NONE))
292 return Optional.empty();
293 return shortcuts.stream()
294 .filter(sc -> sc.isSame(requestedKey, modifier) || (shortText != null && shortText.equals(sc.getShortText())))
295 .findAny();
296
297 }
298
299 /**
300 * Returns a list of all shortcuts.
301 * @return a list of all shortcuts
302 */
303 public static List<Shortcut> listAll() {
304 return shortcuts.stream()
305 .filter(c -> !"core:none".equals(c.shortText))
306 .collect(Collectors.toList());
307 }
308
309 /** None group: used with KeyEvent.CHAR_UNDEFINED if no shortcut is defined */
310 public static final int NONE = 5000;
311 public static final int MNEMONIC = 5001;
312 /** Reserved group: for system shortcuts only */
313 public static final int RESERVED = 5002;
314 /** Direct group: no modifier */
315 public static final int DIRECT = 5003;
316 /** Alt group */
317 public static final int ALT = 5004;
318 /** Shift group */
319 public static final int SHIFT = 5005;
320 /** Command group. Matches CTRL modifier on Windows/Linux but META modifier on OS X */
321 public static final int CTRL = 5006;
322 /** Alt-Shift group */
323 public static final int ALT_SHIFT = 5007;
324 /** Alt-Command group. Matches ALT-CTRL modifier on Windows/Linux but ALT-META modifier on OS X */
325 public static final int ALT_CTRL = 5008;
326 /** Command-Shift group. Matches CTRL-SHIFT modifier on Windows/Linux but META-SHIFT modifier on OS X */
327 public static final int CTRL_SHIFT = 5009;
328 /** Alt-Command-Shift group. Matches ALT-CTRL-SHIFT modifier on Windows/Linux but ALT-META-SHIFT modifier on OS X */
329 public static final int ALT_CTRL_SHIFT = 5010;
330
331 /* for reassignment */
332 private static int[] mods = {ALT_CTRL, ALT_SHIFT, CTRL_SHIFT, ALT_CTRL_SHIFT};
333 private static int[] keys = {KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4,
334 KeyEvent.VK_F5, KeyEvent.VK_F6, KeyEvent.VK_F7, KeyEvent.VK_F8,
335 KeyEvent.VK_F9, KeyEvent.VK_F10, KeyEvent.VK_F11, KeyEvent.VK_F12};
336
337 // bootstrap
338 private static boolean initdone;
339 private static void doInit() {
340 if (initdone) return;
341 initdone = true;
342 int commandDownMask = GuiHelper.getMenuShortcutKeyMaskEx();
343 groups.put(NONE, -1);
344 groups.put(MNEMONIC, KeyEvent.ALT_DOWN_MASK);
345 groups.put(DIRECT, 0);
346 groups.put(ALT, KeyEvent.ALT_DOWN_MASK);
347 groups.put(SHIFT, KeyEvent.SHIFT_DOWN_MASK);
348 groups.put(CTRL, commandDownMask);
349 groups.put(ALT_SHIFT, KeyEvent.ALT_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
350 groups.put(ALT_CTRL, KeyEvent.ALT_DOWN_MASK | commandDownMask);
351 groups.put(CTRL_SHIFT, commandDownMask | KeyEvent.SHIFT_DOWN_MASK);
352 groups.put(ALT_CTRL_SHIFT, KeyEvent.ALT_DOWN_MASK | commandDownMask | KeyEvent.SHIFT_DOWN_MASK);
353
354 // (1) System reserved shortcuts
355 Main.platform.initSystemShortcuts();
356 // (2) User defined shortcuts
357 List<Shortcut> newshortcuts = new LinkedList<>();
358 for (String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
359 newshortcuts.add(new Shortcut(s));
360 }
361
362 for (Shortcut sc : newshortcuts) {
363 if (sc.isAssignedUser()
364 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
365 shortcuts.add(sc);
366 }
367 }
368 // Shortcuts at their default values
369 for (Shortcut sc : newshortcuts) {
370 if (!sc.isAssignedUser() && sc.isAssignedDefault()
371 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
372 shortcuts.add(sc);
373 }
374 }
375 // Shortcuts that were automatically moved
376 for (Shortcut sc : newshortcuts) {
377 if (!sc.isAssignedUser() && !sc.isAssignedDefault()
378 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
379 shortcuts.add(sc);
380 }
381 }
382 }
383
384 private static int getGroupModifier(int group) {
385 Integer m = groups.get(group);
386 if (m == null)
387 m = -1;
388 return m;
389 }
390
391 private static int findModifier(int group, Integer modifier) {
392 if (modifier == null) {
393 modifier = getGroupModifier(group);
394 if (modifier == null) { // garbage in, no shortcut out
395 modifier = getGroupModifier(NONE);
396 }
397 }
398 return modifier;
399 }
400
401 // shutdown handling
402 public static boolean savePrefs() {
403 boolean changed = false;
404 for (Shortcut sc : shortcuts) {
405 changed = changed | sc.save();
406 }
407 return changed;
408 }
409
410 /**
411 * FOR PLATFORMHOOK USE ONLY.
412 * <p>
413 * This registers a system shortcut. See PlatformHook for details.
414 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
415 * @param longText this will be displayed in the shortcut preferences dialog. Better
416 * use something the user will recognize...
417 * @param key the key. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
418 * @param modifier the modifier. Use a {@link KeyEvent KeyEvent.*_MASK} constant here.
419 * @return the system shortcut
420 */
421 public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
422 final Optional<Shortcut> existing = findShortcutByKeyOrShortText(key, modifier, shortText);
423 if (existing.isPresent() && shortText.equals(existing.get().getShortText())) {
424 return existing.get();
425 } else if (existing.isPresent()) {
426 // this always is a logic error in the hook
427 Main.error("CONFLICT WITH SYSTEM KEY " + shortText + ": " + existing.get());
428 return null;
429 }
430 final Shortcut shortcut = new Shortcut(shortText, longText, key, RESERVED, key, modifier, true, false);
431 shortcuts.add(shortcut);
432 return shortcut;
433 }
434
435 /**
436 * Register a shortcut.
437 *
438 * Here you get your shortcuts from. The parameters are:
439 *
440 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
441 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
442 * actions that are part of JOSM's core. Use something like
443 * {@code <pluginname>+":"+<actionname>}.
444 * @param longText this will be displayed in the shortcut preferences dialog. Better
445 * use something the user will recognize...
446 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
447 * @param requestedGroup the group this shortcut fits best. This will determine the
448 * modifiers your shortcut will get assigned. Use the constants defined above.
449 * @return the shortcut
450 */
451 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
452 return registerShortcut(shortText, longText, requestedKey, requestedGroup, null);
453 }
454
455 // and now the workhorse. same parameters as above, just one more
456 private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier) {
457 doInit();
458 Integer defaultModifier = findModifier(requestedGroup, modifier);
459 final Optional<Shortcut> existing = findShortcutByKeyOrShortText(requestedKey, defaultModifier, shortText);
460 if (existing.isPresent() && shortText.equals(existing.get().getShortText())) {
461 // a re-register? maybe a sc already read from the preferences?
462 final Shortcut sc = existing.get();
463 sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
464 sc.saveDefault();
465 return sc;
466 } else if (existing.isPresent()) {
467 final Shortcut conflict = existing.get();
468 if (Main.isPlatformOsx()) {
469 // Try to reassign Meta to Ctrl
470 int newmodifier = findNewOsxModifier(requestedGroup);
471 if (findShortcut(requestedKey, newmodifier) == null) {
472 Main.info("Reassigning OSX shortcut '" + shortText + "' from Meta to Ctrl because of conflict with " + conflict);
473 return reassignShortcut(shortText, longText, requestedKey, conflict, requestedGroup, requestedKey, newmodifier);
474 }
475 }
476 for (int m : mods) {
477 for (int k : keys) {
478 int newmodifier = getGroupModifier(m);
479 if (findShortcut(k, newmodifier) == null) {
480 Main.info("Reassigning shortcut '" + shortText + "' from " + modifier + " to " + newmodifier +
481 " because of conflict with " + conflict);
482 return reassignShortcut(shortText, longText, requestedKey, conflict, m, k, newmodifier);
483 }
484 }
485 }
486 } else {
487 Shortcut newsc = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
488 newsc.saveDefault();
489 shortcuts.add(newsc);
490 return newsc;
491 }
492
493 return null;
494 }
495
496 private static int findNewOsxModifier(int requestedGroup) {
497 switch (requestedGroup) {
498 case CTRL: return KeyEvent.CTRL_DOWN_MASK;
499 case ALT_CTRL: return KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK;
500 case CTRL_SHIFT: return KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK;
501 case ALT_CTRL_SHIFT: return KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK;
502 default: return 0;
503 }
504 }
505
506 private static Shortcut reassignShortcut(String shortText, String longText, int requestedKey, Shortcut conflict,
507 int m, int k, int newmodifier) {
508 Shortcut newsc = new Shortcut(shortText, longText, requestedKey, m, k, newmodifier, false, false);
509 Main.info(tr("Silent shortcut conflict: ''{0}'' moved by ''{1}'' to ''{2}''.",
510 shortText, conflict.getShortText(), newsc.getKeyText()));
511 newsc.saveDefault();
512 shortcuts.replaceAll(sc -> shortText.equals(sc.getShortText()) ? newsc : sc);
513 return newsc;
514 }
515
516 /**
517 * Replies the platform specific key stroke for the 'Copy' command, i.e.
518 * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
519 * copy command isn't known.
520 *
521 * @return the platform specific key stroke for the 'Copy' command
522 */
523 public static KeyStroke getCopyKeyStroke() {
524 return getKeyStrokeForShortKey("system:copy");
525 }
526
527 /**
528 * Replies the platform specific key stroke for the 'Paste' command, i.e.
529 * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
530 * paste command isn't known.
531 *
532 * @return the platform specific key stroke for the 'Paste' command
533 */
534 public static KeyStroke getPasteKeyStroke() {
535 return getKeyStrokeForShortKey("system:paste");
536 }
537
538 /**
539 * Replies the platform specific key stroke for the 'Cut' command, i.e.
540 * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
541 * 'Cut' command isn't known.
542 *
543 * @return the platform specific key stroke for the 'Cut' command
544 */
545 public static KeyStroke getCutKeyStroke() {
546 return getKeyStrokeForShortKey("system:cut");
547 }
548
549 private static KeyStroke getKeyStrokeForShortKey(String shortKey) {
550 return shortcuts.stream()
551 .filter(sc -> shortKey.equals(sc.getShortText()))
552 .findAny()
553 .map(Shortcut::getKeyStroke)
554 .orElse(null);
555 }
556}
Note: See TracBrowser for help on using the repository browser.