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

Last change on this file since 11180 was 11173, checked in by simon04, 7 years ago

Shortcut.findShortcut: return Optional object

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