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
;
57 import javax
.swing
.Timer
;
59 import java
.awt
.event
.*;
60 import java
.lang
.reflect
.Constructor
;
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) {
146 public void addTimerListener(int delay
, final TimerListener listener
) {
147 if (ApplicationManager
.getApplication().isUnitTestMode()) return;
148 if (myTimer
== null) {
149 myTimer
= new MyTimer();
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
);
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
));
218 String className
= stub
.getClassName();
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
);
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
);
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
);
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());
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
)) {
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>.
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() + "\"");
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");
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");
328 if (Boolean
.valueOf(element
.getAttributeValue(INTERNAL_ATTR_NAME
)).booleanValue() && !ApplicationManagerEx
.getApplicationEx().isInternal()) {
329 myNotRegisteredInternalActionIds
.add(id
);
333 String text
= loadTextForElement(element
, bundle
, id
, ACTION_ELEMENT_NAME
);
335 String iconPath
= element
.getAttributeValue(ICON_ATTR_NAME
);
338 @NonNls String message
= "'text' attribute is mandatory (action ID=" + id
+ ";" +
339 (plugin
== null ?
"" : " plugin path: "+plugin
.getPath()) + ")";
340 reportActionError(pluginId
, message
);
344 ActionStub stub
= new ActionStub(className
, id
, text
, loader
, pluginId
, iconPath
);
345 Presentation presentation
= stub
.getTemplatePresentation();
346 presentation
.setText(text
);
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
);
365 reportActionError(pluginId
, "unexpected name of element \"" + e
.getName() + "\"");
369 if (element
.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME
) != null) {
370 ((KeymapManagerEx
)myKeymapManager
).bindShortcuts(element
.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME
), id
);
374 registerAction(id
, stub
, pluginId
);
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
);
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;
396 final Class actionClass
= Class
.forName(className
, true, loader
);
397 setIconFromClass(actionClass
, loader
, iconPath
, className
, presentation
, pluginId
);
399 catch (ClassNotFoundException e
) {
401 reportActionError(pluginId
, "class with name \"" + className
+ "\" not found");
403 catch (NoClassDefFoundError 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
);
415 reportActionError(pluginId
, "Icon cannot be found in '" + iconPath
+ "', action class='" + className
+ "'");
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
);
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() + "\"");
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();
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());
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");
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");
472 if (Boolean
.valueOf(element
.getAttributeValue(INTERNAL_ATTR_NAME
)).booleanValue() && !ApplicationManagerEx
.getApplicationEx().isInternal()) {
473 myNotRegisteredInternalActionIds
.add(id
);
478 registerAction(id
, group
);
480 Presentation presentation
= group
.getTemplatePresentation();
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
);
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
);
497 setIcon(element
.getAttributeValue(ICON_ATTR_NAME
), className
, loader
, presentation
, pluginId
);
499 String popup
= element
.getAttributeValue(POPUP_ATTR_NAME
);
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
));
533 reportActionError(pluginId
, "unexpected name of element \"" + name
+ "\n");
539 catch (ClassNotFoundException e
) {
540 reportActionError(pluginId
, "class with name \"" + className
+ "\" not found");
543 catch (NoClassDefFoundError e
) {
544 reportActionError(pluginId
, "class with name \"" + e
.getMessage() + "\" not found");
547 catch(UnsupportedClassVersionError e
) {
548 reportActionError(pluginId
, "unsupported class version for " + className
);
551 catch (Exception e
) {
552 final String message
= "cannot create class \"" + className
+ "\"";
553 if (pluginId
== null) {
554 LOG
.error(message
, e
);
557 LOG
.error(new PluginException(message
, e
, pluginId
));
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
);
590 * @param element description of link
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() + "\"");
608 final AnAction parentGroup
= getParentGroup(element
.getAttributeValue(GROUPID_ATTR_NAME
), actionName
, pluginId
);
609 if (parentGroup
== null) {
614 final Anchor anchor
= parseAnchor(element
.getAttributeValue(ANCHOR_ELEMENT_NAME
),
615 actionName
, pluginId
);
616 if (anchor
== null) {
620 final String relativeToActionId
= element
.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME
);
621 if (!checkRelativeToAction(relativeToActionId
, anchor
, actionName
, pluginId
)) {
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\"");
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");
648 if (FIRST
.equalsIgnoreCase(anchorStr
)) {
651 else if (LAST
.equalsIgnoreCase(anchorStr
)) {
654 else if (BEFORE
.equalsIgnoreCase(anchorStr
)) {
655 return Anchor
.BEFORE
;
657 else if (AFTER
.equalsIgnoreCase(anchorStr
)) {
661 reportActionError(pluginId
, actionName
+ ": anchor should be one of the following constants: \"first\", \"last\", \"before\" or \"after\"");
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");
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());
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() + "\"");
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
);
716 KeyStroke firstKeyStroke
= getKeyStroke(firstStrokeString
);
717 if (firstKeyStroke
== null) {
718 reportActionError(pluginId
, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId
);
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
);
732 String keymapName
= element
.getAttributeValue(KEYMAP_ATTR_NAME
);
733 if (keymapName
== null || keymapName
.trim().length() == 0) {
734 reportActionError(pluginId
, "attribute \"keymap\" should be defined");
737 Keymap keymap
= myKeymapManager
.getKeymap(keymapName
);
738 if (keymap
== null) {
739 reportActionError(pluginId
, "keymap \"" + keymapName
+ "\" not found");
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
);
752 MouseShortcut shortcut
;
754 shortcut
= KeymapUtil
.parseMouseShortcut(keystrokeString
);
756 catch (Exception ex
) {
757 reportActionError(pluginId
, "\"keystroke\" attribute has invalid value for action with id=" + actionId
);
761 String keymapName
= element
.getAttributeValue(KEYMAP_ATTR_NAME
);
762 if (keymapName
== null || keymapName
.length() == 0) {
763 reportActionError(pluginId
, "attribute \"keymap\" should be defined");
766 Keymap keymap
= KeymapManager
.getInstance().getKeymap(keymapName
);
767 if (keymap
== null) {
768 reportActionError(pluginId
, "keymap \"" + keymapName
+ "\" not found");
772 keymap
.addShortcut(actionId
, shortcut
);
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() + "\"");
781 String ref
= element
.getAttributeValue(REF_ATTR_NAME
);
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");
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
+ ")");
801 assertActionIsGroupOrStub(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() + "\"");
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
);
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
));
854 if (myAction2Id
.containsKey(action
)) {
855 reportActionError(pluginId
, "action was already registered for another ID. ID is " + myAction2Id
.get(action
) +
856 getPluginInfo(pluginId
));
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) {
879 LOG
.error(new PluginException(message
, null, pluginId
));
884 private static String
getPluginInfo(@Nullable PluginId id
) {
886 final IdeaPluginDescriptor plugin
= ApplicationManager
.getApplication().getPlugin(id
);
887 if (plugin
!= null) {
888 String name
= plugin
.getName();
890 name
= id
.getIdString();
892 return " Plugin: " + name
;
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");
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
);
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
) {
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
);
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
) {
1025 listener
.afterActionPerformed(action
, dataContext
, event
);
1027 catch(AbstractMethodError e
) {
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
;
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() {
1077 ApplicationManager
.getApplication().executeOnPooledThread(myPreloadActionsRunnable
);
1081 private void doPreloadActions() {
1083 Thread
.sleep(5000); // wait for project initialization to complete
1085 catch (InterruptedException e
) {
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) {
1126 catch (InterruptedException e
) {
1133 private class MyTimer
extends Timer
implements ActionListener
{
1134 private final List
<TimerListener
> myTimerListeners
= Collections
.synchronizedList(new ArrayList
<TimerListener
>());
1135 private int myLastTimePerformed
;
1138 super(TIMER_DELAY
, null);
1139 addActionListener(this);
1143 public void addTimerListener(TimerListener listener
){
1144 myTimerListeners
.add(listener
);
1147 public void removeTimerListener(TimerListener listener
){
1148 final boolean removed
= myTimerListeners
.remove(listener
);
1150 LOG
.assertTrue(false, "Unknown listener " + listener
);
1154 public void actionPerformed(ActionEvent e
) {
1155 if (myLastTimeEditorWasTypedIn
+ UPDATE_DELAY_AFTER_TYPING
> System
.currentTimeMillis()) {
1159 final int lastEventCount
= myLastTimePerformed
;
1160 myLastTimePerformed
= ActivityTracker
.getInstance().getCount();
1162 if (myLastTimePerformed
== lastEventCount
) {
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
)) {
1179 catch (ProcessCanceledException ex
) {
1182 catch (Throwable e
) {
1189 public ActionCallback
tryToExecute(@NotNull final AnAction action
, @NotNull final InputEvent inputEvent
, @Nullable final Component contextComponent
, @Nullable final String place
,
1192 final Application app
= ApplicationManager
.getApplication();
1193 assert app
.isDispatchThread();
1195 final ActionCallback result
= new ActionCallback();
1196 final Runnable doRunnable
= new Runnable() {
1198 tryToExecuteNow(action
, inputEvent
, contextComponent
, place
, result
);
1205 SwingUtilities
.invokeLater(doRunnable
);
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() {
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();
1232 ActionUtil
.lastUpdateAndCheckDumb(action
, event
, false);
1233 if (!event
.getPresentation().isEnabled()) {
1234 result
.setRejected();
1238 Component component
= PlatformDataKeys
.CONTEXT_COMPONENT
.getData(context
);
1239 if (component
!= null && !component
.isShowing()) {
1240 result
.setRejected();
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() {
1259 }, WindowEvent
.WINDOW_EVENT_MASK
, result
);
1261 action
.actionPerformed(event
);
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();