2 * Copyright 2000-2015 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.featureStatistics.FeatureUsageTracker;
19 import com.intellij.ide.ui.UISettings;
20 import com.intellij.openapi.Disposable;
21 import com.intellij.openapi.actionSystem.*;
22 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
23 import com.intellij.openapi.actionSystem.ex.ActionUtil;
24 import com.intellij.openapi.actionSystem.impl.actionholder.ActionRef;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.application.TransactionGuard;
27 import com.intellij.openapi.keymap.KeymapManager;
28 import com.intellij.openapi.keymap.KeymapUtil;
29 import com.intellij.openapi.util.*;
30 import com.intellij.openapi.util.registry.Registry;
31 import com.intellij.openapi.wm.IdeFocusManager;
32 import com.intellij.ui.SizedIcon;
33 import com.intellij.ui.components.JBCheckBoxMenuItem;
34 import com.intellij.ui.plaf.beg.BegMenuItemUI;
35 import com.intellij.ui.plaf.gtk.GtkMenuItemUI;
36 import com.intellij.util.PlatformIcons;
37 import com.intellij.util.ui.EmptyIcon;
38 import com.intellij.util.ui.UIUtil;
39 import org.jetbrains.annotations.NonNls;
40 import org.jetbrains.annotations.NotNull;
43 import javax.swing.plaf.MenuItemUI;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.KeyEvent;
48 import java.awt.event.MouseEvent;
49 import java.beans.PropertyChangeEvent;
50 import java.beans.PropertyChangeListener;
51 import java.util.HashSet;
54 public class ActionMenuItem extends JBCheckBoxMenuItem {
55 private static final Icon ourCheckedIcon = new SizedIcon(PlatformIcons.CHECK_ICON, 18, 18);
56 private static final Icon ourUncheckedIcon = EmptyIcon.ICON_18;
58 private final ActionRef<AnAction> myAction;
59 private final Presentation myPresentation;
60 private final String myPlace;
61 private final boolean myInsideCheckedGroup;
62 private final boolean myEnableMnemonics;
63 private final boolean myToggleable;
64 private DataContext myContext;
65 private AnActionEvent myEvent;
66 private MenuItemSynchronizer myMenuItemSynchronizer;
67 private boolean myToggled;
69 public ActionMenuItem(final AnAction action,
70 final Presentation presentation,
71 @NotNull final String place,
72 @NotNull DataContext context,
73 final boolean enableMnemonics,
74 final boolean prepareNow,
75 final boolean insideCheckedGroup) {
76 myAction = ActionRef.fromAction(action);
77 myPresentation = presentation;
80 myEnableMnemonics = enableMnemonics;
81 myToggleable = action instanceof Toggleable;
82 myInsideCheckedGroup = insideCheckedGroup;
84 myEvent = new AnActionEvent(null, context, place, myPresentation, ActionManager.getInstance(), 0);
85 addActionListener(new ActionTransmitter());
86 setBorderPainted(false);
94 setText("loading...");
98 private static boolean isEnterKeyStroke(KeyStroke keyStroke) {
99 return keyStroke.getKeyCode() == KeyEvent.VK_ENTER && keyStroke.getModifiers() == 0;
102 public void prepare() {
104 installSynchronizer();
108 * We have to make this method public to allow BegMenuItemUI to invoke it.
111 public void fireActionPerformed(ActionEvent event) {
112 TransactionGuard.submitTransaction(ApplicationManager.getApplication(), () -> super.fireActionPerformed(event));
116 public void addNotify() {
118 installSynchronizer();
123 public void removeNotify() {
124 uninstallSynchronizer();
125 super.removeNotify();
128 private void installSynchronizer() {
129 if (myMenuItemSynchronizer == null) {
130 myMenuItemSynchronizer = new MenuItemSynchronizer();
134 private void uninstallSynchronizer() {
135 if (myMenuItemSynchronizer != null) {
136 Disposer.dispose(myMenuItemSynchronizer);
137 myMenuItemSynchronizer = null;
141 private void init() {
142 setVisible(myPresentation.isVisible());
143 setEnabled(myPresentation.isEnabled());
144 setMnemonic(myEnableMnemonics ? myPresentation.getMnemonic() : 0);
145 setText(myPresentation.getText());
146 final int mnemonicIndex = myEnableMnemonics ? myPresentation.getDisplayedMnemonicIndex() : -1;
148 if (getText() != null && mnemonicIndex >= 0 && mnemonicIndex < getText().length()) {
149 setDisplayedMnemonicIndex(mnemonicIndex);
152 AnAction action = myAction.getAction();
154 String id = ActionManager.getInstance().getId(action);
156 setAcceleratorFromShortcuts(KeymapManager.getInstance().getActiveKeymap().getShortcuts(id));
159 final ShortcutSet shortcutSet = action.getShortcutSet();
160 if (shortcutSet != null) {
161 setAcceleratorFromShortcuts(shortcutSet.getShortcuts());
166 private void setAcceleratorFromShortcuts(@NotNull Shortcut[] shortcuts) {
167 for (Shortcut shortcut : shortcuts) {
168 if (shortcut instanceof KeyboardShortcut) {
169 final KeyStroke firstKeyStroke = ((KeyboardShortcut)shortcut).getFirstKeyStroke();
170 //If action has Enter shortcut, do not add it. Otherwise, user won't be able to chose any ActionMenuItem other than that
171 if (!isEnterKeyStroke(firstKeyStroke)) {
172 setAccelerator(firstKeyStroke);
180 public void updateUI() {
181 if (UIUtil.isStandardMenuLAF()) {
185 setUI(BegMenuItemUI.createUI(this));
190 public void setUI(final MenuItemUI ui) {
191 final MenuItemUI newUi = UIUtil.isUnderGTKLookAndFeel() && GtkMenuItemUI.isUiAcceptable(ui) ? new GtkMenuItemUI(ui) : ui;
196 * Updates long description of action at the status bar.
199 public void menuSelectionChanged(boolean isIncluded) {
200 super.menuSelectionChanged(isIncluded);
201 ActionMenu.showDescriptionInStatusBar(isIncluded, this, myPresentation.getDescription());
204 public String getFirstShortcutText() {
205 return KeymapUtil.getFirstKeyboardShortcutText(myAction.getAction());
208 public void updateContext(@NotNull DataContext context) {
210 myEvent = new AnActionEvent(null, context, myPlace, myPresentation, ActionManager.getInstance(), 0);
213 private void updateIcon(AnAction action) {
214 if (isToggleable() && (myPresentation.getIcon() == null || myInsideCheckedGroup || !UISettings.getInstance().SHOW_ICONS_IN_MENUS)) {
215 action.update(myEvent);
216 myToggled = Boolean.TRUE.equals(myEvent.getPresentation().getClientProperty(Toggleable.SELECTED_PROPERTY));
217 if (ActionPlaces.MAIN_MENU.equals(myPlace) && SystemInfo.isMacSystemMenu ||
218 UIUtil.isUnderNimbusLookAndFeel() ||
219 UIUtil.isUnderWindowsLookAndFeel() && SystemInfo.isWin7OrNewer) {
222 else if (!(getUI() instanceof GtkMenuItemUI)) {
224 setIcon(ourCheckedIcon);
225 setDisabledIcon(IconLoader.getDisabledIcon(ourCheckedIcon));
228 setIcon(ourUncheckedIcon);
229 setDisabledIcon(IconLoader.getDisabledIcon(ourUncheckedIcon));
234 if (UISettings.getInstance().SHOW_ICONS_IN_MENUS) {
235 Icon icon = myPresentation.getIcon();
236 if (action instanceof ToggleAction && ((ToggleAction)action).isSelected(myEvent)) {
237 icon = new PoppedIcon(icon, 16, 16);
240 if (myPresentation.getDisabledIcon() != null) {
241 setDisabledIcon(myPresentation.getDisabledIcon());
244 setDisabledIcon(IconLoader.getDisabledIcon(icon));
251 public void setIcon(Icon icon) {
252 if (SystemInfo.isMacSystemMenu && ActionPlaces.MAIN_MENU.equals(myPlace)) {
253 if (icon instanceof IconLoader.LazyIcon) {
254 // [tav] JDK can't paint correctly our HiDPI icons at the system menu bar
255 icon = ((IconLoader.LazyIcon)icon).inNormalScale(false);
261 public boolean isToggleable() {
266 public boolean isSelected() {
270 private final class ActionTransmitter implements ActionListener {
272 * @param component component
273 * @return whether the component in Swing tree or not. This method is more
274 * weak then {@link Component#isShowing() }
276 private boolean isInTree(final Component component) {
277 if (component instanceof Window) {
278 return component.isShowing();
281 Window windowAncestor = SwingUtilities.getWindowAncestor(component);
282 return windowAncestor != null && windowAncestor.isShowing();
287 public void actionPerformed(final ActionEvent e) {
288 final IdeFocusManager fm = IdeFocusManager.findInstanceByContext(myContext);
289 final ActionCallback typeAhead = new ActionCallback();
290 final String id = ActionManager.getInstance().getId(myAction.getAction());
292 FeatureUsageTracker.getInstance().triggerFeatureUsed("context.menu.click.stats." + id.replace(' ', '.'));
294 fm.typeAheadUntil(typeAhead);
295 fm.runOnOwnContext(myContext, () -> {
296 final AnActionEvent event = new AnActionEvent(
297 new MouseEvent(ActionMenuItem.this, MouseEvent.MOUSE_PRESSED, 0, e.getModifiers(), getWidth() / 2, getHeight() / 2, 1, false),
298 myContext, myPlace, myPresentation, ActionManager.getInstance(), e.getModifiers()
300 final AnAction action1 = myAction.getAction();
301 if (ActionUtil.lastUpdateAndCheckDumb(action1, event, false)) {
302 ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
303 actionManager.fireBeforeActionPerformed(action1, myContext, event);
304 Component component1 = PlatformDataKeys.CONTEXT_COMPONENT.getData(event.getDataContext());
305 if (component1 != null && !isInTree(component1)) {
310 SimpleTimer.getInstance().setUp(() -> {
311 //noinspection SSBasedInspection
312 SwingUtilities.invokeLater(() -> fm.doWhenFocusSettlesDown(typeAhead.createSetDoneRunnable()));
313 }, Registry.intValue("actionSystem.typeAheadTimeAfterPopupAction"));
315 ActionUtil.performActionDumbAware(action1, event);
316 actionManager.queueActionPerformedEvent(action1, myContext, event);
325 private final class MenuItemSynchronizer implements PropertyChangeListener, Disposable {
326 @NonNls private static final String SELECTED = "selected";
328 private final Set<String> mySynchronized = new HashSet<>();
330 private MenuItemSynchronizer() {
331 myPresentation.addPropertyChangeListener(this);
335 public void dispose() {
336 myPresentation.removePropertyChangeListener(this);
340 public void propertyChange(PropertyChangeEvent e) {
341 boolean queueForDispose = getParent() == null;
343 String name = e.getPropertyName();
344 if (mySynchronized.contains(name)) return;
346 mySynchronized.add(name);
349 if (Presentation.PROP_VISIBLE.equals(name)) {
350 final boolean visible = myPresentation.isVisible();
351 if (!visible && SystemInfo.isMacSystemMenu && myPlace.equals(ActionPlaces.MAIN_MENU)) {
358 else if (Presentation.PROP_ENABLED.equals(name)) {
359 setEnabled(myPresentation.isEnabled());
360 updateIcon(myAction.getAction());
362 else if (Presentation.PROP_MNEMONIC_KEY.equals(name)) {
363 setMnemonic(myPresentation.getMnemonic());
365 else if (Presentation.PROP_MNEMONIC_INDEX.equals(name)) {
366 setDisplayedMnemonicIndex(myPresentation.getDisplayedMnemonicIndex());
368 else if (Presentation.PROP_TEXT.equals(name)) {
369 setText(myPresentation.getText());
371 else if (Presentation.PROP_ICON.equals(name) || Presentation.PROP_DISABLED_ICON.equals(name) || SELECTED.equals(name)) {
372 updateIcon(myAction.getAction());
376 mySynchronized.remove(name);
377 if (queueForDispose) {
378 // later since we cannot remove property listeners inside event processing
379 //noinspection SSBasedInspection
380 SwingUtilities.invokeLater(() -> {
381 if (getParent() == null) {
382 uninstallSynchronizer();