platofrm: set fsnotifier glibc compatibility only for i386/amd64
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / actionSystem / impl / ActionManagerImpl.java
1 /*
2  * Copyright 2000-2014 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.actionSystem.impl;
17
18 import com.intellij.AbstractBundle;
19 import com.intellij.CommonBundle;
20 import com.intellij.diagnostic.PluginException;
21 import com.intellij.ide.ActivityTracker;
22 import com.intellij.ide.DataManager;
23 import com.intellij.ide.plugins.IdeaPluginDescriptor;
24 import com.intellij.ide.plugins.PluginManager;
25 import com.intellij.ide.plugins.PluginManagerCore;
26 import com.intellij.ide.ui.search.SearchableOptionsRegistrar;
27 import com.intellij.idea.IdeaLogger;
28 import com.intellij.openapi.Disposable;
29 import com.intellij.openapi.actionSystem.*;
30 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
31 import com.intellij.openapi.actionSystem.ex.ActionUtil;
32 import com.intellij.openapi.actionSystem.ex.AnActionListener;
33 import com.intellij.openapi.application.*;
34 import com.intellij.openapi.application.ex.ApplicationManagerEx;
35 import com.intellij.openapi.components.ApplicationComponent;
36 import com.intellij.openapi.diagnostic.Logger;
37 import com.intellij.openapi.extensions.PluginId;
38 import com.intellij.openapi.keymap.Keymap;
39 import com.intellij.openapi.keymap.KeymapManager;
40 import com.intellij.openapi.keymap.KeymapUtil;
41 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
42 import com.intellij.openapi.progress.ProcessCanceledException;
43 import com.intellij.openapi.project.ProjectType;
44 import com.intellij.openapi.util.ActionCallback;
45 import com.intellij.openapi.util.Computable;
46 import com.intellij.openapi.util.Disposer;
47 import com.intellij.openapi.util.IconLoader;
48 import com.intellij.openapi.util.registry.Registry;
49 import com.intellij.openapi.util.text.StringUtil;
50 import com.intellij.openapi.wm.IdeFocusManager;
51 import com.intellij.openapi.wm.IdeFrame;
52 import com.intellij.util.ArrayUtil;
53 import com.intellij.util.ObjectUtils;
54 import com.intellij.util.ReflectionUtil;
55 import com.intellij.util.containers.ContainerUtil;
56 import com.intellij.util.containers.MultiMap;
57 import com.intellij.util.messages.MessageBusConnection;
58 import com.intellij.util.pico.ConstructorInjectionComponentAdapter;
59 import com.intellij.util.ui.UIUtil;
60 import gnu.trove.THashMap;
61 import gnu.trove.THashSet;
62 import gnu.trove.TObjectIntHashMap;
63 import org.jdom.Element;
64 import org.jetbrains.annotations.NonNls;
65 import org.jetbrains.annotations.NotNull;
66 import org.jetbrains.annotations.Nullable;
67
68 import javax.swing.*;
69 import javax.swing.Timer;
70 import java.awt.*;
71 import java.awt.event.*;
72 import java.util.*;
73 import java.util.List;
74 import java.util.concurrent.Future;
75
76 public final class ActionManagerImpl extends ActionManagerEx implements ApplicationComponent {
77   @NonNls public static final String ACTION_ELEMENT_NAME = "action";
78   @NonNls public static final String GROUP_ELEMENT_NAME = "group";
79   @NonNls public static final String ACTIONS_ELEMENT_NAME = "actions";
80   @NonNls public static final String CLASS_ATTR_NAME = "class";
81   @NonNls public static final String ID_ATTR_NAME = "id";
82   @NonNls public static final String INTERNAL_ATTR_NAME = "internal";
83   @NonNls public static final String ICON_ATTR_NAME = "icon";
84   @NonNls public static final String ADD_TO_GROUP_ELEMENT_NAME = "add-to-group";
85   @NonNls public static final String SHORTCUT_ELEMENT_NAME = "keyboard-shortcut";
86   @NonNls public static final String MOUSE_SHORTCUT_ELEMENT_NAME = "mouse-shortcut";
87   @NonNls public static final String DESCRIPTION = "description";
88   @NonNls public static final String TEXT_ATTR_NAME = "text";
89   @NonNls public static final String POPUP_ATTR_NAME = "popup";
90   @NonNls public static final String COMPACT_ATTR_NAME = "compact";
91   @NonNls public static final String SEPARATOR_ELEMENT_NAME = "separator";
92   @NonNls public static final String REFERENCE_ELEMENT_NAME = "reference";
93   @NonNls public static final String ABBREVIATION_ELEMENT_NAME = "abbreviation";
94   @NonNls public static final String GROUPID_ATTR_NAME = "group-id";
95   @NonNls public static final String ANCHOR_ELEMENT_NAME = "anchor";
96   @NonNls public static final String FIRST = "first";
97   @NonNls public static final String LAST = "last";
98   @NonNls public static final String BEFORE = "before";
99   @NonNls public static final String AFTER = "after";
100   @NonNls public static final String SECONDARY = "secondary";
101   @NonNls public static final String RELATIVE_TO_ACTION_ATTR_NAME = "relative-to-action";
102   @NonNls public static final String FIRST_KEYSTROKE_ATTR_NAME = "first-keystroke";
103   @NonNls public static final String SECOND_KEYSTROKE_ATTR_NAME = "second-keystroke";
104   @NonNls public static final String REMOVE_SHORTCUT_ATTR_NAME = "remove";
105   @NonNls public static final String REPLACE_SHORTCUT_ATTR_NAME = "replace-all";
106   @NonNls public static final String KEYMAP_ATTR_NAME = "keymap";
107   @NonNls public static final String KEYSTROKE_ATTR_NAME = "keystroke";
108   @NonNls public static final String REF_ATTR_NAME = "ref";
109   @NonNls public static final String VALUE_ATTR_NAME = "value";
110   @NonNls public static final String ACTIONS_BUNDLE = "messages.ActionsBundle";
111   @NonNls public static final String USE_SHORTCUT_OF_ATTR_NAME = "use-shortcut-of";
112   @NonNls public static final String OVERRIDES_ATTR_NAME = "overrides";
113   @NonNls public static final String KEEP_CONTENT_ATTR_NAME = "keep-content";
114   @NonNls public static final String PROJECT_TYPE = "project-type";
115   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.actionSystem.impl.ActionManagerImpl");
116   private static final int DEACTIVATED_TIMER_DELAY = 5000;
117   private static final int TIMER_DELAY = 500;
118   private static final int UPDATE_DELAY_AFTER_TYPING = 500;
119   private final Object myLock = new Object();
120   private final Map<String,AnAction> myId2Action = new THashMap<String, AnAction>();
121   private final Map<PluginId, THashSet<String>> myPlugin2Id = new THashMap<PluginId, THashSet<String>>();
122   private final TObjectIntHashMap<String> myId2Index = new TObjectIntHashMap<String>();
123   private final Map<Object,String> myAction2Id = new THashMap<Object, String>();
124   private final MultiMap<String,String> myId2GroupId = new MultiMap<String, String>();
125   private final List<String> myNotRegisteredInternalActionIds = new ArrayList<String>();
126   private final List<AnActionListener> myActionListeners = ContainerUtil.createLockFreeCopyOnWriteList();
127   private final KeymapManager myKeymapManager;
128   private final DataManager myDataManager;
129   private final List<ActionPopupMenuImpl> myPopups = new ArrayList<ActionPopupMenuImpl>();
130   private final Map<AnAction, DataContext> myQueuedNotifications = new LinkedHashMap<AnAction, DataContext>();
131   private final Map<AnAction, AnActionEvent> myQueuedNotificationsEvents = new LinkedHashMap<AnAction, AnActionEvent>();
132   private MyTimer myTimer;
133   private int myRegisteredActionsCount;
134   private String myLastPreformedActionId;
135   private String myPrevPerformedActionId;
136   private long myLastTimeEditorWasTypedIn = 0;
137   private Runnable myPreloadActionsRunnable;
138   private boolean myTransparentOnlyUpdate;
139   private int myActionsPreloaded = 0;
140
141   ActionManagerImpl(KeymapManager keymapManager, DataManager dataManager) {
142     myKeymapManager = keymapManager;
143     myDataManager = dataManager;
144
145     registerPluginActions();
146   }
147
148   static AnAction convertStub(ActionStub stub) {
149     Object obj;
150     String className = stub.getClassName();
151     try {
152       Class<?> aClass = Class.forName(className, true, stub.getLoader());
153       obj = ReflectionUtil.newInstance(aClass);
154     }
155     catch (ClassNotFoundException e) {
156       PluginId pluginId = stub.getPluginId();
157       if (pluginId != null) {
158         throw new PluginException("class with name \"" + className + "\" not found", e, pluginId);
159       }
160       else {
161         throw new IllegalStateException("class with name \"" + className + "\" not found");
162       }
163     }
164     catch(UnsupportedClassVersionError e) {
165       PluginId pluginId = stub.getPluginId();
166       if (pluginId != null) {
167         throw new PluginException(e, pluginId);
168       }
169       else {
170         throw new IllegalStateException(e);
171       }
172     }
173     catch (Exception e) {
174       PluginId pluginId = stub.getPluginId();
175       if (pluginId != null) {
176         throw new PluginException("cannot create class \"" + className + "\"", e, pluginId);
177       }
178       else {
179         throw new IllegalStateException("cannot create class \"" + className + "\"", e);
180       }
181     }
182
183     if (!(obj instanceof AnAction)) {
184       throw new IllegalStateException("class with name '" + className + "' must be an instance of '" + AnAction.class.getName()+"'; got "+obj);
185     }
186
187     AnAction anAction = (AnAction)obj;
188     stub.initAction(anAction);
189     if (StringUtil.isNotEmpty(stub.getText())) {
190       anAction.getTemplatePresentation().setText(stub.getText());
191     }
192     String iconPath = stub.getIconPath();
193     if (iconPath != null) {
194       Class<? extends AnAction> actionClass = anAction.getClass();
195       setIconFromClass(actionClass, actionClass.getClassLoader(), iconPath, anAction.getTemplatePresentation(), stub.getPluginId());
196     }
197     return anAction;
198   }
199
200   private static void processAbbreviationNode(Element e, String id) {
201     final String abbr = e.getAttributeValue(VALUE_ATTR_NAME);
202     if (!StringUtil.isEmpty(abbr)) {
203       final AbbreviationManagerImpl abbreviationManager = ((AbbreviationManagerImpl)AbbreviationManager.getInstance());
204       abbreviationManager.register(abbr, id, true);
205     }
206   }
207
208   @Nullable
209   private static ResourceBundle getActionsResourceBundle(ClassLoader loader, IdeaPluginDescriptor plugin) {
210     @NonNls final String resBundleName = plugin != null && !"com.intellij".equals(plugin.getPluginId().getIdString())
211                                          ? plugin.getResourceBundleBaseName() : ACTIONS_BUNDLE;
212     ResourceBundle bundle = null;
213     if (resBundleName != null) {
214       bundle = AbstractBundle.getResourceBundle(resBundleName, loader);
215     }
216     return bundle;
217   }
218
219   private static boolean isSecondary(Element element) {
220     return "true".equalsIgnoreCase(element.getAttributeValue(SECONDARY));
221   }
222
223   private static void setIcon(@Nullable final String iconPath,
224                               @NotNull String className,
225                               @NotNull ClassLoader loader,
226                               @NotNull Presentation presentation,
227                               final PluginId pluginId) {
228     if (iconPath == null) return;
229
230     try {
231       final Class actionClass = Class.forName(className, true, loader);
232       setIconFromClass(actionClass, loader, iconPath, presentation, pluginId);
233     }
234     catch (ClassNotFoundException e) {
235       LOG.error(e);
236       reportActionError(pluginId, "class with name \"" + className + "\" not found");
237     }
238     catch (NoClassDefFoundError e) {
239       LOG.error(e);
240       reportActionError(pluginId, "class with name \"" + className + "\" not found");
241     }
242   }
243
244   private static void setIconFromClass(@NotNull final Class actionClass,
245                                        @NotNull final ClassLoader classLoader,
246                                        @NotNull final String iconPath,
247                                        @NotNull Presentation presentation,
248                                        final PluginId pluginId) {
249     final IconLoader.LazyIcon lazyIcon = new IconLoader.LazyIcon() {
250       @Override
251       protected Icon compute() {
252         //try to find icon in idea class path
253         Icon icon = IconLoader.findIcon(iconPath, actionClass, true);
254         if (icon == null) {
255           icon = IconLoader.findIcon(iconPath, classLoader);
256         }
257
258         if (icon == null) {
259           reportActionError(pluginId, "Icon cannot be found in '" + iconPath + "', action '" + actionClass + "'");
260         }
261
262         return icon;
263       }
264
265       @Override
266       public String toString() {
267         return "LazyIcon@ActionManagerImpl (path: " + iconPath + ", action class: " + actionClass + ")";
268       }
269     };
270
271     if (!Registry.is("ide.lazyIconLoading")) {
272       lazyIcon.load();
273     }
274
275     presentation.setIcon(lazyIcon);
276   }
277
278   private static String loadDescriptionForElement(final Element element, final ResourceBundle bundle, final String id, String elementType) {
279     final String value = element.getAttributeValue(DESCRIPTION);
280     if (bundle != null) {
281       @NonNls final String key = elementType + "." + id + ".description";
282       return CommonBundle.messageOrDefault(bundle, key, value == null ? "" : value);
283     } else {
284       return value;
285     }
286   }
287
288   private static String loadTextForElement(final Element element, final ResourceBundle bundle, final String id, String elementType) {
289     final String value = element.getAttributeValue(TEXT_ATTR_NAME);
290     return CommonBundle.messageOrDefault(bundle, elementType + "." + id + "." + TEXT_ATTR_NAME, value == null ? "" : value);
291   }
292
293   public static boolean checkRelativeToAction(final String relativeToActionId,
294                                        @NotNull final Anchor anchor,
295                                        @NotNull final String actionName,
296                                        @Nullable final PluginId pluginId) {
297     if ((Anchor.BEFORE == anchor || Anchor.AFTER == anchor) && relativeToActionId == null) {
298       reportActionError(pluginId, actionName + ": \"relative-to-action\" cannot be null if anchor is \"after\" or \"before\"");
299       return false;
300     }
301     return true;
302   }
303
304   @Nullable
305   public static Anchor parseAnchor(final String anchorStr,
306                             @Nullable final String actionName,
307                             @Nullable final PluginId pluginId) {
308     if (anchorStr == null) {
309       return Anchor.LAST;
310     }
311
312     if (FIRST.equalsIgnoreCase(anchorStr)) {
313       return Anchor.FIRST;
314     }
315     else if (LAST.equalsIgnoreCase(anchorStr)) {
316       return Anchor.LAST;
317     }
318     else if (BEFORE.equalsIgnoreCase(anchorStr)) {
319       return Anchor.BEFORE;
320     }
321     else if (AFTER.equalsIgnoreCase(anchorStr)) {
322       return Anchor.AFTER;
323     }
324     else {
325       reportActionError(pluginId, actionName + ": anchor should be one of the following constants: \"first\", \"last\", \"before\" or \"after\"");
326       return null;
327     }
328   }
329
330   private static void processMouseShortcutNode(Element element, String actionId, PluginId pluginId) {
331     String keystrokeString = element.getAttributeValue(KEYSTROKE_ATTR_NAME);
332     if (keystrokeString == null || keystrokeString.trim().isEmpty()) {
333       reportActionError(pluginId, "\"keystroke\" attribute must be specified for action with id=" + actionId);
334       return;
335     }
336     MouseShortcut shortcut;
337     try {
338       shortcut = KeymapUtil.parseMouseShortcut(keystrokeString);
339     }
340     catch (Exception ex) {
341       reportActionError(pluginId, "\"keystroke\" attribute has invalid value for action with id=" + actionId);
342       return;
343     }
344
345     String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
346     if (keymapName == null || keymapName.isEmpty()) {
347       reportActionError(pluginId, "attribute \"keymap\" should be defined");
348       return;
349     }
350     Keymap keymap = KeymapManager.getInstance().getKeymap(keymapName);
351     if (keymap == null) {
352       reportActionError(pluginId, "keymap \"" + keymapName + "\" not found");
353       return;
354     }
355
356     final String removeOption = element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME);
357     if (Boolean.valueOf(removeOption)) {
358       keymap.removeShortcut(actionId, shortcut);
359     } else {
360       keymap.addShortcut(actionId, shortcut);
361     }
362   }
363
364   private static void assertActionIsGroupOrStub(final AnAction action) {
365     if (!(action instanceof ActionGroup || action instanceof ActionStub || action instanceof ChameleonAction)) {
366       LOG.error("Action : " + action + "; class: " + action.getClass());
367     }
368   }
369
370   private static void reportActionError(final PluginId pluginId, @NonNls @NotNull String message) {
371     if (pluginId == null) {
372       LOG.error(message);
373     }
374     else {
375       LOG.error(new PluginException(message, null, pluginId));
376     }
377   }
378   private static void reportActionWarning(final PluginId pluginId, @NonNls @NotNull String message) {
379     if (pluginId == null) {
380       LOG.warn(message);
381     }
382     else {
383       LOG.warn(new PluginException(message, null, pluginId).getMessage());
384     }
385   }
386
387   @NonNls
388   private static String getPluginInfo(@Nullable PluginId id) {
389     if (id != null) {
390       final IdeaPluginDescriptor plugin = PluginManager.getPlugin(id);
391       if (plugin != null) {
392         String name = plugin.getName();
393         if (name == null) {
394           name = id.getIdString();
395         }
396         return " Plugin: " + name;
397       }
398     }
399     return "";
400   }
401
402   private static DataContext getContextBy(Component contextComponent) {
403     final DataManager dataManager = DataManager.getInstance();
404     return contextComponent != null ? dataManager.getDataContext(contextComponent) : dataManager.getDataContext();
405   }
406
407   @Override
408   public void initComponent() {}
409
410   @Override
411   public void disposeComponent() {
412     if (myTimer != null) {
413       myTimer.stop();
414       myTimer = null;
415     }
416   }
417
418   @Override
419   public void addTimerListener(int delay, final TimerListener listener) {
420     _addTimerListener(listener, false);
421   }
422
423   @Override
424   public void removeTimerListener(TimerListener listener) {
425     _removeTimerListener(listener, false);
426   }
427
428   @Override
429   public void addTransparentTimerListener(int delay, TimerListener listener) {
430     _addTimerListener(listener, true);
431   }
432
433   @Override
434   public void removeTransparentTimerListener(TimerListener listener) {
435     _removeTimerListener(listener, true);
436   }
437
438   private void _addTimerListener(final TimerListener listener, boolean transparent) {
439     if (ApplicationManager.getApplication().isUnitTestMode()) return;
440     if (myTimer == null) {
441       myTimer = new MyTimer();
442       myTimer.start();
443     }
444
445     myTimer.addTimerListener(listener, transparent);
446   }
447
448   private void _removeTimerListener(TimerListener listener, boolean transparent) {
449     if (ApplicationManager.getApplication().isUnitTestMode()) return;
450     if (LOG.assertTrue(myTimer != null)) {
451       myTimer.removeTimerListener(listener, transparent);
452     }
453   }
454
455   public ActionPopupMenu createActionPopupMenu(String place, @NotNull ActionGroup group, @Nullable PresentationFactory presentationFactory) {
456     return new ActionPopupMenuImpl(place, group, this, presentationFactory);
457   }
458
459   @Override
460   public ActionPopupMenu createActionPopupMenu(String place, @NotNull ActionGroup group) {
461     return new ActionPopupMenuImpl(place, group, this, null);
462   }
463
464   @Override
465   public ActionToolbar createActionToolbar(final String place, @NotNull final ActionGroup group, final boolean horizontal) {
466     return createActionToolbar(place, group, horizontal, false);
467   }
468
469   @Override
470   public ActionToolbar createActionToolbar(final String place, @NotNull final ActionGroup group, final boolean horizontal, final boolean decorateButtons) {
471     return new ActionToolbarImpl(place, group, horizontal, decorateButtons, myDataManager, this, (KeymapManagerEx)myKeymapManager);
472   }
473
474   private void registerPluginActions() {
475     final IdeaPluginDescriptor[] plugins = PluginManagerCore.getPlugins();
476     for (IdeaPluginDescriptor plugin : plugins) {
477       if (PluginManagerCore.shouldSkipPlugin(plugin)) continue;
478       final List<Element> elementList = plugin.getActionsDescriptionElements();
479       if (elementList != null) {
480         for (Element e : elementList) {
481           processActionsChildElement(plugin.getPluginClassLoader(), plugin.getPluginId(), e);
482         }
483       }
484     }
485   }
486
487   @Override
488   public AnAction getAction(@NotNull String id) {
489     return getActionImpl(id, false, null);
490   }
491
492   @Override
493   public AnAction getAction(@NonNls @NotNull String actionId, @Nullable ProjectType projectType) {
494     return getActionImpl(actionId, false, projectType);
495   }
496
497   private AnAction getActionImpl(String id, boolean canReturnStub, ProjectType projectType) {
498     synchronized (myLock) {
499       AnAction action;
500       Object o = myId2Action.get(id);
501       if (o == null) {
502         return null;
503       }
504       if (o instanceof AnAction) {
505         action = (AnAction)o;
506       }
507       else {
508         //noinspection unchecked
509         action = ((Map<ProjectType, AnAction>)o).get(projectType);
510       }
511       if (!canReturnStub && action instanceof ActionStub) {
512         action = convert((ActionStub)action);
513       }
514       return action;
515     }
516   }
517
518   /**
519    * Converts action's stub to normal action.
520    */
521   @NotNull
522   private AnAction convert(@NotNull ActionStub stub) {
523     LOG.assertTrue(myAction2Id.containsKey(stub));
524     myAction2Id.remove(stub);
525
526     LOG.assertTrue(myId2Action.containsKey(stub.getId()));
527
528     AnAction action = myId2Action.remove(stub.getId());
529     LOG.assertTrue(action != null);
530     LOG.assertTrue(action.equals(stub));
531
532     AnAction anAction = convertStub(stub);
533     myAction2Id.put(anAction, stub.getId());
534
535     return addToMap(stub.getId(), anAction, stub.getPluginId(), stub.getProjectType());
536   }
537
538   @Override
539   public String getId(@NotNull AnAction action) {
540     LOG.assertTrue(!(action instanceof ActionStub));
541     synchronized (myLock) {
542       return myAction2Id.get(action);
543     }
544   }
545
546   @Override
547   public String[] getActionIds(@NotNull String idPrefix) {
548     synchronized (myLock) {
549       ArrayList<String> idList = new ArrayList<String>();
550       for (String id : myId2Action.keySet()) {
551         if (id.startsWith(idPrefix)) {
552           idList.add(id);
553         }
554       }
555       return ArrayUtil.toStringArray(idList);
556     }
557   }
558
559   @Override
560   public boolean isGroup(@NotNull String actionId) {
561     return getActionImpl(actionId, true, null) instanceof ActionGroup;
562   }
563
564   @Override
565   public JComponent createButtonToolbar(final String actionPlace, @NotNull final ActionGroup messageActionGroup) {
566     return new ButtonToolbarImpl(actionPlace, messageActionGroup, myDataManager, this);
567   }
568
569   @Override
570   public AnAction getActionOrStub(String id) {
571     return getActionImpl(id, true, null);
572   }
573
574   /**
575    * @return instance of ActionGroup or ActionStub. The method never returns real subclasses
576    *         of <code>AnAction</code>.
577    */
578   @Nullable
579   private AnAction processActionElement(Element element, final ClassLoader loader, PluginId pluginId) {
580     final IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId);
581     ResourceBundle bundle = getActionsResourceBundle(loader, plugin);
582
583     if (!ACTION_ELEMENT_NAME.equals(element.getName())) {
584       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
585       return null;
586     }
587     String className = element.getAttributeValue(CLASS_ATTR_NAME);
588     if (className == null || className.isEmpty()) {
589       reportActionError(pluginId, "action element should have specified \"class\" attribute");
590       return null;
591     }
592     // read ID and register loaded action
593     String id = element.getAttributeValue(ID_ATTR_NAME);
594     if (id == null || id.isEmpty()) {
595       id = StringUtil.getShortName(className);
596     }
597     if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) {
598       myNotRegisteredInternalActionIds.add(id);
599       return null;
600     }
601
602     String text = loadTextForElement(element, bundle, id, ACTION_ELEMENT_NAME);
603
604     String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
605
606     if (text == null) {
607       @NonNls String message = "'text' attribute is mandatory (action ID=" + id + ";" +
608                                (plugin == null ? "" : " plugin path: "+plugin.getPath()) + ")";
609       reportActionError(pluginId, message);
610       return null;
611     }
612
613     String projectType = element.getAttributeValue(PROJECT_TYPE);
614     ActionStub stub = new ActionStub(className, id, text, loader, pluginId, iconPath, projectType);
615     Presentation presentation = stub.getTemplatePresentation();
616     presentation.setText(text);
617
618     // description
619
620     presentation.setDescription(loadDescriptionForElement(element, bundle, id, ACTION_ELEMENT_NAME));
621
622     // process all links and key bindings if any
623     for (final Object o : element.getChildren()) {
624       Element e = (Element)o;
625       if (ADD_TO_GROUP_ELEMENT_NAME.equals(e.getName())) {
626         processAddToGroupNode(stub, e, pluginId, isSecondary(e));
627       }
628       else if (SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
629         processKeyboardShortcutNode(e, id, pluginId);
630       }
631       else if (MOUSE_SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
632         processMouseShortcutNode(e, id, pluginId);
633       }
634       else if (ABBREVIATION_ELEMENT_NAME.equals(e.getName())) {
635         processAbbreviationNode(e, id);
636       }
637       else {
638         reportActionError(pluginId, "unexpected name of element \"" + e.getName() + "\"");
639         return null;
640       }
641     }
642     if (element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME) != null) {
643       ((KeymapManagerEx)myKeymapManager).bindShortcuts(element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME), id);
644     }
645
646     registerOrReplaceActionInner(element, id, stub, pluginId);
647     return stub;
648   }
649
650   private void registerOrReplaceActionInner(@NotNull Element element, @NotNull String id, @NotNull AnAction action, @Nullable PluginId pluginId) {
651     synchronized (myLock) {
652       if (Boolean.valueOf(element.getAttributeValue(OVERRIDES_ATTR_NAME))) {
653         if (getActionOrStub(id) == null) {
654           throw new RuntimeException(element.getName() + " '" + id + "' doesn't override anything");
655         }
656         AnAction prev = replaceAction(id, action, pluginId);
657         if (action instanceof DefaultActionGroup && prev instanceof DefaultActionGroup) {
658           if (Boolean.valueOf(element.getAttributeValue(KEEP_CONTENT_ATTR_NAME))) {
659             ((DefaultActionGroup)action).copyFromGroup((DefaultActionGroup)prev);
660           }
661         }
662       }
663       else {
664         registerAction(id, action, pluginId, element.getAttributeValue(PROJECT_TYPE));
665       }
666     }
667   }
668
669   private AnAction processGroupElement(Element element, final ClassLoader loader, PluginId pluginId) {
670     final IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId);
671     ResourceBundle bundle = getActionsResourceBundle(loader, plugin);
672
673     if (!GROUP_ELEMENT_NAME.equals(element.getName())) {
674       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
675       return null;
676     }
677     String className = element.getAttributeValue(CLASS_ATTR_NAME);
678     if (className == null) { // use default group if class isn't specified
679       if ("true".equals(element.getAttributeValue(COMPACT_ATTR_NAME))) {
680         className = DefaultCompactActionGroup.class.getName();
681       } else {
682         className = DefaultActionGroup.class.getName();
683       }
684     }
685     try {
686       ActionGroup group;
687       if (DefaultActionGroup.class.getName().equals(className)) {
688         group = new DefaultActionGroup();
689       } else if (DefaultCompactActionGroup.class.getName().equals(className)) {
690         group = new DefaultCompactActionGroup();
691       } else {
692         Class aClass = Class.forName(className, true, loader);
693         Object obj = new ConstructorInjectionComponentAdapter(className, aClass).getComponentInstance(ApplicationManager.getApplication().getPicoContainer());
694
695         if (!(obj instanceof ActionGroup)) {
696           reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + ActionGroup.class.getName());
697           return null;
698         }
699         if (element.getChildren().size() != element.getChildren(ADD_TO_GROUP_ELEMENT_NAME).size() ) {  //
700           if (!(obj instanceof DefaultActionGroup)) {
701             reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + DefaultActionGroup.class.getName() +
702                                         " because there are children specified");
703             return null;
704           }
705         }
706         group = (ActionGroup)obj;
707       }
708       // read ID and register loaded group
709       String id = element.getAttributeValue(ID_ATTR_NAME);
710       if (id != null && id.isEmpty()) {
711         reportActionError(pluginId, "ID of the group cannot be an empty string");
712         return null;
713       }
714       if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) {
715         myNotRegisteredInternalActionIds.add(id);
716         return null;
717       }
718
719       if (id != null) {
720         registerOrReplaceActionInner(element, id, group, pluginId);
721       }
722       Presentation presentation = group.getTemplatePresentation();
723
724       // text
725       String text = loadTextForElement(element, bundle, id, GROUP_ELEMENT_NAME);
726       // don't override value which was set in API with empty value from xml descriptor
727       if (!StringUtil.isEmpty(text) || presentation.getText() == null) {
728         presentation.setText(text);
729       }
730
731       // description
732       String description = loadDescriptionForElement(element, bundle, id, GROUP_ELEMENT_NAME);
733       // don't override value which was set in API with empty value from xml descriptor
734       if (!StringUtil.isEmpty(description) || presentation.getDescription() == null) {
735         presentation.setDescription(description);
736       }
737
738       // icon
739       setIcon(element.getAttributeValue(ICON_ATTR_NAME), className, loader, presentation, pluginId);
740       // popup
741       String popup = element.getAttributeValue(POPUP_ATTR_NAME);
742       if (popup != null) {
743         group.setPopup(Boolean.valueOf(popup).booleanValue());
744       }
745       // process all group's children. There are other groups, actions, references and links
746       for (final Object o : element.getChildren()) {
747         Element child = (Element)o;
748         String name = child.getName();
749         if (ACTION_ELEMENT_NAME.equals(name)) {
750           AnAction action = processActionElement(child, loader, pluginId);
751           if (action != null) {
752             assertActionIsGroupOrStub(action);
753             addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
754           }
755         }
756         else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
757           processSeparatorNode((DefaultActionGroup)group, child, pluginId);
758         }
759         else if (GROUP_ELEMENT_NAME.equals(name)) {
760           AnAction action = processGroupElement(child, loader, pluginId);
761           if (action != null) {
762             addToGroupInner(group, action, Constraints.LAST, false);
763           }
764         }
765         else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) {
766           processAddToGroupNode(group, child, pluginId, isSecondary(child));
767         }
768         else if (REFERENCE_ELEMENT_NAME.equals(name)) {
769           AnAction action = processReferenceElement(child, pluginId);
770           if (action != null) {
771             addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
772           }
773         }
774         else {
775           reportActionError(pluginId, "unexpected name of element \"" + name + "\n");
776           return null;
777         }
778       }
779       return group;
780     }
781     catch (ClassNotFoundException e) {
782       reportActionError(pluginId, "class with name \"" + className + "\" not found");
783       return null;
784     }
785     catch (NoClassDefFoundError e) {
786       reportActionError(pluginId, "class with name \"" + e.getMessage() + "\" not found");
787       return null;
788     }
789     catch(UnsupportedClassVersionError e) {
790       reportActionError(pluginId, "unsupported class version for " + className);
791       return null;
792     }
793     catch (Exception e) {
794       final String message = "cannot create class \"" + className + "\"";
795       if (pluginId == null) {
796         LOG.error(message, e);
797       }
798       else {
799         LOG.error(new PluginException(message, e, pluginId));
800       }
801       return null;
802     }
803   }
804
805   private void processReferenceNode(final Element element, final PluginId pluginId) {
806     final AnAction action = processReferenceElement(element, pluginId);
807
808     for (final Object o : element.getChildren()) {
809       Element child = (Element)o;
810       if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
811         processAddToGroupNode(action, child, pluginId, isSecondary(child));
812       }
813     }
814   }
815
816   /**\
817    * @param element description of link
818    */
819   private void processAddToGroupNode(AnAction action, Element element, final PluginId pluginId, boolean secondary) {
820     // Real subclasses of AnAction should not be here
821     if (!(action instanceof Separator)) {
822       assertActionIsGroupOrStub(action);
823     }
824
825     String actionName = String.format(
826       "%s (%s)", action instanceof ActionStub? ((ActionStub)action).getClassName() : action.getClass().getName(),
827       action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action));
828
829     if (!ADD_TO_GROUP_ELEMENT_NAME.equals(element.getName())) {
830       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
831       return;
832     }
833
834     // parent group
835     final AnAction parentGroup = getParentGroup(element.getAttributeValue(GROUPID_ATTR_NAME), actionName, pluginId);
836     if (parentGroup == null) {
837       return;
838     }
839
840     // anchor attribute
841     final Anchor anchor = parseAnchor(element.getAttributeValue(ANCHOR_ELEMENT_NAME), actionName, pluginId);
842     if (anchor == null) {
843       return;
844     }
845
846     final String relativeToActionId = element.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME);
847     if (!checkRelativeToAction(relativeToActionId, anchor, actionName, pluginId)) {
848       return;
849     }
850     addToGroupInner(parentGroup, action, new Constraints(anchor, relativeToActionId), secondary);
851   }
852
853   private void addToGroupInner(AnAction group, AnAction action, Constraints constraints, boolean secondary) {
854     ((DefaultActionGroup)group).addAction(action, constraints, this).setAsSecondary(secondary);
855     myId2GroupId.putValue(myAction2Id.get(action), myAction2Id.get(group));
856   }
857
858   @Nullable
859   public AnAction getParentGroup(final String groupId,
860                                  @Nullable final String actionName,
861                                  @Nullable final PluginId pluginId) {
862     if (groupId == null || groupId.isEmpty()) {
863       reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined");
864       return null;
865     }
866     AnAction parentGroup = getActionImpl(groupId, true, null);
867     if (parentGroup == null) {
868       reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" isn't registered; action will be added to the \"Other\" group");
869       parentGroup = getActionImpl(IdeActions.GROUP_OTHER_MENU, true, null);
870     }
871     if (!(parentGroup instanceof DefaultActionGroup)) {
872       reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" should be instance of " + DefaultActionGroup.class.getName() +
873                                   " but was " + parentGroup.getClass());
874       return null;
875     }
876     return parentGroup;
877   }
878
879   /**
880    * @param parentGroup group which is the parent of the separator. It can be <code>null</code> in that
881    *                    case separator will be added to group described in the <add-to-group ....> subelement.
882    * @param element     XML element which represent separator.
883    */
884   private void processSeparatorNode(@Nullable DefaultActionGroup parentGroup, Element element, PluginId pluginId) {
885     if (!SEPARATOR_ELEMENT_NAME.equals(element.getName())) {
886       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
887       return;
888     }
889     Separator separator = Separator.getInstance();
890     if (parentGroup != null) {
891       parentGroup.add(separator, this);
892     }
893     // try to find inner <add-to-parent...> tag
894     for (final Object o : element.getChildren()) {
895       Element child = (Element)o;
896       if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
897         processAddToGroupNode(separator, child, pluginId, isSecondary(child));
898       }
899     }
900   }
901
902   private void processKeyboardShortcutNode(Element element, String actionId, PluginId pluginId) {
903     String firstStrokeString = element.getAttributeValue(FIRST_KEYSTROKE_ATTR_NAME);
904     if (firstStrokeString == null) {
905       reportActionError(pluginId, "\"first-keystroke\" attribute must be specified for action with id=" + actionId);
906       return;
907     }
908     KeyStroke firstKeyStroke = getKeyStroke(firstStrokeString);
909     if (firstKeyStroke == null) {
910       reportActionError(pluginId, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId);
911       return;
912     }
913
914     KeyStroke secondKeyStroke = null;
915     String secondStrokeString = element.getAttributeValue(SECOND_KEYSTROKE_ATTR_NAME);
916     if (secondStrokeString != null) {
917       secondKeyStroke = getKeyStroke(secondStrokeString);
918       if (secondKeyStroke == null) {
919         reportActionError(pluginId, "\"second-keystroke\" attribute has invalid value for action with id=" + actionId);
920         return;
921       }
922     }
923
924     String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
925     if (keymapName == null || keymapName.trim().isEmpty()) {
926       reportActionError(pluginId, "attribute \"keymap\" should be defined");
927       return;
928     }
929     Keymap keymap = myKeymapManager.getKeymap(keymapName);
930     if (keymap == null) {
931       reportActionWarning(pluginId, "keymap \"" + keymapName + "\" not found");
932       return;
933     }
934     final String removeOption = element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME);
935     final KeyboardShortcut shortcut = new KeyboardShortcut(firstKeyStroke, secondKeyStroke);
936     final String replaceOption = element.getAttributeValue(REPLACE_SHORTCUT_ATTR_NAME);
937     if (Boolean.valueOf(removeOption)) {
938       keymap.removeShortcut(actionId, shortcut);
939     }
940     if (Boolean.valueOf(replaceOption)) {
941       keymap.removeAllActionShortcuts(actionId);
942     }
943     if (!Boolean.valueOf(removeOption)) {
944       keymap.addShortcut(actionId, shortcut);
945     }
946   }
947
948   @Nullable
949   private AnAction processReferenceElement(Element element, PluginId pluginId) {
950     if (!REFERENCE_ELEMENT_NAME.equals(element.getName())) {
951       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
952       return null;
953     }
954     String ref = element.getAttributeValue(REF_ATTR_NAME);
955
956     if (ref==null) {
957       // support old style references by id
958       ref = element.getAttributeValue(ID_ATTR_NAME);
959     }
960
961     if (ref == null || ref.isEmpty()) {
962       reportActionError(pluginId, "ID of reference element should be defined");
963       return null;
964     }
965
966     AnAction action = getActionImpl(ref, true, null);
967
968     if (action == null) {
969       if (!myNotRegisteredInternalActionIds.contains(ref)) {
970         reportActionError(pluginId, "action specified by reference isn't registered (ID=" + ref + ")");
971       }
972       return null;
973     }
974     assertActionIsGroupOrStub(action);
975     return action;
976   }
977
978   private void processActionsChildElement(final ClassLoader loader, final PluginId pluginId, final Element child) {
979     String name = child.getName();
980     if (ACTION_ELEMENT_NAME.equals(name)) {
981       AnAction action = processActionElement(child, loader, pluginId);
982       if (action != null) {
983         assertActionIsGroupOrStub(action);
984       }
985     }
986     else if (GROUP_ELEMENT_NAME.equals(name)) {
987       processGroupElement(child, loader, pluginId);
988     }
989     else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
990       processSeparatorNode(null, child, pluginId);
991     }
992     else if (REFERENCE_ELEMENT_NAME.equals(name)) {
993       processReferenceNode(child, pluginId);
994     }
995     else {
996       reportActionError(pluginId, "unexpected name of element \"" + name + "\n");
997     }
998   }
999
1000   @Override
1001   public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) {
1002     registerAction(actionId, action, pluginId, null);
1003   }
1004
1005   public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId, @Nullable String projectType) {
1006     synchronized (myLock) {
1007       if (addToMap(actionId, action, pluginId, projectType) == null) return;
1008       if (myAction2Id.containsKey(action)) {
1009         reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) +
1010                                     getPluginInfo(pluginId));
1011         return;
1012       }
1013       myId2Index.put(actionId, myRegisteredActionsCount++);
1014       myAction2Id.put(action, actionId);
1015       if (pluginId != null && !(action instanceof ActionGroup)){
1016         THashSet<String> pluginActionIds = myPlugin2Id.get(pluginId);
1017         if (pluginActionIds == null){
1018           pluginActionIds = new THashSet<String>();
1019           myPlugin2Id.put(pluginId, pluginActionIds);
1020         }
1021         pluginActionIds.add(actionId);
1022       }
1023       action.registerCustomShortcutSet(new ProxyShortcutSet(actionId, myKeymapManager), null);
1024     }
1025   }
1026
1027   private AnAction addToMap(String actionId, AnAction action, PluginId pluginId, String projectType) {
1028     if (projectType != null || myId2Action.containsKey(actionId)) {
1029       return registerChameleon(actionId, action, pluginId, projectType);
1030     }
1031     else {
1032       myId2Action.put(actionId, action);
1033       return action;
1034     }
1035   }
1036
1037   private AnAction registerChameleon(String actionId, AnAction action, PluginId pluginId, String projectType) {
1038     ProjectType type = projectType == null ? null : new ProjectType(projectType);
1039     // make sure id+projectType is unique
1040     AnAction o = myId2Action.get(actionId);
1041     ChameleonAction chameleonAction;
1042     if (o == null) {
1043       chameleonAction = new ChameleonAction(action, type);
1044       myId2Action.put(actionId, chameleonAction);
1045       return chameleonAction;
1046     }
1047     if (o instanceof ChameleonAction) {
1048       chameleonAction = (ChameleonAction)o;
1049     }
1050     else {
1051       chameleonAction = new ChameleonAction(o, type);
1052       myId2Action.put(actionId, chameleonAction);
1053     }
1054     AnAction old = chameleonAction.addAction(action, type);
1055     if (old != null) {
1056       reportActionError(pluginId,
1057                         "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action +
1058                         "; Registered action is " +
1059                         myId2Action.get(actionId) + getPluginInfo(pluginId));
1060       return null;
1061     }
1062     return chameleonAction;
1063   }
1064
1065   @Override
1066   public void registerAction(@NotNull String actionId, @NotNull AnAction action) {
1067     registerAction(actionId, action, null);
1068   }
1069
1070   @Override
1071   public void unregisterAction(@NotNull String actionId) {
1072     synchronized (myLock) {
1073       if (!myId2Action.containsKey(actionId)) {
1074         if (LOG.isDebugEnabled()) {
1075           LOG.debug("action with ID " + actionId + " wasn't registered");
1076           return;
1077         }
1078       }
1079       AnAction oldValue = myId2Action.remove(actionId);
1080       myAction2Id.remove(oldValue);
1081       myId2Index.remove(actionId);
1082       for (PluginId pluginName : myPlugin2Id.keySet()) {
1083         final THashSet<String> pluginActions = myPlugin2Id.get(pluginName);
1084         if (pluginActions != null) {
1085           pluginActions.remove(actionId);
1086         }
1087       }
1088     }
1089   }
1090
1091   @Override
1092   @NotNull
1093   public String getComponentName() {
1094     return "ActionManager";
1095   }
1096
1097   @Override
1098   public Comparator<String> getRegistrationOrderComparator() {
1099     return new Comparator<String>() {
1100       @Override
1101       public int compare(String id1, String id2) {
1102         return myId2Index.get(id1) - myId2Index.get(id2);
1103       }
1104     };
1105   }
1106
1107   @NotNull
1108   @Override
1109   public String[] getPluginActions(PluginId pluginName) {
1110     if (myPlugin2Id.containsKey(pluginName)){
1111       final THashSet<String> pluginActions = myPlugin2Id.get(pluginName);
1112       return ArrayUtil.toStringArray(pluginActions);
1113     }
1114     return ArrayUtil.EMPTY_STRING_ARRAY;
1115   }
1116
1117   public void addActionPopup(final ActionPopupMenuImpl menu) {
1118     myPopups.add(menu);
1119   }
1120
1121   public void removeActionPopup(final ActionPopupMenuImpl menu) {
1122     final boolean removed = myPopups.remove(menu);
1123     if (removed && myPopups.isEmpty()) {
1124       flushActionPerformed();
1125     }
1126   }
1127
1128   @Override
1129   public void queueActionPerformedEvent(final AnAction action, DataContext context, AnActionEvent event) {
1130     if (!myPopups.isEmpty()) {
1131       myQueuedNotifications.put(action, context);
1132     } else {
1133       fireAfterActionPerformed(action, context, event);
1134     }
1135   }
1136
1137   //@Override
1138   //public AnAction replaceAction(String actionId, @NotNull AnAction newAction) {
1139   //  synchronized (myLock) {
1140   //    return replaceAction(actionId, newAction, null);
1141   //  }
1142   //}
1143
1144   @Override
1145   public boolean isActionPopupStackEmpty() {
1146     return myPopups.isEmpty();
1147   }
1148
1149   @Override
1150   public boolean isTransparentOnlyActionsUpdateNow() {
1151     return myTransparentOnlyUpdate;
1152   }
1153
1154   private AnAction replaceAction(@NotNull String actionId, @NotNull AnAction newAction, @Nullable PluginId pluginId) {
1155     AnAction oldAction = getActionOrStub(actionId);
1156     if (oldAction != null) {
1157       boolean isGroup = oldAction instanceof ActionGroup;
1158       if (isGroup != newAction instanceof ActionGroup) {
1159         throw new IllegalStateException("cannot replace a group with an action and vice versa: " + actionId);
1160       }
1161       unregisterAction(actionId);
1162       if (isGroup) {
1163         myId2GroupId.values().remove(actionId);
1164       }
1165     }
1166     registerAction(actionId, newAction, pluginId);
1167     for (String groupId : myId2GroupId.get(actionId)) {
1168       DefaultActionGroup group = ObjectUtils.assertNotNull((DefaultActionGroup)getActionOrStub(groupId));
1169       group.replaceAction(oldAction, newAction);
1170     }
1171     return oldAction;
1172   }
1173
1174   private void flushActionPerformed() {
1175     final Set<AnAction> actions = myQueuedNotifications.keySet();
1176     for (final AnAction eachAction : actions) {
1177       final DataContext eachContext = myQueuedNotifications.get(eachAction);
1178       fireAfterActionPerformed(eachAction, eachContext, myQueuedNotificationsEvents.get(eachAction));
1179     }
1180     myQueuedNotifications.clear();
1181     myQueuedNotificationsEvents.clear();
1182   }
1183
1184   @Override
1185   public void addAnActionListener(AnActionListener listener) {
1186     myActionListeners.add(listener);
1187   }
1188
1189   @Override
1190   public void addAnActionListener(final AnActionListener listener, final Disposable parentDisposable) {
1191     addAnActionListener(listener);
1192     Disposer.register(parentDisposable, new Disposable() {
1193       @Override
1194       public void dispose() {
1195         removeAnActionListener(listener);
1196       }
1197     });
1198   }
1199
1200   @Override
1201   public void removeAnActionListener(AnActionListener listener) {
1202     myActionListeners.remove(listener);
1203   }
1204
1205   @Override
1206   public void fireBeforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
1207     if (action != null) {
1208       myPrevPerformedActionId = myLastPreformedActionId;
1209       myLastPreformedActionId = getId(action);
1210       //noinspection AssignmentToStaticFieldFromInstanceMethod
1211       IdeaLogger.ourLastActionId = myLastPreformedActionId;
1212     }
1213     for (AnActionListener listener : myActionListeners) {
1214       listener.beforeActionPerformed(action, dataContext, event);
1215     }
1216   }
1217
1218   @Override
1219   public void fireAfterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
1220     if (action != null) {
1221       myPrevPerformedActionId = myLastPreformedActionId;
1222       myLastPreformedActionId = getId(action);
1223       //noinspection AssignmentToStaticFieldFromInstanceMethod
1224       IdeaLogger.ourLastActionId = myLastPreformedActionId;
1225     }
1226     for (AnActionListener listener : myActionListeners) {
1227       try {
1228         listener.afterActionPerformed(action, dataContext, event);
1229       }
1230       catch(AbstractMethodError ignored) { }
1231     }
1232   }
1233
1234   @Override
1235   public KeyboardShortcut getKeyboardShortcut(@NotNull String actionId) {
1236     AnAction action = ActionManager.getInstance().getAction(actionId);
1237     final ShortcutSet shortcutSet = action.getShortcutSet();
1238     final Shortcut[] shortcuts = shortcutSet.getShortcuts();
1239     for (final Shortcut shortcut : shortcuts) {
1240       // Shortcut can be MouseShortcut here.
1241       // For example IdeaVIM often assigns them
1242       if (shortcut instanceof KeyboardShortcut){
1243         final KeyboardShortcut kb = (KeyboardShortcut)shortcut;
1244         if (kb.getSecondKeyStroke() == null) {
1245           return (KeyboardShortcut)shortcut;
1246         }
1247       }
1248     }
1249
1250     return null;
1251   }
1252
1253   @Override
1254   public void fireBeforeEditorTyping(char c, DataContext dataContext) {
1255     myLastTimeEditorWasTypedIn = System.currentTimeMillis();
1256     for (AnActionListener listener : myActionListeners) {
1257       listener.beforeEditorTyping(c, dataContext);
1258     }
1259   }
1260
1261   @Override
1262   public String getLastPreformedActionId() {
1263     return myLastPreformedActionId;
1264   }
1265
1266   @Override
1267   public String getPrevPreformedActionId() {
1268     return myPrevPerformedActionId;
1269   }
1270
1271   public Set<String> getActionIds(){
1272     synchronized (myLock) {
1273       return new HashSet<String>(myId2Action.keySet());
1274     }
1275   }
1276
1277   public Future<?> preloadActions() {
1278     if (myPreloadActionsRunnable == null) {
1279       myPreloadActionsRunnable = new Runnable() {
1280         @Override
1281         public void run() {
1282           try {
1283             SearchableOptionsRegistrar.getInstance(); // load inspection descriptions etc. to be used in Goto Action, Search Everywhere 
1284             doPreloadActions();
1285           } catch (RuntimeInterruptedException ignore) {
1286           }
1287         }
1288       };
1289       return ApplicationManager.getApplication().executeOnPooledThread(myPreloadActionsRunnable);
1290     }
1291     return null;
1292   }
1293
1294   private void doPreloadActions() {
1295     try {
1296       Thread.sleep(5000); // wait for project initialization to complete
1297     }
1298     catch (InterruptedException e) {
1299       return; // IDEA exited
1300     }
1301     preloadActionGroup(IdeActions.GROUP_EDITOR_POPUP);
1302     preloadActionGroup(IdeActions.GROUP_EDITOR_TAB_POPUP);
1303     preloadActionGroup(IdeActions.GROUP_PROJECT_VIEW_POPUP);
1304     preloadActionGroup(IdeActions.GROUP_MAIN_MENU);
1305     preloadActionGroup(IdeActions.GROUP_NEW);
1306     // TODO anything else?
1307     LOG.debug("Actions preloading completed");
1308   }
1309
1310   public void preloadActionGroup(final String groupId) {
1311     final AnAction action = getAction(groupId);
1312     if (action instanceof ActionGroup) {
1313       preloadActionGroup((ActionGroup) action);
1314     }
1315   }
1316
1317   private void preloadActionGroup(final ActionGroup group) {
1318     final Application application = ApplicationManager.getApplication();
1319     final AnAction[] children = application.runReadAction(new Computable<AnAction[]>() {
1320       @Override
1321       public AnAction[] compute() {
1322         if (application.isDisposed()) {
1323           return AnAction.EMPTY_ARRAY;
1324         }
1325
1326         return group.getChildren(null);
1327       }
1328     });
1329     for (AnAction action : children) {
1330       if (action instanceof PreloadableAction) {
1331         ((PreloadableAction)action).preload();
1332       }
1333       else if (action instanceof ActionGroup) {
1334         preloadActionGroup((ActionGroup)action);
1335       }
1336
1337       myActionsPreloaded++;
1338       if (myActionsPreloaded % 10 == 0) {
1339         try {
1340           //noinspection BusyWait
1341           Thread.sleep(300);
1342         }
1343         catch (InterruptedException ignored) {
1344           throw new RuntimeInterruptedException(ignored);
1345         }
1346       }
1347     }
1348   }
1349
1350   @Override
1351   public ActionCallback tryToExecute(@NotNull final AnAction action, @NotNull final InputEvent inputEvent, @Nullable final Component contextComponent, @Nullable final String place,
1352                                      boolean now) {
1353
1354     final Application app = ApplicationManager.getApplication();
1355     assert app.isDispatchThread();
1356
1357     final ActionCallback result = new ActionCallback();
1358     final Runnable doRunnable = new Runnable() {
1359       @Override
1360       public void run() {
1361         tryToExecuteNow(action, inputEvent, contextComponent, place, result);
1362       }
1363     };
1364
1365     if (now) {
1366       doRunnable.run();
1367     } else {
1368       //noinspection SSBasedInspection
1369       SwingUtilities.invokeLater(doRunnable);
1370     }
1371
1372     return result;
1373   }
1374
1375   private void tryToExecuteNow(final AnAction action, final InputEvent inputEvent, final Component contextComponent, final String place, final ActionCallback result) {
1376     final Presentation presentation = action.getTemplatePresentation().clone();
1377
1378     IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown(new Runnable() {
1379       @Override
1380       public void run() {
1381         final DataContext context = getContextBy(contextComponent);
1382
1383         AnActionEvent event = new AnActionEvent(
1384           inputEvent, context,
1385           place != null ? place : ActionPlaces.UNKNOWN,
1386           presentation, ActionManagerImpl.this,
1387           inputEvent.getModifiersEx()
1388         );
1389
1390         ActionUtil.performDumbAwareUpdate(action, event, false);
1391         if (!event.getPresentation().isEnabled()) {
1392           result.setRejected();
1393           return;
1394         }
1395
1396         ActionUtil.lastUpdateAndCheckDumb(action, event, false);
1397         if (!event.getPresentation().isEnabled()) {
1398           result.setRejected();
1399           return;
1400         }
1401
1402         Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(context);
1403         if (component != null && !component.isShowing()) {
1404           result.setRejected();
1405           return;
1406         }
1407
1408         fireBeforeActionPerformed(action, context, event);
1409
1410         UIUtil.addAwtListener(new AWTEventListener() {
1411           @Override
1412           public void eventDispatched(AWTEvent event) {
1413             if (event.getID() == WindowEvent.WINDOW_OPENED ||event.getID() == WindowEvent.WINDOW_ACTIVATED) {
1414               if (!result.isProcessed()) {
1415                 final WindowEvent we = (WindowEvent)event;
1416                 IdeFocusManager.findInstanceByComponent(we.getWindow()).doWhenFocusSettlesDown(result.createSetDoneRunnable());
1417               }
1418             }
1419           }
1420         }, AWTEvent.WINDOW_EVENT_MASK, result);
1421
1422         ActionUtil.performActionDumbAware(action, event);
1423         result.setDone();
1424         queueActionPerformedEvent(action, context, event);
1425       }
1426     });
1427   }
1428
1429   private class MyTimer extends Timer implements ActionListener {
1430     private final List<TimerListener> myTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1431     private final List<TimerListener> myTransparentTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1432     private int myLastTimePerformed;
1433
1434     MyTimer() {
1435       super(TIMER_DELAY, null);
1436       addActionListener(this);
1437       setRepeats(true);
1438       final MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
1439       connection.subscribe(ApplicationActivationListener.TOPIC, new ApplicationActivationListener() {
1440         @Override
1441         public void applicationActivated(IdeFrame ideFrame) {
1442           setDelay(TIMER_DELAY);
1443           restart();
1444         }
1445
1446         @Override
1447         public void applicationDeactivated(IdeFrame ideFrame) {
1448           setDelay(DEACTIVATED_TIMER_DELAY);
1449         }
1450       });
1451     }
1452
1453     @Override
1454     public String toString() {
1455       return "Action manager timer";
1456     }
1457
1458     public void addTimerListener(TimerListener listener, boolean transparent){
1459       (transparent ? myTransparentTimerListeners : myTimerListeners).add(listener);
1460     }
1461
1462     public void removeTimerListener(TimerListener listener, boolean transparent){
1463       (transparent ? myTransparentTimerListeners : myTimerListeners).remove(listener);
1464     }
1465
1466     @Override
1467     public void actionPerformed(ActionEvent e) {
1468       if (myLastTimeEditorWasTypedIn + UPDATE_DELAY_AFTER_TYPING > System.currentTimeMillis()) {
1469         return;
1470       }
1471
1472       if (IdeFocusManager.getInstance(null).isFocusBeingTransferred()) return;
1473
1474       final int lastEventCount = myLastTimePerformed;
1475       myLastTimePerformed = ActivityTracker.getInstance().getCount();
1476
1477       boolean transparentOnly = myLastTimePerformed == lastEventCount;
1478
1479       try {
1480         Set<TimerListener> notified = new HashSet<TimerListener>();
1481         myTransparentOnlyUpdate = transparentOnly;
1482         notifyListeners(myTransparentTimerListeners, notified);
1483
1484         if (transparentOnly) {
1485           return;
1486         }
1487
1488         notifyListeners(myTimerListeners, notified);
1489       }
1490       finally {
1491         myTransparentOnlyUpdate = false;
1492       }
1493     }
1494
1495     private void notifyListeners(final List<TimerListener> timerListeners, final Set<TimerListener> notified) {
1496       for (TimerListener listener : timerListeners) {
1497         if (notified.add(listener)) {
1498           runListenerAction(listener);
1499         }
1500       }
1501     }
1502
1503     private void runListenerAction(final TimerListener listener) {
1504       ModalityState modalityState = listener.getModalityState();
1505       if (modalityState == null) return;
1506       if (!ModalityState.current().dominates(modalityState)) {
1507         try {
1508           listener.run();
1509         }
1510         catch (ProcessCanceledException ex) {
1511           // ignore
1512         }
1513         catch (Throwable e) {
1514           LOG.error(e);
1515         }
1516       }
1517     }
1518   }
1519 }