001 //License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.tools;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005
006 import java.awt.event.KeyEvent;
007 import java.util.ArrayList;
008 import java.util.Arrays;
009 import java.util.Collection;
010 import java.util.HashMap;
011 import java.util.LinkedHashMap;
012 import java.util.LinkedList;
013 import java.util.List;
014 import java.util.Map;
015
016 import javax.swing.AbstractAction;
017 import javax.swing.AbstractButton;
018 import javax.swing.JMenu;
019 import javax.swing.JOptionPane;
020 import javax.swing.KeyStroke;
021
022 import org.openstreetmap.josm.Main;
023
024 /**
025 * Global shortcut class.
026 *
027 * Note: This class represents a single shortcut, contains the factory to obtain
028 * shortcut objects from, manages shortcuts and shortcut collisions, and
029 * finally manages loading and saving shortcuts to/from the preferences.
030 *
031 * Action authors: You only need the {@link #registerShortcut} factory. Ignore everything
032 * else.
033 *
034 * All: Use only public methods that are also marked to be used. The others are
035 * public so the shortcut preferences can use them.
036 *
037 */
038 public class Shortcut {
039 private String shortText; // the unique ID of the shortcut
040 private String longText; // a human readable description that will be shown in the preferences
041 private int requestedKey; // the key, the caller requested
042 private int requestedGroup; // the group, the caller requested
043 private int assignedKey; // the key that actually is used
044 private int assignedModifier; // the modifiers that are used
045 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.)
046 private boolean assignedUser; // true if the user changed this shortcut
047 private boolean automatic; // true if the user cannot change this shortcut (Note: it also will not be saved into the preferences)
048 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)
049
050 // simple constructor
051 private Shortcut(String shortText, String longText, int requestedKey, int requestedGroup, int assignedKey, int assignedModifier, boolean assignedDefault, boolean assignedUser) {
052 this.shortText = shortText;
053 this.longText = longText;
054 this.requestedKey = requestedKey;
055 this.requestedGroup = requestedGroup;
056 this.assignedKey = assignedKey;
057 this.assignedModifier = assignedModifier;
058 this.assignedDefault = assignedDefault;
059 this.assignedUser = assignedUser;
060 this.automatic = false;
061 this.reset = false;
062 }
063
064 public String getShortText() {
065 return shortText;
066 }
067
068 public String getLongText() {
069 return longText;
070 }
071
072 // a shortcut will be renamed when it is handed out again, because the original name
073 // may be a dummy
074 private void setLongText(String longText) {
075 this.longText = longText;
076 }
077
078 private int getRequestedKey() {
079 return requestedKey;
080 }
081
082 public int getRequestedGroup() {
083 return requestedGroup;
084 }
085
086 public int getAssignedKey() {
087 return assignedKey;
088 }
089
090 public int getAssignedModifier() {
091 return assignedModifier;
092 }
093
094 public boolean getAssignedDefault() {
095 return assignedDefault;
096 }
097
098 public boolean getAssignedUser() {
099 return assignedUser;
100 }
101
102 public boolean getAutomatic() {
103 return automatic;
104 }
105
106 public boolean isChangeable() {
107 return !automatic && !shortText.equals("core:none");
108 }
109
110 private boolean getReset() {
111 return reset;
112 }
113
114 /**
115 * FOR PREF PANE ONLY
116 */
117 public void setAutomatic() {
118 automatic = true;
119 }
120
121 /**
122 * FOR PREF PANE ONLY
123 */
124 public void setAssignedModifier(int assignedModifier) {
125 this.assignedModifier = assignedModifier;
126 }
127
128 /**
129 * FOR PREF PANE ONLY
130 */
131 public void setAssignedKey(int assignedKey) {
132 this.assignedKey = assignedKey;
133 }
134
135 /**
136 * FOR PREF PANE ONLY
137 */
138 public void setAssignedUser(boolean assignedUser) {
139 this.reset = (this.assignedUser || reset) && !assignedUser;
140 if (assignedUser) {
141 assignedDefault = false;
142 } else if (reset) {
143 assignedKey = requestedKey;
144 assignedModifier = findModifier(requestedGroup, null);
145 }
146 this.assignedUser = assignedUser;
147 }
148
149 /**
150 * Use this to register the shortcut with Swing
151 */
152 public KeyStroke getKeyStroke() {
153 if (assignedModifier != -1)
154 return KeyStroke.getKeyStroke(assignedKey, assignedModifier);
155 return null;
156 }
157
158 // create a shortcut object from an string as saved in the preferences
159 private Shortcut(String prefString) {
160 ArrayList<String> s = (new ArrayList<String>(Main.pref.getCollection(prefString)));
161 this.shortText = prefString.substring(15);
162 this.longText = s.get(0);
163 this.requestedKey = Integer.parseInt(s.get(1));
164 this.requestedGroup = Integer.parseInt(s.get(2));
165 this.assignedKey = Integer.parseInt(s.get(3));
166 this.assignedModifier = Integer.parseInt(s.get(4));
167 this.assignedDefault = Boolean.parseBoolean(s.get(5));
168 this.assignedUser = Boolean.parseBoolean(s.get(6));
169 }
170
171 private void saveDefault() {
172 Main.pref.getCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
173 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(requestedKey),
174 String.valueOf(getGroupModifier(requestedGroup)), String.valueOf(true), String.valueOf(false)}));
175 }
176
177 // get a string that can be put into the preferences
178 private boolean save() {
179 if (getAutomatic() || getReset() || !getAssignedUser()) {
180 return Main.pref.putCollection("shortcut.entry."+shortText, null);
181 } else {
182 return Main.pref.putCollection("shortcut.entry."+shortText, Arrays.asList(new String[]{longText,
183 String.valueOf(requestedKey), String.valueOf(requestedGroup), String.valueOf(assignedKey),
184 String.valueOf(assignedModifier), String.valueOf(assignedDefault), String.valueOf(assignedUser)}));
185 }
186 }
187
188 private boolean isSame(int isKey, int isModifier) {
189 // an unassigned shortcut is different from any other shortcut
190 return isKey == assignedKey && isModifier == assignedModifier && assignedModifier != getGroupModifier(NONE);
191 }
192
193 public boolean isEvent(KeyEvent e) {
194 return getKeyStroke() != null && getKeyStroke().equals(
195 KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()));
196 }
197
198 /**
199 * use this to set a menu's mnemonic
200 */
201 public void setMnemonic(JMenu menu) {
202 if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
203 menu.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
204 }
205 }
206 /**
207 * use this to set a buttons's mnemonic
208 */
209 public void setMnemonic(AbstractButton button) {
210 if (assignedModifier == getGroupModifier(MNEMONIC) && getKeyStroke() != null && KeyEvent.getKeyText(assignedKey).length() == 1) {
211 button.setMnemonic(KeyEvent.getKeyText(assignedKey).charAt(0)); //getKeyStroke().getKeyChar() seems not to work here
212 }
213 }
214 /**
215 * use this to set a actions's accelerator
216 */
217 public void setAccelerator(AbstractAction action) {
218 if (getKeyStroke() != null) {
219 action.putValue(AbstractAction.ACCELERATOR_KEY, getKeyStroke());
220 }
221 }
222
223 /**
224 * use this to get a human readable text for your shortcut
225 */
226 public String getKeyText() {
227 KeyStroke keyStroke = getKeyStroke();
228 if (keyStroke == null) return "";
229 String modifText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
230 if ("".equals (modifText)) return KeyEvent.getKeyText (keyStroke.getKeyCode ());
231 return modifText + "+" + KeyEvent.getKeyText(keyStroke.getKeyCode ());
232 }
233
234 @Override
235 public String toString() {
236 return getKeyText();
237 }
238
239 ///////////////////////////////
240 // everything's static below //
241 ///////////////////////////////
242
243 // here we store our shortcuts
244 private static Map<String, Shortcut> shortcuts = new LinkedHashMap<String, Shortcut>();
245
246 // and here our modifier groups
247 private static Map<Integer, Integer> groups= new HashMap<Integer, Integer>();
248
249 // check if something collides with an existing shortcut
250 private static Shortcut findShortcut(int requestedKey, int modifier) {
251 if (modifier == getGroupModifier(NONE))
252 return null;
253 for (Shortcut sc : shortcuts.values()) {
254 if (sc.isSame(requestedKey, modifier))
255 return sc;
256 }
257 return null;
258 }
259
260 /**
261 * FOR PREF PANE ONLY
262 */
263 public static List<Shortcut> listAll() {
264 List<Shortcut> l = new ArrayList<Shortcut>();
265 for(Shortcut c : shortcuts.values())
266 {
267 if(!c.shortText.equals("core:none")) {
268 l.add(c);
269 }
270 }
271 return l;
272 }
273
274 public static final int NONE = 5000;
275 public static final int MNEMONIC = 5001;
276 public static final int RESERVED = 5002;
277 public static final int DIRECT = 5003;
278 public static final int ALT = 5004;
279 public static final int SHIFT = 5005;
280 public static final int CTRL = 5006;
281 public static final int ALT_SHIFT = 5007;
282 public static final int ALT_CTRL = 5008;
283 public static final int CTRL_SHIFT = 5009;
284 public static final int ALT_CTRL_SHIFT = 5010;
285
286 /* for reassignment */
287 private static int[] mods = {ALT_CTRL, ALT_SHIFT, CTRL_SHIFT, ALT_CTRL_SHIFT};
288 private static int[] keys = {KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4,
289 KeyEvent.VK_F5, KeyEvent.VK_F6, KeyEvent.VK_F7, KeyEvent.VK_F8,
290 KeyEvent.VK_F9, KeyEvent.VK_F10, KeyEvent.VK_F11, KeyEvent.VK_F12};
291
292 // bootstrap
293 private static boolean initdone = false;
294 private static void doInit() {
295 if (initdone) return;
296 initdone = true;
297 groups.put(NONE, -1);
298 groups.put(MNEMONIC, KeyEvent.ALT_DOWN_MASK);
299 groups.put(DIRECT, 0);
300 groups.put(ALT, KeyEvent.ALT_DOWN_MASK);
301 groups.put(SHIFT, KeyEvent.SHIFT_DOWN_MASK);
302 groups.put(CTRL, KeyEvent.CTRL_DOWN_MASK);
303 groups.put(ALT_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
304 groups.put(ALT_CTRL, KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK);
305 groups.put(CTRL_SHIFT, KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
306 groups.put(ALT_CTRL_SHIFT, KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.SHIFT_DOWN_MASK);
307
308 // (1) System reserved shortcuts
309 Main.platform.initSystemShortcuts();
310 // (2) User defined shortcuts
311 LinkedList<Shortcut> newshortcuts = new LinkedList<Shortcut>();
312 for(String s : Main.pref.getAllPrefixCollectionKeys("shortcut.entry.")) {
313 newshortcuts.add(new Shortcut(s));
314 }
315
316 for(Shortcut sc : newshortcuts) {
317 if (sc.getAssignedUser()
318 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
319 shortcuts.put(sc.getShortText(), sc);
320 }
321 }
322 // Shortcuts at their default values
323 for(Shortcut sc : newshortcuts) {
324 if (!sc.getAssignedUser() && sc.getAssignedDefault()
325 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
326 shortcuts.put(sc.getShortText(), sc);
327 }
328 }
329 // Shortcuts that were automatically moved
330 for(Shortcut sc : newshortcuts) {
331 if (!sc.getAssignedUser() && !sc.getAssignedDefault()
332 && findShortcut(sc.getAssignedKey(), sc.getAssignedModifier()) == null) {
333 shortcuts.put(sc.getShortText(), sc);
334 }
335 }
336 }
337
338 private static int getGroupModifier(int group) {
339 Integer m = groups.get(group);
340 if(m == null)
341 m = -1;
342 return m;
343 }
344
345 private static int findModifier(int group, Integer modifier) {
346 if(modifier == null) {
347 modifier = getGroupModifier(group);
348 if (modifier == null) { // garbage in, no shortcut out
349 modifier = getGroupModifier(NONE);
350 }
351 }
352 return modifier;
353 }
354
355 // shutdown handling
356 public static boolean savePrefs() {
357 boolean changed = false;
358 for (Shortcut sc : shortcuts.values()) {
359 changed = changed | sc.save();
360 }
361 return changed;
362 }
363
364 /**
365 * FOR PLATFORMHOOK USE ONLY
366 *
367 * This registers a system shortcut. See PlatformHook for details.
368 */
369 public static Shortcut registerSystemShortcut(String shortText, String longText, int key, int modifier) {
370 if (shortcuts.containsKey(shortText))
371 return shortcuts.get(shortText);
372 Shortcut potentialShortcut = findShortcut(key, modifier);
373 if (potentialShortcut != null) {
374 // this always is a logic error in the hook
375 System.err.println("CONFLICT WITH SYSTEM KEY "+shortText);
376 return null;
377 }
378 potentialShortcut = new Shortcut(shortText, longText, key, RESERVED, key, modifier, true, false);
379 shortcuts.put(shortText, potentialShortcut);
380 return potentialShortcut;
381 }
382
383 /**
384 * Register a shortcut.
385 *
386 * Here you get your shortcuts from. The parameters are:
387 *
388 * @param shortText an ID. re-use a {@code "system:*"} ID if possible, else use something unique.
389 * {@code "menu:*"} is reserved for menu mnemonics, {@code "core:*"} is reserved for
390 * actions that are part of JOSM's core. Use something like
391 * {@code <pluginname>+":"+<actionname>}.
392 * @param longText this will be displayed in the shortcut preferences dialog. Better
393 * use something the user will recognize...
394 * @param requestedKey the key you'd prefer. Use a {@link KeyEvent KeyEvent.VK_*} constant here.
395 * @param requestedGroup the group this shortcut fits best. This will determine the
396 * modifiers your shortcut will get assigned. Use the constants defined above.
397 */
398 public static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup) {
399 return registerShortcut(shortText, longText, requestedKey, requestedGroup, null);
400 }
401
402 // and now the workhorse. same parameters as above, just one more
403 private static Shortcut registerShortcut(String shortText, String longText, int requestedKey, int requestedGroup, Integer modifier) {
404 doInit();
405 Integer defaultModifier = findModifier(requestedGroup, modifier);
406 if (shortcuts.containsKey(shortText)) { // a re-register? maybe a sc already read from the preferences?
407 Shortcut sc = shortcuts.get(shortText);
408 sc.setLongText(longText); // or set by the platformHook, in this case the original longText doesn't match the real action
409 sc.saveDefault();
410 return sc;
411 }
412 Shortcut conflict = findShortcut(requestedKey, defaultModifier);
413 if (conflict != null) {
414 for (int m : mods) {
415 for (int k : keys) {
416 int newmodifier = getGroupModifier(m);
417 if ( findShortcut(k, newmodifier) == null ) {
418 Shortcut newsc = new Shortcut(shortText, longText, requestedKey, m, k, newmodifier, false, false);
419 System.out.println(tr("Silent shortcut conflict: ''{0}'' moved by ''{1}'' to ''{2}''.",
420 shortText, conflict.getShortText(), newsc.getKeyText()));
421 newsc.saveDefault();
422 shortcuts.put(shortText, newsc);
423 return newsc;
424 }
425 }
426 }
427 } else {
428 Shortcut newsc = new Shortcut(shortText, longText, requestedKey, requestedGroup, requestedKey, defaultModifier, true, false);
429 newsc.saveDefault();
430 shortcuts.put(shortText, newsc);
431 return newsc;
432 }
433
434 return null;
435 }
436
437 /**
438 * Replies the platform specific key stroke for the 'Copy' command, i.e.
439 * 'Ctrl-C' on windows or 'Meta-C' on a Mac. null, if the platform specific
440 * copy command isn't known.
441 *
442 * @return the platform specific key stroke for the 'Copy' command
443 */
444 static public KeyStroke getCopyKeyStroke() {
445 Shortcut sc = shortcuts.get("system:copy");
446 if (sc == null) return null;
447 return sc.getKeyStroke();
448 }
449
450 /**
451 * Replies the platform specific key stroke for the 'Paste' command, i.e.
452 * 'Ctrl-V' on windows or 'Meta-V' on a Mac. null, if the platform specific
453 * paste command isn't known.
454 *
455 * @return the platform specific key stroke for the 'Paste' command
456 */
457 static public KeyStroke getPasteKeyStroke() {
458 Shortcut sc = shortcuts.get("system:paste");
459 if (sc == null) return null;
460 return sc.getKeyStroke();
461 }
462
463 /**
464 * Replies the platform specific key stroke for the 'Cut' command, i.e.
465 * 'Ctrl-X' on windows or 'Meta-X' on a Mac. null, if the platform specific
466 * 'Cut' command isn't known.
467 *
468 * @return the platform specific key stroke for the 'Cut' command
469 */
470 static public KeyStroke getCutKeyStroke() {
471 Shortcut sc = shortcuts.get("system:cut");
472 if (sc == null) return null;
473 return sc.getKeyStroke();
474 }
475 }