IDEA-299042 Run widget popup: inline actions have incorrect width
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / popup / list / PopupListElementRenderer.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ui.popup.list;
3
4 import com.intellij.icons.AllIcons;
5 import com.intellij.openapi.actionSystem.*;
6 import com.intellij.openapi.keymap.KeymapUtil;
7 import com.intellij.openapi.ui.popup.ListItemDescriptorAdapter;
8 import com.intellij.openapi.ui.popup.ListPopupStep;
9 import com.intellij.openapi.ui.popup.ListPopupStepEx;
10 import com.intellij.openapi.ui.popup.MnemonicNavigationFilter;
11 import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
12 import com.intellij.openapi.ui.popup.util.PopupUtil;
13 import com.intellij.openapi.util.Comparing;
14 import com.intellij.openapi.util.Key;
15 import com.intellij.openapi.util.NlsSafe;
16 import com.intellij.openapi.util.UserDataHolder;
17 import com.intellij.ui.*;
18 import com.intellij.ui.popup.NumericMnemonicItem;
19 import com.intellij.ui.scale.JBUIScale;
20 import com.intellij.util.ArrayUtil;
21 import com.intellij.util.ui.*;
22 import org.jetbrains.annotations.NotNull;
23 import org.jetbrains.annotations.Nullable;
24
25 import javax.accessibility.AccessibleContext;
26 import javax.swing.*;
27 import javax.swing.border.EmptyBorder;
28 import java.awt.*;
29
30 public class PopupListElementRenderer<E> extends GroupedItemsListRenderer<E> {
31
32   public static final Key<@NlsSafe String> CUSTOM_KEY_STROKE_TEXT = new Key<>("CUSTOM_KEY_STROKE_TEXT");
33   protected final ListPopupImpl myPopup;
34   private JLabel myShortcutLabel;
35   private @Nullable JLabel myValueLabel;
36   protected JLabel myMnemonicLabel;
37   protected JLabel myIconLabel;
38
39   protected JPanel myButtonPane;
40   protected JComponent myMainPane;
41   protected JComponent myButtonsSeparator;
42   protected JComponent myIconBar;
43
44   private final PopupInlineActionsSupport myInlineActionsSupport;
45
46   public PopupListElementRenderer(final ListPopupImpl aPopup) {
47     super(new ListItemDescriptorAdapter<>() {
48       @Override
49       public String getTextFor(E value) {
50         return aPopup.getListStep().getTextFor(value);
51       }
52
53       @Override
54       public Icon getIconFor(E value) {
55         return aPopup.getListStep().getIconFor(value);
56       }
57
58       @Override
59       public Icon getSelectedIconFor(E value) {
60         return aPopup.getListStep().getSelectedIconFor(value);
61       }
62
63       @Override
64       public boolean hasSeparatorAboveOf(E value) {
65         return aPopup.getListModel().isSeparatorAboveOf(value);
66       }
67
68       @Override
69       public String getCaptionAboveOf(E value) {
70         return aPopup.getListModel().getCaptionAboveOf(value);
71       }
72
73       @Nullable
74       @Override
75       public String getTooltipFor(E value) {
76         ListPopupStep<Object> listStep = aPopup.getListStep();
77         if (!(listStep instanceof ListPopupStepEx)) return null;
78         return ((ListPopupStepEx<E>)listStep).getTooltipTextFor(value);
79       }
80     });
81     myPopup = aPopup;
82     myInlineActionsSupport = PopupInlineActionsSupport.Companion.create(myPopup);
83   }
84
85   @Override
86   protected SeparatorWithText createSeparator() {
87     Insets labelInsets = ExperimentalUI.isNewUI() ? JBUI.CurrentTheme.Popup.separatorLabelInsets() :
88                          getDefaultItemComponentBorder().getBorderInsets(new JLabel());
89     return new GroupHeaderSeparator(labelInsets);
90   }
91
92   @Override
93   protected Color getBackground() {
94     return ExperimentalUI.isNewUI() ? JBUI.CurrentTheme.Popup.BACKGROUND : super.getBackground();
95   }
96
97   @Override
98   protected JComponent createItemComponent() {
99     createLabel();
100     JPanel panel = new JPanel(new BorderLayout()) {
101       private final AccessibleContext myAccessibleContext = myTextLabel.getAccessibleContext();
102
103       @Override
104       public AccessibleContext getAccessibleContext() {
105         if (myAccessibleContext == null) {
106           return super.getAccessibleContext();
107         }
108         return myAccessibleContext;
109       }
110
111       @Override
112       public Dimension getPreferredSize() {
113         Dimension size = super.getPreferredSize();
114         if (ExperimentalUI.isNewUI()) {
115           size.height = JBUI.CurrentTheme.List.rowHeight();
116         }
117         return size;
118       }
119     };
120     panel.add(myTextLabel, BorderLayout.WEST);
121
122     myValueLabel = new JLabel();
123     myValueLabel.setEnabled(false);
124     JBEmptyBorder valueBorder = ExperimentalUI.isNewUI() ? JBUI.Borders.empty() : JBUI.Borders.empty(0, JBUIScale.scale(8), 1, 0);
125     myValueLabel.setBorder(valueBorder);
126     myValueLabel.setForeground(UIManager.getColor("MenuItem.acceleratorForeground"));
127     myValueLabel.setOpaque(false);
128     panel.add(myValueLabel, BorderLayout.CENTER);
129
130     myShortcutLabel = new JLabel();
131     JBEmptyBorder shortcutBorder = ExperimentalUI.isNewUI() ? JBUI.Borders.empty() : JBUI.Borders.empty(0,0,1,3);
132     myShortcutLabel.setBorder(shortcutBorder);
133     myShortcutLabel.setForeground(UIManager.getColor("MenuItem.acceleratorForeground"));
134     myShortcutLabel.setOpaque(false);
135     panel.add(myShortcutLabel, BorderLayout.EAST);
136
137     myMnemonicLabel = new JLabel();
138     if (!ExperimentalUI.isNewUI()) {
139       Insets insets = JBUI.CurrentTheme.ActionsList.numberMnemonicInsets();
140       myMnemonicLabel.setBorder(new JBEmptyBorder(insets));
141       //noinspection HardCodedStringLiteral
142       Dimension preferredSize = new JLabel("W").getPreferredSize();
143       JBInsets.addTo(preferredSize, insets);
144       myMnemonicLabel.setPreferredSize(preferredSize);
145     }
146     else {
147       myMnemonicLabel.setBorder(new JBEmptyBorder(JBUI.CurrentTheme.ActionsList.mnemonicInsets()));
148       myMnemonicLabel.setHorizontalAlignment(SwingConstants.RIGHT);
149
150       Dimension preferredSize = new JLabel("W").getPreferredSize();
151       JBInsets.addTo(preferredSize, JBUI.insetsLeft(4));
152       myMnemonicLabel.setPreferredSize(preferredSize);
153       myMnemonicLabel.setMinimumSize(JBUI.size(12, myMnemonicLabel.getMinimumSize().height));
154     }
155
156     myMnemonicLabel.setFont(JBUI.CurrentTheme.ActionsList.applyStylesForNumberMnemonic(myMnemonicLabel.getFont()));
157     myMnemonicLabel.setVisible(false);
158
159     myIconBar = createIconBar();
160
161     return layoutComponent(panel);
162   }
163
164   @Override
165   protected void createLabel() {
166     super.createLabel();
167     myIconLabel = new JLabel();
168   }
169
170   @Override
171   protected JComponent layoutComponent(JComponent middleItemComponent) {
172     myNextStepLabel = new JLabel();
173     myNextStepLabel.setOpaque(false);
174
175     JPanel left = new JPanel(new BorderLayout());
176     left.add(middleItemComponent, BorderLayout.CENTER);
177
178     JPanel right = new JPanel(new GridBagLayout());
179     int leftRightInset = (ListPopupImpl.NEXT_STEP_AREA_WIDTH - AllIcons.Icons.Ide.MenuArrow.getIconWidth()) / 2;
180
181     myButtonsSeparator = createButtonsSeparator();
182     left.add(myButtonsSeparator, BorderLayout.EAST);
183
184     if (myIconBar != null) {
185       left.add(myIconBar, BorderLayout.WEST);
186     }
187
188     JPanel result = new JPanel();
189     if (ExperimentalUI.isNewUI()) {
190       result = new SelectablePanel();
191       result.setOpaque(false);
192       PopupUtil.configSelectablePanel(((SelectablePanel)result));
193     }
194     else {
195       result.setBorder(JBUI.Borders.empty());
196     }
197     result.setLayout(new GridBagLayout());
198
199     Insets insets = getDefaultItemComponentBorder().getBorderInsets(result);
200     if (ExperimentalUI.isNewUI()) {
201       left.setBorder(JBUI.Borders.empty());
202       right.setBorder(JBUI.Borders.empty());
203     } else {
204       left.setBorder(new EmptyBorder(insets.top, insets.left, insets.bottom, 0));
205       right.setBorder(new EmptyBorder(insets.top, leftRightInset, insets.bottom, insets.right));
206     }
207
208     GridBag gbc = new GridBag()
209       .setDefaultAnchor(0, GridBagConstraints.WEST)
210       .setDefaultWeightX(0, 1)
211       .setDefaultAnchor(GridBagConstraints.CENTER)
212       .setDefaultWeightX(0)
213       .setDefaultWeightY(1)
214       .setDefaultPaddingX(0)
215       .setDefaultPaddingY(0)
216       .setDefaultInsets(0, 0, 0, 0)
217       .setDefaultFill(GridBagConstraints.BOTH);
218
219     result.add(left, gbc.next());
220     result.add(right, gbc.next());
221
222     myMainPane = left;
223     myButtonPane = right;
224
225    return result;
226   }
227
228   @Override
229   protected void setComponentIcon(Icon icon, Icon disabledIcon) {
230     if (myIconLabel == null) return;
231     myIconLabel.setIcon(icon);
232     myIconLabel.setDisabledIcon(disabledIcon);
233     if (ExperimentalUI.isNewUI() && icon != null && icon.getIconWidth() != -1 && icon.getIconHeight() != -1) {
234       myIconLabel.setBorder(JBUI.Borders.emptyRight(JBUI.CurrentTheme.ActionsList.elementIconGap() - 2));
235     }
236   }
237
238   @NotNull
239   protected static JComponent createButtonsSeparator() {
240     SeparatorComponent separator = new SeparatorComponent(UIUtil.getListBackground(), SeparatorOrientation.VERTICAL);
241     separator.setHGap(1);
242     separator.setVGap(0);
243     return separator;
244   }
245
246   @Override
247   protected void customizeComponent(JList<? extends E> list, E value, boolean isSelected) {
248     if (mySeparatorComponent.isVisible() && mySeparatorComponent instanceof GroupHeaderSeparator) {
249       ((GroupHeaderSeparator)mySeparatorComponent).setHideLine(myCurrentIndex == 0);
250     }
251
252     ListPopupStep<Object> step = myPopup.getListStep();
253     boolean isSelectable = step.isSelectable(value);
254     myTextLabel.setEnabled(isSelectable);
255
256     myMainPane.setOpaque(false);
257     myButtonPane.setOpaque(false);
258
259     updateExtraButtons(list, value, step, isSelected);
260
261     boolean nextStepButtonSelected = false;
262     if (step.hasSubstep(value)) {
263       myNextStepLabel.setVisible(isSelectable);
264       myNextStepLabel.setIcon(isSelectable && isSelected ? AllIcons.Icons.Ide.MenuArrowSelected : AllIcons.Icons.Ide.MenuArrow);
265       if (!ExperimentalUI.isNewUI() ) {
266         myComponent.setBackground(calcBackground(isSelected && isSelectable, false));
267       }
268       setForegroundSelected(myTextLabel, isSelected && isSelectable);
269     }
270     else {
271       myNextStepLabel.setVisible(false);
272     }
273
274     if (ExperimentalUI.isNewUI() && myComponent instanceof SelectablePanel) {
275       ((SelectablePanel)myComponent).setSelectionColor(isSelected && isSelectable ? UIUtil.getListSelectionBackground(true) : null);
276
277       int leftRightInset = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get();
278       Insets innerInsets = JBUI.CurrentTheme.Popup.Selection.innerInsets();
279       boolean hasNextIcon = myNextStepLabel.getIcon() != null && myNextStepLabel.isVisible();
280       //noinspection UseDPIAwareBorders
281       myComponent.setBorder(
282         new EmptyBorder(0, innerInsets.left + leftRightInset, 0, hasNextIcon ? leftRightInset : leftRightInset + leftRightInset));
283     }
284
285     if (step instanceof BaseListPopupStep) {
286       Color bg = ((BaseListPopupStep<E>)step).getBackgroundFor(value);
287       Color fg = ((BaseListPopupStep<E>)step).getForegroundFor(value);
288       if (!isSelected && fg != null) myTextLabel.setForeground(fg);
289       if (!isSelected && bg != null) UIUtil.setBackgroundRecursively(myComponent, bg);
290       if (bg != null && mySeparatorComponent.isVisible() && myCurrentIndex > 0) {
291         E prevValue = list.getModel().getElementAt(myCurrentIndex - 1);
292         // separator between 2 colored items shall get color too
293         if (Comparing.equal(bg, ((BaseListPopupStep<E>)step).getBackgroundFor(prevValue))) {
294           myRendererComponent.setBackground(bg);
295         }
296       }
297     }
298
299     if (myMnemonicLabel != null && value instanceof NumericMnemonicItem && ((NumericMnemonicItem)value).digitMnemonicsEnabled()) {
300       Character mnemonic = ((NumericMnemonicItem)value).getMnemonicChar();
301       myMnemonicLabel.setText(mnemonic != null ? String.valueOf(mnemonic) : "");
302       if (ExperimentalUI.isNewUI() && mnemonic == null) {
303         //noinspection HardCodedStringLiteral
304         Dimension preferredSize = new JLabel("W").getPreferredSize();
305         JBInsets.addTo(preferredSize, JBUI.CurrentTheme.ActionsList.mnemonicInsets());
306         myMnemonicLabel.setText("  ");
307       }
308       Color foreground =
309         ExperimentalUI.isNewUI() ? JBUI.CurrentTheme.Popup.mnemonicForeground() : JBUI.CurrentTheme.ActionsList.MNEMONIC_FOREGROUND;
310       myMnemonicLabel.setForeground(isSelected && isSelectable && !nextStepButtonSelected ? getSelectionForeground() : foreground);
311       myMnemonicLabel.setVisible(true);
312     }
313
314     if (step.isMnemonicsNavigationEnabled()) {
315       MnemonicNavigationFilter<Object> filter = step.getMnemonicNavigationFilter();
316       int pos = filter == null ? -1 : filter.getMnemonicPos(value);
317       if (pos != -1) {
318         String text = myTextLabel.getText();
319         text = text.substring(0, pos) + text.substring(pos + 1);
320         myTextLabel.setText(text);
321         myTextLabel.setDisplayedMnemonicIndex(pos);
322       }
323     }
324     else {
325       myTextLabel.setDisplayedMnemonicIndex(-1);
326     }
327
328     if (myShortcutLabel != null) {
329       myShortcutLabel.setEnabled(isSelectable);
330       myShortcutLabel.setText("");
331       if (value instanceof ShortcutProvider) {
332         ShortcutSet set = ((ShortcutProvider)value).getShortcut();
333         String shortcutText = null;
334         if (set != null) {
335           Shortcut shortcut = ArrayUtil.getFirstElement(set.getShortcuts());
336           if (shortcut != null) {
337             shortcutText = KeymapUtil.getShortcutText(shortcut);
338           }
339         }
340         if (shortcutText == null && value instanceof AnActionHolder) {
341           AnAction action = ((AnActionHolder)value).getAction();
342           if (action instanceof UserDataHolder) {
343             shortcutText = ((UserDataHolder)action).getUserData(CUSTOM_KEY_STROKE_TEXT);
344           }
345         }
346         if (shortcutText != null) myShortcutLabel.setText("     " + shortcutText);
347       }
348       myShortcutLabel.setForeground(isSelected && isSelectable && !nextStepButtonSelected
349                                     ? UIManager.getColor("MenuItem.acceleratorSelectionForeground")
350                                     : UIManager.getColor("MenuItem.acceleratorForeground"));
351     }
352
353     if (myValueLabel != null) {
354       myValueLabel.setText(step instanceof ListPopupStepEx<?> ? ((ListPopupStepEx<E>)step).getValueFor(value) : null);
355       boolean selected = isSelected && isSelectable && !nextStepButtonSelected;
356       setForegroundSelected(myValueLabel, selected);
357     }
358
359     if (ExperimentalUI.isNewUI() && myComponent instanceof SelectablePanel) {
360       ((SelectablePanel)myComponent).setSelectionColor(isSelected && isSelectable ? UIUtil.getListSelectionBackground(true) : null);
361       setSelected(myMainPane, isSelected && isSelectable);
362     }
363   }
364
365   private void updateExtraButtons(JList<? extends E> list, E value, ListPopupStep<Object> step, boolean isSelected) {
366     myButtonPane.removeAll();
367     GridBag gb = new GridBag().setDefaultFill(GridBagConstraints.BOTH)
368       .setDefaultAnchor(GridBagConstraints.CENTER)
369       .setDefaultWeightX(1.0)
370       .setDefaultWeightY(1.0);
371
372     boolean isSelectable = step.isSelectable(value);
373     if (!isSelected || !isSelectable) {
374       myButtonsSeparator.setVisible(false);
375       myButtonPane.add(myNextStepLabel, gb.next());
376       return;
377     }
378
379     java.util.List<JComponent> extraButtons = myInlineActionsSupport.getExtraButtons(list, value, isSelected);
380     if (!extraButtons.isEmpty()) {
381       myButtonsSeparator.setVisible(true);
382       extraButtons.forEach(comp -> myButtonPane.add(comp, gb.next()));
383     }
384     else {
385       myButtonsSeparator.setVisible(false);
386       myButtonPane.add(myNextStepLabel, gb.next());
387     }
388   }
389
390   protected JComponent createIconBar() {
391     Box res = Box.createHorizontalBox();
392     res.add(myIconLabel);
393
394     if (!ExperimentalUI.isNewUI()) {
395       res.setBorder(JBUI.Borders.emptyRight(JBUI.CurrentTheme.ActionsList.elementIconGap()));
396       res.add(myMnemonicLabel);
397     } else {
398       //need to wrap to align mnemonics to the right
399       JPanel wrapper = new JPanel(new BorderLayout());
400       wrapper.add(myMnemonicLabel);
401       res.add(wrapper);
402     }
403
404     return res;
405   }
406
407   private Color calcBackground(boolean selected, boolean hovered) {
408     if (selected) return getSelectionBackground();
409     if (hovered) return JBUI.CurrentTheme.Table.Hover.background(true);
410
411     return getBackground();
412   }
413
414   @NotNull
415   static Insets getListCellPadding() {
416     if (ExperimentalUI.isNewUI()) {
417       int leftRightInset = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get();
418       return JBUI.insets(0, leftRightInset, 0, leftRightInset);
419     }
420
421     return UIUtil.getListCellPadding();
422   }
423 }