Merge branch 'master' of git.labs.intellij.net:idea/community
[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);
490   }
491
492   private AnAction getActionImpl(String id, boolean canReturnStub) {
493     synchronized (myLock) {
494       AnAction action = myId2Action.get(id);
495       if (!canReturnStub && action instanceof ActionStub) {
496         action = convert((ActionStub)action);
497       }
498       return action;
499     }
500   }
501
502   /**
503    * Converts action's stub to normal action.
504    */
505   @NotNull
506   private AnAction convert(@NotNull ActionStub stub) {
507     LOG.assertTrue(myAction2Id.containsKey(stub));
508     myAction2Id.remove(stub);
509
510     LOG.assertTrue(myId2Action.containsKey(stub.getId()));
511
512     AnAction action = myId2Action.remove(stub.getId());
513     LOG.assertTrue(action != null);
514     LOG.assertTrue(action.equals(stub));
515
516     AnAction anAction = convertStub(stub);
517     myAction2Id.put(anAction, stub.getId());
518
519     return addToMap(stub.getId(), anAction, stub.getPluginId(), stub.getProjectType());
520   }
521
522   @Override
523   public String getId(@NotNull AnAction action) {
524     LOG.assertTrue(!(action instanceof ActionStub));
525     synchronized (myLock) {
526       return myAction2Id.get(action);
527     }
528   }
529
530   @Override
531   public String[] getActionIds(@NotNull String idPrefix) {
532     synchronized (myLock) {
533       ArrayList<String> idList = new ArrayList<String>();
534       for (String id : myId2Action.keySet()) {
535         if (id.startsWith(idPrefix)) {
536           idList.add(id);
537         }
538       }
539       return ArrayUtil.toStringArray(idList);
540     }
541   }
542
543   @Override
544   public boolean isGroup(@NotNull String actionId) {
545     return getActionImpl(actionId, true) instanceof ActionGroup;
546   }
547
548   @Override
549   public JComponent createButtonToolbar(final String actionPlace, @NotNull final ActionGroup messageActionGroup) {
550     return new ButtonToolbarImpl(actionPlace, messageActionGroup, myDataManager, this);
551   }
552
553   @Override
554   public AnAction getActionOrStub(String id) {
555     return getActionImpl(id, true);
556   }
557
558   /**
559    * @return instance of ActionGroup or ActionStub. The method never returns real subclasses
560    *         of <code>AnAction</code>.
561    */
562   @Nullable
563   private AnAction processActionElement(Element element, final ClassLoader loader, PluginId pluginId) {
564     final IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId);
565     ResourceBundle bundle = getActionsResourceBundle(loader, plugin);
566
567     if (!ACTION_ELEMENT_NAME.equals(element.getName())) {
568       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
569       return null;
570     }
571     String className = element.getAttributeValue(CLASS_ATTR_NAME);
572     if (className == null || className.isEmpty()) {
573       reportActionError(pluginId, "action element should have specified \"class\" attribute");
574       return null;
575     }
576     // read ID and register loaded action
577     String id = element.getAttributeValue(ID_ATTR_NAME);
578     if (id == null || id.isEmpty()) {
579       id = StringUtil.getShortName(className);
580     }
581     if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) {
582       myNotRegisteredInternalActionIds.add(id);
583       return null;
584     }
585
586     String text = loadTextForElement(element, bundle, id, ACTION_ELEMENT_NAME);
587
588     String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
589
590     if (text == null) {
591       @NonNls String message = "'text' attribute is mandatory (action ID=" + id + ";" +
592                                (plugin == null ? "" : " plugin path: "+plugin.getPath()) + ")";
593       reportActionError(pluginId, message);
594       return null;
595     }
596
597     String projectType = element.getAttributeValue(PROJECT_TYPE);
598     ActionStub stub = new ActionStub(className, id, text, loader, pluginId, iconPath, projectType);
599     Presentation presentation = stub.getTemplatePresentation();
600     presentation.setText(text);
601
602     // description
603
604     presentation.setDescription(loadDescriptionForElement(element, bundle, id, ACTION_ELEMENT_NAME));
605
606     // process all links and key bindings if any
607     for (final Object o : element.getChildren()) {
608       Element e = (Element)o;
609       if (ADD_TO_GROUP_ELEMENT_NAME.equals(e.getName())) {
610         processAddToGroupNode(stub, e, pluginId, isSecondary(e));
611       }
612       else if (SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
613         processKeyboardShortcutNode(e, id, pluginId);
614       }
615       else if (MOUSE_SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
616         processMouseShortcutNode(e, id, pluginId);
617       }
618       else if (ABBREVIATION_ELEMENT_NAME.equals(e.getName())) {
619         processAbbreviationNode(e, id);
620       }
621       else {
622         reportActionError(pluginId, "unexpected name of element \"" + e.getName() + "\"");
623         return null;
624       }
625     }
626     if (element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME) != null) {
627       ((KeymapManagerEx)myKeymapManager).bindShortcuts(element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME), id);
628     }
629
630     registerOrReplaceActionInner(element, id, stub, pluginId);
631     return stub;
632   }
633
634   private void registerOrReplaceActionInner(@NotNull Element element, @NotNull String id, @NotNull AnAction action, @Nullable PluginId pluginId) {
635     synchronized (myLock) {
636       if (Boolean.valueOf(element.getAttributeValue(OVERRIDES_ATTR_NAME))) {
637         if (getActionOrStub(id) == null) {
638           throw new RuntimeException(element.getName() + " '" + id + "' doesn't override anything");
639         }
640         AnAction prev = replaceAction(id, action, pluginId);
641         if (action instanceof DefaultActionGroup && prev instanceof DefaultActionGroup) {
642           if (Boolean.valueOf(element.getAttributeValue(KEEP_CONTENT_ATTR_NAME))) {
643             ((DefaultActionGroup)action).copyFromGroup((DefaultActionGroup)prev);
644           }
645         }
646       }
647       else {
648         registerAction(id, action, pluginId, element.getAttributeValue(PROJECT_TYPE));
649       }
650     }
651   }
652
653   private AnAction processGroupElement(Element element, final ClassLoader loader, PluginId pluginId) {
654     final IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId);
655     ResourceBundle bundle = getActionsResourceBundle(loader, plugin);
656
657     if (!GROUP_ELEMENT_NAME.equals(element.getName())) {
658       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
659       return null;
660     }
661     String className = element.getAttributeValue(CLASS_ATTR_NAME);
662     if (className == null) { // use default group if class isn't specified
663       if ("true".equals(element.getAttributeValue(COMPACT_ATTR_NAME))) {
664         className = DefaultCompactActionGroup.class.getName();
665       } else {
666         className = DefaultActionGroup.class.getName();
667       }
668     }
669     try {
670       ActionGroup group;
671       if (DefaultActionGroup.class.getName().equals(className)) {
672         group = new DefaultActionGroup();
673       } else if (DefaultCompactActionGroup.class.getName().equals(className)) {
674         group = new DefaultCompactActionGroup();
675       } else {
676         Class aClass = Class.forName(className, true, loader);
677         Object obj = new ConstructorInjectionComponentAdapter(className, aClass).getComponentInstance(ApplicationManager.getApplication().getPicoContainer());
678
679         if (!(obj instanceof ActionGroup)) {
680           reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + ActionGroup.class.getName());
681           return null;
682         }
683         if (element.getChildren().size() != element.getChildren(ADD_TO_GROUP_ELEMENT_NAME).size() ) {  //
684           if (!(obj instanceof DefaultActionGroup)) {
685             reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + DefaultActionGroup.class.getName() +
686                                         " because there are children specified");
687             return null;
688           }
689         }
690         group = (ActionGroup)obj;
691       }
692       // read ID and register loaded group
693       String id = element.getAttributeValue(ID_ATTR_NAME);
694       if (id != null && id.isEmpty()) {
695         reportActionError(pluginId, "ID of the group cannot be an empty string");
696         return null;
697       }
698       if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) {
699         myNotRegisteredInternalActionIds.add(id);
700         return null;
701       }
702
703       if (id != null) {
704         registerOrReplaceActionInner(element, id, group, pluginId);
705       }
706       Presentation presentation = group.getTemplatePresentation();
707
708       // text
709       String text = loadTextForElement(element, bundle, id, GROUP_ELEMENT_NAME);
710       // don't override value which was set in API with empty value from xml descriptor
711       if (!StringUtil.isEmpty(text) || presentation.getText() == null) {
712         presentation.setText(text);
713       }
714
715       // description
716       String description = loadDescriptionForElement(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(description) || presentation.getDescription() == null) {
719         presentation.setDescription(description);
720       }
721
722       // icon
723       setIcon(element.getAttributeValue(ICON_ATTR_NAME), className, loader, presentation, pluginId);
724       // popup
725       String popup = element.getAttributeValue(POPUP_ATTR_NAME);
726       if (popup != null) {
727         group.setPopup(Boolean.valueOf(popup).booleanValue());
728       }
729       // process all group's children. There are other groups, actions, references and links
730       for (final Object o : element.getChildren()) {
731         Element child = (Element)o;
732         String name = child.getName();
733         if (ACTION_ELEMENT_NAME.equals(name)) {
734           AnAction action = processActionElement(child, loader, pluginId);
735           if (action != null) {
736             assertActionIsGroupOrStub(action);
737             addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
738           }
739         }
740         else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
741           processSeparatorNode((DefaultActionGroup)group, child, pluginId);
742         }
743         else if (GROUP_ELEMENT_NAME.equals(name)) {
744           AnAction action = processGroupElement(child, loader, pluginId);
745           if (action != null) {
746             addToGroupInner(group, action, Constraints.LAST, false);
747           }
748         }
749         else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) {
750           processAddToGroupNode(group, child, pluginId, isSecondary(child));
751         }
752         else if (REFERENCE_ELEMENT_NAME.equals(name)) {
753           AnAction action = processReferenceElement(child, pluginId);
754           if (action != null) {
755             addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
756           }
757         }
758         else {
759           reportActionError(pluginId, "unexpected name of element \"" + name + "\n");
760           return null;
761         }
762       }
763       return group;
764     }
765     catch (ClassNotFoundException e) {
766       reportActionError(pluginId, "class with name \"" + className + "\" not found");
767       return null;
768     }
769     catch (NoClassDefFoundError e) {
770       reportActionError(pluginId, "class with name \"" + e.getMessage() + "\" not found");
771       return null;
772     }
773     catch(UnsupportedClassVersionError e) {
774       reportActionError(pluginId, "unsupported class version for " + className);
775       return null;
776     }
777     catch (Exception e) {
778       final String message = "cannot create class \"" + className + "\"";
779       if (pluginId == null) {
780         LOG.error(message, e);
781       }
782       else {
783         LOG.error(new PluginException(message, e, pluginId));
784       }
785       return null;
786     }
787   }
788
789   private void processReferenceNode(final Element element, final PluginId pluginId) {
790     final AnAction action = processReferenceElement(element, pluginId);
791
792     for (final Object o : element.getChildren()) {
793       Element child = (Element)o;
794       if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
795         processAddToGroupNode(action, child, pluginId, isSecondary(child));
796       }
797     }
798   }
799
800   /**\
801    * @param element description of link
802    */
803   private void processAddToGroupNode(AnAction action, Element element, final PluginId pluginId, boolean secondary) {
804     // Real subclasses of AnAction should not be here
805     if (!(action instanceof Separator)) {
806       assertActionIsGroupOrStub(action);
807     }
808
809     String actionName = String.format(
810       "%s (%s)", action instanceof ActionStub? ((ActionStub)action).getClassName() : action.getClass().getName(),
811       action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action));
812
813     if (!ADD_TO_GROUP_ELEMENT_NAME.equals(element.getName())) {
814       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
815       return;
816     }
817
818     // parent group
819     final AnAction parentGroup = getParentGroup(element.getAttributeValue(GROUPID_ATTR_NAME), actionName, pluginId);
820     if (parentGroup == null) {
821       return;
822     }
823
824     // anchor attribute
825     final Anchor anchor = parseAnchor(element.getAttributeValue(ANCHOR_ELEMENT_NAME), actionName, pluginId);
826     if (anchor == null) {
827       return;
828     }
829
830     final String relativeToActionId = element.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME);
831     if (!checkRelativeToAction(relativeToActionId, anchor, actionName, pluginId)) {
832       return;
833     }
834     addToGroupInner(parentGroup, action, new Constraints(anchor, relativeToActionId), secondary);
835   }
836
837   private void addToGroupInner(AnAction group, AnAction action, Constraints constraints, boolean secondary) {
838     ((DefaultActionGroup)group).addAction(action, constraints, this).setAsSecondary(secondary);
839     myId2GroupId.putValue(myAction2Id.get(action), myAction2Id.get(group));
840   }
841
842   @Nullable
843   public AnAction getParentGroup(final String groupId,
844                                  @Nullable final String actionName,
845                                  @Nullable final PluginId pluginId) {
846     if (groupId == null || groupId.isEmpty()) {
847       reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined");
848       return null;
849     }
850     AnAction parentGroup = getActionImpl(groupId, true);
851     if (parentGroup == null) {
852       reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" isn't registered; action will be added to the \"Other\" group");
853       parentGroup = getActionImpl(IdeActions.GROUP_OTHER_MENU, true);
854     }
855     if (!(parentGroup instanceof DefaultActionGroup)) {
856       reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" should be instance of " + DefaultActionGroup.class.getName() +
857                                   " but was " + parentGroup.getClass());
858       return null;
859     }
860     return parentGroup;
861   }
862
863   /**
864    * @param parentGroup group which is the parent of the separator. It can be <code>null</code> in that
865    *                    case separator will be added to group described in the <add-to-group ....> subelement.
866    * @param element     XML element which represent separator.
867    */
868   private void processSeparatorNode(@Nullable DefaultActionGroup parentGroup, Element element, PluginId pluginId) {
869     if (!SEPARATOR_ELEMENT_NAME.equals(element.getName())) {
870       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
871       return;
872     }
873     Separator separator = Separator.getInstance();
874     if (parentGroup != null) {
875       parentGroup.add(separator, this);
876     }
877     // try to find inner <add-to-parent...> tag
878     for (final Object o : element.getChildren()) {
879       Element child = (Element)o;
880       if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
881         processAddToGroupNode(separator, child, pluginId, isSecondary(child));
882       }
883     }
884   }
885
886   private void processKeyboardShortcutNode(Element element, String actionId, PluginId pluginId) {
887     String firstStrokeString = element.getAttributeValue(FIRST_KEYSTROKE_ATTR_NAME);
888     if (firstStrokeString == null) {
889       reportActionError(pluginId, "\"first-keystroke\" attribute must be specified for action with id=" + actionId);
890       return;
891     }
892     KeyStroke firstKeyStroke = getKeyStroke(firstStrokeString);
893     if (firstKeyStroke == null) {
894       reportActionError(pluginId, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId);
895       return;
896     }
897
898     KeyStroke secondKeyStroke = null;
899     String secondStrokeString = element.getAttributeValue(SECOND_KEYSTROKE_ATTR_NAME);
900     if (secondStrokeString != null) {
901       secondKeyStroke = getKeyStroke(secondStrokeString);
902       if (secondKeyStroke == null) {
903         reportActionError(pluginId, "\"second-keystroke\" attribute has invalid value for action with id=" + actionId);
904         return;
905       }
906     }
907
908     String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
909     if (keymapName == null || keymapName.trim().isEmpty()) {
910       reportActionError(pluginId, "attribute \"keymap\" should be defined");
911       return;
912     }
913     Keymap keymap = myKeymapManager.getKeymap(keymapName);
914     if (keymap == null) {
915       reportActionWarning(pluginId, "keymap \"" + keymapName + "\" not found");
916       return;
917     }
918     final String removeOption = element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME);
919     final KeyboardShortcut shortcut = new KeyboardShortcut(firstKeyStroke, secondKeyStroke);
920     final String replaceOption = element.getAttributeValue(REPLACE_SHORTCUT_ATTR_NAME);
921     if (Boolean.valueOf(removeOption)) {
922       keymap.removeShortcut(actionId, shortcut);
923     }
924     if (Boolean.valueOf(replaceOption)) {
925       keymap.removeAllActionShortcuts(actionId);
926     }
927     if (!Boolean.valueOf(removeOption)) {
928       keymap.addShortcut(actionId, shortcut);
929     }
930   }
931
932   @Nullable
933   private AnAction processReferenceElement(Element element, PluginId pluginId) {
934     if (!REFERENCE_ELEMENT_NAME.equals(element.getName())) {
935       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
936       return null;
937     }
938     String ref = element.getAttributeValue(REF_ATTR_NAME);
939
940     if (ref==null) {
941       // support old style references by id
942       ref = element.getAttributeValue(ID_ATTR_NAME);
943     }
944
945     if (ref == null || ref.isEmpty()) {
946       reportActionError(pluginId, "ID of reference element should be defined");
947       return null;
948     }
949
950     AnAction action = getActionImpl(ref, true);
951
952     if (action == null) {
953       if (!myNotRegisteredInternalActionIds.contains(ref)) {
954         reportActionError(pluginId, "action specified by reference isn't registered (ID=" + ref + ")");
955       }
956       return null;
957     }
958     assertActionIsGroupOrStub(action);
959     return action;
960   }
961
962   private void processActionsChildElement(final ClassLoader loader, final PluginId pluginId, final Element child) {
963     String name = child.getName();
964     if (ACTION_ELEMENT_NAME.equals(name)) {
965       AnAction action = processActionElement(child, loader, pluginId);
966       if (action != null) {
967         assertActionIsGroupOrStub(action);
968       }
969     }
970     else if (GROUP_ELEMENT_NAME.equals(name)) {
971       processGroupElement(child, loader, pluginId);
972     }
973     else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
974       processSeparatorNode(null, child, pluginId);
975     }
976     else if (REFERENCE_ELEMENT_NAME.equals(name)) {
977       processReferenceNode(child, pluginId);
978     }
979     else {
980       reportActionError(pluginId, "unexpected name of element \"" + name + "\n");
981     }
982   }
983
984   @Override
985   public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) {
986     registerAction(actionId, action, pluginId, null);
987   }
988
989   public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId, @Nullable String projectType) {
990     synchronized (myLock) {
991       if (addToMap(actionId, action, pluginId, projectType) == null) return;
992       if (myAction2Id.containsKey(action)) {
993         reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) +
994                                     getPluginInfo(pluginId));
995         return;
996       }
997       myId2Index.put(actionId, myRegisteredActionsCount++);
998       myAction2Id.put(action, actionId);
999       if (pluginId != null && !(action instanceof ActionGroup)){
1000         THashSet<String> pluginActionIds = myPlugin2Id.get(pluginId);
1001         if (pluginActionIds == null){
1002           pluginActionIds = new THashSet<String>();
1003           myPlugin2Id.put(pluginId, pluginActionIds);
1004         }
1005         pluginActionIds.add(actionId);
1006       }
1007       action.registerCustomShortcutSet(new ProxyShortcutSet(actionId, myKeymapManager), null);
1008     }
1009   }
1010
1011   private AnAction addToMap(String actionId, AnAction action, PluginId pluginId, String projectType) {
1012     if (projectType != null || myId2Action.containsKey(actionId)) {
1013       return registerChameleon(actionId, action, pluginId, projectType);
1014     }
1015     else {
1016       myId2Action.put(actionId, action);
1017       return action;
1018     }
1019   }
1020
1021   private AnAction registerChameleon(String actionId, AnAction action, PluginId pluginId, String projectType) {
1022     ProjectType type = projectType == null ? null : new ProjectType(projectType);
1023     // make sure id+projectType is unique
1024     AnAction o = myId2Action.get(actionId);
1025     ChameleonAction chameleonAction;
1026     if (o == null) {
1027       chameleonAction = new ChameleonAction(action, type);
1028       myId2Action.put(actionId, chameleonAction);
1029       return chameleonAction;
1030     }
1031     if (o instanceof ChameleonAction) {
1032       chameleonAction = (ChameleonAction)o;
1033     }
1034     else {
1035       chameleonAction = new ChameleonAction(o, type);
1036       myId2Action.put(actionId, chameleonAction);
1037     }
1038     AnAction old = chameleonAction.addAction(action, type);
1039     if (old != null) {
1040       reportActionError(pluginId,
1041                         "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action +
1042                         "; Registered action is " +
1043                         myId2Action.get(actionId) + getPluginInfo(pluginId));
1044       return null;
1045     }
1046     return chameleonAction;
1047   }
1048
1049   @Override
1050   public void registerAction(@NotNull String actionId, @NotNull AnAction action) {
1051     registerAction(actionId, action, null);
1052   }
1053
1054   @Override
1055   public void unregisterAction(@NotNull String actionId) {
1056     synchronized (myLock) {
1057       if (!myId2Action.containsKey(actionId)) {
1058         if (LOG.isDebugEnabled()) {
1059           LOG.debug("action with ID " + actionId + " wasn't registered");
1060           return;
1061         }
1062       }
1063       AnAction oldValue = myId2Action.remove(actionId);
1064       myAction2Id.remove(oldValue);
1065       myId2Index.remove(actionId);
1066       for (PluginId pluginName : myPlugin2Id.keySet()) {
1067         final THashSet<String> pluginActions = myPlugin2Id.get(pluginName);
1068         if (pluginActions != null) {
1069           pluginActions.remove(actionId);
1070         }
1071       }
1072     }
1073   }
1074
1075   @Override
1076   @NotNull
1077   public String getComponentName() {
1078     return "ActionManager";
1079   }
1080
1081   @Override
1082   public Comparator<String> getRegistrationOrderComparator() {
1083     return new Comparator<String>() {
1084       @Override
1085       public int compare(String id1, String id2) {
1086         return myId2Index.get(id1) - myId2Index.get(id2);
1087       }
1088     };
1089   }
1090
1091   @NotNull
1092   @Override
1093   public String[] getPluginActions(PluginId pluginName) {
1094     if (myPlugin2Id.containsKey(pluginName)){
1095       final THashSet<String> pluginActions = myPlugin2Id.get(pluginName);
1096       return ArrayUtil.toStringArray(pluginActions);
1097     }
1098     return ArrayUtil.EMPTY_STRING_ARRAY;
1099   }
1100
1101   public void addActionPopup(final ActionPopupMenuImpl menu) {
1102     myPopups.add(menu);
1103   }
1104
1105   public void removeActionPopup(final ActionPopupMenuImpl menu) {
1106     final boolean removed = myPopups.remove(menu);
1107     if (removed && myPopups.isEmpty()) {
1108       flushActionPerformed();
1109     }
1110   }
1111
1112   @Override
1113   public void queueActionPerformedEvent(final AnAction action, DataContext context, AnActionEvent event) {
1114     if (!myPopups.isEmpty()) {
1115       myQueuedNotifications.put(action, context);
1116     } else {
1117       fireAfterActionPerformed(action, context, event);
1118     }
1119   }
1120
1121   //@Override
1122   //public AnAction replaceAction(String actionId, @NotNull AnAction newAction) {
1123   //  synchronized (myLock) {
1124   //    return replaceAction(actionId, newAction, null);
1125   //  }
1126   //}
1127
1128   @Override
1129   public boolean isActionPopupStackEmpty() {
1130     return myPopups.isEmpty();
1131   }
1132
1133   @Override
1134   public boolean isTransparentOnlyActionsUpdateNow() {
1135     return myTransparentOnlyUpdate;
1136   }
1137
1138   private AnAction replaceAction(@NotNull String actionId, @NotNull AnAction newAction, @Nullable PluginId pluginId) {
1139     AnAction oldAction = getActionOrStub(actionId);
1140     if (oldAction != null) {
1141       boolean isGroup = oldAction instanceof ActionGroup;
1142       if (isGroup != newAction instanceof ActionGroup) {
1143         throw new IllegalStateException("cannot replace a group with an action and vice versa: " + actionId);
1144       }
1145       unregisterAction(actionId);
1146       if (isGroup) {
1147         myId2GroupId.values().remove(actionId);
1148       }
1149     }
1150     registerAction(actionId, newAction, pluginId);
1151     for (String groupId : myId2GroupId.get(actionId)) {
1152       DefaultActionGroup group = ObjectUtils.assertNotNull((DefaultActionGroup)getActionOrStub(groupId));
1153       group.replaceAction(oldAction, newAction);
1154     }
1155     return oldAction;
1156   }
1157
1158   private void flushActionPerformed() {
1159     final Set<AnAction> actions = myQueuedNotifications.keySet();
1160     for (final AnAction eachAction : actions) {
1161       final DataContext eachContext = myQueuedNotifications.get(eachAction);
1162       fireAfterActionPerformed(eachAction, eachContext, myQueuedNotificationsEvents.get(eachAction));
1163     }
1164     myQueuedNotifications.clear();
1165     myQueuedNotificationsEvents.clear();
1166   }
1167
1168   @Override
1169   public void addAnActionListener(AnActionListener listener) {
1170     myActionListeners.add(listener);
1171   }
1172
1173   @Override
1174   public void addAnActionListener(final AnActionListener listener, final Disposable parentDisposable) {
1175     addAnActionListener(listener);
1176     Disposer.register(parentDisposable, new Disposable() {
1177       @Override
1178       public void dispose() {
1179         removeAnActionListener(listener);
1180       }
1181     });
1182   }
1183
1184   @Override
1185   public void removeAnActionListener(AnActionListener listener) {
1186     myActionListeners.remove(listener);
1187   }
1188
1189   @Override
1190   public void fireBeforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
1191     if (action != null) {
1192       myPrevPerformedActionId = myLastPreformedActionId;
1193       myLastPreformedActionId = getId(action);
1194       //noinspection AssignmentToStaticFieldFromInstanceMethod
1195       IdeaLogger.ourLastActionId = myLastPreformedActionId;
1196     }
1197     for (AnActionListener listener : myActionListeners) {
1198       listener.beforeActionPerformed(action, dataContext, event);
1199     }
1200   }
1201
1202   @Override
1203   public void fireAfterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
1204     if (action != null) {
1205       myPrevPerformedActionId = myLastPreformedActionId;
1206       myLastPreformedActionId = getId(action);
1207       //noinspection AssignmentToStaticFieldFromInstanceMethod
1208       IdeaLogger.ourLastActionId = myLastPreformedActionId;
1209     }
1210     for (AnActionListener listener : myActionListeners) {
1211       try {
1212         listener.afterActionPerformed(action, dataContext, event);
1213       }
1214       catch(AbstractMethodError ignored) { }
1215     }
1216   }
1217
1218   @Override
1219   public KeyboardShortcut getKeyboardShortcut(@NotNull String actionId) {
1220     AnAction action = ActionManager.getInstance().getAction(actionId);
1221     final ShortcutSet shortcutSet = action.getShortcutSet();
1222     final Shortcut[] shortcuts = shortcutSet.getShortcuts();
1223     for (final Shortcut shortcut : shortcuts) {
1224       // Shortcut can be MouseShortcut here.
1225       // For example IdeaVIM often assigns them
1226       if (shortcut instanceof KeyboardShortcut){
1227         final KeyboardShortcut kb = (KeyboardShortcut)shortcut;
1228         if (kb.getSecondKeyStroke() == null) {
1229           return (KeyboardShortcut)shortcut;
1230         }
1231       }
1232     }
1233
1234     return null;
1235   }
1236
1237   @Override
1238   public void fireBeforeEditorTyping(char c, DataContext dataContext) {
1239     myLastTimeEditorWasTypedIn = System.currentTimeMillis();
1240     for (AnActionListener listener : myActionListeners) {
1241       listener.beforeEditorTyping(c, dataContext);
1242     }
1243   }
1244
1245   @Override
1246   public String getLastPreformedActionId() {
1247     return myLastPreformedActionId;
1248   }
1249
1250   @Override
1251   public String getPrevPreformedActionId() {
1252     return myPrevPerformedActionId;
1253   }
1254
1255   public Set<String> getActionIds(){
1256     synchronized (myLock) {
1257       return new HashSet<String>(myId2Action.keySet());
1258     }
1259   }
1260
1261   public Future<?> preloadActions() {
1262     if (myPreloadActionsRunnable == null) {
1263       myPreloadActionsRunnable = new Runnable() {
1264         @Override
1265         public void run() {
1266           try {
1267             SearchableOptionsRegistrar.getInstance(); // load inspection descriptions etc. to be used in Goto Action, Search Everywhere 
1268             doPreloadActions();
1269           } catch (RuntimeInterruptedException ignore) {
1270           }
1271         }
1272       };
1273       return ApplicationManager.getApplication().executeOnPooledThread(myPreloadActionsRunnable);
1274     }
1275     return null;
1276   }
1277
1278   private void doPreloadActions() {
1279     try {
1280       Thread.sleep(5000); // wait for project initialization to complete
1281     }
1282     catch (InterruptedException e) {
1283       return; // IDEA exited
1284     }
1285     preloadActionGroup(IdeActions.GROUP_EDITOR_POPUP);
1286     preloadActionGroup(IdeActions.GROUP_EDITOR_TAB_POPUP);
1287     preloadActionGroup(IdeActions.GROUP_PROJECT_VIEW_POPUP);
1288     preloadActionGroup(IdeActions.GROUP_MAIN_MENU);
1289     preloadActionGroup(IdeActions.GROUP_NEW);
1290     // TODO anything else?
1291     LOG.debug("Actions preloading completed");
1292   }
1293
1294   public void preloadActionGroup(final String groupId) {
1295     final AnAction action = getAction(groupId);
1296     if (action instanceof ActionGroup) {
1297       preloadActionGroup((ActionGroup) action);
1298     }
1299   }
1300
1301   private void preloadActionGroup(final ActionGroup group) {
1302     final Application application = ApplicationManager.getApplication();
1303     final AnAction[] children = application.runReadAction(new Computable<AnAction[]>() {
1304       @Override
1305       public AnAction[] compute() {
1306         if (application.isDisposed()) {
1307           return AnAction.EMPTY_ARRAY;
1308         }
1309
1310         return group.getChildren(null);
1311       }
1312     });
1313     for (AnAction action : children) {
1314       if (action instanceof PreloadableAction) {
1315         ((PreloadableAction)action).preload();
1316       }
1317       else if (action instanceof ActionGroup) {
1318         preloadActionGroup((ActionGroup)action);
1319       }
1320
1321       myActionsPreloaded++;
1322       if (myActionsPreloaded % 10 == 0) {
1323         try {
1324           //noinspection BusyWait
1325           Thread.sleep(300);
1326         }
1327         catch (InterruptedException ignored) {
1328           throw new RuntimeInterruptedException(ignored);
1329         }
1330       }
1331     }
1332   }
1333
1334   @Override
1335   public ActionCallback tryToExecute(@NotNull final AnAction action, @NotNull final InputEvent inputEvent, @Nullable final Component contextComponent, @Nullable final String place,
1336                                      boolean now) {
1337
1338     final Application app = ApplicationManager.getApplication();
1339     assert app.isDispatchThread();
1340
1341     final ActionCallback result = new ActionCallback();
1342     final Runnable doRunnable = new Runnable() {
1343       @Override
1344       public void run() {
1345         tryToExecuteNow(action, inputEvent, contextComponent, place, result);
1346       }
1347     };
1348
1349     if (now) {
1350       doRunnable.run();
1351     } else {
1352       //noinspection SSBasedInspection
1353       SwingUtilities.invokeLater(doRunnable);
1354     }
1355
1356     return result;
1357   }
1358
1359   private void tryToExecuteNow(final AnAction action, final InputEvent inputEvent, final Component contextComponent, final String place, final ActionCallback result) {
1360     final Presentation presentation = action.getTemplatePresentation().clone();
1361
1362     IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown(new Runnable() {
1363       @Override
1364       public void run() {
1365         final DataContext context = getContextBy(contextComponent);
1366
1367         AnActionEvent event = new AnActionEvent(
1368           inputEvent, context,
1369           place != null ? place : ActionPlaces.UNKNOWN,
1370           presentation, ActionManagerImpl.this,
1371           inputEvent.getModifiersEx()
1372         );
1373
1374         ActionUtil.performDumbAwareUpdate(action, event, false);
1375         if (!event.getPresentation().isEnabled()) {
1376           result.setRejected();
1377           return;
1378         }
1379
1380         ActionUtil.lastUpdateAndCheckDumb(action, event, false);
1381         if (!event.getPresentation().isEnabled()) {
1382           result.setRejected();
1383           return;
1384         }
1385
1386         Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(context);
1387         if (component != null && !component.isShowing()) {
1388           result.setRejected();
1389           return;
1390         }
1391
1392         fireBeforeActionPerformed(action, context, event);
1393
1394         UIUtil.addAwtListener(new AWTEventListener() {
1395           @Override
1396           public void eventDispatched(AWTEvent event) {
1397             if (event.getID() == WindowEvent.WINDOW_OPENED ||event.getID() == WindowEvent.WINDOW_ACTIVATED) {
1398               if (!result.isProcessed()) {
1399                 final WindowEvent we = (WindowEvent)event;
1400                 IdeFocusManager.findInstanceByComponent(we.getWindow()).doWhenFocusSettlesDown(result.createSetDoneRunnable());
1401               }
1402             }
1403           }
1404         }, AWTEvent.WINDOW_EVENT_MASK, result);
1405
1406         ActionUtil.performActionDumbAware(action, event);
1407         result.setDone();
1408         queueActionPerformedEvent(action, context, event);
1409       }
1410     });
1411   }
1412
1413   private class MyTimer extends Timer implements ActionListener {
1414     private final List<TimerListener> myTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1415     private final List<TimerListener> myTransparentTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1416     private int myLastTimePerformed;
1417
1418     MyTimer() {
1419       super(TIMER_DELAY, null);
1420       addActionListener(this);
1421       setRepeats(true);
1422       final MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
1423       connection.subscribe(ApplicationActivationListener.TOPIC, new ApplicationActivationListener() {
1424         @Override
1425         public void applicationActivated(IdeFrame ideFrame) {
1426           setDelay(TIMER_DELAY);
1427           restart();
1428         }
1429
1430         @Override
1431         public void applicationDeactivated(IdeFrame ideFrame) {
1432           setDelay(DEACTIVATED_TIMER_DELAY);
1433         }
1434       });
1435     }
1436
1437     @Override
1438     public String toString() {
1439       return "Action manager timer";
1440     }
1441
1442     public void addTimerListener(TimerListener listener, boolean transparent){
1443       (transparent ? myTransparentTimerListeners : myTimerListeners).add(listener);
1444     }
1445
1446     public void removeTimerListener(TimerListener listener, boolean transparent){
1447       (transparent ? myTransparentTimerListeners : myTimerListeners).remove(listener);
1448     }
1449
1450     @Override
1451     public void actionPerformed(ActionEvent e) {
1452       if (myLastTimeEditorWasTypedIn + UPDATE_DELAY_AFTER_TYPING > System.currentTimeMillis()) {
1453         return;
1454       }
1455
1456       if (IdeFocusManager.getInstance(null).isFocusBeingTransferred()) return;
1457
1458       final int lastEventCount = myLastTimePerformed;
1459       myLastTimePerformed = ActivityTracker.getInstance().getCount();
1460
1461       boolean transparentOnly = myLastTimePerformed == lastEventCount;
1462
1463       try {
1464         Set<TimerListener> notified = new HashSet<TimerListener>();
1465         myTransparentOnlyUpdate = transparentOnly;
1466         notifyListeners(myTransparentTimerListeners, notified);
1467
1468         if (transparentOnly) {
1469           return;
1470         }
1471
1472         notifyListeners(myTimerListeners, notified);
1473       }
1474       finally {
1475         myTransparentOnlyUpdate = false;
1476       }
1477     }
1478
1479     private void notifyListeners(final List<TimerListener> timerListeners, final Set<TimerListener> notified) {
1480       for (TimerListener listener : timerListeners) {
1481         if (notified.add(listener)) {
1482           runListenerAction(listener);
1483         }
1484       }
1485     }
1486
1487     private void runListenerAction(final TimerListener listener) {
1488       ModalityState modalityState = listener.getModalityState();
1489       if (modalityState == null) return;
1490       if (!ModalityState.current().dominates(modalityState)) {
1491         try {
1492           listener.run();
1493         }
1494         catch (ProcessCanceledException ex) {
1495           // ignore
1496         }
1497         catch (Throwable e) {
1498           LOG.error(e);
1499         }
1500       }
1501     }
1502   }
1503 }