sticky documentation popup [take 1]
[fedora-idea.git] / platform / platform-impl / src / com / intellij / openapi / actionSystem / impl / ActionManagerImpl.java
blob520db075c1ad0a43a5c4367725bf039512e9900d
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.openapi.actionSystem.impl;
18 import com.intellij.CommonBundle;
19 import com.intellij.diagnostic.PluginException;
20 import com.intellij.ide.ActivityTracker;
21 import com.intellij.ide.DataManager;
22 import com.intellij.ide.plugins.IdeaPluginDescriptor;
23 import com.intellij.ide.plugins.PluginManager;
24 import com.intellij.idea.IdeaLogger;
25 import com.intellij.openapi.Disposable;
26 import com.intellij.openapi.actionSystem.*;
27 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
28 import com.intellij.openapi.actionSystem.ex.ActionUtil;
29 import com.intellij.openapi.actionSystem.ex.AnActionListener;
30 import com.intellij.openapi.application.Application;
31 import com.intellij.openapi.application.ApplicationManager;
32 import com.intellij.openapi.application.ModalityState;
33 import com.intellij.openapi.application.ex.ApplicationManagerEx;
34 import com.intellij.openapi.components.ApplicationComponent;
35 import com.intellij.openapi.diagnostic.Logger;
36 import com.intellij.openapi.extensions.PluginId;
37 import com.intellij.openapi.keymap.Keymap;
38 import com.intellij.openapi.keymap.KeymapManager;
39 import com.intellij.openapi.keymap.KeymapUtil;
40 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
41 import com.intellij.openapi.progress.ProcessCanceledException;
42 import com.intellij.openapi.util.*;
43 import com.intellij.openapi.util.text.StringUtil;
44 import com.intellij.openapi.wm.IdeFocusManager;
45 import com.intellij.util.ArrayUtil;
46 import com.intellij.util.ui.UIUtil;
47 import gnu.trove.THashMap;
48 import gnu.trove.THashSet;
49 import gnu.trove.TObjectIntHashMap;
50 import org.jdom.Element;
51 import org.jetbrains.annotations.NonNls;
52 import org.jetbrains.annotations.NotNull;
53 import org.jetbrains.annotations.Nullable;
54 import org.picocontainer.defaults.ConstructorInjectionComponentAdapter;
56 import javax.swing.*;
57 import javax.swing.Timer;
58 import java.awt.*;
59 import java.awt.event.*;
60 import java.lang.reflect.Constructor;
61 import java.util.*;
62 import java.util.List;
64 public final class ActionManagerImpl extends ActionManagerEx implements ApplicationComponent {
65 private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.actionSystem.impl.ActionManagerImpl");
66 private static final int TIMER_DELAY = 500;
67 private static final int UPDATE_DELAY_AFTER_TYPING = 500;
69 private final Object myLock = new Object();
70 private final THashMap<String,Object> myId2Action;
71 private final THashMap<PluginId, THashSet<String>> myPlugin2Id;
72 private final TObjectIntHashMap<String> myId2Index;
73 private final THashMap<Object,String> myAction2Id;
74 private final ArrayList<String> myNotRegisteredInternalActionIds;
75 private MyTimer myTimer;
77 private int myRegisteredActionsCount;
78 private final ArrayList<AnActionListener> myActionListeners;
79 private AnActionListener[] myCachedActionListeners;
80 private String myLastPreformedActionId;
81 private final KeymapManager myKeymapManager;
82 private final DataManager myDataManager;
83 private String myPrevPerformedActionId;
84 private long myLastTimeEditorWasTypedIn = 0;
85 @NonNls public static final String ACTION_ELEMENT_NAME = "action";
86 @NonNls public static final String GROUP_ELEMENT_NAME = "group";
87 @NonNls public static final String ACTIONS_ELEMENT_NAME = "actions";
88 @NonNls public static final String CLASS_ATTR_NAME = "class";
89 @NonNls public static final String ID_ATTR_NAME = "id";
90 @NonNls public static final String INTERNAL_ATTR_NAME = "internal";
91 @NonNls public static final String ICON_ATTR_NAME = "icon";
92 @NonNls public static final String ADD_TO_GROUP_ELEMENT_NAME = "add-to-group";
93 @NonNls public static final String SHORTCUT_ELEMENT_NAME = "keyboard-shortcut";
94 @NonNls public static final String MOUSE_SHORTCUT_ELEMENT_NAME = "mouse-shortcut";
95 @NonNls public static final String DESCRIPTION = "description";
96 @NonNls public static final String TEXT_ATTR_NAME = "text";
97 @NonNls public static final String POPUP_ATTR_NAME = "popup";
98 @NonNls public static final String SEPARATOR_ELEMENT_NAME = "separator";
99 @NonNls public static final String REFERENCE_ELEMENT_NAME = "reference";
100 @NonNls public static final String GROUPID_ATTR_NAME = "group-id";
101 @NonNls public static final String ANCHOR_ELEMENT_NAME = "anchor";
102 @NonNls public static final String FIRST = "first";
103 @NonNls public static final String LAST = "last";
104 @NonNls public static final String BEFORE = "before";
105 @NonNls public static final String AFTER = "after";
106 @NonNls public static final String SECONDARY = "secondary";
107 @NonNls public static final String RELATIVE_TO_ACTION_ATTR_NAME = "relative-to-action";
108 @NonNls public static final String FIRST_KEYSTROKE_ATTR_NAME = "first-keystroke";
109 @NonNls public static final String SECOND_KEYSTROKE_ATTR_NAME = "second-keystroke";
110 @NonNls public static final String KEYMAP_ATTR_NAME = "keymap";
111 @NonNls public static final String KEYSTROKE_ATTR_NAME = "keystroke";
112 @NonNls public static final String REF_ATTR_NAME = "ref";
113 @NonNls public static final String ACTIONS_BUNDLE = "messages.ActionsBundle";
114 @NonNls public static final String USE_SHORTCUT_OF_ATTR_NAME = "use-shortcut-of";
116 private final List<ActionPopupMenuImpl> myPopups = new ArrayList<ActionPopupMenuImpl>();
118 private final Map<AnAction, DataContext> myQueuedNotifications = new LinkedHashMap<AnAction, DataContext>();
119 private final Map<AnAction, AnActionEvent> myQueuedNotificationsEvents = new LinkedHashMap<AnAction, AnActionEvent>();
121 private Runnable myPreloadActionsRunnable;
123 ActionManagerImpl(KeymapManager keymapManager, DataManager dataManager) {
124 myId2Action = new THashMap<String, Object>();
125 myId2Index = new TObjectIntHashMap<String>();
126 myAction2Id = new THashMap<Object, String>();
127 myPlugin2Id = new THashMap<PluginId, THashSet<String>>();
128 myNotRegisteredInternalActionIds = new ArrayList<String>();
129 myActionListeners = new ArrayList<AnActionListener>();
130 myCachedActionListeners = null;
131 myKeymapManager = keymapManager;
132 myDataManager = dataManager;
134 registerPluginActions();
137 public void initComponent() {}
139 public void disposeComponent() {
140 if (myTimer != null) {
141 myTimer.stop();
142 myTimer = null;
146 public void addTimerListener(int delay, final TimerListener listener) {
147 if (ApplicationManager.getApplication().isUnitTestMode()) return;
148 if (myTimer == null) {
149 myTimer = new MyTimer();
150 myTimer.start();
153 myTimer.addTimerListener(listener);
156 public void removeTimerListener(TimerListener listener) {
157 if (ApplicationManager.getApplication().isUnitTestMode()) return;
158 LOG.assertTrue(myTimer != null);
160 myTimer.removeTimerListener(listener);
163 public ActionPopupMenu createActionPopupMenu(String place, @NotNull ActionGroup group, @Nullable PresentationFactory presentationFactory) {
164 return new ActionPopupMenuImpl(place, group, this, presentationFactory);
167 public ActionPopupMenu createActionPopupMenu(String place, @NotNull ActionGroup group) {
168 return new ActionPopupMenuImpl(place, group, this, null);
171 public ActionToolbar createActionToolbar(final String place, final ActionGroup group, final boolean horizontal) {
172 return new ActionToolbarImpl(place, group, horizontal, myDataManager, this, (KeymapManagerEx)myKeymapManager);
176 private void registerPluginActions() {
177 final Application app = ApplicationManager.getApplication();
178 final IdeaPluginDescriptor[] plugins = app.getPlugins();
179 for (IdeaPluginDescriptor plugin : plugins) {
180 if (PluginManager.shouldSkipPlugin(plugin)) continue;
181 final List<Element> elementList = plugin.getActionsDescriptionElements();
182 if (elementList != null) {
183 for (Element e : elementList) {
184 processActionsChildElement(plugin.getPluginClassLoader(), plugin.getPluginId(), e);
190 public AnAction getAction(@NotNull String id) {
191 return getActionImpl(id, false);
194 private AnAction getActionImpl(String id, boolean canReturnStub) {
195 synchronized (myLock) {
196 AnAction action = (AnAction)myId2Action.get(id);
197 if (!canReturnStub && action instanceof ActionStub) {
198 action = convert((ActionStub)action);
200 return action;
205 * Converts action's stub to normal action.
207 private AnAction convert(ActionStub stub) {
208 LOG.assertTrue(myAction2Id.contains(stub));
209 myAction2Id.remove(stub);
211 LOG.assertTrue(myId2Action.contains(stub.getId()));
213 AnAction action = (AnAction)myId2Action.remove(stub.getId());
214 LOG.assertTrue(action != null);
215 LOG.assertTrue(action.equals(stub));
217 Object obj;
218 String className = stub.getClassName();
219 try {
220 Constructor<?> constructor = Class.forName(className, true, stub.getLoader()).getDeclaredConstructor();
221 constructor.setAccessible(true);
222 obj = constructor.newInstance();
224 catch (ClassNotFoundException e) {
225 PluginId pluginId = stub.getPluginId();
226 if (pluginId != null) {
227 throw new PluginException("class with name \"" + className + "\" not found", e, pluginId);
229 else {
230 throw new IllegalStateException("class with name \"" + className + "\" not found");
233 catch(UnsupportedClassVersionError e) {
234 PluginId pluginId = stub.getPluginId();
235 if (pluginId != null) {
236 throw new PluginException(e, pluginId);
238 else {
239 throw new IllegalStateException(e);
242 catch (Exception e) {
243 PluginId pluginId = stub.getPluginId();
244 if (pluginId != null) {
245 throw new PluginException("cannot create class \"" + className + "\"", e, pluginId);
247 else {
248 throw new IllegalStateException("cannot create class \"" + className + "\"", e);
252 if (!(obj instanceof AnAction)) {
253 throw new IllegalStateException("class with name \"" + className + "\" should be instance of " + AnAction.class.getName());
256 AnAction anAction = (AnAction)obj;
257 stub.initAction(anAction);
258 if (StringUtil.isNotEmpty(stub.getText())) {
259 anAction.getTemplatePresentation().setText(stub.getText());
261 String iconPath = stub.getIconPath();
262 if (iconPath != null) {
263 setIconFromClass(anAction.getClass(), anAction.getClass().getClassLoader(), iconPath, stub.getClassName(), anAction.getTemplatePresentation(), stub.getPluginId());
266 myId2Action.put(stub.getId(), obj);
267 myAction2Id.put(obj, stub.getId());
269 return anAction;
272 public String getId(@NotNull AnAction action) {
273 LOG.assertTrue(!(action instanceof ActionStub));
274 synchronized (myLock) {
275 return myAction2Id.get(action);
279 public String[] getActionIds(@NotNull String idPrefix) {
280 synchronized (myLock) {
281 ArrayList<String> idList = new ArrayList<String>();
282 for (String id : myId2Action.keySet()) {
283 if (id.startsWith(idPrefix)) {
284 idList.add(id);
287 return ArrayUtil.toStringArray(idList);
291 public boolean isGroup(@NotNull String actionId) {
292 return getActionImpl(actionId, true) instanceof ActionGroup;
295 public JComponent createButtonToolbar(final String actionPlace, final ActionGroup messageActionGroup) {
296 return new ButtonToolbarImpl(actionPlace, messageActionGroup, myDataManager, this);
299 public AnAction getActionOrStub(String id) {
300 return getActionImpl(id, true);
304 * @return instance of ActionGroup or ActionStub. The method never returns real subclasses
305 * of <code>AnAction</code>.
307 @Nullable
308 private AnAction processActionElement(Element element, final ClassLoader loader, PluginId pluginId) {
309 final Application app = ApplicationManager.getApplication();
310 final IdeaPluginDescriptor plugin = app.getPlugin(pluginId);
311 ResourceBundle bundle = getActionsResourceBundle(loader, plugin);
313 if (!ACTION_ELEMENT_NAME.equals(element.getName())) {
314 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
315 return null;
317 String className = element.getAttributeValue(CLASS_ATTR_NAME);
318 if (className == null || className.length() == 0) {
319 reportActionError(pluginId, "action element should have specified \"class\" attribute");
320 return null;
322 // read ID and register loaded action
323 String id = element.getAttributeValue(ID_ATTR_NAME);
324 if (id == null || id.length() == 0) {
325 reportActionError(pluginId, "ID of the action cannot be an empty string");
326 return null;
328 if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) {
329 myNotRegisteredInternalActionIds.add(id);
330 return null;
333 String text = loadTextForElement(element, bundle, id, ACTION_ELEMENT_NAME);
335 String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
337 if (text == null) {
338 @NonNls String message = "'text' attribute is mandatory (action ID=" + id + ";" +
339 (plugin == null ? "" : " plugin path: "+plugin.getPath()) + ")";
340 reportActionError(pluginId, message);
341 return null;
344 ActionStub stub = new ActionStub(className, id, text, loader, pluginId, iconPath);
345 Presentation presentation = stub.getTemplatePresentation();
346 presentation.setText(text);
348 // description
350 presentation.setDescription(loadDescriptionForElement(element, bundle, id, ACTION_ELEMENT_NAME));
352 // process all links and key bindings if any
353 for (final Object o : element.getChildren()) {
354 Element e = (Element)o;
355 if (ADD_TO_GROUP_ELEMENT_NAME.equals(e.getName())) {
356 processAddToGroupNode(stub, e, pluginId, isSecondary(e));
358 else if (SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
359 processKeyboardShortcutNode(e, id, pluginId);
361 else if (MOUSE_SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
362 processMouseShortcutNode(e, id, pluginId);
364 else {
365 reportActionError(pluginId, "unexpected name of element \"" + e.getName() + "\"");
366 return null;
369 if (element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME) != null) {
370 ((KeymapManagerEx)myKeymapManager).bindShortcuts(element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME), id);
373 // register action
374 registerAction(id, stub, pluginId);
375 return stub;
378 private ResourceBundle getActionsResourceBundle(ClassLoader loader, IdeaPluginDescriptor plugin) {
379 @NonNls final String resBundleName = plugin != null && !plugin.getPluginId().getIdString().equals("com.intellij") ? plugin.getResourceBundleBaseName() : ACTIONS_BUNDLE;
380 ResourceBundle bundle = null;
381 if (resBundleName != null) {
382 bundle = getBundle(loader, resBundleName);
384 return bundle;
387 private static boolean isSecondary(Element element) {
388 return "true".equalsIgnoreCase(element.getAttributeValue(SECONDARY));
391 private static void setIcon(@Nullable final String iconPath, final String className, final ClassLoader loader, final Presentation presentation,
392 final PluginId pluginId) {
393 if (iconPath == null) return;
395 try {
396 final Class actionClass = Class.forName(className, true, loader);
397 setIconFromClass(actionClass, loader, iconPath, className, presentation, pluginId);
399 catch (ClassNotFoundException e) {
400 LOG.error(e);
401 reportActionError(pluginId, "class with name \"" + className + "\" not found");
403 catch (NoClassDefFoundError e) {
404 LOG.error(e);
405 reportActionError(pluginId, "class with name \"" + className + "\" not found");
409 private static void setIconFromClass(@NotNull final Class actionClass, @NotNull ClassLoader classLoader, @NotNull final String iconPath, final String className,
410 final Presentation presentation, final PluginId pluginId) {
411 //try to find icon in idea class path
412 Icon icon = IconLoader.findIcon(iconPath, actionClass);
413 if (icon == null) icon = IconLoader.findIcon(iconPath, classLoader);
414 if (icon == null) {
415 reportActionError(pluginId, "Icon cannot be found in '" + iconPath + "', action class='" + className + "'");
417 else {
418 presentation.setIcon(icon);
422 private static String loadDescriptionForElement(final Element element, final ResourceBundle bundle, final String id, String elementType) {
423 final String value = element.getAttributeValue(DESCRIPTION);
424 if (bundle != null) {
425 @NonNls final String key = elementType + "." + id + ".description";
426 return CommonBundle.messageOrDefault(bundle, key, value == null ? "" : value);
427 } else {
428 return value;
432 private static String loadTextForElement(final Element element, final ResourceBundle bundle, final String id, String elementType) {
433 final String value = element.getAttributeValue(TEXT_ATTR_NAME);
434 return CommonBundle.messageOrDefault(bundle, elementType + "." + id + "." + TEXT_ATTR_NAME, value == null ? "" : value);
437 private AnAction processGroupElement(Element element, final ClassLoader loader, PluginId pluginId) {
438 final Application app = ApplicationManager.getApplication();
439 final IdeaPluginDescriptor plugin = app.getPlugin(pluginId);
440 ResourceBundle bundle = getActionsResourceBundle(loader, plugin);
442 if (!GROUP_ELEMENT_NAME.equals(element.getName())) {
443 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
444 return null;
446 String className = element.getAttributeValue(CLASS_ATTR_NAME);
447 if (className == null) { // use default group if class isn't specified
448 className = DefaultActionGroup.class.getName();
450 try {
451 Class aClass = Class.forName(className, true, loader);
452 Object obj = new ConstructorInjectionComponentAdapter(className, aClass).getComponentInstance(ApplicationManager.getApplication().getPicoContainer());
454 if (!(obj instanceof ActionGroup)) {
455 reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + ActionGroup.class.getName());
456 return null;
458 if (element.getChildren().size() != element.getChildren(ADD_TO_GROUP_ELEMENT_NAME).size() ) { //
459 if (!(obj instanceof DefaultActionGroup)) {
460 reportActionError(pluginId, "class with name \"" + className + "\" should be instance of " + DefaultActionGroup.class.getName() +
461 " because there are children specified");
462 return null;
465 ActionGroup group = (ActionGroup)obj;
466 // read ID and register loaded group
467 String id = element.getAttributeValue(ID_ATTR_NAME);
468 if (id != null && id.length() == 0) {
469 reportActionError(pluginId, "ID of the group cannot be an empty string");
470 return null;
472 if (Boolean.valueOf(element.getAttributeValue(INTERNAL_ATTR_NAME)).booleanValue() && !ApplicationManagerEx.getApplicationEx().isInternal()) {
473 myNotRegisteredInternalActionIds.add(id);
474 return null;
477 if (id != null) {
478 registerAction(id, group);
480 Presentation presentation = group.getTemplatePresentation();
482 // text
483 String text = loadTextForElement(element, bundle, id, GROUP_ELEMENT_NAME);
484 // don't override value which was set in API with empty value from xml descriptor
485 if (!StringUtil.isEmpty(text) || presentation.getText() == null) {
486 presentation.setText(text);
489 // description
490 String description = loadDescriptionForElement(element, bundle, id, GROUP_ELEMENT_NAME);
491 // don't override value which was set in API with empty value from xml descriptor
492 if (!StringUtil.isEmpty(description) || presentation.getDescription() == null) {
493 presentation.setDescription(description);
496 // icon
497 setIcon(element.getAttributeValue(ICON_ATTR_NAME), className, loader, presentation, pluginId);
498 // popup
499 String popup = element.getAttributeValue(POPUP_ATTR_NAME);
500 if (popup != null) {
501 group.setPopup(Boolean.valueOf(popup).booleanValue());
503 // process all group's children. There are other groups, actions, references and links
504 for (final Object o : element.getChildren()) {
505 Element child = (Element)o;
506 String name = child.getName();
507 if (ACTION_ELEMENT_NAME.equals(name)) {
508 AnAction action = processActionElement(child, loader, pluginId);
509 if (action != null) {
510 assertActionIsGroupOrStub(action);
511 ((DefaultActionGroup)group).addAction(action, Constraints.LAST, this).setAsSecondary(isSecondary(child));
514 else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
515 processSeparatorNode((DefaultActionGroup)group, child, pluginId);
517 else if (GROUP_ELEMENT_NAME.equals(name)) {
518 AnAction action = processGroupElement(child, loader, pluginId);
519 if (action != null) {
520 ((DefaultActionGroup)group).add(action, this);
523 else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) {
524 processAddToGroupNode(group, child, pluginId, isSecondary(child));
526 else if (REFERENCE_ELEMENT_NAME.equals(name)) {
527 AnAction action = processReferenceElement(child, pluginId);
528 if (action != null) {
529 ((DefaultActionGroup)group).addAction(action, Constraints.LAST, this).setAsSecondary(isSecondary(child));
532 else {
533 reportActionError(pluginId, "unexpected name of element \"" + name + "\n");
534 return null;
537 return group;
539 catch (ClassNotFoundException e) {
540 reportActionError(pluginId, "class with name \"" + className + "\" not found");
541 return null;
543 catch (NoClassDefFoundError e) {
544 reportActionError(pluginId, "class with name \"" + e.getMessage() + "\" not found");
545 return null;
547 catch(UnsupportedClassVersionError e) {
548 reportActionError(pluginId, "unsupported class version for " + className);
549 return null;
551 catch (Exception e) {
552 final String message = "cannot create class \"" + className + "\"";
553 if (pluginId == null) {
554 LOG.error(message, e);
556 else {
557 LOG.error(new PluginException(message, e, pluginId));
559 return null;
563 private void processReferenceNode(final Element element, final PluginId pluginId) {
564 final AnAction action = processReferenceElement(element, pluginId);
566 for (final Object o : element.getChildren()) {
567 Element child = (Element)o;
568 if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
569 processAddToGroupNode(action, child, pluginId, isSecondary(child));
574 private static final Map<String, ResourceBundle> ourBundlesCache = new HashMap<String, ResourceBundle>();
576 private static ResourceBundle getBundle(final ClassLoader loader, final String resBundleName) {
578 if (ourBundlesCache.containsKey(resBundleName)) {
579 return ourBundlesCache.get(resBundleName);
582 final ResourceBundle bundle = ResourceBundle.getBundle(resBundleName, Locale.getDefault(), loader);
584 ourBundlesCache.put(resBundleName, bundle);
586 return bundle;
589 /**\
590 * @param element description of link
591 * @param pluginId
592 * @param secondary
594 private void processAddToGroupNode(AnAction action, Element element, final PluginId pluginId, boolean secondary) {
595 // Real subclasses of AnAction should not be here
596 if (!(action instanceof Separator)) {
597 assertActionIsGroupOrStub(action);
600 String actionName = action instanceof ActionStub ? ((ActionStub)action).getClassName() : action.getClass().getName();
602 if (!ADD_TO_GROUP_ELEMENT_NAME.equals(element.getName())) {
603 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
604 return;
607 // parent group
608 final AnAction parentGroup = getParentGroup(element.getAttributeValue(GROUPID_ATTR_NAME), actionName, pluginId);
609 if (parentGroup == null) {
610 return;
613 // anchor attribute
614 final Anchor anchor = parseAnchor(element.getAttributeValue(ANCHOR_ELEMENT_NAME),
615 actionName, pluginId);
616 if (anchor == null) {
617 return;
620 final String relativeToActionId = element.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME);
621 if (!checkRelativeToAction(relativeToActionId, anchor, actionName, pluginId)) {
622 return;
624 final DefaultActionGroup group = (DefaultActionGroup)parentGroup;
625 group.addAction(action, new Constraints(anchor, relativeToActionId), this).setAsSecondary(secondary);
628 public boolean checkRelativeToAction(final String relativeToActionId,
629 @NotNull final Anchor anchor,
630 @NotNull final String actionName,
631 @Nullable final PluginId pluginId) {
632 if ((Anchor.BEFORE == anchor || Anchor.AFTER == anchor) && relativeToActionId == null) {
633 reportActionError(pluginId, actionName + ": \"relative-to-action\" cannot be null if anchor is \"after\" or \"before\"");
634 return false;
636 return true;
639 @Nullable
640 public Anchor parseAnchor(final String anchorStr,
641 @Nullable final String actionName,
642 @Nullable final PluginId pluginId) {
643 if (anchorStr == null) {
644 reportActionError(pluginId, actionName + ": attribute \"anchor\" should be defined");
645 return null;
648 if (FIRST.equalsIgnoreCase(anchorStr)) {
649 return Anchor.FIRST;
651 else if (LAST.equalsIgnoreCase(anchorStr)) {
652 return Anchor.LAST;
654 else if (BEFORE.equalsIgnoreCase(anchorStr)) {
655 return Anchor.BEFORE;
657 else if (AFTER.equalsIgnoreCase(anchorStr)) {
658 return Anchor.AFTER;
660 else {
661 reportActionError(pluginId, actionName + ": anchor should be one of the following constants: \"first\", \"last\", \"before\" or \"after\"");
662 return null;
666 @Nullable
667 public AnAction getParentGroup(final String groupId,
668 @Nullable final String actionName,
669 @Nullable final PluginId pluginId) {
670 if (groupId == null || groupId.length() == 0) {
671 reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined");
672 return null;
674 AnAction parentGroup = getActionImpl(groupId, true);
675 if (parentGroup == null) {
676 reportActionError(pluginId, actionName + ": action with id \"" + groupId + "\" isn't registered; action will be added to the \"Other\" group");
677 parentGroup = getActionImpl(IdeActions.GROUP_OTHER_MENU, true);
679 if (!(parentGroup instanceof DefaultActionGroup)) {
680 reportActionError(pluginId, actionName + ": action with id \"" + groupId + "\" should be instance of " + DefaultActionGroup.class.getName() +
681 " but was " + parentGroup.getClass());
682 return null;
684 return parentGroup;
688 * @param parentGroup group wich is the parent of the separator. It can be <code>null</code> in that
689 * case separator will be added to group described in the <add-to-group ....> subelement.
690 * @param element XML element which represent separator.
692 private void processSeparatorNode(DefaultActionGroup parentGroup, Element element, PluginId pluginId) {
693 if (!SEPARATOR_ELEMENT_NAME.equals(element.getName())) {
694 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
695 return;
697 Separator separator = Separator.getInstance();
698 if (parentGroup != null) {
699 parentGroup.add(separator, this);
701 // try to find inner <add-to-parent...> tag
702 for (final Object o : element.getChildren()) {
703 Element child = (Element)o;
704 if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
705 processAddToGroupNode(separator, child, pluginId, isSecondary(child));
710 private void processKeyboardShortcutNode(Element element, String actionId, PluginId pluginId) {
711 String firstStrokeString = element.getAttributeValue(FIRST_KEYSTROKE_ATTR_NAME);
712 if (firstStrokeString == null) {
713 reportActionError(pluginId, "\"first-keystroke\" attribute must be specified for action with id=" + actionId);
714 return;
716 KeyStroke firstKeyStroke = getKeyStroke(firstStrokeString);
717 if (firstKeyStroke == null) {
718 reportActionError(pluginId, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId);
719 return;
722 KeyStroke secondKeyStroke = null;
723 String secondStrokeString = element.getAttributeValue(SECOND_KEYSTROKE_ATTR_NAME);
724 if (secondStrokeString != null) {
725 secondKeyStroke = getKeyStroke(secondStrokeString);
726 if (secondKeyStroke == null) {
727 reportActionError(pluginId, "\"second-keystroke\" attribute has invalid value for action with id=" + actionId);
728 return;
732 String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
733 if (keymapName == null || keymapName.trim().length() == 0) {
734 reportActionError(pluginId, "attribute \"keymap\" should be defined");
735 return;
737 Keymap keymap = myKeymapManager.getKeymap(keymapName);
738 if (keymap == null) {
739 reportActionError(pluginId, "keymap \"" + keymapName + "\" not found");
740 return;
743 keymap.addShortcut(actionId, new KeyboardShortcut(firstKeyStroke, secondKeyStroke));
746 private static void processMouseShortcutNode(Element element, String actionId, PluginId pluginId) {
747 String keystrokeString = element.getAttributeValue(KEYSTROKE_ATTR_NAME);
748 if (keystrokeString == null || keystrokeString.trim().length() == 0) {
749 reportActionError(pluginId, "\"keystroke\" attribute must be specified for action with id=" + actionId);
750 return;
752 MouseShortcut shortcut;
753 try {
754 shortcut = KeymapUtil.parseMouseShortcut(keystrokeString);
756 catch (Exception ex) {
757 reportActionError(pluginId, "\"keystroke\" attribute has invalid value for action with id=" + actionId);
758 return;
761 String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
762 if (keymapName == null || keymapName.length() == 0) {
763 reportActionError(pluginId, "attribute \"keymap\" should be defined");
764 return;
766 Keymap keymap = KeymapManager.getInstance().getKeymap(keymapName);
767 if (keymap == null) {
768 reportActionError(pluginId, "keymap \"" + keymapName + "\" not found");
769 return;
772 keymap.addShortcut(actionId, shortcut);
775 @Nullable
776 private AnAction processReferenceElement(Element element, PluginId pluginId) {
777 if (!REFERENCE_ELEMENT_NAME.equals(element.getName())) {
778 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
779 return null;
781 String ref = element.getAttributeValue(REF_ATTR_NAME);
783 if (ref==null) {
784 // support old style references by id
785 ref = element.getAttributeValue(ID_ATTR_NAME);
788 if (ref == null || ref.length() == 0) {
789 reportActionError(pluginId, "ID of reference element should be defined");
790 return null;
793 AnAction action = getActionImpl(ref, true);
795 if (action == null) {
796 if (!myNotRegisteredInternalActionIds.contains(ref)) {
797 reportActionError(pluginId, "action specified by reference isn't registered (ID=" + ref + ")");
799 return null;
801 assertActionIsGroupOrStub(action);
802 return action;
805 private void processActionsElement(Element element, ClassLoader loader, PluginId pluginId) {
806 if (!ACTIONS_ELEMENT_NAME.equals(element.getName())) {
807 reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
808 return;
810 synchronized (myLock) {
811 for (final Object o : element.getChildren()) {
812 Element child = (Element)o;
813 processActionsChildElement(loader, pluginId, child);
818 private void processActionsChildElement(final ClassLoader loader, final PluginId pluginId, final Element child) {
819 String name = child.getName();
820 if (ACTION_ELEMENT_NAME.equals(name)) {
821 AnAction action = processActionElement(child, loader, pluginId);
822 if (action != null) {
823 assertActionIsGroupOrStub(action);
826 else if (GROUP_ELEMENT_NAME.equals(name)) {
827 processGroupElement(child, loader, pluginId);
829 else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
830 processSeparatorNode(null, child, pluginId);
832 else if (REFERENCE_ELEMENT_NAME.equals(name)) {
833 processReferenceNode(child, pluginId);
835 else {
836 reportActionError(pluginId, "unexpected name of element \"" + name + "\n");
840 private static void assertActionIsGroupOrStub(final AnAction action) {
841 if (!(action instanceof ActionGroup || action instanceof ActionStub)) {
842 LOG.assertTrue(false, "Action : "+action + "; class: "+action.getClass());
846 public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) {
847 synchronized (myLock) {
848 if (myId2Action.containsKey(actionId)) {
849 reportActionError(pluginId, "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action.toString() +
850 "; Registered action is " +
851 myId2Action.get(actionId) + getPluginInfo(pluginId));
852 return;
854 if (myAction2Id.containsKey(action)) {
855 reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) +
856 getPluginInfo(pluginId));
857 return;
859 myId2Action.put(actionId, action);
860 myId2Index.put(actionId, myRegisteredActionsCount++);
861 myAction2Id.put(action, actionId);
862 if (pluginId != null && !(action instanceof ActionGroup)){
863 THashSet<String> pluginActionIds = myPlugin2Id.get(pluginId);
864 if (pluginActionIds == null){
865 pluginActionIds = new THashSet<String>();
866 myPlugin2Id.put(pluginId, pluginActionIds);
868 pluginActionIds.add(actionId);
870 action.registerCustomShortcutSet(new ProxyShortcutSet(actionId, myKeymapManager), null);
874 private static void reportActionError(final PluginId pluginId, @NonNls final String message) {
875 if (pluginId == null) {
876 LOG.error(message);
878 else {
879 LOG.error(new PluginException(message, null, pluginId));
883 @NonNls
884 private static String getPluginInfo(@Nullable PluginId id) {
885 if (id != null) {
886 final IdeaPluginDescriptor plugin = ApplicationManager.getApplication().getPlugin(id);
887 if (plugin != null) {
888 String name = plugin.getName();
889 if (name == null) {
890 name = id.getIdString();
892 return " Plugin: " + name;
895 return "";
898 public void registerAction(@NotNull String actionId, @NotNull AnAction action) {
899 registerAction(actionId, action, null);
902 public void unregisterAction(@NotNull String actionId) {
903 synchronized (myLock) {
904 if (!myId2Action.containsKey(actionId)) {
905 if (LOG.isDebugEnabled()) {
906 LOG.debug("action with ID " + actionId + " wasn't registered");
907 return;
910 AnAction oldValue = (AnAction)myId2Action.remove(actionId);
911 myAction2Id.remove(oldValue);
912 myId2Index.remove(actionId);
913 for (PluginId pluginName : myPlugin2Id.keySet()) {
914 final THashSet<String> pluginActions = myPlugin2Id.get(pluginName);
915 if (pluginActions != null) {
916 pluginActions.remove(actionId);
922 @NotNull
923 public String getComponentName() {
924 return "ActionManager";
927 public Comparator<String> getRegistrationOrderComparator() {
928 return new Comparator<String>() {
929 public int compare(String id1, String id2) {
930 return myId2Index.get(id1) - myId2Index.get(id2);
935 public String[] getPluginActions(PluginId pluginName) {
936 if (myPlugin2Id.containsKey(pluginName)){
937 final THashSet<String> pluginActions = myPlugin2Id.get(pluginName);
938 return ArrayUtil.toStringArray(pluginActions);
940 return ArrayUtil.EMPTY_STRING_ARRAY;
943 public void addActionPopup(final ActionPopupMenuImpl menu) {
944 myPopups.add(menu);
947 public void removeActionPopup(final ActionPopupMenuImpl menu) {
948 final boolean removed = myPopups.remove(menu);
949 if (removed && myPopups.size() == 0) {
950 flushActionPerformed();
954 public void queueActionPerformedEvent(final AnAction action, DataContext context, AnActionEvent event) {
955 if (myPopups.size() > 0) {
956 myQueuedNotifications.put(action, context);
957 } else {
958 fireAfterActionPerformed(action, context, event);
963 public boolean isActionPopupStackEmpty() {
964 return myPopups.size() == 0;
967 private void flushActionPerformed() {
968 final Set<AnAction> actions = myQueuedNotifications.keySet();
969 for (final AnAction eachAction : actions) {
970 final DataContext eachContext = myQueuedNotifications.get(eachAction);
971 fireAfterActionPerformed(eachAction, eachContext, myQueuedNotificationsEvents.get(eachAction));
973 myQueuedNotifications.clear();
974 myQueuedNotificationsEvents.clear();
977 private AnActionListener[] getActionListeners() {
978 if (myCachedActionListeners == null) {
979 myCachedActionListeners = myActionListeners.toArray(new AnActionListener[myActionListeners.size()]);
982 return myCachedActionListeners;
985 public void addAnActionListener(AnActionListener listener) {
986 myActionListeners.add(listener);
987 myCachedActionListeners = null;
990 public void addAnActionListener(final AnActionListener listener, final Disposable parentDisposable) {
991 addAnActionListener(listener);
992 Disposer.register(parentDisposable, new Disposable() {
993 public void dispose() {
994 removeAnActionListener(listener);
999 public void removeAnActionListener(AnActionListener listener) {
1000 myActionListeners.remove(listener);
1001 myCachedActionListeners = null;
1004 public void fireBeforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
1005 if (action != null) {
1006 myPrevPerformedActionId = myLastPreformedActionId;
1007 myLastPreformedActionId = getId(action);
1008 IdeaLogger.ourLastActionId = myLastPreformedActionId;
1010 AnActionListener[] listeners = getActionListeners();
1011 for (AnActionListener listener : listeners) {
1012 listener.beforeActionPerformed(action, dataContext, event);
1016 public void fireAfterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
1017 if (action != null) {
1018 myPrevPerformedActionId = myLastPreformedActionId;
1019 myLastPreformedActionId = getId(action);
1020 IdeaLogger.ourLastActionId = myLastPreformedActionId;
1022 AnActionListener[] listeners = getActionListeners();
1023 for (AnActionListener listener : listeners) {
1024 try {
1025 listener.afterActionPerformed(action, dataContext, event);
1027 catch(AbstractMethodError e) {
1028 // ignore
1033 @Override
1034 public KeyboardShortcut getKeyboardShortcut(@NotNull String actionId) {
1035 AnAction action = ActionManager.getInstance().getAction(actionId);
1036 final ShortcutSet shortcutSet = action.getShortcutSet();
1037 final Shortcut[] shortcuts = shortcutSet.getShortcuts();
1038 for (final Shortcut shortcut : shortcuts) {
1039 KeyboardShortcut kb = (KeyboardShortcut)shortcut;
1040 if (kb.getSecondKeyStroke() == null) {
1041 return (KeyboardShortcut)shortcut;
1045 return null;
1048 public void fireBeforeEditorTyping(char c, DataContext dataContext) {
1049 myLastTimeEditorWasTypedIn = System.currentTimeMillis();
1050 AnActionListener[] listeners = getActionListeners();
1051 for (AnActionListener listener : listeners) {
1052 listener.beforeEditorTyping(c, dataContext);
1056 public String getLastPreformedActionId() {
1057 return myLastPreformedActionId;
1060 public String getPrevPreformedActionId() {
1061 return myPrevPerformedActionId;
1064 public Set<String> getActionIds(){
1065 return new HashSet<String>(myId2Action.keySet());
1068 private int myActionsPreloaded = 0;
1070 public void preloadActions() {
1071 if (myPreloadActionsRunnable == null) {
1072 myPreloadActionsRunnable = new Runnable() {
1073 public void run() {
1074 doPreloadActions();
1077 ApplicationManager.getApplication().executeOnPooledThread(myPreloadActionsRunnable);
1081 private void doPreloadActions() {
1082 try {
1083 Thread.sleep(5000); // wait for project initialization to complete
1085 catch (InterruptedException e) {
1086 // ignore
1088 preloadActionGroup(IdeActions.GROUP_EDITOR_POPUP);
1089 preloadActionGroup(IdeActions.GROUP_EDITOR_TAB_POPUP);
1090 preloadActionGroup(IdeActions.GROUP_PROJECT_VIEW_POPUP);
1091 preloadActionGroup(IdeActions.GROUP_MAIN_MENU);
1092 // TODO anything else?
1093 LOG.debug("Actions preloading completed");
1096 public void preloadActionGroup(final String groupId) {
1097 final AnAction action = getAction(groupId);
1098 if (action instanceof ActionGroup) {
1099 preloadActionGroup((ActionGroup) action);
1103 private void preloadActionGroup(final ActionGroup group) {
1104 final AnAction[] children = ApplicationManager.getApplication().runReadAction(new Computable<AnAction[]>() {
1105 public AnAction[] compute() {
1106 if (ApplicationManager.getApplication().isDisposed()) {
1107 return AnAction.EMPTY_ARRAY;
1110 return group.getChildren(null);
1113 for (AnAction action : children) {
1114 if (action instanceof PreloadableAction) {
1115 ((PreloadableAction)action).preload();
1117 else if (action instanceof ActionGroup) {
1118 preloadActionGroup((ActionGroup)action);
1121 myActionsPreloaded++;
1122 if (myActionsPreloaded % 10 == 0) {
1123 try {
1124 Thread.sleep(300);
1126 catch (InterruptedException e) {
1127 // ignore
1133 private class MyTimer extends Timer implements ActionListener {
1134 private final List<TimerListener> myTimerListeners = Collections.synchronizedList(new ArrayList<TimerListener>());
1135 private int myLastTimePerformed;
1137 MyTimer() {
1138 super(TIMER_DELAY, null);
1139 addActionListener(this);
1140 setRepeats(true);
1143 public void addTimerListener(TimerListener listener){
1144 myTimerListeners.add(listener);
1147 public void removeTimerListener(TimerListener listener){
1148 final boolean removed = myTimerListeners.remove(listener);
1149 if (!removed) {
1150 LOG.assertTrue(false, "Unknown listener " + listener);
1154 public void actionPerformed(ActionEvent e) {
1155 if (myLastTimeEditorWasTypedIn + UPDATE_DELAY_AFTER_TYPING > System.currentTimeMillis()) {
1156 return;
1159 final int lastEventCount = myLastTimePerformed;
1160 myLastTimePerformed = ActivityTracker.getInstance().getCount();
1162 if (myLastTimePerformed == lastEventCount) {
1163 return;
1166 TimerListener[] listeners = myTimerListeners.toArray(new TimerListener[myTimerListeners.size()]);
1167 for (TimerListener listener : listeners) {
1168 runListenerAction(listener);
1172 private void runListenerAction(final TimerListener listener) {
1173 ModalityState modalityState = listener.getModalityState();
1174 if (modalityState == null) return;
1175 if (!ModalityState.current().dominates(modalityState)) {
1176 try {
1177 listener.run();
1179 catch (ProcessCanceledException ex) {
1180 // ignore
1182 catch (Throwable e) {
1183 LOG.error(e);
1189 public ActionCallback tryToExecute(@NotNull final AnAction action, @NotNull final InputEvent inputEvent, @Nullable final Component contextComponent, @Nullable final String place,
1190 boolean now) {
1192 final Application app = ApplicationManager.getApplication();
1193 assert app.isDispatchThread();
1195 final ActionCallback result = new ActionCallback();
1196 final Runnable doRunnable = new Runnable() {
1197 public void run() {
1198 tryToExecuteNow(action, inputEvent, contextComponent, place, result);
1202 if (now) {
1203 doRunnable.run();
1204 } else {
1205 SwingUtilities.invokeLater(doRunnable);
1208 return result;
1212 private void tryToExecuteNow(final AnAction action, final InputEvent inputEvent, final Component contextComponent, final String place, final ActionCallback result) {
1213 final Presentation presenation = (Presentation)action.getTemplatePresentation().clone();
1215 IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown(new Runnable() {
1216 public void run() {
1217 final DataContext context = getContextBy(contextComponent);
1219 AnActionEvent event = new AnActionEvent(
1220 inputEvent, context,
1221 place != null ? place : ActionPlaces.UNKNOWN,
1222 presenation, ActionManagerImpl.this,
1223 inputEvent.getModifiersEx()
1226 ActionUtil.performDumbAwareUpdate(action, event, false);
1227 if (!event.getPresentation().isEnabled()) {
1228 result.setRejected();
1229 return;
1232 ActionUtil.lastUpdateAndCheckDumb(action, event, false);
1233 if (!event.getPresentation().isEnabled()) {
1234 result.setRejected();
1235 return;
1238 Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(context);
1239 if (component != null && !component.isShowing()) {
1240 result.setRejected();
1241 return;
1244 fireBeforeActionPerformed(action, context, event);
1246 UIUtil.addAwtListener(new AWTEventListener() {
1247 public void eventDispatched(AWTEvent event) {
1248 if (event.getID() == WindowEvent.WINDOW_OPENED ||event.getID() == WindowEvent.WINDOW_ACTIVATED) {
1249 if (!result.isProcessed()) {
1250 final WindowEvent we = (WindowEvent)event;
1251 IdeFocusManager.findInstanceByComponent(we.getWindow()).doWhenFocusSettlesDown(new Runnable() {
1252 public void run() {
1253 result.setDone();
1259 }, WindowEvent.WINDOW_EVENT_MASK, result);
1261 action.actionPerformed(event);
1262 result.setDone();
1263 queueActionPerformedEvent(action, context, event);
1268 private DataContext getContextBy(Component contextComponent) {
1269 final DataManager dataManager = DataManager.getInstance();
1270 return contextComponent != null ? dataManager.getDataContext(contextComponent) : dataManager.getDataContext();