Merge branch 'db/javac-ast'
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / actionSystem / impl / ActionMenuItem.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.openapi.actionSystem.impl;
17
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;
41
42 import javax.swing.*;
43 import javax.swing.plaf.MenuItemUI;
44 import java.awt.*;
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;
52 import java.util.Set;
53
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;
57
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;
68
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;
78     myPlace = place;
79     myContext = context;
80     myEnableMnemonics = enableMnemonics;
81     myToggleable = action instanceof Toggleable;
82     myInsideCheckedGroup = insideCheckedGroup;
83
84     myEvent = new AnActionEvent(null, context, place, myPresentation, ActionManager.getInstance(), 0);
85     addActionListener(new ActionTransmitter());
86     setBorderPainted(false);
87
88     updateUI();
89
90     if (prepareNow) {
91       init();
92     }
93     else {
94       setText("loading...");
95     }
96   }
97
98   private static boolean isEnterKeyStroke(KeyStroke keyStroke) {
99     return keyStroke.getKeyCode() == KeyEvent.VK_ENTER && keyStroke.getModifiers() == 0;
100   }
101
102   public void prepare() {
103     init();
104     installSynchronizer();
105   }
106
107   /**
108    * We have to make this method public to allow BegMenuItemUI to invoke it.
109    */
110   @Override
111   public void fireActionPerformed(ActionEvent event) {
112     TransactionGuard.submitTransaction(ApplicationManager.getApplication(), () -> super.fireActionPerformed(event));
113   }
114
115   @Override
116   public void addNotify() {
117     super.addNotify();
118     installSynchronizer();
119     init();
120   }
121
122   @Override
123   public void removeNotify() {
124     uninstallSynchronizer();
125     super.removeNotify();
126   }
127
128   private void installSynchronizer() {
129     if (myMenuItemSynchronizer == null) {
130       myMenuItemSynchronizer = new MenuItemSynchronizer();
131     }
132   }
133
134   private void uninstallSynchronizer() {
135     if (myMenuItemSynchronizer != null) {
136       Disposer.dispose(myMenuItemSynchronizer);
137       myMenuItemSynchronizer = null;
138     }
139   }
140
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;
147
148     if (getText() != null && mnemonicIndex >= 0 && mnemonicIndex < getText().length()) {
149       setDisplayedMnemonicIndex(mnemonicIndex);
150     }
151
152     AnAction action = myAction.getAction();
153     updateIcon(action);
154     String id = ActionManager.getInstance().getId(action);
155     if (id != null) {
156       setAcceleratorFromShortcuts(KeymapManager.getInstance().getActiveKeymap().getShortcuts(id));
157     }
158     else {
159       final ShortcutSet shortcutSet = action.getShortcutSet();
160       if (shortcutSet != null) {
161         setAcceleratorFromShortcuts(shortcutSet.getShortcuts());
162       }
163     }
164   }
165
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);
173         }
174         break;
175       }
176     }
177   }
178
179   @Override
180   public void updateUI() {
181     if (UIUtil.isStandardMenuLAF()) {
182       super.updateUI();
183     }
184     else {
185       setUI(BegMenuItemUI.createUI(this));
186     }
187   }
188
189   @Override
190   public void setUI(final MenuItemUI ui) {
191     final MenuItemUI newUi = UIUtil.isUnderGTKLookAndFeel() && GtkMenuItemUI.isUiAcceptable(ui) ? new GtkMenuItemUI(ui) : ui;
192     super.setUI(newUi);
193   }
194
195   /**
196    * Updates long description of action at the status bar.
197    */
198   @Override
199   public void menuSelectionChanged(boolean isIncluded) {
200     super.menuSelectionChanged(isIncluded);
201     ActionMenu.showDescriptionInStatusBar(isIncluded, this, myPresentation.getDescription());
202   }
203
204   public String getFirstShortcutText() {
205     return KeymapUtil.getFirstKeyboardShortcutText(myAction.getAction());
206   }
207
208   public void updateContext(@NotNull DataContext context) {
209     myContext = context;
210     myEvent = new AnActionEvent(null, context, myPlace, myPresentation, ActionManager.getInstance(), 0);
211   }
212
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) {
220         setState(myToggled);
221       }
222       else if (!(getUI() instanceof GtkMenuItemUI)) {
223         if (myToggled) {
224           setIcon(ourCheckedIcon);
225           setDisabledIcon(IconLoader.getDisabledIcon(ourCheckedIcon));
226         }
227         else {
228           setIcon(ourUncheckedIcon);
229           setDisabledIcon(IconLoader.getDisabledIcon(ourUncheckedIcon));
230         }
231       }
232     }
233     else {
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);
238         }
239         setIcon(icon);
240         if (myPresentation.getDisabledIcon() != null) {
241           setDisabledIcon(myPresentation.getDisabledIcon());
242         }
243         else {
244           setDisabledIcon(IconLoader.getDisabledIcon(icon));
245         }
246       }
247     }
248   }
249
250   @Override
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);
256       }
257     }
258     super.setIcon(icon);
259   }
260
261   public boolean isToggleable() {
262     return myToggleable;
263   }
264
265   @Override
266   public boolean isSelected() {
267     return myToggled;
268   }
269
270   private final class ActionTransmitter implements ActionListener {
271     /**
272      * @param component component
273      * @return whether the component in Swing tree or not. This method is more
274      *         weak then {@link Component#isShowing() }
275      */
276     private boolean isInTree(final Component component) {
277       if (component instanceof Window) {
278         return component.isShowing();
279       }
280       else {
281         Window windowAncestor = SwingUtilities.getWindowAncestor(component);
282         return windowAncestor != null && windowAncestor.isShowing();
283       }
284     }
285
286     @Override
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());
291       if (id != null) {
292         FeatureUsageTracker.getInstance().triggerFeatureUsed("context.menu.click.stats." + id.replace(' ', '.'));
293       }
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()
299         );
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)) {
306             typeAhead.setDone();
307             return;
308           }
309
310           SimpleTimer.getInstance().setUp(() -> {
311             //noinspection SSBasedInspection
312             SwingUtilities.invokeLater(() -> fm.doWhenFocusSettlesDown(typeAhead.createSetDoneRunnable()));
313           }, Registry.intValue("actionSystem.typeAheadTimeAfterPopupAction"));
314
315           ActionUtil.performActionDumbAware(action1, event);
316           actionManager.queueActionPerformedEvent(action1, myContext, event);
317         }
318         else {
319           typeAhead.setDone();
320         }
321       });
322     }
323   }
324
325   private final class MenuItemSynchronizer implements PropertyChangeListener, Disposable {
326     @NonNls private static final String SELECTED = "selected";
327
328     private final Set<String> mySynchronized = new HashSet<>();
329
330     private MenuItemSynchronizer() {
331       myPresentation.addPropertyChangeListener(this);
332     }
333
334     @Override
335     public void dispose() {
336       myPresentation.removePropertyChangeListener(this);
337     }
338
339     @Override
340     public void propertyChange(PropertyChangeEvent e) {
341       boolean queueForDispose = getParent() == null;
342
343       String name = e.getPropertyName();
344       if (mySynchronized.contains(name)) return;
345
346       mySynchronized.add(name);
347
348       try {
349         if (Presentation.PROP_VISIBLE.equals(name)) {
350           final boolean visible = myPresentation.isVisible();
351           if (!visible && SystemInfo.isMacSystemMenu && myPlace.equals(ActionPlaces.MAIN_MENU)) {
352             setEnabled(false);
353           }
354           else {
355             setVisible(visible);
356           }
357         }
358         else if (Presentation.PROP_ENABLED.equals(name)) {
359           setEnabled(myPresentation.isEnabled());
360           updateIcon(myAction.getAction());
361         }
362         else if (Presentation.PROP_MNEMONIC_KEY.equals(name)) {
363           setMnemonic(myPresentation.getMnemonic());
364         }
365         else if (Presentation.PROP_MNEMONIC_INDEX.equals(name)) {
366           setDisplayedMnemonicIndex(myPresentation.getDisplayedMnemonicIndex());
367         }
368         else if (Presentation.PROP_TEXT.equals(name)) {
369           setText(myPresentation.getText());
370         }
371         else if (Presentation.PROP_ICON.equals(name) || Presentation.PROP_DISABLED_ICON.equals(name) || SELECTED.equals(name)) {
372           updateIcon(myAction.getAction());
373         }
374       }
375       finally {
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();
383             }
384           });
385         }
386       }
387     }
388   }
389 }