Merge branch 'master' of git@git.labs.intellij.net:idea/community
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / keymap / impl / KeymapImpl.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.keymap.impl;
17
18 import com.intellij.openapi.actionSystem.*;
19 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.keymap.Keymap;
22 import com.intellij.openapi.keymap.KeymapUtil;
23 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
24 import com.intellij.openapi.options.ExternalInfo;
25 import com.intellij.openapi.options.ExternalizableScheme;
26 import com.intellij.openapi.util.Comparing;
27 import com.intellij.openapi.util.InvalidDataException;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.util.ArrayUtil;
30 import com.intellij.util.containers.HashMap;
31 import gnu.trove.THashMap;
32 import org.jdom.Element;
33 import org.jetbrains.annotations.NonNls;
34 import org.jetbrains.annotations.NotNull;
35
36 import javax.swing.*;
37 import java.awt.event.InputEvent;
38 import java.awt.event.KeyEvent;
39 import java.awt.event.MouseEvent;
40 import java.lang.reflect.Field;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Iterator;
44 import java.util.Set;
45
46 /**
47  * @author Eugene Belyaev
48  * @author Anton Katilin
49  * @author Vladimir Kondratyev
50  */
51 public class KeymapImpl implements Keymap, ExternalizableScheme {
52   @NonNls
53   private static final String KEY_MAP = "keymap";
54   @NonNls
55   private static final String KEYBOARD_SHORTCUT = "keyboard-shortcut";
56   @NonNls
57   private static final String KEYBOARD_GESTURE_SHORTCUT = "keyboard-gesture-shortcut";
58   @NonNls
59   private static final String KEYBOARD_GESTURE_KEY = "keystroke";
60   @NonNls
61   private static final String KEYBOARD_GESTURE_MODIFIER = "modifier";
62   @NonNls
63   private static final String KEYSTROKE_ATTRIBUTE = "keystroke";
64   @NonNls
65   private static final String FIRST_KEYSTROKE_ATTRIBUTE = "first-keystroke";
66   @NonNls
67   private static final String SECOND_KEYSTROKE_ATTRIBUTE = "second-keystroke";
68   @NonNls
69   private static final String ACTION = "action";
70   @NonNls
71   private static final String VERSION_ATTRIBUTE = "version";
72   @NonNls
73   private static final String PARENT_ATTRIBUTE = "parent";
74   @NonNls
75   private static final String NAME_ATTRIBUTE = "name";
76   @NonNls
77   private static final String ID_ATTRIBUTE = "id";
78   @NonNls
79   private static final String TRUE_WORD = "true";
80   @NonNls
81   private static final String FALSE_WORD = "false";
82   @NonNls
83   private static final String DISABLE_MNEMONICS_ATTRIBUTE = "disable-mnemonics";
84   @NonNls
85   private static final String MOUSE_SHORTCUT = "mouse-shortcut";
86   @NonNls
87   private static final String SHIFT = "shift";
88   @NonNls
89   private static final String CONTROL = "control";
90   @NonNls
91   private static final String META = "meta";
92   @NonNls
93   private static final String ALT = "alt";
94   @NonNls
95   private static final String ALT_GRAPH = "altGraph";
96   @NonNls
97   private static final String BUTTON1 = "button1";
98   @NonNls
99   private static final String BUTTON2 = "button2";
100   @NonNls
101   private static final String BUTTON3 = "button3";
102   @NonNls
103   private static final String DOUBLE_CLICK = "doubleClick";
104   @NonNls
105   private static final String VIRTUAL_KEY_PREFIX = "VK_";
106   @NonNls
107   private static final String EDITOR_ACTION_PREFIX = "Editor";
108
109   private static final Logger LOG = Logger.getInstance("#com.intellij.keymap.KeymapImpl");
110
111   private String myName;
112   private KeymapImpl myParent;
113   private boolean myCanModify = true;
114
115
116   private THashMap<String, ArrayList<Shortcut>> myActionId2ListOfShortcuts = new THashMap<String, ArrayList<Shortcut>>();
117
118   /**
119    * Don't use this field directly! Use it only through <code>getKeystroke2ListOfIds</code>.
120    */
121   private THashMap<KeyStroke, ArrayList<String>> myKeystroke2ListOfIds = null;
122   private THashMap<KeyboardModifierGestureShortuct, ArrayList<String>> myGesture2ListOfIds = null;
123   // TODO[vova,anton] it should be final member
124
125   /**
126    * Don't use this field directly! Use it only through <code>getMouseShortcut2ListOfIds</code>.
127    */
128   private THashMap myMouseShortcut2ListOfIds = null;
129   // TODO[vova,anton] it should be final member
130
131   private static HashMap<Integer,String> ourNamesForKeycodes = null;
132   private static final Shortcut[] ourEmptyShortcutsArray = new Shortcut[0];
133   private final ArrayList<Keymap.Listener> myListeners = new ArrayList<Keymap.Listener>();
134   private KeymapManagerEx myKeymapManager;
135   private final ExternalInfo myExternalInfo = new ExternalInfo();
136
137   static {
138     ourNamesForKeycodes = new HashMap<Integer, String>();
139     try {
140       Field[] fields = KeyEvent.class.getDeclaredFields();
141       for (Field field : fields) {
142         String fieldName = field.getName();
143         if (fieldName.startsWith(VIRTUAL_KEY_PREFIX)) {
144           int keyCode = field.getInt(KeyEvent.class);
145           ourNamesForKeycodes.put(keyCode, fieldName.substring(3));
146         }
147       }
148     }
149     catch (Exception e) {
150       LOG.error(e);
151     }
152   }
153
154   public String getName() {
155     return myName;
156   }
157
158   public String getPresentableName() {
159     return getName();
160   }
161
162   public void setName(String name) {
163     myName = name;
164   }
165
166
167   public KeymapImpl deriveKeymap() {
168     if (!canModify()) {
169       KeymapImpl newKeymap = new KeymapImpl();
170
171       newKeymap.myParent = this;
172       newKeymap.myName = null;
173       newKeymap.myCanModify = canModify();
174       return newKeymap;
175     }
176     else {
177       return copy(false);
178     }
179   }
180
181   public KeymapImpl copy(boolean copyExternalInfo) {
182     KeymapImpl newKeymap = new KeymapImpl();
183     newKeymap.myParent = myParent;
184     newKeymap.myName = myName;
185     newKeymap.myCanModify = canModify();
186
187     newKeymap.myKeystroke2ListOfIds = null;
188     newKeymap.myMouseShortcut2ListOfIds = null;
189
190     THashMap actionsIdsToListOfShortcuts = new THashMap();
191     for (String key : myActionId2ListOfShortcuts.keySet()) {
192       ArrayList<Shortcut> list = myActionId2ListOfShortcuts.get(key);
193       actionsIdsToListOfShortcuts.put(key, list.clone());
194     }
195
196     newKeymap.myActionId2ListOfShortcuts = actionsIdsToListOfShortcuts;
197
198     if (copyExternalInfo) {
199       newKeymap.myExternalInfo.copy(myExternalInfo);
200     }
201
202     return newKeymap;
203   }
204
205   public boolean equals(Object object) {
206     if (!(object instanceof Keymap)) return false;
207     KeymapImpl secondKeymap = (KeymapImpl)object;
208     if (!Comparing.equal(myName, secondKeymap.myName)) return false;
209     if (myCanModify != secondKeymap.myCanModify) return false;
210     if (!Comparing.equal(myParent, secondKeymap.myParent)) return false;
211     if (!Comparing.equal(myActionId2ListOfShortcuts, secondKeymap.myActionId2ListOfShortcuts)) return false;
212     return true;
213   }
214
215   public int hashCode(){
216     int hashCode=0;
217     if(myName!=null){
218       hashCode+=myName.hashCode();
219     }
220     return hashCode;
221   }
222
223   public Keymap getParent() {
224     return myParent;
225   }
226
227   public boolean canModify() {
228     return myCanModify;
229   }
230
231   public void setCanModify(boolean val) {
232     myCanModify = val;
233   }
234
235   protected Shortcut[] getParentShortcuts(String actionId) {
236     return myParent.getShortcuts(actionId);
237   }
238
239   public void addShortcut(String actionId, Shortcut shortcut) {
240     addShortcutSilently(actionId, shortcut, true);
241     fireShortcutChanged(actionId);
242   }
243
244   private void addShortcutSilently(String actionId, Shortcut shortcut, final boolean checkParentShortcut) {
245     ArrayList<Shortcut> list = myActionId2ListOfShortcuts.get(actionId);
246     if (list == null) {
247       list = new ArrayList<Shortcut>();
248       myActionId2ListOfShortcuts.put(actionId, list);
249       if (myParent != null) {
250         // copy parent shortcuts for this actionId
251         Shortcut[] shortcuts = getParentShortcuts(actionId);
252         for (Shortcut parentShortcut : shortcuts) {
253           // shortcuts are immutables
254           list.add(parentShortcut);
255         }
256       }
257     }
258     list.add(shortcut);
259
260     if (checkParentShortcut && myParent != null && areShortcutsEqual(getParentShortcuts(actionId), getShortcuts(actionId))) {
261       myActionId2ListOfShortcuts.remove(actionId);
262     }
263     myKeystroke2ListOfIds = null;
264     myMouseShortcut2ListOfIds = null;
265   }
266
267   public void removeAllActionShortcuts(String actionId) {
268     Shortcut[] allShortcuts = getShortcuts(actionId);
269     for (Shortcut shortcut : allShortcuts) {
270       removeShortcut(actionId, shortcut);
271     }
272   }
273
274   public void removeShortcut(String actionId, Shortcut shortcut) {
275     ArrayList<Shortcut> list = myActionId2ListOfShortcuts.get(actionId);
276     if (list != null) {
277       for(int i=0; i<list.size(); i++) {
278         if(shortcut.equals(list.get(i))) {
279           list.remove(i);
280           if (myParent != null && areShortcutsEqual(getParentShortcuts(actionId), getShortcuts(actionId))) {
281             myActionId2ListOfShortcuts.remove(actionId);
282           }
283           break;
284         }
285       }
286     }
287     else if (myParent != null) {
288       // put to the map the parent's bindings except for the removed binding
289       Shortcut[] parentShortcuts = getParentShortcuts(actionId);
290       ArrayList<Shortcut> listOfShortcuts = new ArrayList<Shortcut>();
291       for (Shortcut parentShortcut : parentShortcuts) {
292         if (!shortcut.equals(parentShortcut)) {
293           listOfShortcuts.add(parentShortcut);
294         }
295       }
296       myActionId2ListOfShortcuts.put(actionId, listOfShortcuts);
297     }
298     myKeystroke2ListOfIds = null;
299     myMouseShortcut2ListOfIds = null;
300     fireShortcutChanged(actionId);
301   }
302
303   private THashMap<KeyStroke,ArrayList<String>> getKeystroke2ListOfIds() {
304     myKeystroke2ListOfIds = null;
305
306     if (myKeystroke2ListOfIds == null) {
307       myKeystroke2ListOfIds = new THashMap<KeyStroke, ArrayList<String>>();
308       fillKeystroke2ListOfIds(myKeystroke2ListOfIds, KeyboardShortcut.class);
309     }
310     return myKeystroke2ListOfIds;
311   }
312
313   private THashMap<KeyboardModifierGestureShortuct,ArrayList<String>> getGesture2ListOfIds() {
314     if (myGesture2ListOfIds == null) {
315       myGesture2ListOfIds = new THashMap<KeyboardModifierGestureShortuct, ArrayList<String>>();
316       fillKeystroke2ListOfIds(myGesture2ListOfIds, KeyboardModifierGestureShortuct.class);
317     }
318     return myGesture2ListOfIds;
319   }
320
321   private void fillKeystroke2ListOfIds(final THashMap map, final Class shortcutClass) {
322     for (String id : myActionId2ListOfShortcuts.keySet()) {
323       addAction2ShortcutsMap(id, map, shortcutClass);
324     }
325
326     final Set<String> boundActions = getKeymapManager().getBoundActions();
327     for (String id : boundActions) {
328       addAction2ShortcutsMap(id, map, shortcutClass);
329     }
330   }
331
332   private THashMap getMouseShortcut2ListOfIds(){
333     if(myMouseShortcut2ListOfIds==null){
334       myMouseShortcut2ListOfIds=new THashMap();
335
336       for (String id : myActionId2ListOfShortcuts.keySet()) {
337         addAction2ShortcutsMap(id, myMouseShortcut2ListOfIds, MouseShortcut.class);
338       }
339
340       final Set<String> boundActions = getKeymapManager().getBoundActions();
341       for (String id : boundActions) {
342         addAction2ShortcutsMap(id, myMouseShortcut2ListOfIds, MouseShortcut.class);
343       }
344     }
345     return myMouseShortcut2ListOfIds;
346   }
347
348   private void addAction2ShortcutsMap(
349     final String actionId,
350     final THashMap strokesMap,
351     final Class shortcutClass) {
352     ArrayList<Shortcut> listOfShortcuts = _getShortcuts(actionId);
353     for (Shortcut shortcut : listOfShortcuts) {
354       if (!shortcutClass.isAssignableFrom(shortcut.getClass())) {
355         continue;
356       }
357
358       ArrayList<String> listOfIds = null;
359       if (shortcut instanceof KeyboardShortcut) {
360         KeyStroke firstKeyStroke = ((KeyboardShortcut)shortcut).getFirstKeyStroke();
361         listOfIds = (ArrayList<String>)strokesMap.get(firstKeyStroke);
362         if (listOfIds == null) {
363           listOfIds = new ArrayList<String>();
364           strokesMap.put(firstKeyStroke, listOfIds);
365         }
366       } else if (shortcut instanceof KeyboardModifierGestureShortuct) {
367         final KeyboardModifierGestureShortuct gesture = (KeyboardModifierGestureShortuct)shortcut;
368         if (listOfIds == null) {
369           listOfIds = new ArrayList<String>();
370           strokesMap.put(gesture, listOfIds);
371         }
372       } else {
373         listOfIds = (ArrayList)strokesMap.get(shortcut);
374         if (listOfIds == null) {
375           listOfIds = new ArrayList<String>();
376           strokesMap.put(shortcut, listOfIds);
377         }
378       }
379
380       // action may have more that 1 shortcut with same first keystroke
381       if (!listOfIds.contains(actionId)) {
382         listOfIds.add(actionId);
383       }
384     }
385   }
386
387   private ArrayList<Shortcut> _getShortcuts(final String actionId) {
388     KeymapManagerEx keymapManager = getKeymapManager();
389     ArrayList<Shortcut> listOfShortcuts = myActionId2ListOfShortcuts.get(actionId);
390     if (listOfShortcuts != null) {
391       listOfShortcuts = new ArrayList<Shortcut>(listOfShortcuts);
392     }
393     else {
394       listOfShortcuts = new ArrayList<Shortcut>();
395     }
396
397     final String actionBinding = keymapManager.getActionBinding(actionId);
398     if (actionBinding != null) {
399       listOfShortcuts.addAll(_getShortcuts(actionBinding));
400     }
401
402     return listOfShortcuts;
403   }
404
405
406   protected String[] getParentActionIds(KeyStroke firstKeyStroke) {
407     return myParent.getActionIds(firstKeyStroke);
408   }
409
410   protected String[] getParentActionIds(KeyboardModifierGestureShortuct gesture) {
411     return myParent.getActionIds(gesture);
412   }
413
414   private String[] getActionIds(KeyboardModifierGestureShortuct shortuct) {
415     // first, get keystrokes from own map
416     final THashMap<KeyboardModifierGestureShortuct, ArrayList<String>> map = getGesture2ListOfIds();
417     ArrayList<String> list = new ArrayList<String>();
418
419     final Iterator<KeyboardModifierGestureShortuct> all = map.keySet().iterator();
420     while (all.hasNext()) {
421       KeyboardModifierGestureShortuct each = all.next();
422       if (shortuct.startsWith(each)) {
423         list.addAll(map.get(each));
424       }
425     }
426
427     if (myParent != null) {
428       String[] ids = getParentActionIds(shortuct);
429       if (ids.length > 0) {
430         boolean originalListInstance = true;
431         for (String id : ids) {
432           // add actions from parent keymap only if they are absent in this keymap
433           if (!myActionId2ListOfShortcuts.containsKey(id)) {
434             if (list == null) {
435               list = new ArrayList<String>();
436               originalListInstance = false;
437             }
438             else if (originalListInstance) {
439               list = (ArrayList<String>)list.clone();
440             }
441             list.add(id);
442           }
443         }
444       }
445     }
446     if (list == null) return ArrayUtil.EMPTY_STRING_ARRAY;
447     return sortInOrderOfRegistration(ArrayUtil.toStringArray(list));
448   }
449
450   public String[] getActionIds(KeyStroke firstKeyStroke) {
451     // first, get keystrokes from own map
452     ArrayList<String> list = getKeystroke2ListOfIds().get(firstKeyStroke);
453     if (myParent != null) {
454       String[] ids = getParentActionIds(firstKeyStroke);
455       if (ids.length > 0) {
456         boolean originalListInstance = true;
457         for (String id : ids) {
458           // add actions from parent keymap only if they are absent in this keymap
459           if (!myActionId2ListOfShortcuts.containsKey(id)) {
460             if (list == null) {
461               list = new ArrayList<String>();
462               originalListInstance = false;
463             }
464             else if (originalListInstance) {
465               list = (ArrayList<String>)list.clone();
466             }
467             list.add(id);
468           }
469         }
470       }
471     }
472     if (list == null) return ArrayUtil.EMPTY_STRING_ARRAY;
473     return sortInOrderOfRegistration(ArrayUtil.toStringArray(list));
474   }
475
476   public String[] getActionIds(KeyStroke firstKeyStroke, KeyStroke secondKeyStroke) {
477     String[] ids = getActionIds(firstKeyStroke);
478     ArrayList<String> actualBindings = new ArrayList<String>();
479     for (String id : ids) {
480       Shortcut[] shortcuts = getShortcuts(id);
481       for (Shortcut shortcut : shortcuts) {
482         if (!(shortcut instanceof KeyboardShortcut)) {
483           continue;
484         }
485         if (Comparing.equal(firstKeyStroke, ((KeyboardShortcut)shortcut).getFirstKeyStroke()) &&
486             Comparing.equal(secondKeyStroke, ((KeyboardShortcut)shortcut).getSecondKeyStroke())) {
487           actualBindings.add(id);
488           break;
489         }
490       }
491     }
492     return ArrayUtil.toStringArray(actualBindings);
493   }
494
495   public String[] getActionIds(final Shortcut shortcut) {
496     if (shortcut instanceof KeyboardShortcut) {
497       final KeyboardShortcut kb = (KeyboardShortcut)shortcut;
498       final KeyStroke first = kb.getFirstKeyStroke();
499       final KeyStroke second = kb.getSecondKeyStroke();
500       return second != null ? getActionIds(first, second) : getActionIds(first);
501     } else if (shortcut instanceof MouseShortcut) {
502       return getActionIds(((MouseShortcut)shortcut));
503     } else if (shortcut instanceof KeyboardModifierGestureShortuct) {
504       return getActionIds(((KeyboardModifierGestureShortuct)shortcut));
505     } else {
506       return ArrayUtil.EMPTY_STRING_ARRAY;
507     }
508   }
509
510   protected String[] getParentActionIds(MouseShortcut shortcut) {
511     return myParent.getActionIds(shortcut);
512   }
513
514
515   public String[] getActionIds(MouseShortcut shortcut){
516     // first, get shortcuts from own map
517     ArrayList<String> list = (ArrayList<String>)getMouseShortcut2ListOfIds().get(shortcut);
518     if (myParent != null) {
519       String[] ids = getParentActionIds(shortcut);
520       if (ids.length > 0) {
521         boolean originalListInstance = true;
522         for (String id : ids) {
523           // add actions from parent keymap only if they are absent in this keymap
524           if (!myActionId2ListOfShortcuts.containsKey(id)) {
525             if (list == null) {
526               list = new ArrayList<String>();
527               originalListInstance = false;
528             }
529             else if (originalListInstance) {
530               list = (ArrayList<String>)list.clone();
531             }
532             list.add(id);
533           }
534         }
535       }
536     }
537     if (list == null){
538       return ArrayUtil.EMPTY_STRING_ARRAY;
539     }
540     return sortInOrderOfRegistration(ArrayUtil.toStringArray(list));
541   }
542
543   private static String[] sortInOrderOfRegistration(String[] ids) {
544     Arrays.sort(ids, ActionManagerEx.getInstanceEx().getRegistrationOrderComparator());
545     return ids;
546   }
547
548   public Shortcut[] getShortcuts(String actionId) {
549     KeymapManagerEx keymapManager = getKeymapManager();
550     if (keymapManager.getBoundActions().contains(actionId)) {
551       return getShortcuts(keymapManager.getActionBinding(actionId));
552     }
553
554     ArrayList<Shortcut> shortcuts = myActionId2ListOfShortcuts.get(actionId);
555
556     if (shortcuts == null) {
557       if (myParent != null) {
558         return getParentShortcuts(actionId);
559       }else{
560         return ourEmptyShortcutsArray;
561       }
562     }
563     return shortcuts.toArray(new Shortcut[shortcuts.size()]);
564   }
565
566   private KeymapManagerEx getKeymapManager() {
567     if (myKeymapManager == null) {
568       myKeymapManager = KeymapManagerEx.getInstanceEx();
569     }
570     return myKeymapManager;
571   }
572
573   /**
574    * @param keymapElement element which corresponds to "keymap" tag.
575    */
576   public void readExternal(Element keymapElement, Keymap[] existingKeymaps) throws InvalidDataException {
577     // Check and convert parameters
578     if(!KEY_MAP.equals(keymapElement.getName())){
579       throw new InvalidDataException("unknown element: "+keymapElement);
580     }
581     if(keymapElement.getAttributeValue(VERSION_ATTRIBUTE)==null){
582       Converter01.convert(keymapElement);
583     }
584     //
585     String parentName = keymapElement.getAttributeValue(PARENT_ATTRIBUTE);
586     if(parentName != null) {
587       for (Keymap existingKeymap : existingKeymaps) {
588         if (parentName.equals(existingKeymap.getName())) {
589           myParent = (KeymapImpl)existingKeymap;
590           myCanModify = true;
591           break;
592         }
593       }
594     }
595     myName = keymapElement.getAttributeValue(NAME_ATTRIBUTE);
596
597     HashMap<String,ArrayList<Shortcut>> id2shortcuts=new HashMap<String, ArrayList<Shortcut>>();
598     for (final Object o : keymapElement.getChildren()) {
599       Element actionElement = (Element)o;
600       if (ACTION.equals(actionElement.getName())) {
601         String id = actionElement.getAttributeValue(ID_ATTRIBUTE);
602         if (id == null) {
603           throw new InvalidDataException("Attribute 'id' cannot be null; Keymap's name=" + myName);
604         }
605         id2shortcuts.put(id, new ArrayList<Shortcut>(1));
606         for (final Object o1 : actionElement.getChildren()) {
607           Element shortcutElement = (Element)o1;
608           if (KEYBOARD_SHORTCUT.equals(shortcutElement.getName())) {
609
610             // Parse first keystroke
611
612             KeyStroke firstKeyStroke;
613             String firstKeyStrokeStr = shortcutElement.getAttributeValue(FIRST_KEYSTROKE_ATTRIBUTE);
614             if (firstKeyStrokeStr != null) {
615               firstKeyStroke = ActionManagerEx.getKeyStroke(firstKeyStrokeStr);
616               if (firstKeyStroke == null) {
617                 throw new InvalidDataException(
618                   "Cannot parse first-keystroke: '" + firstKeyStrokeStr + "'; " + "Action's id=" + id + "; Keymap's name=" + myName);
619               }
620             }
621             else {
622               throw new InvalidDataException("Attribute 'first-keystroke' cannot be null; Action's id=" + id + "; Keymap's name=" + myName);
623             }
624
625             // Parse second keystroke
626
627             KeyStroke secondKeyStroke = null;
628             String secondKeyStrokeStr = shortcutElement.getAttributeValue(SECOND_KEYSTROKE_ATTRIBUTE);
629             if (secondKeyStrokeStr != null) {
630               secondKeyStroke = ActionManagerEx.getKeyStroke(secondKeyStrokeStr);
631               if (secondKeyStroke == null) {
632                 throw new InvalidDataException(
633                   "Wrong second-keystroke: '" + secondKeyStrokeStr + "'; Action's id=" + id + "; Keymap's name=" + myName);
634               }
635             }
636             Shortcut shortcut = new KeyboardShortcut(firstKeyStroke, secondKeyStroke);
637             ArrayList<Shortcut> shortcuts = id2shortcuts.get(id);
638             shortcuts.add(shortcut);
639           } else if (KEYBOARD_GESTURE_SHORTCUT.equals(shortcutElement.getName())) {
640             KeyStroke stroke = null;
641             KeyboardGestureAction.ModifierType modifier = null;
642             final String strokeText = shortcutElement.getAttributeValue(KEYBOARD_GESTURE_KEY);
643             if (strokeText != null) {
644               stroke = ActionManagerEx.getKeyStroke(strokeText);
645             }
646
647             final String modifierText = shortcutElement.getAttributeValue(KEYBOARD_GESTURE_MODIFIER);
648             if (KeyboardGestureAction.ModifierType.dblClick.toString().equalsIgnoreCase(modifierText)) {
649               modifier = KeyboardGestureAction.ModifierType.dblClick;
650             } else if (KeyboardGestureAction.ModifierType.hold.toString().equalsIgnoreCase(modifierText)) {
651               modifier = KeyboardGestureAction.ModifierType.hold;
652             }
653
654             if (stroke == null) {
655               throw new InvalidDataException("Wrong keystroke=" + strokeText + " action id=" + id + " keymap=" + myName);
656             }
657             if (modifier == null) {
658               throw new InvalidDataException("Wrong modifier=" + modifierText + " action id=" + id + " keymap=" + myName);
659             }
660
661             Shortcut shortcut = KeyboardModifierGestureShortuct.newInstance(modifier, stroke);
662             final ArrayList<Shortcut> shortcuts = id2shortcuts.get(id);
663             shortcuts.add(shortcut);
664           } else if (MOUSE_SHORTCUT.equals(shortcutElement.getName())) {
665             String keystrokeString = shortcutElement.getAttributeValue(KEYSTROKE_ATTRIBUTE);
666             if (keystrokeString == null) {
667               throw new InvalidDataException("Attribute 'keystroke' cannot be null; Action's id=" + id + "; Keymap's name=" + myName);
668             }
669
670             try {
671               MouseShortcut shortcut = KeymapUtil.parseMouseShortcut(keystrokeString);
672               ArrayList<Shortcut> shortcuts = id2shortcuts.get(id);
673               shortcuts.add(shortcut);
674             }
675             catch (InvalidDataException exc) {
676               throw new InvalidDataException(
677                 "Wrong mouse-shortcut: '" + keystrokeString + "'; Action's id=" + id + "; Keymap's name=" + myName);
678             }
679           }
680           else {
681             throw new InvalidDataException("unknown element: " + shortcutElement + "; Keymap's name=" + myName);
682           }
683         }
684       }
685       else {
686         throw new InvalidDataException("unknown element: " + actionElement + "; Keymap's name=" + myName);
687       }
688     }
689     // Add read shortcuts
690     for (String id : id2shortcuts.keySet()) {
691       myActionId2ListOfShortcuts
692         .put(id, new ArrayList<Shortcut>(2)); // It's a trick! After that paren's shortcuts are not added to the keymap
693       ArrayList<Shortcut> shortcuts = id2shortcuts.get(id);
694       for (Shortcut shortcut : shortcuts) {
695         addShortcutSilently(id, shortcut, false);
696       }
697     }
698   }
699
700   public Element writeExternal() {
701     Element keymapElement = new Element(KEY_MAP);
702     keymapElement.setAttribute(VERSION_ATTRIBUTE,Integer.toString(1));
703     keymapElement.setAttribute(NAME_ATTRIBUTE, myName);
704
705     if(myParent != null) {
706       keymapElement.setAttribute(PARENT_ATTRIBUTE, myParent.getName());
707     }
708     writeOwnActionIds(keymapElement);
709     return keymapElement;
710   }
711
712   private void writeOwnActionIds(final Element keymapElement) {
713     String[] ownActionIds = getOwnActionIds();
714     Arrays.sort(ownActionIds);
715     for (String actionId : ownActionIds) {
716       Element actionElement = new Element(ACTION);
717       actionElement.setAttribute(ID_ATTRIBUTE, actionId);
718       // Save keyboad shortcuts
719       Shortcut[] shortcuts = getShortcuts(actionId);
720       for (Shortcut shortcut : shortcuts) {
721         if (shortcut instanceof KeyboardShortcut) {
722           KeyboardShortcut keyboardShortcut = (KeyboardShortcut)shortcut;
723           Element element = new Element(KEYBOARD_SHORTCUT);
724           element.setAttribute(FIRST_KEYSTROKE_ATTRIBUTE, getKeyShortcutString(keyboardShortcut.getFirstKeyStroke()));
725           if (keyboardShortcut.getSecondKeyStroke() != null) {
726             element.setAttribute(SECOND_KEYSTROKE_ATTRIBUTE, getKeyShortcutString(keyboardShortcut.getSecondKeyStroke()));
727           }
728           actionElement.addContent(element);
729         }
730         else if (shortcut instanceof MouseShortcut) {
731           MouseShortcut mouseShortcut = (MouseShortcut)shortcut;
732           Element element = new Element(MOUSE_SHORTCUT);
733           element.setAttribute(KEYSTROKE_ATTRIBUTE, getMouseShortcutString(mouseShortcut));
734           actionElement.addContent(element);
735         }
736         else if (shortcut instanceof KeyboardModifierGestureShortuct) {
737           final KeyboardModifierGestureShortuct gesture = (KeyboardModifierGestureShortuct)shortcut;
738           final Element element = new Element(KEYBOARD_GESTURE_SHORTCUT);
739           element.setAttribute(KEYBOARD_GESTURE_SHORTCUT, getKeyShortcutString(gesture.getStroke()));
740           element.setAttribute(KEYBOARD_GESTURE_MODIFIER, gesture.getType().name());
741           actionElement.addContent(element);
742         } else {
743           throw new IllegalStateException("unknown shortcut class: " + shortcut);
744         }
745       }
746       keymapElement.addContent(actionElement);
747     }
748   }
749
750   private static boolean areShortcutsEqual(Shortcut[] shortcuts1, Shortcut[] shortcuts2) {
751     if(shortcuts1.length != shortcuts2.length) {
752       return false;
753     }
754     for (Shortcut shortcut : shortcuts1) {
755       Shortcut parentShortcutEqual = null;
756       for (Shortcut parentShortcut : shortcuts2) {
757         if (shortcut.equals(parentShortcut)) {
758           parentShortcutEqual = parentShortcut;
759           break;
760         }
761       }
762       if (parentShortcutEqual == null) {
763         return false;
764       }
765     }
766     return true;
767   }
768
769   /**
770    * @return string representation of passed keystroke.
771    */
772   private static String getKeyShortcutString(KeyStroke keyStroke) {
773     StringBuffer buf = new StringBuffer();
774     int modifiers = keyStroke.getModifiers();
775     if((modifiers & InputEvent.SHIFT_MASK) != 0) {
776       buf.append(SHIFT);
777       buf.append(' ');
778     }
779     if((modifiers & InputEvent.CTRL_MASK) != 0) {
780       buf.append(CONTROL);
781       buf.append(' ');
782     }
783     if((modifiers & InputEvent.META_MASK) != 0) {
784       buf.append(META);
785       buf.append(' ');
786     }
787     if((modifiers & InputEvent.ALT_MASK) != 0) {
788       buf.append(ALT);
789       buf.append(' ');
790     }
791     if((modifiers & InputEvent.ALT_GRAPH_MASK) != 0) {
792       buf.append(ALT_GRAPH);
793       buf.append(' ');
794     }
795
796     buf.append(ourNamesForKeycodes.get(new Integer(keyStroke.getKeyCode())));
797
798     return buf.toString();
799   }
800
801   /**
802    * @return string representation of passed mouse shortcut. This method should
803    * be used only for serializing of the <code>MouseShortcut</code>
804    */
805   private static String getMouseShortcutString(MouseShortcut shortcut){
806     StringBuffer buffer=new StringBuffer();
807
808     // modifiers
809
810     int modifiers=shortcut.getModifiers();
811     if((MouseEvent.SHIFT_DOWN_MASK&modifiers)>0){
812       buffer.append(SHIFT);
813       buffer.append(' ');
814     }
815     if((MouseEvent.CTRL_DOWN_MASK&modifiers)>0){
816       buffer.append(CONTROL);
817       buffer.append(' ');
818     }
819     if((MouseEvent.META_DOWN_MASK&modifiers)>0){
820       buffer.append(META);
821       buffer.append(' ');
822     }
823     if((MouseEvent.ALT_DOWN_MASK&modifiers)>0){
824       buffer.append(ALT);
825       buffer.append(' ');
826     }
827     if((MouseEvent.ALT_GRAPH_DOWN_MASK&modifiers)>0){
828       buffer.append(ALT_GRAPH);
829       buffer.append(' ');
830     }
831
832     // button
833
834     int button=shortcut.getButton();
835     if(MouseEvent.BUTTON1==button){
836       buffer.append(BUTTON1);
837       buffer.append(' ');
838     }else if(MouseEvent.BUTTON2==button){
839       buffer.append(BUTTON2);
840       buffer.append(' ');
841     }else if(MouseEvent.BUTTON3==button){
842       buffer.append(BUTTON3);
843       buffer.append(' ');
844     }else{
845       throw new IllegalStateException("unknown button: "+button);
846     }
847
848     if(shortcut.getClickCount()>1){
849       buffer.append(DOUBLE_CLICK);
850     }
851     return buffer.toString().trim(); // trim trailing space (if any)
852   }
853
854   /**
855    * @return IDs of the action which are specified in the keymap. It doesn't
856    * return IDs of action from parent keymap.
857    */
858   public String[] getOwnActionIds() {
859     return myActionId2ListOfShortcuts.keySet().toArray(new String[myActionId2ListOfShortcuts.size()]);
860   }
861
862   public void clearOwnActionsIds(){
863     myActionId2ListOfShortcuts.clear();
864   }
865
866   public String[] getActionIds() {
867     ArrayList<String> ids = new ArrayList<String>();
868     if (myParent != null) {
869       String[] parentIds = getParentActionIds();
870       for (String id : parentIds) {
871         ids.add(id);
872       }
873     }
874     String[] ownActionIds = getOwnActionIds();
875     for (String id : ownActionIds) {
876       if (!ids.contains(id)) {
877         ids.add(id);
878       }
879     }
880     return ArrayUtil.toStringArray(ids);
881   }
882
883   protected String[] getParentActionIds() {
884     return myParent.getActionIds();
885   }
886
887
888   public HashMap<String, ArrayList<KeyboardShortcut>> getConflicts(String actionId, KeyboardShortcut keyboardShortcut) {
889     HashMap<String, ArrayList<KeyboardShortcut>> result = new HashMap<String, ArrayList<KeyboardShortcut>>();
890
891     String[] actionIds = getActionIds(keyboardShortcut.getFirstKeyStroke());
892     for (String id : actionIds) {
893       if (id.equals(actionId)) {
894         continue;
895       }
896
897       if (actionId.startsWith(EDITOR_ACTION_PREFIX) && id.equals("$" + actionId.substring(6))) {
898         continue;
899       }
900       if (StringUtil.startsWithChar(actionId, '$') && id.equals(EDITOR_ACTION_PREFIX + actionId.substring(1))) {
901         continue;
902       }
903
904       Shortcut[] shortcuts = getShortcuts(id);
905       for (Shortcut shortcut1 : shortcuts) {
906         if (!(shortcut1 instanceof KeyboardShortcut)) {
907           continue;
908         }
909
910         KeyboardShortcut shortcut = (KeyboardShortcut)shortcut1;
911
912         if (!shortcut.getFirstKeyStroke().equals(keyboardShortcut.getFirstKeyStroke())) {
913           continue;
914         }
915
916         if (keyboardShortcut.getSecondKeyStroke() != null && shortcut.getSecondKeyStroke() != null &&
917             !keyboardShortcut.getSecondKeyStroke().equals(shortcut.getSecondKeyStroke())) {
918           continue;
919         }
920
921         ArrayList<KeyboardShortcut> list = result.get(id);
922         if (list == null) {
923           list = new ArrayList<KeyboardShortcut>();
924           result.put(id, list);
925         }
926
927         list.add(shortcut);
928       }
929     }
930
931     return result;
932   }
933
934   public void addShortcutChangeListener(Keymap.Listener listener) {
935     myListeners.add(listener);
936   }
937
938   public void removeShortcutChangeListener(Keymap.Listener listener) {
939     myListeners.remove(listener);
940   }
941
942   private void fireShortcutChanged(String actionId) {
943     Keymap.Listener[] listeners = myListeners.toArray(new Keymap.Listener[myListeners.size()]);
944     for (Listener listener : listeners) {
945       listener.onShortcutChanged(actionId);
946     }
947   }
948
949   @NotNull
950   public ExternalInfo getExternalInfo() {
951     return myExternalInfo;
952   }
953 }