1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.actionSystem.impl;
4 import com.intellij.AbstractBundle;
5 import com.intellij.BundleBase;
6 import com.intellij.DynamicBundle;
7 import com.intellij.diagnostic.LoadingState;
8 import com.intellij.diagnostic.PluginException;
9 import com.intellij.diagnostic.StartUpMeasurer;
10 import com.intellij.icons.AllIcons;
11 import com.intellij.ide.ActivityTracker;
12 import com.intellij.ide.DataManager;
13 import com.intellij.ide.plugins.IdeaPluginDescriptor;
14 import com.intellij.ide.plugins.IdeaPluginDescriptorImpl;
15 import com.intellij.ide.plugins.PluginManagerCore;
16 import com.intellij.ide.ui.customization.ActionUrl;
17 import com.intellij.ide.ui.customization.CustomActionsSchema;
18 import com.intellij.idea.IdeaLogger;
19 import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionIdProvider;
20 import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionsCollectorImpl;
21 import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionsEventLogGroup;
22 import com.intellij.internal.statistic.eventLog.events.EventFields;
23 import com.intellij.internal.statistic.eventLog.events.EventPair;
24 import com.intellij.internal.statistic.eventLog.events.ObjectEventData;
25 import com.intellij.lang.Language;
26 import com.intellij.openapi.Disposable;
27 import com.intellij.openapi.actionSystem.*;
28 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
29 import com.intellij.openapi.actionSystem.ex.ActionPopupMenuListener;
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.impl.LaterInvocator;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.editor.Editor;
36 import com.intellij.openapi.editor.actionSystem.EditorAction;
37 import com.intellij.openapi.editor.actionSystem.EditorActionHandlerBean;
38 import com.intellij.openapi.extensions.ExtensionPointListener;
39 import com.intellij.openapi.extensions.ExtensionPointName;
40 import com.intellij.openapi.extensions.PluginDescriptor;
41 import com.intellij.openapi.extensions.PluginId;
42 import com.intellij.openapi.keymap.Keymap;
43 import com.intellij.openapi.keymap.KeymapManager;
44 import com.intellij.openapi.keymap.KeymapUtil;
45 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
46 import com.intellij.openapi.keymap.impl.DefaultKeymap;
47 import com.intellij.openapi.progress.ProcessCanceledException;
48 import com.intellij.openapi.progress.ProgressIndicator;
49 import com.intellij.openapi.project.Project;
50 import com.intellij.openapi.project.ProjectType;
51 import com.intellij.openapi.util.ActionCallback;
52 import com.intellij.openapi.util.Disposer;
53 import com.intellij.openapi.util.IconLoader;
54 import com.intellij.openapi.util.NlsActions;
55 import com.intellij.openapi.util.registry.Registry;
56 import com.intellij.openapi.util.text.StringUtil;
57 import com.intellij.openapi.wm.IdeFocusManager;
58 import com.intellij.openapi.wm.IdeFrame;
59 import com.intellij.psi.PsiDocumentManager;
60 import com.intellij.psi.PsiFile;
61 import com.intellij.util.ArrayUtilRt;
62 import com.intellij.util.ReflectionUtil;
63 import com.intellij.util.containers.CollectionFactory;
64 import com.intellij.util.containers.ContainerUtil;
65 import com.intellij.util.containers.MultiMap;
66 import com.intellij.util.messages.MessageBusConnection;
67 import com.intellij.util.ui.UIUtil;
68 import it.unimi.dsi.fastutil.objects.Object2IntMap;
69 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
70 import org.jdom.Element;
71 import org.jetbrains.annotations.ApiStatus;
72 import org.jetbrains.annotations.NotNull;
73 import org.jetbrains.annotations.Nullable;
75 import javax.swing.Timer;
78 import java.awt.event.ActionEvent;
79 import java.awt.event.ActionListener;
80 import java.awt.event.InputEvent;
81 import java.awt.event.WindowEvent;
82 import java.util.List;
84 import java.util.function.Supplier;
86 public final class ActionManagerImpl extends ActionManagerEx implements Disposable {
87 private static final ExtensionPointName<ActionConfigurationCustomizer> EP =
88 new ExtensionPointName<>("com.intellij.actionConfigurationCustomizer");
89 private static final ExtensionPointName<EditorActionHandlerBean> EDITOR_ACTION_HANDLER_EP =
90 new ExtensionPointName<>("com.intellij.editorActionHandler");
92 private static final String ACTION_ELEMENT_NAME = "action";
93 private static final String GROUP_ELEMENT_NAME = "group";
94 private static final String CLASS_ATTR_NAME = "class";
95 private static final String ID_ATTR_NAME = "id";
96 private static final String INTERNAL_ATTR_NAME = "internal";
97 private static final String ICON_ATTR_NAME = "icon";
98 private static final String ADD_TO_GROUP_ELEMENT_NAME = "add-to-group";
99 private static final String SHORTCUT_ELEMENT_NAME = "keyboard-shortcut";
100 private static final String MOUSE_SHORTCUT_ELEMENT_NAME = "mouse-shortcut";
101 private static final String DESCRIPTION = "description";
102 private static final String TEXT_ATTR_NAME = "text";
103 private static final String KEY_ATTR_NAME = "key";
104 private static final String POPUP_ATTR_NAME = "popup";
105 private static final String COMPACT_ATTR_NAME = "compact";
106 private static final String SEARCHABLE_ATTR_NAME = "searchable";
107 private static final String SEPARATOR_ELEMENT_NAME = "separator";
108 private static final String REFERENCE_ELEMENT_NAME = "reference";
109 private static final String ABBREVIATION_ELEMENT_NAME = "abbreviation";
110 private static final String GROUPID_ATTR_NAME = "group-id";
111 private static final String ANCHOR_ELEMENT_NAME = "anchor";
112 private static final String FIRST = "first";
113 private static final String LAST = "last";
114 private static final String BEFORE = "before";
115 private static final String AFTER = "after";
116 private static final String SECONDARY = "secondary";
117 private static final String RELATIVE_TO_ACTION_ATTR_NAME = "relative-to-action";
118 private static final String FIRST_KEYSTROKE_ATTR_NAME = "first-keystroke";
119 private static final String SECOND_KEYSTROKE_ATTR_NAME = "second-keystroke";
120 private static final String REMOVE_SHORTCUT_ATTR_NAME = "remove";
121 private static final String REPLACE_SHORTCUT_ATTR_NAME = "replace-all";
122 private static final String KEYMAP_ATTR_NAME = "keymap";
123 private static final String KEYSTROKE_ATTR_NAME = "keystroke";
124 private static final String REF_ATTR_NAME = "ref";
125 private static final String VALUE_ATTR_NAME = "value";
126 private static final String ACTIONS_BUNDLE = "messages.ActionsBundle";
127 private static final String USE_SHORTCUT_OF_ATTR_NAME = "use-shortcut-of";
128 private static final String OVERRIDES_ATTR_NAME = "overrides";
129 private static final String KEEP_CONTENT_ATTR_NAME = "keep-content";
130 private static final String PROJECT_TYPE = "project-type";
131 private static final String UNREGISTER_ELEMENT_NAME = "unregister";
132 private static final String OVERRIDE_TEXT_ELEMENT_NAME = "override-text";
133 private static final String SYNONYM_ELEMENT_NAME = "synonym";
134 private static final String PLACE_ATTR_NAME = "place";
135 private static final String USE_TEXT_OF_PLACE_ATTR_NAME = "use-text-of-place";
136 private static final String RESOURCE_BUNDLE_ATTR_NAME = "resource-bundle";
138 private static final Logger LOG = Logger.getInstance(ActionManagerImpl.class);
139 private static final int DEACTIVATED_TIMER_DELAY = 5000;
140 private static final int TIMER_DELAY = 500;
141 private static final int UPDATE_DELAY_AFTER_TYPING = 500;
143 private final Object myLock = new Object();
144 private final Map<String, AnAction> myId2Action = CollectionFactory.createSmallMemoryFootprintMap();
145 private final MultiMap<PluginId, String> myPlugin2Id = new MultiMap<>();
146 private final Object2IntMap<String> myId2Index = new Object2IntOpenHashMap<>();
147 private final Map<Object, String> myAction2Id = CollectionFactory.createSmallMemoryFootprintMap();
148 private final MultiMap<String, String> myId2GroupId = new MultiMap<>();
149 private final List<String> myNotRegisteredInternalActionIds = new ArrayList<>();
150 private final List<AnActionListener> myActionListeners = ContainerUtil.createLockFreeCopyOnWriteList();
151 private final List<ActionPopupMenuListener> myActionPopupMenuListeners = ContainerUtil.createLockFreeCopyOnWriteList();
152 private final List<Object/*ActionPopupMenuImpl|JBPopup*/> myPopups = new ArrayList<>();
153 private MyTimer myTimer;
154 private int myRegisteredActionsCount;
155 private String myLastPreformedActionId;
156 private String myPrevPerformedActionId;
157 private long myLastTimeEditorWasTypedIn;
158 private boolean myTransparentOnlyUpdate;
159 private final Map<OverridingAction, AnAction> myBaseActions = new HashMap<>();
160 private int myAnonymousGroupIdCounter;
162 ActionManagerImpl() {
163 Application app = ApplicationManager.getApplication();
164 if (!app.isUnitTestMode()) {
165 LoadingState.COMPONENTS_LOADED.checkOccurred();
166 if (!app.isHeadlessEnvironment() && !app.isCommandLine()) {
167 LOG.assertTrue(!app.isDispatchThread());
171 for (IdeaPluginDescriptorImpl plugin : PluginManagerCore.getLoadedPlugins(null)) {
172 registerPluginActions(plugin, plugin.getActionDescriptionElements(), true);
175 EP.forEachExtensionSafe(customizer -> customizer.customize(this));
176 DynamicActionConfigurationCustomizer.EP_NAME.forEachExtensionSafe(customizer -> customizer.registerActions(this));
177 DynamicActionConfigurationCustomizer.EP_NAME.addExtensionPointListener(new ExtensionPointListener<>() {
179 public void extensionAdded(@NotNull DynamicActionConfigurationCustomizer extension, @NotNull PluginDescriptor pluginDescriptor) {
180 extension.registerActions(ActionManagerImpl.this);
184 public void extensionRemoved(@NotNull DynamicActionConfigurationCustomizer extension, @NotNull PluginDescriptor pluginDescriptor) {
185 extension.unregisterActions(ActionManagerImpl.this);
188 EDITOR_ACTION_HANDLER_EP.addChangeListener(this::updateAllHandlers, this);
192 private static AnActionListener publisher() {
193 return ApplicationManager.getApplication().getMessageBus().syncPublisher(AnActionListener.TOPIC);
197 static AnAction convertStub(@NotNull ActionStub stub) {
198 AnAction anAction = instantiate(stub.getClassName(), stub.getPlugin(), AnAction.class);
199 if (anAction == null) {
203 stub.initAction(anAction);
204 updateIconFromStub(stub, anAction);
210 private static <T> T instantiate(@NotNull String stubClassName, @NotNull PluginDescriptor pluginDescriptor, Class<T> expectedClass) {
213 if (expectedClass == ActionGroup.class) {
214 obj = ApplicationManager.getApplication().instantiateExtensionWithPicoContainerOnlyIfNeeded(stubClassName, pluginDescriptor);
217 obj = ReflectionUtil.newInstance(Class.forName(stubClassName, true, pluginDescriptor.getPluginClassLoader()), false);
220 catch (ProcessCanceledException e) {
223 catch (PluginException e) {
227 catch (Throwable e) {
228 LOG.error(new PluginException(e, pluginDescriptor.getPluginId()));
232 if (!expectedClass.isInstance(obj)) {
233 LOG.error(new PluginException("class with name '" +
234 stubClassName + "' must be an instance of '" + expectedClass.getName() + "'; got " + obj, pluginDescriptor.getPluginId()));
237 //noinspection unchecked
241 private static void updateIconFromStub(@NotNull ActionStubBase stub, AnAction anAction) {
242 String iconPath = stub.getIconPath();
243 if (iconPath == null) {
247 setIconFromClass(anAction.getClass(), stub.getPlugin(), iconPath, anAction.getTemplatePresentation());
251 private static ActionGroup convertGroupStub(@NotNull ActionGroupStub stub, @NotNull ActionManager actionManager) {
252 IdeaPluginDescriptor plugin = stub.getPlugin();
253 ActionGroup group = instantiate(stub.getActionClass(), plugin, ActionGroup.class);
258 stub.initGroup(group, actionManager);
259 updateIconFromStub(stub, group);
263 private static void processAbbreviationNode(@NotNull Element e, @NotNull String id) {
264 final String abbr = e.getAttributeValue(VALUE_ATTR_NAME);
265 if (!StringUtil.isEmpty(abbr)) {
266 final AbbreviationManagerImpl abbreviationManager = (AbbreviationManagerImpl)AbbreviationManager.getInstance();
267 abbreviationManager.register(abbr, id, true);
272 private static ResourceBundle getActionsResourceBundle(@NotNull IdeaPluginDescriptor plugin, @Nullable String bundleName) {
273 String resBundleName = bundleName != null ? bundleName :
274 plugin.getPluginId() != PluginManagerCore.CORE_ID ? plugin.getResourceBundleBaseName() :
276 return resBundleName == null ? null : DynamicBundle.INSTANCE.getResourceBundle(resBundleName, plugin.getPluginClassLoader());
279 private static boolean isSecondary(Element element) {
280 return "true".equalsIgnoreCase(element.getAttributeValue(SECONDARY));
283 private static void setIcon(@Nullable String iconPath,
284 @NotNull String className,
285 @NotNull PluginDescriptor pluginDescriptor,
286 @NotNull Presentation presentation) {
287 if (iconPath == null) {
292 Class<?> actionClass = Class.forName(className, true, pluginDescriptor.getPluginClassLoader());
293 setIconFromClass(actionClass, pluginDescriptor, iconPath, presentation);
295 catch (ClassNotFoundException | NoClassDefFoundError e) {
297 reportActionError(pluginDescriptor.getPluginId(), "class with name \"" + className + "\" not found");
301 private static void setIconFromClass(@NotNull Class<?> actionClass,
302 @NotNull PluginDescriptor pluginDescriptor,
303 @NotNull String iconPath,
304 @NotNull Presentation presentation) {
305 //noinspection deprecation
306 presentation.setIcon(new IconLoader.LazyIcon() {
309 protected Icon compute() {
310 // try to find icon in idea class path
311 Icon icon = IconLoader.findIcon(iconPath, actionClass, true, false);
313 icon = IconLoader.findIcon(iconPath, pluginDescriptor.getPluginClassLoader());
317 reportActionError(pluginDescriptor.getPluginId(), "Icon cannot be found in '" + iconPath + "', action '" + actionClass + "'");
318 return AllIcons.Nodes.Unknown;
325 public String toString() {
326 return "LazyIcon@ActionManagerImpl (path: " + iconPath + ", action class: " + actionClass + ")";
331 @SuppressWarnings("HardCodedStringLiteral")
332 private static @NlsActions.ActionDescription String computeDescription(ResourceBundle bundle, String id, String elementType, String descriptionValue) {
333 if (bundle != null) {
334 final String key = elementType + "." + id + ".description";
335 return AbstractBundle.messageOrDefault(bundle, key, StringUtil.notNullize(descriptionValue));
338 return descriptionValue;
342 @SuppressWarnings("HardCodedStringLiteral")
343 private static @NlsActions.ActionText String computeActionText(ResourceBundle bundle, String id, String elementType, String textValue) {
344 return AbstractBundle.messageOrDefault(bundle, elementType + "." + id + "." + TEXT_ATTR_NAME, StringUtil.notNullize(textValue));
347 private static boolean checkRelativeToAction(String relativeToActionId,
348 @NotNull Anchor anchor,
349 @NotNull String actionName,
350 @Nullable PluginId pluginId) {
351 if ((Anchor.BEFORE == anchor || Anchor.AFTER == anchor) && relativeToActionId == null) {
352 reportActionError(pluginId, actionName + ": \"relative-to-action\" cannot be null if anchor is \"after\" or \"before\"");
359 private static Anchor parseAnchor(String anchorStr, @Nullable String actionName, @Nullable PluginId pluginId) {
360 if (anchorStr == null) {
364 if (FIRST.equalsIgnoreCase(anchorStr)) {
367 else if (LAST.equalsIgnoreCase(anchorStr)) {
370 else if (BEFORE.equalsIgnoreCase(anchorStr)) {
371 return Anchor.BEFORE;
373 else if (AFTER.equalsIgnoreCase(anchorStr)) {
377 reportActionError(pluginId, actionName + ": anchor should be one of the following constants: \"first\", \"last\", \"before\" or \"after\"");
382 private static void processMouseShortcutNode(Element element, String actionId, PluginId pluginId, @NotNull KeymapManager keymapManager) {
383 String keystrokeString = element.getAttributeValue(KEYSTROKE_ATTR_NAME);
384 if (keystrokeString == null || keystrokeString.trim().isEmpty()) {
385 reportActionError(pluginId, "\"keystroke\" attribute must be specified for action with id=" + actionId);
388 MouseShortcut shortcut;
390 shortcut = KeymapUtil.parseMouseShortcut(keystrokeString);
392 catch (Exception ex) {
393 reportActionError(pluginId, "\"keystroke\" attribute has invalid value for action with id=" + actionId);
397 String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
398 if (keymapName == null || keymapName.isEmpty()) {
399 reportActionError(pluginId, "attribute \"keymap\" should be defined");
402 Keymap keymap = keymapManager.getKeymap(keymapName);
403 if (keymap == null) {
404 reportKeymapNotFoundWarning(pluginId, keymapName);
407 processRemoveAndReplace(element, actionId, keymap, shortcut);
410 private static void reportActionError(@Nullable PluginId pluginId, @NotNull String message) {
411 reportActionError(pluginId, message, null);
414 private static void reportActionError(@Nullable PluginId pluginId, @NotNull String message, @Nullable Throwable cause) {
415 if (pluginId != null) {
416 LOG.error(new PluginException(message, cause, pluginId));
418 else if (cause != null) {
419 LOG.error(message, cause);
426 private static void reportKeymapNotFoundWarning(@Nullable PluginId pluginId, @NotNull String keymapName) {
427 if (DefaultKeymap.isBundledKeymapHidden(keymapName)) return;
428 String message = "keymap \"" + keymapName + "\" not found";
429 LOG.warn(pluginId == null ? message : new PluginException(message, null, pluginId).getMessage());
432 private static String getPluginInfo(@Nullable PluginId id) {
434 IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(id);
435 if (plugin != null) {
436 String name = plugin.getName();
438 name = id.getIdString();
440 return " Plugin: " + name;
447 private static DataContext getContextBy(Component contextComponent) {
448 final DataManager dataManager = DataManager.getInstance();
449 return contextComponent != null ? dataManager.getDataContext(contextComponent) : dataManager.getDataContext();
453 public void dispose() {
454 if (myTimer != null) {
461 public void addTimerListener(int delay, @NotNull final TimerListener listener) {
462 _addTimerListener(listener, false);
466 public void removeTimerListener(@NotNull TimerListener listener) {
467 _removeTimerListener(listener, false);
471 public void addTransparentTimerListener(int delay, @NotNull TimerListener listener) {
472 _addTimerListener(listener, true);
476 public void removeTransparentTimerListener(@NotNull TimerListener listener) {
477 _removeTimerListener(listener, true);
480 private void _addTimerListener(final TimerListener listener, boolean transparent) {
481 if (ApplicationManager.getApplication().isUnitTestMode()) return;
482 if (myTimer == null) {
483 myTimer = new MyTimer();
487 myTimer.addTimerListener(listener, transparent);
490 private void _removeTimerListener(TimerListener listener, boolean transparent) {
491 if (ApplicationManager.getApplication().isUnitTestMode()) return;
492 if (LOG.assertTrue(myTimer != null)) {
493 myTimer.removeTimerListener(listener, transparent);
498 public ActionPopupMenu createActionPopupMenu(@NotNull String place, @NotNull ActionGroup group, @Nullable PresentationFactory presentationFactory) {
499 return new ActionPopupMenuImpl(place, group, this, presentationFactory);
504 public ActionPopupMenu createActionPopupMenu(@NotNull String place, @NotNull ActionGroup group) {
505 return new ActionPopupMenuImpl(place, group, this, null);
510 public ActionToolbar createActionToolbar(@NotNull final String place, @NotNull final ActionGroup group, final boolean horizontal) {
511 return createActionToolbar(place, group, horizontal, false);
516 public ActionToolbar createActionToolbar(@NotNull final String place, @NotNull final ActionGroup group, final boolean horizontal, final boolean decorateButtons) {
517 return new ActionToolbarImpl(place, group, horizontal, decorateButtons);
520 public void registerPluginActions(@NotNull IdeaPluginDescriptorImpl plugin, @Nullable List<Element> actionDescriptionElements, boolean initialStartup) {
521 if (actionDescriptionElements == null) {
525 long startTime = StartUpMeasurer.getCurrentTime();
526 for (Element e : actionDescriptionElements) {
527 Element parent = e.getParentElement();
528 String bundleName = parent == null ? null : parent.getAttributeValue(RESOURCE_BUNDLE_ATTR_NAME);
529 processActionsChildElement(e, plugin, getActionsResourceBundle(plugin, bundleName));
531 StartUpMeasurer.addPluginCost(plugin.getPluginId().getIdString(), "Actions", StartUpMeasurer.getCurrentTime() - startTime);
536 public AnAction getAction(@NotNull String id) {
537 return getActionImpl(id, false);
541 private AnAction getActionImpl(@NotNull String id, boolean canReturnStub) {
543 synchronized (myLock) {
544 action = myId2Action.get(id);
545 if (canReturnStub || !(action instanceof ActionStubBase)) {
549 AnAction converted = action instanceof ActionStub ? convertStub((ActionStub)action) : convertGroupStub((ActionGroupStub)action, this);
550 if (converted == null) {
551 unregisterAction(id);
555 synchronized (myLock) {
556 action = myId2Action.get(id);
557 if (action instanceof ActionStubBase) {
558 action = replaceStub((ActionStubBase)action, converted);
565 private AnAction replaceStub(@NotNull ActionStubBase stub, AnAction anAction) {
566 LOG.assertTrue(myAction2Id.containsKey(stub));
567 myAction2Id.remove(stub);
569 LOG.assertTrue(myId2Action.containsKey(stub.getId()));
571 AnAction action = myId2Action.remove(stub.getId());
572 LOG.assertTrue(action != null);
573 LOG.assertTrue(action.equals(stub));
575 myAction2Id.put(anAction, stub.getId());
576 updateHandlers(anAction);
578 return addToMap(stub.getId(), anAction, stub.getPlugin().getPluginId(), stub instanceof ActionStub ? ((ActionStub)stub).getProjectType() : null);
582 public String getId(@NotNull AnAction action) {
583 if (action instanceof ActionStubBase) {
584 return ((ActionStubBase)action).getId();
586 synchronized (myLock) {
587 return myAction2Id.get(action);
592 public @NotNull List<String> getActionIdList(@NotNull String idPrefix) {
593 List<String> result = new ArrayList<>();
594 synchronized (myLock) {
595 for (String id : myId2Action.keySet()) {
596 if (id.startsWith(idPrefix)) {
605 public String @NotNull [] getActionIds(@NotNull String idPrefix) {
606 return ArrayUtilRt.toStringArray(getActionIdList(idPrefix));
610 public boolean isGroup(@NotNull String actionId) {
611 return getActionImpl(actionId, true) instanceof ActionGroup;
616 public JComponent createButtonToolbar(@NotNull final String actionPlace, @NotNull final ActionGroup messageActionGroup) {
617 return new ButtonToolbarImpl(actionPlace, messageActionGroup);
621 public AnAction getActionOrStub(@NotNull String id) {
622 return getActionImpl(id, true);
626 * @return instance of ActionGroup or ActionStub. The method never returns real subclasses of {@code AnAction}.
629 private AnAction processActionElement(@NotNull Element element,
630 @NotNull IdeaPluginDescriptorImpl plugin,
631 @Nullable ResourceBundle bundle) {
632 String className = element.getAttributeValue(CLASS_ATTR_NAME);
633 if (className == null || className.isEmpty()) {
634 reportActionError(plugin.getPluginId(), "action element should have specified \"class\" attribute");
638 // read ID and register loaded action
639 String id = obtainActionId(element, className);
640 if (Boolean.parseBoolean(element.getAttributeValue(INTERNAL_ATTR_NAME)) &&
641 !ApplicationManager.getApplication().isInternal()) {
642 myNotRegisteredInternalActionIds.add(id);
646 String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
647 String projectType = element.getAttributeValue(PROJECT_TYPE);
649 String textValue = element.getAttributeValue(TEXT_ATTR_NAME);
650 String descriptionValue = element.getAttributeValue(DESCRIPTION);
652 ActionStub stub = new ActionStub(className, id, plugin, iconPath, projectType, () -> {
653 Supplier<String> text = () -> computeActionText(bundle, id, ACTION_ELEMENT_NAME, textValue);
654 if (text.get() == null) {
655 reportActionError(plugin.getPluginId(), "'text' attribute is mandatory (actionId=" + id +
656 ", plugin=" + plugin + ")");
658 Presentation presentation = new Presentation();
659 presentation.setText(text);
660 presentation.setDescription(() -> computeDescription(bundle, id, ACTION_ELEMENT_NAME, descriptionValue));
664 KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx();
665 // process all links and key bindings if any
666 for (Element e : element.getChildren()) {
667 if (ADD_TO_GROUP_ELEMENT_NAME.equals(e.getName())) {
668 processAddToGroupNode(stub, e, plugin.getPluginId(), isSecondary(e));
670 else if (SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
671 processKeyboardShortcutNode(e, id, plugin.getPluginId(), keymapManager);
673 else if (MOUSE_SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
674 processMouseShortcutNode(e, id, plugin.getPluginId(), keymapManager);
676 else if (ABBREVIATION_ELEMENT_NAME.equals(e.getName())) {
677 processAbbreviationNode(e, id);
679 else if (OVERRIDE_TEXT_ELEMENT_NAME.equals(e.getName())) {
680 processOverrideTextNode(stub, stub.getId(), e, plugin.getPluginId(), bundle);
682 else if (SYNONYM_ELEMENT_NAME.equals(e.getName())) {
683 processSynonymNode(stub, e, plugin.getPluginId(), bundle);
686 reportActionError(plugin.getPluginId(), "unexpected name of element \"" + e.getName() + "\"");
690 String shortcutOfActionId = element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME);
691 if (shortcutOfActionId != null) {
692 keymapManager.bindShortcuts(shortcutOfActionId, id);
695 registerOrReplaceActionInner(element, id, stub, plugin);
699 private static String obtainActionId(Element element, String className) {
700 String id = element.getAttributeValue(ID_ATTR_NAME);
701 return StringUtil.isEmpty(id) ? StringUtil.getShortName(className) : id;
704 private void registerOrReplaceActionInner(@NotNull Element element,
706 @NotNull AnAction action,
707 @NotNull IdeaPluginDescriptor plugin) {
708 synchronized (myLock) {
709 if (Boolean.parseBoolean(element.getAttributeValue(OVERRIDES_ATTR_NAME))) {
710 if (getActionOrStub(id) == null) {
711 LOG.error(element.getName() + " '" + id + "' doesn't override anything");
714 AnAction prev = replaceAction(id, action, plugin.getPluginId());
715 if (action instanceof DefaultActionGroup && prev instanceof DefaultActionGroup) {
716 if (Boolean.parseBoolean(element.getAttributeValue(KEEP_CONTENT_ATTR_NAME))) {
717 ((DefaultActionGroup)action).copyFromGroup((DefaultActionGroup)prev);
722 registerAction(id, action, plugin.getPluginId(), element.getAttributeValue(PROJECT_TYPE));
724 ActionsCollectorImpl.onActionLoadedFromXml(action, id, plugin);
728 private AnAction processGroupElement(@NotNull Element element,
729 @NotNull IdeaPluginDescriptorImpl plugin,
730 @Nullable ResourceBundle bundle) {
731 if (!GROUP_ELEMENT_NAME.equals(element.getName())) {
732 reportActionError(plugin.getPluginId(), "unexpected name of element \"" + element.getName() + "\"");
735 String className = element.getAttributeValue(CLASS_ATTR_NAME);
736 if (className == null) { // use default group if class isn't specified
737 className = "true".equals(element.getAttributeValue(COMPACT_ATTR_NAME))
738 ? DefaultCompactActionGroup.class.getName()
739 : DefaultActionGroup.class.getName();
742 String id = element.getAttributeValue(ID_ATTR_NAME);
743 if (id != null && id.isEmpty()) {
744 reportActionError(plugin.getPluginId(), "ID of the group cannot be an empty string");
749 boolean customClass = false;
750 if (DefaultActionGroup.class.getName().equals(className)) {
751 group = new DefaultActionGroup();
753 else if (DefaultCompactActionGroup.class.getName().equals(className)) {
754 group = new DefaultCompactActionGroup();
756 else if (id == null) {
757 Object obj = ApplicationManager.getApplication().instantiateExtensionWithPicoContainerOnlyIfNeeded(className, plugin);
758 if (!(obj instanceof ActionGroup)) {
759 reportActionError(plugin.getPluginId(), "class with name \"" + className + "\" should be instance of " + ActionGroup.class.getName());
762 if (element.getChildren().size() != element.getChildren(ADD_TO_GROUP_ELEMENT_NAME).size() ) { //
763 if (!(obj instanceof DefaultActionGroup)) {
764 reportActionError(plugin.getPluginId(), "class with name \"" + className + "\" should be instance of " + DefaultActionGroup.class.getName() +
765 " because there are children specified");
770 group = (ActionGroup)obj;
773 group = new ActionGroupStub(id, className, plugin);
776 // read ID and register loaded group
777 if (Boolean.parseBoolean(element.getAttributeValue(INTERNAL_ATTR_NAME)) && !ApplicationManager.getApplication().isInternal()) {
778 myNotRegisteredInternalActionIds.add(id);
783 id = "<anonymous-group-" + myAnonymousGroupIdCounter++ + ">";
786 registerOrReplaceActionInner(element, id, group, plugin);
787 Presentation presentation = group.getTemplatePresentation();
791 Supplier<String> text = () -> computeActionText(bundle, finalId, GROUP_ELEMENT_NAME, element.getAttributeValue(TEXT_ATTR_NAME));
792 // don't override value which was set in API with empty value from xml descriptor
793 if (!StringUtil.isEmpty(text.get()) || presentation.getText() == null) {
794 presentation.setText(text);
798 Supplier<String> description = () -> computeDescription(bundle, finalId, GROUP_ELEMENT_NAME, element.getAttributeValue(DESCRIPTION));
799 // don't override value which was set in API with empty value from xml descriptor
800 if (!StringUtil.isEmpty(description.get()) || presentation.getDescription() == null) {
801 presentation.setDescription(description);
805 String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
806 if (group instanceof ActionGroupStub) {
807 ((ActionGroupStub)group).setIconPath(iconPath);
810 setIcon(iconPath, className, plugin, presentation);
814 String popup = element.getAttributeValue(POPUP_ATTR_NAME);
816 group.setPopup(Boolean.parseBoolean(popup));
817 if (group instanceof ActionGroupStub) {
818 ((ActionGroupStub)group).setPopupDefinedInXml(true);
822 String searchable = element.getAttributeValue(SEARCHABLE_ATTR_NAME);
823 if (searchable != null) {
824 group.setSearchable(Boolean.parseBoolean(searchable));
827 String shortcutOfActionId = element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME);
828 if (customClass && shortcutOfActionId != null) {
829 KeymapManagerEx.getInstanceEx().bindShortcuts(shortcutOfActionId, id);
832 // process all group's children. There are other groups, actions, references and links
833 for (Element child : element.getChildren()) {
834 String name = child.getName();
835 if (ACTION_ELEMENT_NAME.equals(name)) {
836 AnAction action = processActionElement(child, plugin, bundle);
837 if (action != null) {
838 addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
841 else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
842 processSeparatorNode((DefaultActionGroup)group, child, plugin.getPluginId(), bundle);
844 else if (GROUP_ELEMENT_NAME.equals(name)) {
845 AnAction action = processGroupElement(child, plugin, bundle);
846 if (action != null) {
847 addToGroupInner(group, action, Constraints.LAST, false);
850 else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) {
851 processAddToGroupNode(group, child, plugin.getPluginId(), isSecondary(child));
853 else if (REFERENCE_ELEMENT_NAME.equals(name)) {
854 AnAction action = processReferenceElement(child, plugin.getPluginId());
855 if (action != null) {
856 addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
859 else if (OVERRIDE_TEXT_ELEMENT_NAME.equals(name)) {
860 processOverrideTextNode(group, id, child, plugin.getPluginId(), bundle);
863 reportActionError(plugin.getPluginId(), "unexpected name of element \"" + name + "\n");
869 catch (Exception e) {
870 String message = "cannot create class \"" + className + "\"";
871 reportActionError(plugin.getPluginId(), message, e);
876 private void processReferenceNode(@NotNull Element element, @Nullable PluginId pluginId, @Nullable ResourceBundle bundle) {
877 AnAction action = processReferenceElement(element, pluginId);
878 if (action == null) {
882 for (Element child : element.getChildren()) {
883 if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
884 processAddToGroupNode(action, child, pluginId, isSecondary(child));
886 else if (SYNONYM_ELEMENT_NAME.equals(child.getName())) {
887 processSynonymNode(action, child, pluginId, bundle);
893 * @param element description of link
895 private void processAddToGroupNode(AnAction action, Element element, PluginId pluginId, boolean secondary) {
896 String name = action instanceof ActionStub ? ((ActionStub)action).getClassName() : action.getClass().getName();
897 String id = action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action);
898 String actionName = name + " (" + id + ")";
900 if (!ADD_TO_GROUP_ELEMENT_NAME.equals(element.getName())) {
901 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
906 final AnAction parentGroup = getParentGroup(element.getAttributeValue(GROUPID_ATTR_NAME), actionName, pluginId);
907 if (parentGroup == null) {
912 final Anchor anchor = parseAnchor(element.getAttributeValue(ANCHOR_ELEMENT_NAME), actionName, pluginId);
913 if (anchor == null) {
917 final String relativeToActionId = element.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME);
918 if (!checkRelativeToAction(relativeToActionId, anchor, actionName, pluginId)) {
921 addToGroupInner(parentGroup, action, new Constraints(anchor, relativeToActionId), secondary);
924 private void addToGroupInner(AnAction group, AnAction action, Constraints constraints, boolean secondary) {
925 String actionId = action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action);
926 ((DefaultActionGroup)group).addAction(action, constraints, this).setAsSecondary(secondary);
927 myId2GroupId.putValue(actionId, myAction2Id.get(group));
931 public DefaultActionGroup getParentGroup(final String groupId,
932 @Nullable final String actionName,
933 @Nullable final PluginId pluginId) {
934 if (groupId == null || groupId.isEmpty()) {
935 reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined");
938 AnAction parentGroup = getActionImpl(groupId, true);
939 if (parentGroup == null) {
940 reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" isn't registered; action will be added to the \"Other\" group", null);
941 parentGroup = getActionImpl(IdeActions.GROUP_OTHER_MENU, true);
943 if (!(parentGroup instanceof DefaultActionGroup)) {
944 reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" should be instance of " + DefaultActionGroup.class.getName() +
945 " but was " + (parentGroup != null ? parentGroup.getClass() : "[null]"));
948 return (DefaultActionGroup)parentGroup;
951 private static void processOverrideTextNode(AnAction action, String id, Element element, PluginId pluginId,
952 @Nullable ResourceBundle bundle) {
953 if (!OVERRIDE_TEXT_ELEMENT_NAME.equals(element.getName())) {
954 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
957 String place = element.getAttributeValue(PLACE_ATTR_NAME);
959 reportActionError(pluginId, id + ": override-text specified without place");
962 String useTextOfPlace = element.getAttributeValue(USE_TEXT_OF_PLACE_ATTR_NAME);
963 if (useTextOfPlace != null) {
964 action.copyActionTextOverride(useTextOfPlace, place, id);
967 String text = element.getAttributeValue(TEXT_ATTR_NAME, "");
968 if (text.isEmpty() && bundle != null) {
969 String prefix = action instanceof ActionGroup ? "group" : "action";
970 String key = prefix + "." + id + "." + place + ".text";
971 action.addTextOverride(place, () -> BundleBase.message(bundle, key));
974 action.addTextOverride(place, () -> text);
979 private static void processSynonymNode(AnAction action, Element element, PluginId pluginId, @Nullable ResourceBundle bundle) {
980 if (!SYNONYM_ELEMENT_NAME.equals(element.getName())) {
981 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
984 String text = element.getAttributeValue(TEXT_ATTR_NAME, "");
985 if (!text.isEmpty()) {
986 action.addSynonym(() -> text);
989 String key = element.getAttributeValue(KEY_ATTR_NAME);
990 if (key != null && bundle != null) {
991 action.addSynonym(() -> BundleBase.message(bundle, key));
994 reportActionError(pluginId, "Can't process synonym: neither text nor resource bundle key is specified");
1000 * @param parentGroup group which is the parent of the separator. It can be {@code null} in that
1001 * case separator will be added to group described in the <add-to-group ....> subelement.
1002 * @param element XML element which represent separator.
1004 private void processSeparatorNode(@Nullable DefaultActionGroup parentGroup, @NotNull Element element, PluginId pluginId, @Nullable ResourceBundle bundle) {
1005 if (!SEPARATOR_ELEMENT_NAME.equals(element.getName())) {
1006 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
1009 @SuppressWarnings("HardCodedStringLiteral")
1010 String text = element.getAttributeValue(TEXT_ATTR_NAME);
1011 String key = element.getAttributeValue(KEY_ATTR_NAME);
1012 Separator separator =
1013 text != null ? new Separator(text) : key != null ? createSeparator(bundle, key) : Separator.getInstance();
1014 if (parentGroup != null) {
1015 parentGroup.add(separator, this);
1017 // try to find inner <add-to-parent...> tag
1018 for (Element child : element.getChildren()) {
1019 if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
1020 processAddToGroupNode(separator, child, pluginId, isSecondary(child));
1026 private static Separator createSeparator(@Nullable ResourceBundle bundle, @NotNull String key) {
1027 String text = bundle != null ? AbstractBundle.messageOrNull(bundle, key) : null;
1028 return text != null ? new Separator(text) : Separator.getInstance();
1031 private void processUnregisterNode(Element element, PluginId pluginId) {
1032 String id = element.getAttributeValue(ID_ATTR_NAME);
1034 reportActionError(pluginId, "'id' attribute is required for 'unregister' elements");
1037 AnAction action = getAction(id);
1038 if (action == null) {
1039 reportActionError(pluginId, "Trying to unregister non-existing action " + id);
1043 AbbreviationManager.getInstance().removeAllAbbreviations(id);
1044 unregisterAction(id);
1047 private static void processKeyboardShortcutNode(Element element,
1050 @NotNull KeymapManagerEx keymapManager) {
1051 String firstStrokeString = element.getAttributeValue(FIRST_KEYSTROKE_ATTR_NAME);
1052 if (firstStrokeString == null) {
1053 reportActionError(pluginId, "\"first-keystroke\" attribute must be specified for action with id=" + actionId);
1056 KeyStroke firstKeyStroke = getKeyStroke(firstStrokeString);
1057 if (firstKeyStroke == null) {
1058 reportActionError(pluginId, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId);
1062 KeyStroke secondKeyStroke = null;
1063 String secondStrokeString = element.getAttributeValue(SECOND_KEYSTROKE_ATTR_NAME);
1064 if (secondStrokeString != null) {
1065 secondKeyStroke = getKeyStroke(secondStrokeString);
1066 if (secondKeyStroke == null) {
1067 reportActionError(pluginId, "\"second-keystroke\" attribute has invalid value for action with id=" + actionId);
1072 String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
1073 if (keymapName == null || keymapName.trim().isEmpty()) {
1074 reportActionError(pluginId, "attribute \"keymap\" should be defined");
1077 Keymap keymap = keymapManager.getKeymap(keymapName);
1078 if (keymap == null) {
1079 reportKeymapNotFoundWarning(pluginId, keymapName);
1082 final KeyboardShortcut shortcut = new KeyboardShortcut(firstKeyStroke, secondKeyStroke);
1083 processRemoveAndReplace(element, actionId, keymap, shortcut);
1086 private static void processRemoveAndReplace(@NotNull Element element, String actionId, @NotNull Keymap keymap, @NotNull Shortcut shortcut) {
1087 boolean remove = Boolean.parseBoolean(element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME));
1088 boolean replace = Boolean.parseBoolean(element.getAttributeValue(REPLACE_SHORTCUT_ATTR_NAME));
1090 keymap.removeShortcut(actionId, shortcut);
1093 keymap.removeAllActionShortcuts(actionId);
1096 keymap.addShortcut(actionId, shortcut);
1100 private @Nullable AnAction processReferenceElement(Element element, PluginId pluginId) {
1101 if (!REFERENCE_ELEMENT_NAME.equals(element.getName())) {
1102 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"", null);
1106 String ref = getReferenceActionId(element);
1107 if (ref == null || ref.isEmpty()) {
1108 reportActionError(pluginId, "ID of reference element should be defined", null);
1112 AnAction action = getActionImpl(ref, true);
1113 if (action == null) {
1114 if (!myNotRegisteredInternalActionIds.contains(ref)) {
1115 reportActionError(pluginId, "action specified by reference isn't registered (ID=" + ref + ")", null);
1122 private static String getReferenceActionId(@NotNull Element element) {
1123 String ref = element.getAttributeValue(REF_ATTR_NAME);
1125 // support old style references by id
1126 ref = element.getAttributeValue(ID_ATTR_NAME);
1131 private void processActionsChildElement(@NotNull Element child,
1132 @NotNull IdeaPluginDescriptorImpl plugin,
1133 @Nullable ResourceBundle bundle) {
1134 String name = child.getName();
1136 case ACTION_ELEMENT_NAME:
1137 processActionElement(child, plugin, bundle);
1139 case GROUP_ELEMENT_NAME:
1140 processGroupElement(child, plugin, bundle);
1142 case SEPARATOR_ELEMENT_NAME:
1143 processSeparatorNode(null, child, plugin.getPluginId(), bundle);
1145 case REFERENCE_ELEMENT_NAME:
1146 processReferenceNode(child, plugin.getPluginId(), bundle);
1148 case UNREGISTER_ELEMENT_NAME:
1149 processUnregisterNode(child, plugin.getPluginId());
1152 reportActionError(plugin.getPluginId(), "unexpected name of element \"" + name + "\n");
1158 public static @Nullable String checkUnloadActions(PluginId pluginId, @NotNull IdeaPluginDescriptorImpl pluginDescriptor) {
1159 List<Element> elements = pluginDescriptor.getActionDescriptionElements();
1160 if (elements == null) {
1164 for (Element element : elements) {
1165 if (!element.getName().equals(ACTION_ELEMENT_NAME) &&
1166 !(element.getName().equals(GROUP_ELEMENT_NAME) && canUnloadGroup(element)) &&
1167 !element.getName().equals(REFERENCE_ELEMENT_NAME)) {
1168 return "Plugin " + pluginId + " is not unload-safe because of action element " + element.getName();
1174 private static boolean canUnloadGroup(@NotNull Element element) {
1175 if (element.getAttributeValue(ID_ATTR_NAME) == null) {
1178 for (Element child : element.getChildren()) {
1179 if (child.getName().equals(GROUP_ELEMENT_NAME) && !canUnloadGroup(child)) return false;
1184 public void unloadActions(@NotNull IdeaPluginDescriptorImpl pluginDescriptor) {
1185 List<Element> elements = pluginDescriptor.getActionDescriptionElements();
1186 if (elements == null) {
1190 for (Element element : ContainerUtil.reverse(elements)) {
1191 switch (element.getName()) {
1192 case ACTION_ELEMENT_NAME:
1193 unloadActionElement(element);
1195 case GROUP_ELEMENT_NAME:
1196 unloadGroupElement(element);
1198 case REFERENCE_ELEMENT_NAME:
1199 PluginId pluginId = pluginDescriptor.getPluginId();
1200 AnAction action = processReferenceElement(element, pluginId);
1201 if (action == null) return;
1202 String actionId = getReferenceActionId(element);
1204 for (Element child : element.getChildren(ADD_TO_GROUP_ELEMENT_NAME)) {
1205 String groupId = child.getAttributeValue(GROUPID_ATTR_NAME);
1206 final DefaultActionGroup parentGroup = getParentGroup(groupId, actionId, pluginId);
1207 if (parentGroup == null) return;
1208 parentGroup.remove(action);
1209 myId2GroupId.remove(actionId, groupId);
1216 private void unloadGroupElement(Element element) {
1217 String id = element.getAttributeValue(ID_ATTR_NAME);
1219 throw new IllegalStateException("Cannot unload groups with no ID");
1221 for (Element groupChild : element.getChildren()) {
1222 if (groupChild.getName().equals(ACTION_ELEMENT_NAME)) {
1223 unloadActionElement(groupChild);
1225 else if (groupChild.getName().equals(GROUP_ELEMENT_NAME)) {
1226 unloadGroupElement(groupChild);
1229 unregisterAction(id);
1232 private void unloadActionElement(@NotNull Element element) {
1233 String className = element.getAttributeValue(CLASS_ATTR_NAME);
1234 String id = obtainActionId(element, className);
1235 unregisterAction(id);
1239 public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) {
1240 registerAction(actionId, action, pluginId, null);
1243 public void registerAction(@NotNull String actionId,
1244 @NotNull AnAction action,
1245 @Nullable PluginId pluginId,
1246 @Nullable String projectType) {
1247 synchronized (myLock) {
1248 if (addToMap(actionId, action, pluginId, projectType) == null) return;
1249 if (myAction2Id.containsKey(action)) {
1250 reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) +
1251 getPluginInfo(pluginId));
1254 myId2Index.put(actionId, myRegisteredActionsCount++);
1255 myAction2Id.put(action, actionId);
1256 if (pluginId != null && !(action instanceof ActionGroup)) {
1257 myPlugin2Id.putValue(pluginId, actionId);
1259 action.registerCustomShortcutSet(new ProxyShortcutSet(actionId), null);
1260 notifyCustomActionsSchema(actionId);
1261 updateHandlers(action);
1265 private static void notifyCustomActionsSchema(@NotNull String registeredID) {
1266 CustomActionsSchema schema = ApplicationManager.getApplication().getServiceIfCreated(CustomActionsSchema.class);
1267 if (schema == null) return;
1268 for (ActionUrl url : schema.getActions()) {
1269 if (registeredID.equals(url.getComponent())) {
1270 schema.incrementModificationStamp();
1276 private AnAction addToMap(String actionId, AnAction action, PluginId pluginId, String projectType) {
1277 if (projectType != null || myId2Action.containsKey(actionId)) {
1278 return registerChameleon(actionId, action, pluginId, projectType);
1281 myId2Action.put(actionId, action);
1286 private AnAction registerChameleon(String actionId, AnAction action, PluginId pluginId, String projectType) {
1287 ProjectType type = projectType == null ? null : new ProjectType(projectType);
1288 // make sure id+projectType is unique
1289 AnAction o = myId2Action.get(actionId);
1290 ChameleonAction chameleonAction;
1292 chameleonAction = new ChameleonAction(action, type);
1293 myId2Action.put(actionId, chameleonAction);
1294 return chameleonAction;
1296 if (o instanceof ChameleonAction) {
1297 chameleonAction = (ChameleonAction)o;
1300 chameleonAction = new ChameleonAction(o, type);
1301 myId2Action.put(actionId, chameleonAction);
1303 AnAction old = chameleonAction.addAction(action, type);
1305 reportActionError(pluginId,
1306 "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action +
1307 "; Registered action is " +
1308 myId2Action.get(actionId) + getPluginInfo(pluginId));
1311 return chameleonAction;
1315 public void registerAction(@NotNull String actionId, @NotNull AnAction action) {
1316 registerAction(actionId, action, null);
1320 public void unregisterAction(@NotNull String actionId) {
1321 unregisterAction(actionId, true);
1324 private void unregisterAction(@NotNull String actionId, boolean removeFromGroups) {
1325 synchronized (myLock) {
1326 if (!myId2Action.containsKey(actionId)) {
1327 if (LOG.isDebugEnabled()) {
1328 LOG.debug("action with ID " + actionId + " wasn't registered");
1332 AnAction actionToRemove = myId2Action.remove(actionId);
1333 myAction2Id.remove(actionToRemove);
1334 myId2Index.removeInt(actionId);
1336 for (Map.Entry<PluginId, Collection<String>> entry : myPlugin2Id.entrySet()) {
1337 entry.getValue().remove(actionId);
1340 if (removeFromGroups) {
1341 CustomActionsSchema customActionSchema = ApplicationManager.getApplication().getServiceIfCreated(CustomActionsSchema.class);
1342 for (String groupId : myId2GroupId.get(actionId)) {
1343 if (customActionSchema != null) {
1344 customActionSchema.invalidateCustomizedActionGroup(groupId);
1346 DefaultActionGroup group = (DefaultActionGroup)getActionOrStub(groupId);
1347 if (group == null) {
1348 LOG.error("Trying to remove action " + actionId + " from non-existing group " + groupId);
1351 group.remove(actionToRemove, actionId);
1352 if (!(group instanceof ActionGroupStub)) {
1353 //group can be used as a stub in other actions
1354 for (String parentOfGroup : myId2GroupId.get(groupId)) {
1355 DefaultActionGroup parentOfGroupAction = (DefaultActionGroup) getActionOrStub(parentOfGroup);
1356 if (parentOfGroupAction == null) {
1357 LOG.error("Trying to remove action " + actionId + " from non-existing group " + parentOfGroup);
1360 for (AnAction stub : parentOfGroupAction.getChildActionsOrStubs()) {
1361 if (stub instanceof ActionGroupStub && ((ActionGroupStub)stub).getId() == groupId) {
1362 ((ActionGroupStub)stub).remove(actionToRemove, actionId);
1369 if (actionToRemove instanceof ActionGroup) {
1370 for (Map.Entry<String, Collection<String>> entry : myId2GroupId.entrySet()) {
1371 entry.getValue().remove(actionId);
1374 updateHandlers(actionToRemove);
1380 public Comparator<String> getRegistrationOrderComparator() {
1381 return Comparator.comparingInt(myId2Index::getInt);
1385 public String @NotNull [] getPluginActions(@NotNull PluginId pluginName) {
1386 return ArrayUtilRt.toStringArray(myPlugin2Id.get(pluginName));
1389 public void addActionPopup(@NotNull Object menu) {
1391 if (menu instanceof ActionPopupMenu) {
1392 for (ActionPopupMenuListener listener : myActionPopupMenuListeners) {
1393 listener.actionPopupMenuCreated((ActionPopupMenu)menu);
1398 void removeActionPopup(@NotNull Object menu) {
1399 final boolean removed = myPopups.remove(menu);
1400 if (removed && menu instanceof ActionPopupMenu) {
1401 for (ActionPopupMenuListener listener : myActionPopupMenuListeners) {
1402 listener.actionPopupMenuReleased((ActionPopupMenu)menu);
1408 public void queueActionPerformedEvent(@NotNull final AnAction action, @NotNull DataContext context, @NotNull AnActionEvent event) {
1409 if (myPopups.isEmpty()) {
1410 fireAfterActionPerformed(action, context, event);
1414 public boolean isToolWindowContextMenuVisible() {
1415 for (Object popup : myPopups) {
1416 if (popup instanceof ActionPopupMenuImpl &&
1417 ((ActionPopupMenuImpl)popup).isToolWindowContextMenu()) {
1425 public boolean isActionPopupStackEmpty() {
1426 return myPopups.isEmpty();
1430 public boolean isTransparentOnlyActionsUpdateNow() {
1431 return myTransparentOnlyUpdate;
1435 public void addActionPopupMenuListener(@NotNull ActionPopupMenuListener listener, @NotNull Disposable parentDisposable) {
1436 myActionPopupMenuListeners.add(listener);
1437 Disposer.register(parentDisposable, () -> myActionPopupMenuListeners.remove(listener));
1441 public void replaceAction(@NotNull String actionId, @NotNull AnAction newAction) {
1442 Class<?> callerClass = ReflectionUtil.getGrandCallerClass();
1443 PluginId pluginId = callerClass != null ? PluginManagerCore.getPluginByClassName(callerClass.getName()) : null;
1444 replaceAction(actionId, newAction, pluginId);
1447 private AnAction replaceAction(@NotNull String actionId, @NotNull AnAction newAction, @Nullable PluginId pluginId) {
1448 AnAction oldAction = newAction instanceof OverridingAction ? getAction(actionId) : getActionOrStub(actionId);
1449 if (oldAction != null) {
1450 if (newAction instanceof OverridingAction) {
1451 myBaseActions.put((OverridingAction)newAction, oldAction);
1453 boolean isGroup = oldAction instanceof ActionGroup;
1454 if (isGroup != newAction instanceof ActionGroup) {
1455 throw new IllegalStateException("cannot replace a group with an action and vice versa: " + actionId);
1457 for (String groupId : myId2GroupId.get(actionId)) {
1458 DefaultActionGroup group = (DefaultActionGroup)getActionOrStub(groupId);
1459 if (group == null) {
1460 throw new IllegalStateException("Trying to replace action which has been added to a non-existing group " + groupId);
1462 group.replaceAction(oldAction, newAction);
1464 unregisterAction(actionId, false);
1466 registerAction(actionId, newAction, pluginId);
1471 * Returns the action overridden by the specified overriding action (with overrides="true" in plugin.xml).
1473 public AnAction getBaseAction(OverridingAction overridingAction) {
1474 return myBaseActions.get(overridingAction);
1477 public Collection<String> getParentGroupIds(String actionId) {
1478 return myId2GroupId.get(actionId);
1482 public void addAnActionListener(AnActionListener listener) {
1483 myActionListeners.add(listener);
1487 public void removeAnActionListener(AnActionListener listener) {
1488 myActionListeners.remove(listener);
1492 public void fireBeforeActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) {
1493 myPrevPerformedActionId = myLastPreformedActionId;
1494 myLastPreformedActionId = getId(action);
1495 if (myLastPreformedActionId == null && action instanceof ActionIdProvider) {
1496 myLastPreformedActionId = ((ActionIdProvider)action).getId();
1498 //noinspection AssignmentToStaticFieldFromInstanceMethod
1499 IdeaLogger.ourLastActionId = myLastPreformedActionId;
1500 final PsiFile file = CommonDataKeys.PSI_FILE.getData(dataContext);
1501 final Language language = file != null ? file.getLanguage() : null;
1502 final List<EventPair> customData = new ArrayList<>();
1503 customData.add(EventFields.CurrentFile.with(language));
1504 Project project = CommonDataKeys.PROJECT.getData(dataContext);
1505 customData.add(EventFields.Language.with(getHostFileLanguage(dataContext, project)));
1506 if (action instanceof FusAwareAction) {
1507 List<EventPair> additionalUsageData = ((FusAwareAction)action).getAdditionalUsageData(event);
1508 customData.add(ActionsEventLogGroup.ADDITIONAL.with(new ObjectEventData(additionalUsageData.toArray(new EventPair[0]))));
1510 ActionsCollectorImpl.recordActionInvoked(project, action, event, customData);
1511 for (AnActionListener listener : myActionListeners) {
1512 listener.beforeActionPerformed(action, dataContext, event);
1514 publisher().beforeActionPerformed(action, dataContext, event);
1517 private static @Nullable Language getHostFileLanguage(@NotNull DataContext dataContext, @Nullable Project project) {
1518 if (project == null) return null;
1519 Editor editor = CommonDataKeys.HOST_EDITOR.getData(dataContext);
1520 if (editor == null) return null;
1521 PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
1522 return file != null ? file.getLanguage() : null;
1526 public void fireAfterActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) {
1527 myPrevPerformedActionId = myLastPreformedActionId;
1528 myLastPreformedActionId = getId(action);
1529 //noinspection AssignmentToStaticFieldFromInstanceMethod
1530 IdeaLogger.ourLastActionId = myLastPreformedActionId;
1531 for (AnActionListener listener : myActionListeners) {
1533 listener.afterActionPerformed(action, dataContext, event);
1535 catch (AbstractMethodError ignored) {
1538 publisher().afterActionPerformed(action, dataContext, event);
1542 public KeyboardShortcut getKeyboardShortcut(@NotNull String actionId) {
1543 AnAction action = ActionManager.getInstance().getAction(actionId);
1544 final ShortcutSet shortcutSet = action.getShortcutSet();
1545 final Shortcut[] shortcuts = shortcutSet.getShortcuts();
1546 for (final Shortcut shortcut : shortcuts) {
1547 // Shortcut can be MouseShortcut here.
1548 // For example IdeaVIM often assigns them
1549 if (shortcut instanceof KeyboardShortcut) {
1550 final KeyboardShortcut kb = (KeyboardShortcut)shortcut;
1551 if (kb.getSecondKeyStroke() == null) {
1552 return (KeyboardShortcut)shortcut;
1561 public void fireBeforeEditorTyping(char c, @NotNull DataContext dataContext) {
1562 myLastTimeEditorWasTypedIn = System.currentTimeMillis();
1563 for (AnActionListener listener : myActionListeners) {
1564 listener.beforeEditorTyping(c, dataContext);
1566 publisher().beforeEditorTyping(c, dataContext);
1570 public void fireAfterEditorTyping(char c, @NotNull DataContext dataContext) {
1571 for (AnActionListener listener : myActionListeners) {
1572 listener.afterEditorTyping(c, dataContext);
1574 publisher().afterEditorTyping(c, dataContext);
1578 public String getLastPreformedActionId() {
1579 return myLastPreformedActionId;
1583 public String getPrevPreformedActionId() {
1584 return myPrevPerformedActionId;
1587 public @NotNull Set<String> getActionIds() {
1588 synchronized (myLock) {
1589 return new HashSet<>(myId2Action.keySet());
1593 public void preloadActions(@NotNull ProgressIndicator indicator) {
1595 synchronized (myLock) {
1596 ids = new ArrayList<>(myId2Action.keySet());
1598 for (String id : ids) {
1599 indicator.checkCanceled();
1600 getActionImpl(id, false);
1601 // don't preload ActionGroup.getChildren() because that would un-stub child actions
1602 // and make it impossible to replace the corresponding actions later
1603 // (via unregisterAction+registerAction, as some app components do)
1609 public ActionCallback tryToExecute(@NotNull AnAction action,
1610 @NotNull InputEvent inputEvent,
1611 @Nullable Component contextComponent,
1612 @Nullable String place,
1614 assert ApplicationManager.getApplication().isDispatchThread();
1616 ActionCallback result = new ActionCallback();
1617 Runnable doRunnable = () -> tryToExecuteNow(action, inputEvent, contextComponent, place, result);
1622 //noinspection SSBasedInspection
1623 SwingUtilities.invokeLater(doRunnable);
1629 private void tryToExecuteNow(@NotNull AnAction action, @NotNull InputEvent inputEvent, @Nullable Component contextComponent, String place, ActionCallback result) {
1630 Presentation presentation = action.getTemplatePresentation().clone();
1631 IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown(() -> {
1632 ((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity(() -> {
1633 DataContext context = getContextBy(contextComponent);
1635 AnActionEvent event = new AnActionEvent(
1636 inputEvent, context,
1637 place != null ? place : ActionPlaces.UNKNOWN,
1639 inputEvent.getModifiersEx()
1642 ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, event, false);
1643 if (!event.getPresentation().isEnabled()) {
1644 result.setRejected();
1648 ActionUtil.lastUpdateAndCheckDumb(action, event, false);
1649 if (!event.getPresentation().isEnabled()) {
1650 result.setRejected();
1654 Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(context);
1655 if (component != null && !component.isShowing() && !ActionPlaces.TOUCHBAR_GENERAL.equals(place)) {
1656 result.setRejected();
1660 fireBeforeActionPerformed(action, context, event);
1662 UIUtil.addAwtListener(event1 -> {
1663 if (event1.getID() == WindowEvent.WINDOW_OPENED || event1.getID() == WindowEvent.WINDOW_ACTIVATED) {
1664 if (!result.isProcessed()) {
1665 final WindowEvent we = (WindowEvent)event1;
1666 IdeFocusManager.findInstanceByComponent(we.getWindow()).doWhenFocusSettlesDown(result.createSetDoneRunnable(),
1667 ModalityState.defaultModalityState());
1670 }, AWTEvent.WINDOW_EVENT_MASK, result);
1672 ActionUtil.performActionDumbAware(action, event);
1674 queueActionPerformedEvent(action, context, event);
1676 }, ModalityState.defaultModalityState());
1680 public @NotNull List<EditorActionHandlerBean> getRegisteredHandlers(@NotNull EditorAction editorAction) {
1681 List<EditorActionHandlerBean> result = new ArrayList<>();
1682 String id = getId(editorAction);
1684 List<EditorActionHandlerBean> extensions = EDITOR_ACTION_HANDLER_EP.getExtensionList();
1685 for (int i = extensions.size() - 1; i >= 0; i--) {
1686 EditorActionHandlerBean handlerBean = extensions.get(i);
1687 if (handlerBean.action.equals(id)) {
1688 result.add(handlerBean);
1695 private void updateAllHandlers() {
1696 synchronized (myLock) {
1697 myAction2Id.keySet().forEach(ActionManagerImpl::updateHandlers);
1701 private static void updateHandlers(Object action) {
1702 if (action instanceof EditorAction) {
1703 ((EditorAction)action).clearDynamicHandlersCache();
1707 private final class MyTimer extends Timer implements ActionListener {
1708 private final List<TimerListener> myTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1709 private final List<TimerListener> myTransparentTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1710 private int myLastTimePerformed;
1713 super(TIMER_DELAY, null);
1714 addActionListener(this);
1716 final MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
1717 connection.subscribe(ApplicationActivationListener.TOPIC, new ApplicationActivationListener() {
1719 public void applicationActivated(@NotNull IdeFrame ideFrame) {
1720 setDelay(TIMER_DELAY);
1725 public void applicationDeactivated(@NotNull IdeFrame ideFrame) {
1726 setDelay(DEACTIVATED_TIMER_DELAY);
1732 public String toString() {
1733 return "Action manager timer";
1736 void addTimerListener(@NotNull TimerListener listener, boolean transparent) {
1737 (transparent ? myTransparentTimerListeners : myTimerListeners).add(listener);
1740 void removeTimerListener(@NotNull TimerListener listener, boolean transparent) {
1741 (transparent ? myTransparentTimerListeners : myTimerListeners).remove(listener);
1745 public void actionPerformed(ActionEvent e) {
1746 if (myLastTimeEditorWasTypedIn + UPDATE_DELAY_AFTER_TYPING > System.currentTimeMillis()) {
1750 final int lastEventCount = myLastTimePerformed;
1751 myLastTimePerformed = ActivityTracker.getInstance().getCount();
1753 if (myLastTimePerformed == lastEventCount && !Registry.is("actionSystem.always.update.toolbar.actions")) {
1757 boolean transparentOnly = myLastTimePerformed == lastEventCount;
1760 myTransparentOnlyUpdate = transparentOnly;
1761 Set<TimerListener> notified = new HashSet<>();
1762 notifyListeners(myTransparentTimerListeners, notified);
1764 if (transparentOnly) {
1768 notifyListeners(myTimerListeners, notified);
1771 myTransparentOnlyUpdate = false;
1775 private void notifyListeners(final List<? extends TimerListener> timerListeners, final Set<? super TimerListener> notified) {
1776 for (TimerListener listener : timerListeners) {
1777 if (notified.add(listener)) {
1778 runListenerAction(listener);
1783 private void runListenerAction(@NotNull TimerListener listener) {
1784 ModalityState modalityState = listener.getModalityState();
1785 if (modalityState == null) return;
1786 LOG.debug("notify ", listener);
1787 if (!ModalityState.current().dominates(modalityState)) {
1791 catch (ProcessCanceledException ex) {
1794 catch (Throwable e) {