7e38b1a737775ca5f43daba3fa0e91343705b73e
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / actionSystem / impl / ActionButton.java
1 /*
2  * Copyright 2000-2017 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.icons.AllIcons;
19 import com.intellij.ide.DataManager;
20 import com.intellij.internal.statistic.ToolbarClicksCollector;
21 import com.intellij.openapi.actionSystem.*;
22 import com.intellij.openapi.actionSystem.ex.ActionButtonLook;
23 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
24 import com.intellij.openapi.actionSystem.ex.ActionUtil;
25 import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
26 import com.intellij.openapi.application.impl.LaterInvocator;
27 import com.intellij.openapi.keymap.KeymapUtil;
28 import com.intellij.openapi.util.IconLoader;
29 import com.intellij.util.ui.EmptyIcon;
30 import com.intellij.util.ui.JBDimension;
31 import com.intellij.util.ui.JBUI;
32 import com.intellij.util.ui.UIUtil;
33 import com.intellij.util.ui.accessibility.ScreenReader;
34 import org.jetbrains.annotations.NotNull;
35 import org.jetbrains.annotations.Nullable;
36
37 import javax.accessibility.*;
38 import javax.swing.*;
39 import java.awt.*;
40 import java.awt.event.*;
41 import java.beans.PropertyChangeEvent;
42 import java.beans.PropertyChangeListener;
43
44 import static java.awt.event.KeyEvent.VK_SPACE;
45
46 public class ActionButton extends JComponent implements ActionButtonComponent, AnActionHolder, Accessible {
47
48   private JBDimension myMinimumButtonSize;
49   private PropertyChangeListener myPresentationListener;
50   private Icon myDisabledIcon;
51   private Icon myIcon;
52   protected final Presentation myPresentation;
53   protected final AnAction myAction;
54   protected final String myPlace;
55   private ActionButtonLook myLook = ActionButtonLook.IDEA_LOOK;
56   private boolean myMouseDown;
57   private boolean myRollover;
58   private static boolean ourGlobalMouseDown = false;
59
60   private boolean myNoIconsInPopup = false;
61   private Insets myInsets;
62
63   public ActionButton(AnAction action,
64                       Presentation presentation,
65                       String place,
66                       @NotNull Dimension minimumSize) {
67     setMinimumButtonSize(minimumSize);
68     setIconInsets(null);
69     myRollover = false;
70     myMouseDown = false;
71     myAction = action;
72     myPresentation = presentation;
73     myPlace = place;
74     // Button should be focusable if screen reader is active
75     setFocusable(ScreenReader.isActive());
76     enableEvents(AWTEvent.MOUSE_EVENT_MASK);
77     // Pressing the SPACE key is the same as clicking the button
78     addKeyListener(new KeyAdapter() {
79       @Override
80       public void keyReleased(KeyEvent e) {
81         if (e.getModifiers() == 0 && e.getKeyCode() == VK_SPACE) {
82           click();
83         }
84       }
85     });
86     addFocusListener(new FocusListener() {
87       @Override
88       public void focusGained(FocusEvent e) {
89         repaint();
90       }
91
92       @Override
93       public void focusLost(FocusEvent e) {
94         repaint();
95       }
96     });
97
98     putClientProperty(UIUtil.CENTER_TOOLTIP_DEFAULT, Boolean.TRUE);
99   }
100
101   public void setNoIconsInPopup(boolean noIconsInPopup) {
102     myNoIconsInPopup = noIconsInPopup;
103   }
104
105   public void setMinimumButtonSize(@NotNull Dimension size) {
106     myMinimumButtonSize = JBDimension.create(size);
107   }
108
109   public void paintChildren(Graphics g) {}
110
111   public int getPopState() {
112     if (myAction instanceof Toggleable) {
113       Boolean selected = (Boolean)myPresentation.getClientProperty(Toggleable.SELECTED_PROPERTY);
114       boolean flag1 = selected != null && selected.booleanValue();
115       return getPopState(flag1);
116     }
117     else {
118       return getPopState(false);
119     }
120   }
121
122   @Override
123   public boolean isEnabled() {
124     return super.isEnabled() && myPresentation.isEnabled();
125   }
126
127   protected boolean isButtonEnabled() {
128     return isEnabled();
129   }
130
131   private void onMousePresenceChanged(boolean setInfo) {
132     ActionMenu.showDescriptionInStatusBar(setInfo, this, myPresentation.getDescription());
133   }
134
135   public void click() {
136     performAction(new MouseEvent(this, MouseEvent.MOUSE_CLICKED, System.currentTimeMillis(), 0, 0, 0, 1, false));
137   }
138
139   private void performAction(MouseEvent e) {
140     AnActionEvent event = AnActionEvent.createFromInputEvent(e, myPlace, myPresentation, getDataContext(), false, true);
141     if (!ActionUtil.lastUpdateAndCheckDumb(myAction, event, false)) {
142       return;
143     }
144
145     if (isButtonEnabled()) {
146       final ActionManagerEx manager = ActionManagerEx.getInstanceEx();
147       final DataContext dataContext = event.getDataContext();
148       manager.fireBeforeActionPerformed(myAction, dataContext, event);
149       Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext);
150       if (component != null && !component.isShowing()) {
151         return;
152       }
153       actionPerformed(event);
154       manager.queueActionPerformedEvent(myAction, dataContext, event);
155       if (event.getInputEvent() instanceof MouseEvent) {
156         ToolbarClicksCollector.record(myAction, myPlace);
157       }
158     }
159   }
160
161   protected DataContext getDataContext() {
162     ActionToolbar actionToolbar = UIUtil.getParentOfType(ActionToolbar.class, this);
163     return actionToolbar != null ? actionToolbar.getToolbarDataContext() : DataManager.getInstance().getDataContext();
164   }
165
166   private void actionPerformed(final AnActionEvent event) {
167     if (myAction instanceof ActionGroup &&
168         !(myAction instanceof CustomComponentAction) &&
169         ((ActionGroup)myAction).isPopup() &&
170         !((ActionGroup)myAction).canBePerformed(event.getDataContext())) {
171       final ActionManagerImpl am = (ActionManagerImpl)ActionManager.getInstance();
172       ActionPopupMenuImpl popupMenu = (ActionPopupMenuImpl)am.createActionPopupMenu(event.getPlace(), (ActionGroup)myAction, new MenuItemPresentationFactory() {
173         @Override
174         protected void processPresentation(Presentation presentation) {
175           if (myNoIconsInPopup) {
176             presentation.setIcon(null);
177             presentation.setHoveredIcon(null);
178           }
179         }
180       });
181       popupMenu.setDataContextProvider(() -> this.getDataContext());
182       if (event.isActionToolbar()) {
183         popupMenu.getComponent().show(this, 0, getHeight());
184       }
185       else {
186         popupMenu.getComponent().show(this, getWidth(), 0);
187       }
188
189     } else {
190       ActionUtil.performActionDumbAware(myAction, event);
191     }
192   }
193
194   public void removeNotify() {
195     if (myPresentationListener != null) {
196       myPresentation.removePropertyChangeListener(myPresentationListener);
197       myPresentationListener = null;
198     }
199     super.removeNotify();
200   }
201
202   public void addNotify() {
203     super.addNotify();
204     if (myPresentationListener == null) {
205       myPresentation.addPropertyChangeListener(myPresentationListener = this::presentationPropertyChanded);
206     }
207     AnActionEvent e = AnActionEvent.createFromInputEvent(null, myPlace, myPresentation, getDataContext(), false, true);
208     ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), myAction, e, false);
209     updateToolTipText();
210     updateIcon();
211   }
212
213   public void setToolTipText(String s) {
214     String tooltipText = KeymapUtil.createTooltipText(s, myAction);
215     super.setToolTipText(tooltipText.length() > 0 ? tooltipText : null);
216   }
217
218   public Dimension getPreferredSize() {
219     Icon icon = getIcon();
220     if (icon.getIconWidth() < myMinimumButtonSize.width &&
221         icon.getIconHeight() < myMinimumButtonSize.height) {
222       return myMinimumButtonSize;
223     }
224     else {
225       return new Dimension(
226         Math.max(myMinimumButtonSize.width, icon.getIconWidth() + myInsets.left + myInsets.right),
227         Math.max(myMinimumButtonSize.height, icon.getIconHeight() + myInsets.top + myInsets.bottom)
228       );
229     }
230   }
231
232
233   public void setIconInsets(@Nullable Insets insets) {
234     myInsets = insets != null ? JBUI.insets(insets) : new Insets(0,0,0,0);
235   }
236
237   public Dimension getMinimumSize() {
238     return getPreferredSize();
239   }
240
241   /**
242    * @return button's icon. Icon depends on action's state. It means that the method returns
243    *         disabled icon if action is disabled. If the action's icon is <code>null</code> then it returns
244    *         an empty icon.
245    */
246   public Icon getIcon() {
247     Icon icon = isButtonEnabled() ? myIcon : myDisabledIcon;
248     return icon == null ? EmptyIcon.ICON_18 : icon;
249   }
250
251   public void updateIcon() {
252     myIcon = myPresentation.getIcon();
253     if (myPresentation.getDisabledIcon() != null) { // set disabled icon if it is specified
254       myDisabledIcon = myPresentation.getDisabledIcon();
255     }
256     else {
257       myDisabledIcon = IconLoader.getDisabledIcon(myIcon);
258     }
259   }
260
261   private void setDisabledIcon(Icon icon) {
262     myDisabledIcon = icon;
263   }
264
265   void updateToolTipText() {
266     String text = myPresentation.getText();
267     setToolTipText(text == null ? myPresentation.getDescription() : text);
268   }
269
270   public void paintComponent(Graphics g) {
271     super.paintComponent(g);
272
273     paintButtonLook(g);
274
275     if (myAction instanceof ActionGroup && ((ActionGroup)myAction).isPopup()) {
276       AllIcons.General.Dropdown.paintIcon(this, g, JBUI.scale(5), JBUI.scale(4));
277     }
278   }
279
280   protected void paintButtonLook(Graphics g) {
281     ActionButtonLook look = getButtonLook();
282     look.paintBackground(g, this);
283     look.paintIcon(g, this, getIcon());
284     look.paintBorder(g, this);
285   }
286
287   protected ActionButtonLook getButtonLook() {
288     return myLook;
289   }
290
291   public void setLook(ActionButtonLook look) {
292     if (look != null) {
293       myLook = look;
294     }
295     else {
296       myLook = ActionButtonLook.IDEA_LOOK;
297     }
298     repaint();
299   }
300
301   protected void processMouseEvent(MouseEvent e) {
302     super.processMouseEvent(e);
303     if (e.isConsumed()) return;
304     boolean skipPress = e.isMetaDown() || e.getButton() != MouseEvent.BUTTON1;
305     switch (e.getID()) {
306       case MouseEvent.MOUSE_PRESSED:
307         if (skipPress || !isButtonEnabled()) return;
308         myMouseDown = true;
309         ourGlobalMouseDown = true;
310         repaint();
311         break;
312
313       case MouseEvent.MOUSE_RELEASED:
314         if (skipPress || !isButtonEnabled()) return;
315         myMouseDown = false;
316         ourGlobalMouseDown = false;
317         if (myRollover) {
318           performAction(e);
319         }
320         repaint();
321         break;
322
323       case MouseEvent.MOUSE_ENTERED:
324         if (!myMouseDown && ourGlobalMouseDown) break;
325         myRollover = true;
326         repaint();
327         onMousePresenceChanged(true);
328         break;
329
330       case MouseEvent.MOUSE_EXITED:
331         myRollover = false;
332         if (!myMouseDown && ourGlobalMouseDown) break;
333         repaint();
334         onMousePresenceChanged(false);
335         break;
336     }
337   }
338
339   private int getPopState(boolean isPushed) {
340     if (isPushed || myRollover && myMouseDown && isButtonEnabled()) {
341       return PUSHED;
342     }
343     else if (myRollover && isButtonEnabled()) {
344       return POPPED;
345     }
346     else if (isFocusOwner()) {
347       return SELECTED;
348     }
349     else {
350       return NORMAL;
351     }
352   }
353
354   public AnAction getAction() {
355     return myAction;
356   }
357
358   protected void presentationPropertyChanded(PropertyChangeEvent e) {
359     String propertyName = e.getPropertyName();
360     if (Presentation.PROP_TEXT.equals(propertyName)) {
361       updateToolTipText();
362     }
363     else if (Presentation.PROP_ENABLED.equals(propertyName)) {
364       updateIcon();
365       repaint();
366     }
367     else if (Presentation.PROP_ICON.equals(propertyName)) {
368       updateIcon();
369       repaint();
370     }
371     else if (Presentation.PROP_DISABLED_ICON.equals(propertyName)) {
372       setDisabledIcon(myPresentation.getDisabledIcon());
373       repaint();
374     }
375     else if (Presentation.PROP_VISIBLE.equals(propertyName)) {
376     }
377     else if ("selected".equals(propertyName)) {
378       repaint();
379     }
380   }
381
382   // Accessibility
383
384   @Override
385   public AccessibleContext getAccessibleContext() {
386     if(this.accessibleContext == null) {
387       this.accessibleContext = new AccessibleActionButton();
388     }
389
390     return this.accessibleContext;
391   }
392
393
394   protected class AccessibleActionButton extends JComponent.AccessibleJComponent implements AccessibleAction {
395     public AccessibleActionButton() {
396     }
397
398     @Override
399     public AccessibleRole getAccessibleRole() {
400       return AccessibleRole.PUSH_BUTTON;
401     }
402
403     @Override
404     public String getAccessibleName() {
405       String name = accessibleName;
406       if (name == null) {
407         name = (String)ActionButton.this.getClientProperty(ACCESSIBLE_NAME_PROPERTY);
408         if (name == null) {
409           name = ActionButton.this.getToolTipText();
410           if (name == null) {
411             name = ActionButton.this.myPresentation.getText();
412             if (name == null) {
413               name = super.getAccessibleName();
414             }
415           }
416         }
417       }
418
419       return name;
420     }
421
422     @Override
423     public AccessibleIcon[] getAccessibleIcon() {
424       Icon icon = ActionButton.this.getIcon();
425       if (icon instanceof Accessible) {
426         AccessibleContext context = ((Accessible)icon).getAccessibleContext();
427         if (context != null && context instanceof AccessibleIcon) {
428           return new AccessibleIcon[]{(AccessibleIcon)context};
429         }
430       }
431
432       return null;
433     }
434
435     @Override
436     public AccessibleStateSet getAccessibleStateSet() {
437       AccessibleStateSet var1 = super.getAccessibleStateSet();
438       int state = ActionButton.this.getPopState();
439
440       // TODO: Not sure what the "POPPED" state represents
441       //if (state == POPPED) {
442       //  var1.add(AccessibleState.?);
443       //}
444
445       if (state == ActionButtonComponent.PUSHED) {
446         var1.add(AccessibleState.PRESSED);
447       }
448       if (state == ActionButtonComponent.SELECTED) {
449         var1.add(AccessibleState.CHECKED);
450       }
451
452       if (ActionButton.this.isFocusOwner()) {
453         var1.add(AccessibleState.FOCUSED);
454       }
455
456       return var1;
457     }
458
459     @Override
460     public AccessibleAction getAccessibleAction() {
461       return this;
462     }
463
464     // Implements AccessibleAction
465
466     @Override
467     public int getAccessibleActionCount() {
468       return 1;
469     }
470
471     @Override
472     public String getAccessibleActionDescription(int index) {
473       return index == 0 ? UIManager.getString("AbstractButton.clickText") : null;
474     }
475
476     @Override
477     public boolean doAccessibleAction(int index) {
478       if (index == 0) { //
479         ActionButton.this.click();
480         return true;
481       }
482       else {
483         return false;
484       }
485     }
486   }
487 }