| /* |
| * Copyright 2000-2014 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.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.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.project.ProjectType; |
| import com.intellij.openapi.util.ActionCallback; |
| import com.intellij.openapi.util.Computable; |
| 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.ObjectUtils; |
| import com.intellij.util.ReflectionUtil; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.MultiMap; |
| import com.intellij.util.messages.MessageBusConnection; |
| import com.intellij.util.pico.ConstructorInjectionComponentAdapter; |
| import com.intellij.util.ui.UIUtil; |
| 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 javax.swing.*; |
| import javax.swing.Timer; |
| import java.awt.*; |
| import java.awt.event.*; |
| import java.util.*; |
| import java.util.List; |
| import java.util.concurrent.Future; |
| |
| public final class ActionManagerImpl extends ActionManagerEx implements ApplicationComponent { |
| @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 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 COMPACT_ATTR_NAME = "compact"; |
| @NonNls public static final String SEPARATOR_ELEMENT_NAME = "separator"; |
| @NonNls public static final String REFERENCE_ELEMENT_NAME = "reference"; |
| @NonNls public static final String ABBREVIATION_ELEMENT_NAME = "abbreviation"; |
| @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 VALUE_ATTR_NAME = "value"; |
| @NonNls public static final String ACTIONS_BUNDLE = "messages.ActionsBundle"; |
| @NonNls public static final String USE_SHORTCUT_OF_ATTR_NAME = "use-shortcut-of"; |
| @NonNls public static final String OVERRIDES_ATTR_NAME = "overrides"; |
| @NonNls public static final String KEEP_CONTENT_ATTR_NAME = "keep-content"; |
| @NonNls public static final String PROJECT_TYPE = "project-type"; |
| 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<String, AnAction>(); |
| private final Map<PluginId, THashSet<String>> myPlugin2Id = new THashMap<PluginId, THashSet<String>>(); |
| private final TObjectIntHashMap<String> myId2Index = new TObjectIntHashMap<String>(); |
| private final Map<Object,String> myAction2Id = new THashMap<Object, String>(); |
| private final MultiMap<String,String> myId2GroupId = new MultiMap<String, String>(); |
| private final List<String> myNotRegisteredInternalActionIds = new ArrayList<String>(); |
| private final List<AnActionListener> myActionListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final KeymapManager myKeymapManager; |
| private final DataManager myDataManager; |
| private final List<ActionPopupMenuImpl> myPopups = new ArrayList<ActionPopupMenuImpl>(); |
| private final Map<AnAction, DataContext> myQueuedNotifications = new LinkedHashMap<AnAction, DataContext>(); |
| private final Map<AnAction, AnActionEvent> myQueuedNotificationsEvents = new LinkedHashMap<AnAction, AnActionEvent>(); |
| private MyTimer myTimer; |
| private int myRegisteredActionsCount; |
| private String myLastPreformedActionId; |
| private String myPrevPerformedActionId; |
| private long myLastTimeEditorWasTypedIn = 0; |
| private Runnable myPreloadActionsRunnable; |
| private boolean myTransparentOnlyUpdate; |
| private int myActionsPreloaded = 0; |
| |
| ActionManagerImpl(KeymapManager keymapManager, DataManager dataManager) { |
| myKeymapManager = keymapManager; |
| myDataManager = dataManager; |
| |
| registerPluginActions(); |
| } |
| |
| static AnAction convertStub(ActionStub stub) { |
| Object obj; |
| String className = stub.getClassName(); |
| try { |
| Class<?> aClass = Class.forName(className, true, stub.getLoader()); |
| obj = ReflectionUtil.newInstance(aClass); |
| } |
| 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 + "' must be an instance of '" + AnAction.class.getName()+"'; got "+obj); |
| } |
| |
| AnAction anAction = (AnAction)obj; |
| stub.initAction(anAction); |
| if (StringUtil.isNotEmpty(stub.getText())) { |
| anAction.getTemplatePresentation().setText(stub.getText()); |
| } |
| String iconPath = stub.getIconPath(); |
| if (iconPath != null) { |
| Class<? extends AnAction> actionClass = anAction.getClass(); |
| setIconFromClass(actionClass, actionClass.getClassLoader(), iconPath, anAction.getTemplatePresentation(), stub.getPluginId()); |
| } |
| return anAction; |
| } |
| |
| private static void processAbbreviationNode(Element e, String id) { |
| final String abbr = e.getAttributeValue(VALUE_ATTR_NAME); |
| if (!StringUtil.isEmpty(abbr)) { |
| final AbbreviationManagerImpl abbreviationManager = ((AbbreviationManagerImpl)AbbreviationManager.getInstance()); |
| abbreviationManager.register(abbr, id, true); |
| } |
| } |
| |
| @Nullable |
| private static ResourceBundle getActionsResourceBundle(ClassLoader loader, IdeaPluginDescriptor plugin) { |
| @NonNls final String resBundleName = plugin != null && !"com.intellij".equals(plugin.getPluginId().getIdString()) |
| ? 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, |
| @NotNull String className, |
| @NotNull ClassLoader loader, |
| @NotNull Presentation presentation, |
| final PluginId pluginId) { |
| if (iconPath == null) return; |
| |
| try { |
| final Class actionClass = Class.forName(className, true, loader); |
| setIconFromClass(actionClass, loader, iconPath, 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, |
| @NotNull 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 '" + actionClass + "'"); |
| } |
| |
| 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 loadDescriptionForElement(final Element element, final ResourceBundle bundle, final String id, String elementType) { |
| final String value = element.getAttributeValue(DESCRIPTION); |
| if (bundle != null) { |
| @NonNls final String key = elementType + "." + id + ".description"; |
| return CommonBundle.messageOrDefault(bundle, key, value == null ? "" : value); |
| } else { |
| return value; |
| } |
| } |
| |
| private static String loadTextForElement(final Element element, final ResourceBundle bundle, final String id, String elementType) { |
| final String value = element.getAttributeValue(TEXT_ATTR_NAME); |
| return CommonBundle.messageOrDefault(bundle, elementType + "." + id + "." + TEXT_ATTR_NAME, value == null ? "" : value); |
| } |
| |
| 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; |
| } |
| } |
| |
| private static void processMouseShortcutNode(Element element, String actionId, PluginId pluginId) { |
| String keystrokeString = element.getAttributeValue(KEYSTROKE_ATTR_NAME); |
| if (keystrokeString == null || keystrokeString.trim().isEmpty()) { |
| 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.isEmpty()) { |
| 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); |
| } |
| } |
| |
| private static void assertActionIsGroupOrStub(final AnAction action) { |
| if (!(action instanceof ActionGroup || action instanceof ActionStub || action instanceof ChameleonAction)) { |
| LOG.error("Action : " + action + "; class: " + action.getClass()); |
| } |
| } |
| |
| private static void reportActionError(final PluginId pluginId, @NonNls @NotNull 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 ""; |
| } |
| |
| private static DataContext getContextBy(Component contextComponent) { |
| final DataManager dataManager = DataManager.getInstance(); |
| return contextComponent != null ? dataManager.getDataContext(contextComponent) : dataManager.getDataContext(); |
| } |
| |
| @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, @NotNull final ActionGroup group, final boolean horizontal) { |
| return createActionToolbar(place, group, horizontal, false); |
| } |
| |
| @Override |
| public ActionToolbar createActionToolbar(final String place, @NotNull 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, null); |
| } |
| |
| @Override |
| public AnAction getAction(@NonNls @NotNull String actionId, @Nullable ProjectType projectType) { |
| return getActionImpl(actionId, false, projectType); |
| } |
| |
| private AnAction getActionImpl(String id, boolean canReturnStub, ProjectType projectType) { |
| synchronized (myLock) { |
| AnAction action; |
| Object o = myId2Action.get(id); |
| if (o == null) { |
| return null; |
| } |
| if (o instanceof AnAction) { |
| action = (AnAction)o; |
| } |
| else { |
| //noinspection unchecked |
| action = ((Map<ProjectType, AnAction>)o).get(projectType); |
| } |
| if (!canReturnStub && action instanceof ActionStub) { |
| action = convert((ActionStub)action); |
| } |
| return action; |
| } |
| } |
| |
| /** |
| * Converts action's stub to normal action. |
| */ |
| @NotNull |
| private AnAction convert(@NotNull ActionStub stub) { |
| 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)); |
| |
| AnAction anAction = convertStub(stub); |
| myAction2Id.put(anAction, stub.getId()); |
| |
| return addToMap(stub.getId(), anAction, stub.getPluginId(), stub.getProjectType()); |
| } |
| |
| @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<String>(); |
| 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, null) instanceof ActionGroup; |
| } |
| |
| @Override |
| public JComponent createButtonToolbar(final String actionPlace, @NotNull final ActionGroup messageActionGroup) { |
| return new ButtonToolbarImpl(actionPlace, messageActionGroup, myDataManager, this); |
| } |
| |
| @Override |
| public AnAction getActionOrStub(String id) { |
| return getActionImpl(id, true, null); |
| } |
| |
| /** |
| * @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.isEmpty()) { |
| 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.isEmpty()) { |
| id = StringUtil.getShortName(className); |
| } |
| if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) { |
| myNotRegisteredInternalActionIds.add(id); |
| return null; |
| } |
| |
| String text = loadTextForElement(element, bundle, id, ACTION_ELEMENT_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; |
| } |
| |
| String projectType = element.getAttributeValue(PROJECT_TYPE); |
| ActionStub stub = new ActionStub(className, id, text, loader, pluginId, iconPath, projectType); |
| Presentation presentation = stub.getTemplatePresentation(); |
| presentation.setText(text); |
| |
| // description |
| |
| presentation.setDescription(loadDescriptionForElement(element, bundle, id, ACTION_ELEMENT_NAME)); |
| |
| // 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 if (ABBREVIATION_ELEMENT_NAME.equals(e.getName())) { |
| processAbbreviationNode(e, id); |
| } |
| 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); |
| } |
| |
| registerOrReplaceActionInner(element, id, stub, pluginId); |
| return stub; |
| } |
| |
| private void registerOrReplaceActionInner(@NotNull Element element, @NotNull String id, @NotNull AnAction action, @Nullable PluginId pluginId) { |
| synchronized (myLock) { |
| if (Boolean.valueOf(element.getAttributeValue(OVERRIDES_ATTR_NAME))) { |
| if (getActionOrStub(id) == null) { |
| throw new RuntimeException(element.getName() + " '" + id + "' doesn't override anything"); |
| } |
| AnAction prev = replaceAction(id, action, pluginId); |
| if (action instanceof DefaultActionGroup && prev instanceof DefaultActionGroup) { |
| if (Boolean.valueOf(element.getAttributeValue(KEEP_CONTENT_ATTR_NAME))) { |
| ((DefaultActionGroup)action).copyFromGroup((DefaultActionGroup)prev); |
| } |
| } |
| } |
| else { |
| registerAction(id, action, pluginId, element.getAttributeValue(PROJECT_TYPE)); |
| } |
| } |
| } |
| |
| 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 |
| if ("true".equals(element.getAttributeValue(COMPACT_ATTR_NAME))) { |
| className = DefaultCompactActionGroup.class.getName(); |
| } else { |
| className = DefaultActionGroup.class.getName(); |
| } |
| } |
| try { |
| ActionGroup group; |
| if (DefaultActionGroup.class.getName().equals(className)) { |
| group = new DefaultActionGroup(); |
| } else if (DefaultCompactActionGroup.class.getName().equals(className)) { |
| group = new DefaultCompactActionGroup(); |
| } else { |
| 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; |
| } |
| } |
| group = (ActionGroup)obj; |
| } |
| // read ID and register loaded group |
| String id = element.getAttributeValue(ID_ATTR_NAME); |
| if (id != null && id.isEmpty()) { |
| 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) { |
| registerOrReplaceActionInner(element, id, group, pluginId); |
| } |
| Presentation presentation = group.getTemplatePresentation(); |
| |
| // text |
| String text = loadTextForElement(element, bundle, id, GROUP_ELEMENT_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 = loadDescriptionForElement(element, bundle, id, GROUP_ELEMENT_NAME); |
| // 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); |
| } |
| |
| // 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 Object o : element.getChildren()) { |
| Element child = (Element)o; |
| String name = child.getName(); |
| if (ACTION_ELEMENT_NAME.equals(name)) { |
| AnAction action = processActionElement(child, loader, pluginId); |
| if (action != null) { |
| assertActionIsGroupOrStub(action); |
| addToGroupInner(group, action, Constraints.LAST, isSecondary(child)); |
| } |
| } |
| else if (SEPARATOR_ELEMENT_NAME.equals(name)) { |
| processSeparatorNode((DefaultActionGroup)group, child, pluginId); |
| } |
| else if (GROUP_ELEMENT_NAME.equals(name)) { |
| AnAction action = processGroupElement(child, loader, pluginId); |
| if (action != null) { |
| addToGroupInner(group, action, Constraints.LAST, false); |
| } |
| } |
| else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) { |
| processAddToGroupNode(group, child, pluginId, isSecondary(child)); |
| } |
| else if (REFERENCE_ELEMENT_NAME.equals(name)) { |
| AnAction action = processReferenceElement(child, pluginId); |
| if (action != null) { |
| addToGroupInner(group, action, Constraints.LAST, isSecondary(child)); |
| } |
| } |
| 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 |
| */ |
| private void processAddToGroupNode(AnAction action, Element element, final PluginId pluginId, boolean secondary) { |
| // Real subclasses of AnAction should not be here |
| if (!(action instanceof Separator)) { |
| assertActionIsGroupOrStub(action); |
| } |
| |
| String actionName = String.format( |
| "%s (%s)", action instanceof ActionStub? ((ActionStub)action).getClassName() : action.getClass().getName(), |
| action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action)); |
| |
| 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; |
| } |
| addToGroupInner(parentGroup, action, new Constraints(anchor, relativeToActionId), secondary); |
| } |
| |
| private void addToGroupInner(AnAction group, AnAction action, Constraints constraints, boolean secondary) { |
| ((DefaultActionGroup)group).addAction(action, constraints, this).setAsSecondary(secondary); |
| myId2GroupId.putValue(myAction2Id.get(action), myAction2Id.get(group)); |
| } |
| |
| @Nullable |
| public AnAction getParentGroup(final String groupId, |
| @Nullable final String actionName, |
| @Nullable final PluginId pluginId) { |
| if (groupId == null || groupId.isEmpty()) { |
| reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined"); |
| return null; |
| } |
| AnAction parentGroup = getActionImpl(groupId, true, null); |
| if (parentGroup == null) { |
| reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" isn't registered; action will be added to the \"Other\" group"); |
| parentGroup = getActionImpl(IdeActions.GROUP_OTHER_MENU, true, 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; |
| } |
| Separator separator = Separator.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().isEmpty()) { |
| 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); |
| } |
| } |
| |
| @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.isEmpty()) { |
| reportActionError(pluginId, "ID of reference element should be defined"); |
| return null; |
| } |
| |
| AnAction action = getActionImpl(ref, true, null); |
| |
| 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"); |
| } |
| } |
| |
| @Override |
| public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) { |
| registerAction(actionId, action, pluginId, null); |
| } |
| |
| public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId, @Nullable String projectType) { |
| synchronized (myLock) { |
| if (addToMap(actionId, action, pluginId, projectType) == null) return; |
| if (myAction2Id.containsKey(action)) { |
| reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) + |
| getPluginInfo(pluginId)); |
| return; |
| } |
| 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<String>(); |
| myPlugin2Id.put(pluginId, pluginActionIds); |
| } |
| pluginActionIds.add(actionId); |
| } |
| action.registerCustomShortcutSet(new ProxyShortcutSet(actionId, myKeymapManager), null); |
| } |
| } |
| |
| private AnAction addToMap(String actionId, AnAction action, PluginId pluginId, String projectType) { |
| if (projectType != null || myId2Action.containsKey(actionId)) { |
| return registerChameleon(actionId, action, pluginId, projectType); |
| } |
| else { |
| myId2Action.put(actionId, action); |
| return action; |
| } |
| } |
| |
| private AnAction registerChameleon(String actionId, AnAction action, PluginId pluginId, String projectType) { |
| ProjectType type = projectType == null ? null : new ProjectType(projectType); |
| // make sure id+projectType is unique |
| AnAction o = myId2Action.get(actionId); |
| ChameleonAction chameleonAction; |
| if (o == null) { |
| chameleonAction = new ChameleonAction(action, type); |
| myId2Action.put(actionId, chameleonAction); |
| return chameleonAction; |
| } |
| if (o instanceof ChameleonAction) { |
| chameleonAction = (ChameleonAction)o; |
| } |
| else { |
| chameleonAction = new ChameleonAction(o, type); |
| myId2Action.put(actionId, chameleonAction); |
| } |
| AnAction old = chameleonAction.addAction(action, type); |
| if (old != null) { |
| reportActionError(pluginId, |
| "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action + |
| "; Registered action is " + |
| myId2Action.get(actionId) + getPluginInfo(pluginId)); |
| return null; |
| } |
| return chameleonAction; |
| } |
| |
| @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 = 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); |
| } |
| }; |
| } |
| |
| @NotNull |
| @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 AnAction replaceAction(String actionId, @NotNull AnAction newAction) { |
| // synchronized (myLock) { |
| // return replaceAction(actionId, newAction, null); |
| // } |
| //} |
| |
| @Override |
| public boolean isActionPopupStackEmpty() { |
| return myPopups.isEmpty(); |
| } |
| |
| @Override |
| public boolean isTransparentOnlyActionsUpdateNow() { |
| return myTransparentOnlyUpdate; |
| } |
| |
| private AnAction replaceAction(@NotNull String actionId, @NotNull AnAction newAction, @Nullable PluginId pluginId) { |
| AnAction oldAction = getActionOrStub(actionId); |
| if (oldAction != null) { |
| boolean isGroup = oldAction instanceof ActionGroup; |
| if (isGroup != newAction instanceof ActionGroup) { |
| throw new IllegalStateException("cannot replace a group with an action and vice versa: " + actionId); |
| } |
| unregisterAction(actionId); |
| if (isGroup) { |
| myId2GroupId.values().remove(actionId); |
| } |
| } |
| registerAction(actionId, newAction, pluginId); |
| for (String groupId : myId2GroupId.get(actionId)) { |
| DefaultActionGroup group = ObjectUtils.assertNotNull((DefaultActionGroup)getActionOrStub(groupId)); |
| group.replaceAction(oldAction, newAction); |
| } |
| return oldAction; |
| } |
| |
| 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<String>(myId2Action.keySet()); |
| } |
| } |
| |
| public Future<?> preloadActions() { |
| if (myPreloadActionsRunnable == null) { |
| myPreloadActionsRunnable = new Runnable() { |
| @Override |
| public void run() { |
| try { |
| doPreloadActions(); |
| } catch (RuntimeInterruptedException ignore) { |
| } |
| } |
| }; |
| return ApplicationManager.getApplication().executeOnPooledThread(myPreloadActionsRunnable); |
| } |
| return null; |
| } |
| |
| private void doPreloadActions() { |
| try { |
| Thread.sleep(5000); // wait for project initialization to complete |
| } |
| catch (InterruptedException e) { |
| return; // IDEA exited |
| } |
| preloadActionGroup(IdeActions.GROUP_EDITOR_POPUP); |
| preloadActionGroup(IdeActions.GROUP_EDITOR_TAB_POPUP); |
| preloadActionGroup(IdeActions.GROUP_PROJECT_VIEW_POPUP); |
| preloadActionGroup(IdeActions.GROUP_MAIN_MENU); |
| preloadActionGroup(IdeActions.GROUP_NEW); |
| // TODO anything else? |
| LOG.debug("Actions preloading completed"); |
| } |
| |
| public void preloadActionGroup(final String groupId) { |
| final AnAction action = getAction(groupId); |
| if (action instanceof ActionGroup) { |
| preloadActionGroup((ActionGroup) action); |
| } |
| } |
| |
| private void preloadActionGroup(final ActionGroup group) { |
| final Application application = ApplicationManager.getApplication(); |
| final AnAction[] children = application.runReadAction(new Computable<AnAction[]>() { |
| @Override |
| public AnAction[] compute() { |
| if (application.isDisposed()) { |
| return AnAction.EMPTY_ARRAY; |
| } |
| |
| return group.getChildren(null); |
| } |
| }); |
| for (AnAction action : children) { |
| if (action instanceof PreloadableAction) { |
| ((PreloadableAction)action).preload(); |
| } |
| else if (action instanceof ActionGroup) { |
| preloadActionGroup((ActionGroup)action); |
| } |
| |
| myActionsPreloaded++; |
| if (myActionsPreloaded % 10 == 0) { |
| try { |
| //noinspection BusyWait |
| Thread.sleep(300); |
| } |
| catch (InterruptedException ignored) { |
| throw new RuntimeInterruptedException(ignored); |
| } |
| } |
| } |
| } |
| |
| @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(new Runnable() { |
| @Override |
| public void run() { |
| final DataContext context = getContextBy(contextComponent); |
| |
| AnActionEvent event = new AnActionEvent( |
| inputEvent, context, |
| place != null ? place : ActionPlaces.UNKNOWN, |
| presentation, ActionManagerImpl.this, |
| inputEvent.getModifiersEx() |
| ); |
| |
| ActionUtil.performDumbAwareUpdate(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 class MyTimer extends Timer implements ActionListener { |
| private final List<TimerListener> myTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final List<TimerListener> myTransparentTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| 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() { |
| @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){ |
| (transparent ? myTransparentTimerListeners : myTimerListeners).add(listener); |
| } |
| |
| public void removeTimerListener(TimerListener listener, boolean transparent){ |
| (transparent ? myTransparentTimerListeners : 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 { |
| Set<TimerListener> notified = new HashSet<TimerListener>(); |
| 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) { |
| for (TimerListener listener : timerListeners) { |
| if (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); |
| } |
| } |
| } |
| } |
| } |