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