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