Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / actionSystem / impl / ActionMenu.java
1 /*
2  * Copyright 2000-2016 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.ide.DataManager;
19 import com.intellij.ide.IdeEventQueue;
20 import com.intellij.ide.ui.UISettings;
21 import com.intellij.openapi.Disposable;
22 import com.intellij.openapi.actionSystem.*;
23 import com.intellij.openapi.actionSystem.impl.actionholder.ActionRef;
24 import com.intellij.openapi.ui.JBPopupMenu;
25 import com.intellij.openapi.util.Disposer;
26 import com.intellij.openapi.util.IconLoader;
27 import com.intellij.openapi.util.SystemInfo;
28 import com.intellij.openapi.wm.IdeFocusManager;
29 import com.intellij.openapi.wm.IdeFrame;
30 import com.intellij.openapi.wm.StatusBar;
31 import com.intellij.ui.components.JBMenu;
32 import com.intellij.ui.plaf.beg.IdeaMenuUI;
33 import com.intellij.ui.plaf.gtk.GtkMenuUI;
34 import com.intellij.util.ReflectionUtil;
35 import com.intellij.util.SingleAlarm;
36 import com.intellij.util.ui.JBUI;
37 import com.intellij.util.ui.UIUtil;
38 import org.jetbrains.annotations.NotNull;
39
40 import javax.swing.*;
41 import javax.swing.event.MenuEvent;
42 import javax.swing.event.MenuListener;
43 import javax.swing.plaf.MenuItemUI;
44 import javax.swing.plaf.synth.SynthMenuUI;
45 import java.awt.*;
46 import java.awt.event.AWTEventListener;
47 import java.awt.event.ComponentEvent;
48 import java.awt.event.KeyEvent;
49 import java.awt.event.MouseEvent;
50 import java.beans.PropertyChangeEvent;
51 import java.beans.PropertyChangeListener;
52
53 public final class ActionMenu extends JBMenu {
54   private final String myPlace;
55   private DataContext myContext;
56   private final ActionRef<ActionGroup> myGroup;
57   private final PresentationFactory myPresentationFactory;
58   private final Presentation myPresentation;
59   private boolean myMnemonicEnabled;
60   private MenuItemSynchronizer myMenuItemSynchronizer;
61   private StubItem myStubItem;  // A PATCH!!! Do not remove this code, otherwise you will lose all keyboard navigation in JMenuBar.
62   private final boolean myTopLevel;
63   private Disposable myDisposable;
64
65   public ActionMenu(final DataContext context,
66                     @NotNull final String place,
67                     final ActionGroup group,
68                     final PresentationFactory presentationFactory,
69                     final boolean enableMnemonics,
70                     final boolean topLevel) {
71     myContext = context;
72     myPlace = place;
73     myGroup = ActionRef.fromAction(group);
74     myPresentationFactory = presentationFactory;
75     myPresentation = myPresentationFactory.getPresentation(group);
76     myMnemonicEnabled = enableMnemonics;
77     myTopLevel = topLevel;
78
79     updateUI();
80
81     init();
82
83     // addNotify won't be called for menus in MacOS system menu
84     if (SystemInfo.isMacSystemMenu) {
85       installSynchronizer();
86     }
87     if (UIUtil.isUnderIntelliJLaF()) {
88       setOpaque(true);
89     }
90
91     // Triggering initialization of private field "popupMenu" from JMenu with our own JBPopupMenu
92     getPopupMenu();
93   }
94
95   public void updateContext(DataContext context) {
96     myContext = context;
97   }
98
99   @Override
100   public void addNotify() {
101     super.addNotify();
102     installSynchronizer();
103   }
104
105   private void installSynchronizer() {
106     if (myMenuItemSynchronizer == null) {
107       myMenuItemSynchronizer = new MenuItemSynchronizer();
108       myGroup.getAction().addPropertyChangeListener(myMenuItemSynchronizer);
109       myPresentation.addPropertyChangeListener(myMenuItemSynchronizer);
110     }
111   }
112
113   @Override
114   public void removeNotify() {
115     uninstallSynchronizer();
116     super.removeNotify();
117     if (myDisposable != null) {
118       Disposer.dispose(myDisposable);
119       myDisposable = null;
120     }
121   }
122
123   private void uninstallSynchronizer() {
124     if (myMenuItemSynchronizer != null) {
125       myGroup.getAction().removePropertyChangeListener(myMenuItemSynchronizer);
126       myPresentation.removePropertyChangeListener(myMenuItemSynchronizer);
127       myMenuItemSynchronizer = null;
128     }
129   }
130
131   private JPopupMenu mySpecialMenu;
132   @Override
133   public JPopupMenu getPopupMenu() {
134     if (mySpecialMenu == null) {
135       mySpecialMenu = new JBPopupMenu();
136       mySpecialMenu.setInvoker(this);
137       popupListener = createWinListener(mySpecialMenu);
138       ReflectionUtil.setField(JMenu.class, this, JPopupMenu.class, "popupMenu", mySpecialMenu);
139     }
140     return super.getPopupMenu();
141   }
142
143   @Override
144   public void updateUI() {
145     boolean isAmbiance = UIUtil.isUnderGTKLookAndFeel() && "Ambiance".equalsIgnoreCase(UIUtil.getGtkThemeName());
146     if (myTopLevel && !isAmbiance && UIUtil.GTK_AMBIANCE_TEXT_COLOR.equals(getForeground())) {
147       setForeground(null);
148     }
149
150     if (UIUtil.isStandardMenuLAF()) {
151       super.updateUI();
152     }
153     else {
154       setUI(IdeaMenuUI.createUI(this));
155       setFont(UIUtil.getMenuFont());
156
157       JPopupMenu popupMenu = getPopupMenu();
158       if (popupMenu != null) {
159         popupMenu.updateUI();
160       }
161     }
162
163     if (myTopLevel && isAmbiance) {
164       setForeground(UIUtil.GTK_AMBIANCE_TEXT_COLOR);
165     }
166
167     if (myTopLevel && UIUtil.isUnderGTKLookAndFeel()) {
168       Insets insets = getInsets();
169       @SuppressWarnings("UseDPIAwareInsets") Insets newInsets = new Insets(insets.top, insets.left, insets.bottom, insets.right);
170       if (insets.top + insets.bottom < JBUI.scale(6)) {
171         newInsets.top = newInsets.bottom = JBUI.scale(3);
172       }
173       if (insets.left + insets.right < JBUI.scale(12)) {
174         newInsets.left = newInsets.right = JBUI.scale(6);
175       }
176       if (!newInsets.equals(insets)) {
177         setBorder(BorderFactory.createEmptyBorder(newInsets.top, newInsets.left, newInsets.bottom, newInsets.right));
178       }
179     }
180   }
181
182   @Override
183   public void setUI(MenuItemUI ui) {
184     MenuItemUI newUi = !myTopLevel && UIUtil.isUnderGTKLookAndFeel() && ui instanceof SynthMenuUI ? new GtkMenuUI((SynthMenuUI)ui) : ui;
185     super.setUI(newUi);
186   }
187
188   private void init() {
189     boolean macSystemMenu = SystemInfo.isMacSystemMenu && myPlace.equals(ActionPlaces.MAIN_MENU);
190
191     myStubItem = macSystemMenu ? null : new StubItem();
192     addStubItem();
193     addMenuListener(new MenuListenerImpl());
194     setBorderPainted(false);
195
196     setVisible(myPresentation.isVisible());
197     setEnabled(myPresentation.isEnabled());
198     setText(myPresentation.getText());
199     updateIcon();
200
201     setMnemonicEnabled(myMnemonicEnabled);
202   }
203
204   private void addStubItem() {
205     if (myStubItem != null) {
206       add(myStubItem);
207     }
208   }
209
210   public void setMnemonicEnabled(boolean enable) {
211     myMnemonicEnabled = enable;
212     setMnemonic(myPresentation.getMnemonic());
213     setDisplayedMnemonicIndex(myPresentation.getDisplayedMnemonicIndex());
214   }
215
216   @Override
217   public void setDisplayedMnemonicIndex(final int index) throws IllegalArgumentException {
218     super.setDisplayedMnemonicIndex(myMnemonicEnabled ? index : -1);
219   }
220
221   @Override
222   public void setMnemonic(int mnemonic) {
223     super.setMnemonic(myMnemonicEnabled ? mnemonic : 0);
224   }
225
226   private void updateIcon() {
227     UISettings settings = UISettings.getInstance();
228     if (settings != null && settings.SHOW_ICONS_IN_MENUS) {
229       final Presentation presentation = myPresentation;
230       final Icon icon = presentation.getIcon();
231       setIcon(icon);
232       if (presentation.getDisabledIcon() != null) {
233         setDisabledIcon(presentation.getDisabledIcon());
234       }
235       else {
236         setDisabledIcon(IconLoader.getDisabledIcon(icon));
237       }
238     }
239   }
240
241   @Override
242   public void menuSelectionChanged(boolean isIncluded) {
243     super.menuSelectionChanged(isIncluded);
244     showDescriptionInStatusBar(isIncluded, this, myPresentation.getDescription());
245   }
246
247   public static void showDescriptionInStatusBar(boolean isIncluded, Component component, String description) {
248     IdeFrame frame = (IdeFrame)(component instanceof IdeFrame ? component : SwingUtilities.getAncestorOfClass(IdeFrame.class, component));
249     StatusBar statusBar;
250     if (frame != null && (statusBar = frame.getStatusBar()) != null) {
251       statusBar.setInfo(isIncluded ? description : null);
252     }
253   }
254
255   private class MenuListenerImpl implements MenuListener {
256     @Override
257     public void menuCanceled(MenuEvent e) {
258       clearItems();
259       addStubItem();
260     }
261
262     @Override
263     public void menuDeselected(MenuEvent e) {
264       if (myDisposable != null) {
265         Disposer.dispose(myDisposable);
266         myDisposable = null;
267       }
268       clearItems();
269       addStubItem();
270     }
271
272     @Override
273     public void menuSelected(MenuEvent e) {
274       UsabilityHelper helper = new UsabilityHelper(ActionMenu.this);
275       if (myDisposable == null) {
276         myDisposable = Disposer.newDisposable();
277       }
278       Disposer.register(myDisposable, helper);
279       fillMenu();
280     }
281   }
282
283   private void clearItems() {
284     if (SystemInfo.isMacSystemMenu && myPlace.equals(ActionPlaces.MAIN_MENU)) {
285       for (Component menuComponent : getMenuComponents()) {
286         if (menuComponent instanceof ActionMenu) {
287           ((ActionMenu)menuComponent).clearItems();
288           if (SystemInfo.isMacSystemMenu) {
289             // hideNotify is not called on Macs
290             ((ActionMenu)menuComponent).uninstallSynchronizer();
291           }
292         }
293         else if (menuComponent instanceof ActionMenuItem) {
294           // Looks like an old-fashioned ugly workaround
295           // JDK 1.7 on Mac works wrong with such functional keys
296           if (!(SystemInfo.isJavaVersionAtLeast("1.7") && SystemInfo.isMac)) {
297             ((ActionMenuItem)menuComponent).setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F24, 0));
298           }
299         }
300       }
301     }
302
303     removeAll();
304     validate();
305   }
306
307   private void fillMenu() {
308     DataContext context;
309     boolean mayContextBeInvalid;
310
311     if (myContext != null) {
312       context = myContext;
313       mayContextBeInvalid = false;
314     }
315     else {
316       @SuppressWarnings("deprecation") DataContext contextFromFocus = DataManager.getInstance().getDataContext();
317       context = contextFromFocus;
318       if (PlatformDataKeys.CONTEXT_COMPONENT.getData(context) == null) {
319         IdeFrame frame = UIUtil.getParentOfType(IdeFrame.class, this);
320         context = DataManager.getInstance().getDataContext(IdeFocusManager.getGlobalInstance().getLastFocusedFor(frame));
321       }
322       mayContextBeInvalid = true;
323     }
324
325     Utils.fillMenu(myGroup.getAction(), this, myMnemonicEnabled, myPresentationFactory, context, myPlace, true, mayContextBeInvalid);
326   }
327
328   private class MenuItemSynchronizer implements PropertyChangeListener {
329     @Override
330     public void propertyChange(PropertyChangeEvent e) {
331       String name = e.getPropertyName();
332       if (Presentation.PROP_VISIBLE.equals(name)) {
333         setVisible(myPresentation.isVisible());
334         if (SystemInfo.isMacSystemMenu && myPlace.equals(ActionPlaces.MAIN_MENU)) {
335           validateTree();
336         }
337       }
338       else if (Presentation.PROP_ENABLED.equals(name)) {
339         setEnabled(myPresentation.isEnabled());
340       }
341       else if (Presentation.PROP_MNEMONIC_KEY.equals(name)) {
342         setMnemonic(myPresentation.getMnemonic());
343       }
344       else if (Presentation.PROP_MNEMONIC_INDEX.equals(name)) {
345         setDisplayedMnemonicIndex(myPresentation.getDisplayedMnemonicIndex());
346       }
347       else if (Presentation.PROP_TEXT.equals(name)) {
348         setText(myPresentation.getText());
349       }
350       else if (Presentation.PROP_ICON.equals(name) || Presentation.PROP_DISABLED_ICON.equals(name)) {
351         updateIcon();
352       }
353     }
354   }
355   private static class UsabilityHelper implements IdeEventQueue.EventDispatcher, AWTEventListener, Disposable {
356
357     private Component myComponent;
358     private Point myLastMousePoint;
359     private Point myUpperTargetPoint;
360     private Point myLowerTargetPoint;
361     private SingleAlarm myCallbackAlarm;
362     private MouseEvent myEventToRedispatch;
363
364     private long myLastEventTime = 0L;
365     private boolean myInBounds = false;
366     private SingleAlarm myCheckAlarm;
367
368     private UsabilityHelper(Component component) {
369       myCallbackAlarm = new SingleAlarm(() -> {
370         Disposer.dispose(myCallbackAlarm);
371         myCallbackAlarm = null;
372         if (myEventToRedispatch != null) {
373           IdeEventQueue.getInstance().dispatchEvent(myEventToRedispatch);
374         }
375       }, 50, this);
376       myCheckAlarm = new SingleAlarm(() -> {
377         if (myLastEventTime > 0 && System.currentTimeMillis() - myLastEventTime > 1500) {
378           if (!myInBounds && myCallbackAlarm != null && !myCallbackAlarm.isDisposed()) {
379             myCallbackAlarm.request();
380           }
381         }
382         myCheckAlarm.request();
383       }, 100, this);
384       myComponent = component;
385       PointerInfo info = MouseInfo.getPointerInfo();
386       myLastMousePoint = info != null ? info.getLocation() : null;
387       if (myLastMousePoint != null) {
388         Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.COMPONENT_EVENT_MASK);
389         IdeEventQueue.getInstance().addDispatcher(this, this);
390       }
391     }
392
393     @Override
394     public void eventDispatched(AWTEvent event) {
395       if (event instanceof ComponentEvent) {
396         ComponentEvent componentEvent = (ComponentEvent)event;
397         Component component = componentEvent.getComponent();
398         JPopupMenu popup = UIUtil.getParentOfType(JPopupMenu.class, component);
399         if (popup != null && popup.getInvoker() == myComponent) {
400           Rectangle bounds = popup.getBounds();
401           if (bounds.isEmpty()) return;
402           bounds.setLocation(popup.getLocationOnScreen());
403           if (myLastMousePoint.x < bounds.x) {
404             myUpperTargetPoint = new Point(bounds.x, bounds.y);
405             myLowerTargetPoint = new Point(bounds.x, bounds.y + bounds.height);
406           }
407           if (myLastMousePoint.x > bounds.x + bounds.width) {
408             myUpperTargetPoint = new Point(bounds.x + bounds.width, bounds.y);
409             myLowerTargetPoint = new Point(bounds.x + bounds.width, bounds.y + bounds.height);
410           }
411         }
412       }
413     }
414
415     @Override
416     public boolean dispatch(AWTEvent e) {
417       if (e instanceof MouseEvent && myUpperTargetPoint != null && myLowerTargetPoint != null && myCallbackAlarm != null) {
418         if (e.getID() == MouseEvent.MOUSE_PRESSED || e.getID() == MouseEvent.MOUSE_RELEASED || e.getID() == MouseEvent.MOUSE_CLICKED) {
419           return false;
420         }
421         Point point = ((MouseEvent)e).getLocationOnScreen();
422         Rectangle bounds = myComponent.getBounds();
423         bounds.setLocation(myComponent.getLocationOnScreen());
424         myInBounds = bounds.contains(point);
425         boolean isMouseMovingTowardsSubmenu = myInBounds || new Polygon(
426           new int[]{myLastMousePoint.x, myUpperTargetPoint.x, myLowerTargetPoint.x},
427           new int[]{myLastMousePoint.y, myUpperTargetPoint.y, myLowerTargetPoint.y},
428           3).contains(point);
429
430         myEventToRedispatch = (MouseEvent)e;
431         myLastEventTime = System.currentTimeMillis();
432
433         if (!isMouseMovingTowardsSubmenu) {
434           myCallbackAlarm.request();
435         } else {
436           myCallbackAlarm.cancel();
437         }
438         myLastMousePoint = point;
439         return true;
440       }
441       return false;
442     }
443
444     @Override
445     public void dispose() {
446       myComponent = null;
447       myEventToRedispatch = null;
448       myLastMousePoint = myUpperTargetPoint = myLowerTargetPoint = null;
449       Toolkit.getDefaultToolkit().removeAWTEventListener(this);
450     }
451   }
452 }