/* * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.actionSystem.impl; import com.intellij.AbstractBundle; import com.intellij.CommonBundle; import com.intellij.diagnostic.PluginException; import com.intellij.ide.ActivityTracker; import com.intellij.ide.DataManager; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.PluginManager; import com.intellij.ide.plugins.PluginManagerCore; import com.intellij.idea.ActionsBundle; import com.intellij.idea.IdeaLogger; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.actionSystem.ex.AnActionListener; import com.intellij.openapi.application.*; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.application.impl.LaterInvocator; import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.keymap.ex.KeymapManagerEx; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeFrame; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.ui.UIUtil; import consulo.extensions.ListOfElementsEP; import gnu.trove.THashMap; import gnu.trove.THashSet; import gnu.trove.TObjectIntHashMap; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.picocontainer.defaults.ConstructorInjectionComponentAdapter; import javax.swing.*; import javax.swing.Timer; import java.awt.*; import java.awt.event.*; import java.lang.reflect.Constructor; import java.util.*; import java.util.List; public final class ActionManagerImpl extends ActionManagerEx implements ApplicationComponent { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.actionSystem.impl.ActionManagerImpl"); private static final int DEACTIVATED_TIMER_DELAY = 5000; private static final int TIMER_DELAY = 500; private static final int UPDATE_DELAY_AFTER_TYPING = 500; private final Object myLock = new Object(); private final Map<String, AnAction> myId2Action = new THashMap<>(); private final Map<PluginId, THashSet<String>> myPlugin2Id = new THashMap<>(); private final TObjectIntHashMap<String> myId2Index = new TObjectIntHashMap<>(); private final Map<Object, String> myAction2Id = new THashMap<>(); private final List<String> myNotRegisteredInternalActionIds = new ArrayList<>(); private MyTimer myTimer; private int myRegisteredActionsCount; private final List<AnActionListener> myActionListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private String myLastPreformedActionId; private final KeymapManager myKeymapManager; private final DataManager myDataManager; private String myPrevPerformedActionId; private long myLastTimeEditorWasTypedIn = 0; @NonNls public static final String ACTION_ELEMENT_NAME = "action"; @NonNls public static final String GROUP_ELEMENT_NAME = "group"; @NonNls public static final String ACTIONS_ELEMENT_NAME = "actions"; @NonNls public static final String CLASS_ATTR_NAME = "class"; @NonNls public static final String ID_ATTR_NAME = "id"; @NonNls public static final String INTERNAL_ATTR_NAME = "internal"; @NonNls public static final String ICON_ATTR_NAME = "icon"; @NonNls public static final String REQUIRE_MODULE_EXTENSIONS = "require-module-extensions"; @NonNls public static final String CAN_USE_PROJECT_AS_DEFAULT = "can-use-project-as-default"; @NonNls public static final String ADD_TO_GROUP_ELEMENT_NAME = "add-to-group"; @NonNls public static final String SHORTCUT_ELEMENT_NAME = "keyboard-shortcut"; @NonNls public static final String MOUSE_SHORTCUT_ELEMENT_NAME = "mouse-shortcut"; @NonNls public static final String DESCRIPTION = "description"; @NonNls public static final String TEXT_ATTR_NAME = "text"; @NonNls public static final String POPUP_ATTR_NAME = "popup"; @NonNls public static final String SEPARATOR_ELEMENT_NAME = "separator"; @NonNls public static final String REFERENCE_ELEMENT_NAME = "reference"; @NonNls public static final String GROUPID_ATTR_NAME = "group-id"; @NonNls public static final String ANCHOR_ELEMENT_NAME = "anchor"; @NonNls public static final String FIRST = "first"; @NonNls public static final String LAST = "last"; @NonNls public static final String BEFORE = "before"; @NonNls public static final String AFTER = "after"; @NonNls public static final String SECONDARY = "secondary"; @NonNls public static final String RELATIVE_TO_ACTION_ATTR_NAME = "relative-to-action"; @NonNls public static final String FIRST_KEYSTROKE_ATTR_NAME = "first-keystroke"; @NonNls public static final String SECOND_KEYSTROKE_ATTR_NAME = "second-keystroke"; @NonNls public static final String REMOVE_SHORTCUT_ATTR_NAME = "remove"; @NonNls public static final String REPLACE_SHORTCUT_ATTR_NAME = "replace-all"; @NonNls public static final String KEYMAP_ATTR_NAME = "keymap"; @NonNls public static final String KEYSTROKE_ATTR_NAME = "keystroke"; @NonNls public static final String REF_ATTR_NAME = "ref"; @NonNls public static final String ACTIONS_BUNDLE = "messages.ActionsBundle"; @NonNls public static final String USE_SHORTCUT_OF_ATTR_NAME = "use-shortcut-of"; private final List<ActionPopupMenuImpl> myPopups = new ArrayList<>(); private final Map<AnAction, DataContext> myQueuedNotifications = new LinkedHashMap<>(); private final Map<AnAction, AnActionEvent> myQueuedNotificationsEvents = new LinkedHashMap<>(); private boolean myTransparentOnlyUpdate; ActionManagerImpl(KeymapManager keymapManager, DataManager dataManager) { myKeymapManager = keymapManager; myDataManager = dataManager; registerPluginActions(); } @Override public void initComponent() { } @Override public void disposeComponent() { if (myTimer != null) { myTimer.stop(); myTimer = null; } } @Override public void addTimerListener(int delay, final TimerListener listener) { _addTimerListener(listener, false); } @Override public void removeTimerListener(TimerListener listener) { _removeTimerListener(listener, false); } @Override public void addTransparentTimerListener(int delay, TimerListener listener) { _addTimerListener(listener, true); } @Override public void removeTransparentTimerListener(TimerListener listener) { _removeTimerListener(listener, true); } private void _addTimerListener(final TimerListener listener, boolean transparent) { if (ApplicationManager.getApplication().isUnitTestMode()) return; if (myTimer == null) { myTimer = new MyTimer(); myTimer.start(); } myTimer.addTimerListener(listener, transparent); } private void _removeTimerListener(TimerListener listener, boolean transparent) { if (ApplicationManager.getApplication().isUnitTestMode()) return; LOG.assertTrue(myTimer != null); myTimer.removeTimerListener(listener, transparent); } public ActionPopupMenu createActionPopupMenu(String place, @NotNull ActionGroup group, @Nullable PresentationFactory presentationFactory) { return new ActionPopupMenuImpl(place, group, this, presentationFactory); } @Override public ActionPopupMenu createActionPopupMenu(String place, @NotNull ActionGroup group) { return new ActionPopupMenuImpl(place, group, this, null); } @Override public ActionToolbar createActionToolbar(final String place, final ActionGroup group, final boolean horizontal) { return createActionToolbar(place, group, horizontal, false); } @Override public ActionToolbar createActionToolbar(final String place, final ActionGroup group, final boolean horizontal, final boolean decorateButtons) { return new ActionToolbarImpl(place, group, horizontal, decorateButtons, myDataManager, this, (KeymapManagerEx)myKeymapManager); } private void registerPluginActions() { final IdeaPluginDescriptor[] plugins = PluginManagerCore.getPlugins(); for (IdeaPluginDescriptor plugin : plugins) { if (PluginManagerCore.shouldSkipPlugin(plugin)) continue; final List<Element> elementList = plugin.getActionsDescriptionElements(); if (elementList != null) { for (Element e : elementList) { processActionsChildElement(plugin.getPluginClassLoader(), plugin.getPluginId(), e); } } } } @Override public AnAction getAction(@NotNull String id) { return getActionImpl(id, false); } private AnAction getActionImpl(String id, boolean canReturnStub) { AnAction action; synchronized (myLock) { action = myId2Action.get(id); if (canReturnStub || !(action instanceof ActionStub)) { return action; } } AnAction converted = convertStub((ActionStub)action); if (converted == null) return null; synchronized (myLock) { action = myId2Action.get(id); if (action instanceof ActionStub) { action = replaceStub((ActionStub)action, converted); } return action; } } /** * Converts action's stub to normal action. */ AnAction convertStub(ActionStub stub) { Object obj; String className = stub.getClassName(); try { Constructor<?> constructor = Class.forName(className, true, stub.getLoader()).getDeclaredConstructor(); constructor.setAccessible(true); obj = constructor.newInstance(); } catch (ClassNotFoundException e) { PluginId pluginId = stub.getPluginId(); if (pluginId != null) { throw new PluginException("class with name \"" + className + "\" not found", e, pluginId); } else { throw new IllegalStateException("class with name \"" + className + "\" not found"); } } catch (UnsupportedClassVersionError e) { PluginId pluginId = stub.getPluginId(); if (pluginId != null) { throw new PluginException(e, pluginId); } else { throw new IllegalStateException(e); } } catch (Exception e) { PluginId pluginId = stub.getPluginId(); if (pluginId != null) { throw new PluginException("cannot create class \"" + className + "\"", e, pluginId); } else { throw new IllegalStateException("cannot create class \"" + className + "\"", e); } } if (!(obj instanceof AnAction)) { throw new IllegalStateException("class with name \"" + className + "\" should be instance of " + AnAction.class.getName()); } AnAction anAction = (AnAction)obj; stub.initAction(anAction); if (StringUtil.isNotEmpty(stub.getText())) { anAction.getTemplatePresentation().setText(stub.getText()); } String iconPath = stub.getIconPath(); if (iconPath != null) { setIconFromClass(anAction.getClass(), anAction.getClass().getClassLoader(), iconPath, stub.getClassName(), anAction.getTemplatePresentation(), stub.getPluginId()); } return anAction; } @NotNull private AnAction replaceStub(@NotNull ActionStub stub, AnAction anAction) { LOG.assertTrue(myAction2Id.containsKey(stub)); myAction2Id.remove(stub); LOG.assertTrue(myId2Action.containsKey(stub.getId())); AnAction action = myId2Action.remove(stub.getId()); LOG.assertTrue(action != null); LOG.assertTrue(action.equals(stub)); myAction2Id.put(anAction, stub.getId()); return addToMap(stub.getId(), anAction); } private AnAction addToMap(String actionId, AnAction action) { myId2Action.put(actionId, action); return action; } @Override public String getId(@NotNull AnAction action) { LOG.assertTrue(!(action instanceof ActionStub)); synchronized (myLock) { return myAction2Id.get(action); } } @Override public String[] getActionIds(@NotNull String idPrefix) { synchronized (myLock) { ArrayList<String> idList = new ArrayList<>(); for (String id : myId2Action.keySet()) { if (id.startsWith(idPrefix)) { idList.add(id); } } return ArrayUtil.toStringArray(idList); } } @Override public boolean isGroup(@NotNull String actionId) { return getActionImpl(actionId, true) instanceof ActionGroup; } @Override public JComponent createButtonToolbar(final String actionPlace, final ActionGroup messageActionGroup) { return new ButtonToolbarImpl(actionPlace, messageActionGroup, myDataManager, this); } @Override public AnAction getActionOrStub(String id) { return getActionImpl(id, true); } /** * @return instance of ActionGroup or ActionStub. The method never returns real subclasses * of <code>AnAction</code>. */ @Nullable private AnAction processActionElement(Element element, final ClassLoader loader, PluginId pluginId) { final IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); ResourceBundle bundle = getActionsResourceBundle(loader, plugin); if (!ACTION_ELEMENT_NAME.equals(element.getName())) { reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\""); return null; } String className = element.getAttributeValue(CLASS_ATTR_NAME); if (className == null || className.length() == 0) { reportActionError(pluginId, "action element should have specified \"class\" attribute"); return null; } // read ID and register loaded action String id = element.getAttributeValue(ID_ATTR_NAME); if (id == null || id.length() == 0) { id = StringUtil.getShortName(className); } if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) { myNotRegisteredInternalActionIds.add(id); return null; } String text = loadTextValueForElement(element, bundle, id, ACTION_ELEMENT_NAME, TEXT_ATTR_NAME); String iconPath = element.getAttributeValue(ICON_ATTR_NAME); if (text == null) { @NonNls String message = "'text' attribute is mandatory (action ID=" + id + ";" + (plugin == null ? "" : " plugin path: " + plugin.getPath()) + ")"; reportActionError(pluginId, message); return null; } ActionStub stub = new ActionStub(className, id, text, loader, pluginId, iconPath); Presentation presentation = stub.getTemplatePresentation(); presentation.setText(text); // description presentation.setDescription(loadTextValueForElement(element, bundle, id, ACTION_ELEMENT_NAME, DESCRIPTION)); processModuleExtensionOptions(element, stub); // process all links and key bindings if any for (final Object o : element.getChildren()) { Element e = (Element)o; if (ADD_TO_GROUP_ELEMENT_NAME.equals(e.getName())) { processAddToGroupNode(stub, e, pluginId, isSecondary(e)); } else if (SHORTCUT_ELEMENT_NAME.equals(e.getName())) { processKeyboardShortcutNode(e, id, pluginId); } else if (MOUSE_SHORTCUT_ELEMENT_NAME.equals(e.getName())) { processMouseShortcutNode(e, id, pluginId); } else { reportActionError(pluginId, "unexpected name of element \"" + e.getName() + "\""); return null; } } if (element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME) != null) { ((KeymapManagerEx)myKeymapManager).bindShortcuts(element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME), id); } // register action registerAction(id, stub, pluginId); return stub; } private static void processModuleExtensionOptions(Element element, AnAction action) { String canUseProjectAsDefaultText = element.getAttributeValue(CAN_USE_PROJECT_AS_DEFAULT); boolean canUseProjectAsDefault = !StringUtil.isEmpty(canUseProjectAsDefaultText) && Boolean.parseBoolean(canUseProjectAsDefaultText); action.setCanUseProjectAsDefault(canUseProjectAsDefault); String requestModuleExtensionsValue = element.getAttributeValue(REQUIRE_MODULE_EXTENSIONS); action.setModuleExtensionIds(ListOfElementsEP.getValuesOfVariableIfFound(requestModuleExtensionsValue)); } @Nullable private static ResourceBundle getActionsResourceBundle(ClassLoader loader, IdeaPluginDescriptor plugin) { @NonNls final String resBundleName = plugin != null && !plugin.getPluginId().equals(PluginManagerCore.CORE_PLUGIN) ? plugin.getResourceBundleBaseName() : ACTIONS_BUNDLE; ResourceBundle bundle = null; if (resBundleName != null) { bundle = AbstractBundle.getResourceBundle(resBundleName, loader); } return bundle; } private static boolean isSecondary(Element element) { return "true".equalsIgnoreCase(element.getAttributeValue(SECONDARY)); } private static void setIcon(@Nullable final String iconPath, final String className, final ClassLoader loader, final Presentation presentation, final PluginId pluginId) { if (iconPath == null) return; try { final Class actionClass = Class.forName(className, true, loader); setIconFromClass(actionClass, loader, iconPath, className, presentation, pluginId); } catch (ClassNotFoundException e) { LOG.error(e); reportActionError(pluginId, "class with name \"" + className + "\" not found"); } catch (NoClassDefFoundError e) { LOG.error(e); reportActionError(pluginId, "class with name \"" + className + "\" not found"); } } private static void setIconFromClass(@NotNull final Class actionClass, @NotNull final ClassLoader classLoader, @NotNull final String iconPath, final String className, final Presentation presentation, final PluginId pluginId) { final IconLoader.LazyIcon lazyIcon = new IconLoader.LazyIcon() { @Override protected Icon compute() { //try to find icon in idea class path Icon icon = IconLoader.findIcon(iconPath, actionClass, true); if (icon == null) { icon = IconLoader.findIcon(iconPath, classLoader); } if (icon == null) { reportActionError(pluginId, "Icon cannot be found in '" + iconPath + "', action class='" + className + "'"); } return icon; } @Override public String toString() { return "LazyIcon@ActionManagerImpl (path: " + iconPath + ", action class: " + actionClass + ")"; } }; if (!Registry.is("ide.lazyIconLoading")) { lazyIcon.load(); } presentation.setIcon(lazyIcon); } private static String loadTextValueForElement(final Element element, final ResourceBundle bundle, final String id, String elementType, String type) { final String value = element.getAttributeValue(type); String key = elementType + "." + id + "." + type; String text = CommonBundle.messageOrDefault(bundle, key, value == null ? "" : value); return getDefaultInInternalOrNull(key, text); } @NotNull private static String getDefaultInInternalOrNull(@NotNull String key, @NotNull String text) { if (!StringUtil.isEmpty(text)) { return text; } text = ActionsBundle.message(key); if (text.isEmpty()) { return text; } if (text.charAt(0) == '!' && text.charAt(text.length() - 1) == '!') { if (ApplicationManager.getApplication().isInternal()) { return text; } else { return ""; } } else { return text; } } private AnAction processGroupElement(Element element, final ClassLoader loader, PluginId pluginId) { final IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); ResourceBundle bundle = getActionsResourceBundle(loader, plugin); if (!GROUP_ELEMENT_NAME.equals(element.getName())) { reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\""); return null; } String className = element.getAttributeValue(CLASS_ATTR_NAME); if (className == null) { // use default group if class isn't specified className = DefaultActionGroup.class.getName(); } try { Class aClass = Class.forName(className, true, loader); Object obj = new ConstructorInjectionComponentAdapter(className, aClass).getComponentInstance(ApplicationManager.getApplication().getPicoContainer()); if (!(obj instanceof ActionGroup)) { reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + ActionGroup.class.getName()); return null; } if (element.getChildren().size() != element.getChildren(ADD_TO_GROUP_ELEMENT_NAME).size()) { // if (!(obj instanceof DefaultActionGroup)) { reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + DefaultActionGroup.class.getName() + " because there are children specified"); return null; } } ActionGroup group = (ActionGroup)obj; // read ID and register loaded group String id = element.getAttributeValue(ID_ATTR_NAME); if (id != null && id.length() == 0) { reportActionError(pluginId, "ID of the group cannot be an empty string"); return null; } if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) { myNotRegisteredInternalActionIds.add(id); return null; } if (id != null) { registerAction(id, group); } Presentation presentation = group.getTemplatePresentation(); // text String text = loadTextValueForElement(element, bundle, id, GROUP_ELEMENT_NAME, TEXT_ATTR_NAME); // don't override value which was set in API with empty value from xml descriptor if (!StringUtil.isEmpty(text) || presentation.getText() == null) { presentation.setText(text); } // description String description = loadTextValueForElement(element, bundle, id, GROUP_ELEMENT_NAME, DESCRIPTION); // don't override value which was set in API with empty value from xml descriptor if (!StringUtil.isEmpty(description) || presentation.getDescription() == null) { presentation.setDescription(description); } processModuleExtensionOptions(element, group); // icon setIcon(element.getAttributeValue(ICON_ATTR_NAME), className, loader, presentation, pluginId); // popup String popup = element.getAttributeValue(POPUP_ATTR_NAME); if (popup != null) { group.setPopup(Boolean.valueOf(popup).booleanValue()); } // process all group's children. There are other groups, actions, references and links for (final Element o : element.getChildren()) { String name = o.getName(); if (ACTION_ELEMENT_NAME.equals(name)) { AnAction action = processActionElement(o, loader, pluginId); if (action != null) { assertActionIsGroupOrStub(action); ((DefaultActionGroup)group).addAction(action, Constraints.LAST, this).setAsSecondary(isSecondary(o)); } } else if (SEPARATOR_ELEMENT_NAME.equals(name)) { processSeparatorNode((DefaultActionGroup)group, o, pluginId); } else if (GROUP_ELEMENT_NAME.equals(name)) { AnAction action = processGroupElement(o, loader, pluginId); if (action != null) { ((DefaultActionGroup)group).add(action, this); } } else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) { processAddToGroupNode(group, o, pluginId, isSecondary(o)); } else if (REFERENCE_ELEMENT_NAME.equals(name)) { AnAction action = processReferenceElement(o, pluginId); if (action != null) { ((DefaultActionGroup)group).addAction(action, Constraints.LAST, this).setAsSecondary(isSecondary(o)); } } else { reportActionError(pluginId, "unexpected name of element \"" + name + "\n"); return null; } } return group; } catch (ClassNotFoundException e) { reportActionError(pluginId, "class with name \"" + className + "\" not found"); return null; } catch (NoClassDefFoundError e) { reportActionError(pluginId, "class with name \"" + e.getMessage() + "\" not found"); return null; } catch (UnsupportedClassVersionError e) { reportActionError(pluginId, "unsupported class version for " + className); return null; } catch (Exception e) { final String message = "cannot create class \"" + className + "\""; if (pluginId == null) { LOG.error(message, e); } else { LOG.error(new PluginException(message, e, pluginId)); } return null; } } private void processReferenceNode(final Element element, final PluginId pluginId) { final AnAction action = processReferenceElement(element, pluginId); for (final Object o : element.getChildren()) { Element child = (Element)o; if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) { processAddToGroupNode(action, child, pluginId, isSecondary(child)); } } } /** * \ * * @param element description of link * @param pluginId * @param secondary */ private void processAddToGroupNode(AnAction action, Element element, final PluginId pluginId, boolean secondary) { // Real subclasses of AnAction should not be here if (!(action instanceof AnSeparator)) { assertActionIsGroupOrStub(action); } String actionName = action instanceof ActionStub ? ((ActionStub)action).getClassName() : action.getClass().getName(); if (!ADD_TO_GROUP_ELEMENT_NAME.equals(element.getName())) { reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\""); return; } // parent group final AnAction parentGroup = getParentGroup(element.getAttributeValue(GROUPID_ATTR_NAME), actionName, pluginId); if (parentGroup == null) { return; } // anchor attribute final Anchor anchor = parseAnchor(element.getAttributeValue(ANCHOR_ELEMENT_NAME), actionName, pluginId); if (anchor == null) { return; } final String relativeToActionId = element.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME); if (!checkRelativeToAction(relativeToActionId, anchor, actionName, pluginId)) { return; } final DefaultActionGroup group = (DefaultActionGroup)parentGroup; group.addAction(action, new Constraints(anchor, relativeToActionId), this).setAsSecondary(secondary); } public static boolean checkRelativeToAction(final String relativeToActionId, @NotNull final Anchor anchor, @NotNull final String actionName, @Nullable final PluginId pluginId) { if ((Anchor.BEFORE == anchor || Anchor.AFTER == anchor) && relativeToActionId == null) { reportActionError(pluginId, actionName + ": \"relative-to-action\" cannot be null if anchor is \"after\" or \"before\""); return false; } return true; } @Nullable public static Anchor parseAnchor(final String anchorStr, @Nullable final String actionName, @Nullable final PluginId pluginId) { if (anchorStr == null) { return Anchor.LAST; } if (FIRST.equalsIgnoreCase(anchorStr)) { return Anchor.FIRST; } else if (LAST.equalsIgnoreCase(anchorStr)) { return Anchor.LAST; } else if (BEFORE.equalsIgnoreCase(anchorStr)) { return Anchor.BEFORE; } else if (AFTER.equalsIgnoreCase(anchorStr)) { return Anchor.AFTER; } else { reportActionError(pluginId, actionName + ": anchor should be one of the following constants: \"first\", \"last\", \"before\" or \"after\""); return null; } } @Nullable public AnAction getParentGroup(final String groupId, @Nullable final String actionName, @Nullable final PluginId pluginId) { if (groupId == null || groupId.length() == 0) { reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined"); return null; } AnAction parentGroup = getActionImpl(groupId, true); if (parentGroup == null) { reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" isn't registered"); return null; } if (!(parentGroup instanceof DefaultActionGroup)) { reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" should be instance of " + DefaultActionGroup.class.getName() + " but was " + parentGroup.getClass()); return null; } return parentGroup; } /** * @param parentGroup group which is the parent of the separator. It can be <code>null</code> in that * case separator will be added to group described in the <add-to-group ....> subelement. * @param element XML element which represent separator. */ private void processSeparatorNode(@Nullable DefaultActionGroup parentGroup, Element element, PluginId pluginId) { if (!SEPARATOR_ELEMENT_NAME.equals(element.getName())) { reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\""); return; } AnSeparator separator = AnSeparator.getInstance(); if (parentGroup != null) { parentGroup.add(separator, this); } // try to find inner <add-to-parent...> tag for (final Object o : element.getChildren()) { Element child = (Element)o; if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) { processAddToGroupNode(separator, child, pluginId, isSecondary(child)); } } } private void processKeyboardShortcutNode(Element element, String actionId, PluginId pluginId) { String firstStrokeString = element.getAttributeValue(FIRST_KEYSTROKE_ATTR_NAME); if (firstStrokeString == null) { reportActionError(pluginId, "\"first-keystroke\" attribute must be specified for action with id=" + actionId); return; } KeyStroke firstKeyStroke = getKeyStroke(firstStrokeString); if (firstKeyStroke == null) { reportActionError(pluginId, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId); return; } KeyStroke secondKeyStroke = null; String secondStrokeString = element.getAttributeValue(SECOND_KEYSTROKE_ATTR_NAME); if (secondStrokeString != null) { secondKeyStroke = getKeyStroke(secondStrokeString); if (secondKeyStroke == null) { reportActionError(pluginId, "\"second-keystroke\" attribute has invalid value for action with id=" + actionId); return; } } String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME); if (keymapName == null || keymapName.trim().length() == 0) { reportActionError(pluginId, "attribute \"keymap\" should be defined"); return; } Keymap keymap = myKeymapManager.getKeymap(keymapName); if (keymap == null) { reportActionError(pluginId, "keymap \"" + keymapName + "\" not found"); return; } final String removeOption = element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME); final KeyboardShortcut shortcut = new KeyboardShortcut(firstKeyStroke, secondKeyStroke); final String replaceOption = element.getAttributeValue(REPLACE_SHORTCUT_ATTR_NAME); if (Boolean.valueOf(removeOption)) { keymap.removeShortcut(actionId, shortcut); } if (Boolean.valueOf(replaceOption)) { keymap.removeAllActionShortcuts(actionId); } if (!Boolean.valueOf(removeOption)) { keymap.addShortcut(actionId, shortcut); } } private static void processMouseShortcutNode(Element element, String actionId, PluginId pluginId) { String keystrokeString = element.getAttributeValue(KEYSTROKE_ATTR_NAME); if (keystrokeString == null || keystrokeString.trim().length() == 0) { reportActionError(pluginId, "\"keystroke\" attribute must be specified for action with id=" + actionId); return; } MouseShortcut shortcut; try { shortcut = KeymapUtil.parseMouseShortcut(keystrokeString); } catch (Exception ex) { reportActionError(pluginId, "\"keystroke\" attribute has invalid value for action with id=" + actionId); return; } String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME); if (keymapName == null || keymapName.length() == 0) { reportActionError(pluginId, "attribute \"keymap\" should be defined"); return; } Keymap keymap = KeymapManager.getInstance().getKeymap(keymapName); if (keymap == null) { reportActionError(pluginId, "keymap \"" + keymapName + "\" not found"); return; } final String removeOption = element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME); if (Boolean.valueOf(removeOption)) { keymap.removeShortcut(actionId, shortcut); } else { keymap.addShortcut(actionId, shortcut); } } @Nullable private AnAction processReferenceElement(Element element, PluginId pluginId) { if (!REFERENCE_ELEMENT_NAME.equals(element.getName())) { reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\""); return null; } String ref = element.getAttributeValue(REF_ATTR_NAME); if (ref == null) { // support old style references by id ref = element.getAttributeValue(ID_ATTR_NAME); } if (ref == null || ref.length() == 0) { reportActionError(pluginId, "ID of reference element should be defined"); return null; } AnAction action = getActionImpl(ref, true); if (action == null) { if (!myNotRegisteredInternalActionIds.contains(ref)) { reportActionError(pluginId, "action specified by reference isn't registered (ID=" + ref + ")"); } return null; } assertActionIsGroupOrStub(action); return action; } private void processActionsChildElement(final ClassLoader loader, final PluginId pluginId, final Element child) { String name = child.getName(); if (ACTION_ELEMENT_NAME.equals(name)) { AnAction action = processActionElement(child, loader, pluginId); if (action != null) { assertActionIsGroupOrStub(action); } } else if (GROUP_ELEMENT_NAME.equals(name)) { processGroupElement(child, loader, pluginId); } else if (SEPARATOR_ELEMENT_NAME.equals(name)) { processSeparatorNode(null, child, pluginId); } else if (REFERENCE_ELEMENT_NAME.equals(name)) { processReferenceNode(child, pluginId); } else { reportActionError(pluginId, "unexpected name of element \"" + name + "\n"); } } private static void assertActionIsGroupOrStub(final AnAction action) { if (!(action instanceof ActionGroup || action instanceof ActionStub)) { LOG.error("Action : " + action + "; class: " + action.getClass()); } } @Override public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) { synchronized (myLock) { if (myId2Action.containsKey(actionId)) { reportActionError(pluginId, "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action.toString() + "; Registered action is " + myId2Action.get(actionId) + getPluginInfo(pluginId)); return; } if (myAction2Id.containsKey(action)) { reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) + getPluginInfo(pluginId)); return; } myId2Action.put(actionId, action); myId2Index.put(actionId, myRegisteredActionsCount++); myAction2Id.put(action, actionId); if (pluginId != null && !(action instanceof ActionGroup)) { THashSet<String> pluginActionIds = myPlugin2Id.get(pluginId); if (pluginActionIds == null) { pluginActionIds = new THashSet<>(); myPlugin2Id.put(pluginId, pluginActionIds); } pluginActionIds.add(actionId); } action.registerCustomShortcutSet(new ProxyShortcutSet(actionId, myKeymapManager), null); } } private static void reportActionError(final PluginId pluginId, @NonNls final String message) { if (pluginId == null) { LOG.error(message); } else { LOG.error(new PluginException(message, null, pluginId)); } } @NonNls private static String getPluginInfo(@Nullable PluginId id) { if (id != null) { final IdeaPluginDescriptor plugin = PluginManager.getPlugin(id); if (plugin != null) { String name = plugin.getName(); if (name == null) { name = id.getIdString(); } return " Plugin: " + name; } } return ""; } @Override public void registerAction(@NotNull String actionId, @NotNull AnAction action) { registerAction(actionId, action, null); } @Override public void unregisterAction(@NotNull String actionId) { synchronized (myLock) { if (!myId2Action.containsKey(actionId)) { if (LOG.isDebugEnabled()) { LOG.debug("action with ID " + actionId + " wasn't registered"); return; } } AnAction oldValue = (AnAction)myId2Action.remove(actionId); myAction2Id.remove(oldValue); myId2Index.remove(actionId); for (PluginId pluginName : myPlugin2Id.keySet()) { final THashSet<String> pluginActions = myPlugin2Id.get(pluginName); if (pluginActions != null) { pluginActions.remove(actionId); } } } } @Override @NotNull public String getComponentName() { return "ActionManager"; } @Override public Comparator<String> getRegistrationOrderComparator() { return new Comparator<String>() { @Override public int compare(String id1, String id2) { return myId2Index.get(id1) - myId2Index.get(id2); } }; } @Override public String[] getPluginActions(PluginId pluginName) { if (myPlugin2Id.containsKey(pluginName)) { final THashSet<String> pluginActions = myPlugin2Id.get(pluginName); return ArrayUtil.toStringArray(pluginActions); } return ArrayUtil.EMPTY_STRING_ARRAY; } public void addActionPopup(final ActionPopupMenuImpl menu) { myPopups.add(menu); } public void removeActionPopup(final ActionPopupMenuImpl menu) { final boolean removed = myPopups.remove(menu); if (removed && myPopups.isEmpty()) { flushActionPerformed(); } } @Override public void queueActionPerformedEvent(final AnAction action, DataContext context, AnActionEvent event) { if (!myPopups.isEmpty()) { myQueuedNotifications.put(action, context); } else { fireAfterActionPerformed(action, context, event); } } @Override public boolean isActionPopupStackEmpty() { return myPopups.isEmpty(); } @Override public boolean isTransparentOnlyActionsUpdateNow() { return myTransparentOnlyUpdate; } private void flushActionPerformed() { final Set<AnAction> actions = myQueuedNotifications.keySet(); for (final AnAction eachAction : actions) { final DataContext eachContext = myQueuedNotifications.get(eachAction); fireAfterActionPerformed(eachAction, eachContext, myQueuedNotificationsEvents.get(eachAction)); } myQueuedNotifications.clear(); myQueuedNotificationsEvents.clear(); } @Override public void addAnActionListener(AnActionListener listener) { myActionListeners.add(listener); } @Override public void addAnActionListener(final AnActionListener listener, final Disposable parentDisposable) { addAnActionListener(listener); Disposer.register(parentDisposable, new Disposable() { @Override public void dispose() { removeAnActionListener(listener); } }); } @Override public void removeAnActionListener(AnActionListener listener) { myActionListeners.remove(listener); } @Override public void fireBeforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { if (action != null) { myPrevPerformedActionId = myLastPreformedActionId; myLastPreformedActionId = getId(action); //noinspection AssignmentToStaticFieldFromInstanceMethod IdeaLogger.ourLastActionId = myLastPreformedActionId; } for (AnActionListener listener : myActionListeners) { listener.beforeActionPerformed(action, dataContext, event); } } @Override public void fireAfterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { if (action != null) { myPrevPerformedActionId = myLastPreformedActionId; myLastPreformedActionId = getId(action); //noinspection AssignmentToStaticFieldFromInstanceMethod IdeaLogger.ourLastActionId = myLastPreformedActionId; } for (AnActionListener listener : myActionListeners) { try { listener.afterActionPerformed(action, dataContext, event); } catch (AbstractMethodError ignored) { } } } @Override public KeyboardShortcut getKeyboardShortcut(@NotNull String actionId) { AnAction action = ActionManager.getInstance().getAction(actionId); final ShortcutSet shortcutSet = action.getShortcutSet(); final Shortcut[] shortcuts = shortcutSet.getShortcuts(); for (final Shortcut shortcut : shortcuts) { // Shortcut can be MouseShortcut here. // For example IdeaVIM often assigns them if (shortcut instanceof KeyboardShortcut) { final KeyboardShortcut kb = (KeyboardShortcut)shortcut; if (kb.getSecondKeyStroke() == null) { return (KeyboardShortcut)shortcut; } } } return null; } @Override public void fireBeforeEditorTyping(char c, DataContext dataContext) { myLastTimeEditorWasTypedIn = System.currentTimeMillis(); for (AnActionListener listener : myActionListeners) { listener.beforeEditorTyping(c, dataContext); } } @Override public String getLastPreformedActionId() { return myLastPreformedActionId; } @Override public String getPrevPreformedActionId() { return myPrevPerformedActionId; } public Set<String> getActionIds() { synchronized (myLock) { return new HashSet<>(myId2Action.keySet()); } } public void preloadActions(ProgressIndicator indicator) { final Application application = ApplicationManager.getApplication(); for (String id : getActionIds()) { indicator.checkCanceled(); if (application.isDisposed()) return; final AnAction action = getAction(id); if (action instanceof PreloadableAction) { ((PreloadableAction)action).preload(); } // don't preload ActionGroup.getChildren() because that would unstub child actions // and make it impossible to replace the corresponding actions later // (via unregisterAction+registerAction, as some app components do) } } private class MyTimer extends Timer implements ActionListener { private final List<TimerListener> myTimerListeners = Collections.synchronizedList(new ArrayList<TimerListener>()); private final List<TimerListener> myTransparentTimerListeners = Collections.synchronizedList(new ArrayList<TimerListener>()); private int myLastTimePerformed; MyTimer() { super(TIMER_DELAY, null); addActionListener(this); setRepeats(true); final MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(); connection.subscribe(ApplicationActivationListener.TOPIC, new ApplicationActivationListener.Adapter() { @Override public void applicationActivated(IdeFrame ideFrame) { setDelay(TIMER_DELAY); restart(); } @Override public void applicationDeactivated(IdeFrame ideFrame) { setDelay(DEACTIVATED_TIMER_DELAY); } }); } @Override public String toString() { return "Action manager timer"; } public void addTimerListener(TimerListener listener, boolean transparent) { if (transparent) { myTransparentTimerListeners.add(listener); } else { myTimerListeners.add(listener); } } public void removeTimerListener(TimerListener listener, boolean transparent) { if (transparent) { myTransparentTimerListeners.remove(listener); } else { myTimerListeners.remove(listener); } } @Override public void actionPerformed(ActionEvent e) { if (myLastTimeEditorWasTypedIn + UPDATE_DELAY_AFTER_TYPING > System.currentTimeMillis()) { return; } if (IdeFocusManager.getInstance(null).isFocusBeingTransferred()) return; final int lastEventCount = myLastTimePerformed; myLastTimePerformed = ActivityTracker.getInstance().getCount(); boolean transparentOnly = myLastTimePerformed == lastEventCount; try { HashSet<TimerListener> notified = new HashSet<>(); myTransparentOnlyUpdate = transparentOnly; notifyListeners(myTransparentTimerListeners, notified); if (transparentOnly) { return; } notifyListeners(myTimerListeners, notified); } finally { myTransparentOnlyUpdate = false; } } private void notifyListeners(final List<TimerListener> timerListeners, final Set<TimerListener> notified) { final TimerListener[] listeners = timerListeners.toArray(new TimerListener[timerListeners.size()]); for (TimerListener listener : listeners) { if (timerListeners.contains(listener)) { if (!notified.contains(listener)) { notified.add(listener); runListenerAction(listener); } } } } private void runListenerAction(final TimerListener listener) { ModalityState modalityState = listener.getModalityState(); if (modalityState == null) return; if (!ModalityState.current().dominates(modalityState)) { try { listener.run(); } catch (ProcessCanceledException ex) { // ignore } catch (Throwable e) { LOG.error(e); } } } } @Override public ActionCallback tryToExecute(@NotNull final AnAction action, @NotNull final InputEvent inputEvent, @Nullable final Component contextComponent, @Nullable final String place, boolean now) { final Application app = ApplicationManager.getApplication(); assert app.isDispatchThread(); final ActionCallback result = new ActionCallback(); final Runnable doRunnable = new Runnable() { @Override public void run() { tryToExecuteNow(action, inputEvent, contextComponent, place, result); } }; if (now) { doRunnable.run(); } else { //noinspection SSBasedInspection SwingUtilities.invokeLater(doRunnable); } return result; } private void tryToExecuteNow(final AnAction action, final InputEvent inputEvent, final Component contextComponent, final String place, final ActionCallback result) { final Presentation presentation = action.getTemplatePresentation().clone(); IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)) .doWhenFocusSettlesDown(() -> ((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity(() -> { final DataContext context = getContextBy(contextComponent); AnActionEvent event = new AnActionEvent(inputEvent, context, place != null ? place : ActionPlaces.UNKNOWN, presentation, this, inputEvent.getModifiersEx()); ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, event, false); if (!event.getPresentation().isEnabled()) { result.setRejected(); return; } ActionUtil.lastUpdateAndCheckDumb(action, event, false); if (!event.getPresentation().isEnabled()) { result.setRejected(); return; } Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(context); if (component != null && !component.isShowing()) { result.setRejected(); return; } fireBeforeActionPerformed(action, context, event); UIUtil.addAwtListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent event) { if (event.getID() == WindowEvent.WINDOW_OPENED || event.getID() == WindowEvent.WINDOW_ACTIVATED) { if (!result.isProcessed()) { final WindowEvent we = (WindowEvent)event; IdeFocusManager.findInstanceByComponent(we.getWindow()).doWhenFocusSettlesDown(result.createSetDoneRunnable()); } } } }, AWTEvent.WINDOW_EVENT_MASK, result); ActionUtil.performActionDumbAware(action, event); result.setDone(); queueActionPerformedEvent(action, context, event); })); } private static DataContext getContextBy(Component contextComponent) { final DataManager dataManager = DataManager.getInstance(); return contextComponent != null ? dataManager.getDataContext(contextComponent) : dataManager.getDataContext(); } }