make all "Group By" and "View Options" popups multi-choice
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / popup / PopupFactoryImpl.java
1 // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2 package com.intellij.ui.popup;
3
4 import com.intellij.CommonBundle;
5 import com.intellij.ide.DataManager;
6 import com.intellij.ide.IdeEventQueue;
7 import com.intellij.ide.IdeTooltipManager;
8 import com.intellij.internal.inspector.UiInspectorUtil;
9 import com.intellij.openapi.Disposable;
10 import com.intellij.openapi.actionSystem.*;
11 import com.intellij.openapi.actionSystem.impl.ActionMenu;
12 import com.intellij.openapi.actionSystem.impl.PresentationFactory;
13 import com.intellij.openapi.actionSystem.impl.Utils;
14 import com.intellij.openapi.application.Application;
15 import com.intellij.openapi.application.ApplicationManager;
16 import com.intellij.openapi.diagnostic.Logger;
17 import com.intellij.openapi.editor.CaretModel;
18 import com.intellij.openapi.editor.Editor;
19 import com.intellij.openapi.editor.VisualPosition;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.ui.MessageType;
22 import com.intellij.openapi.ui.popup.*;
23 import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
24 import com.intellij.openapi.util.*;
25 import com.intellij.openapi.util.NlsContexts.PopupTitle;
26 import com.intellij.openapi.util.text.TextWithMnemonic;
27 import com.intellij.openapi.wm.WindowManager;
28 import com.intellij.ui.*;
29 import com.intellij.ui.awt.RelativePoint;
30 import com.intellij.ui.components.JBList;
31 import com.intellij.ui.components.panels.NonOpaquePanel;
32 import com.intellij.ui.popup.list.ListPopupImpl;
33 import com.intellij.ui.popup.mock.MockConfirmation;
34 import com.intellij.ui.popup.tree.TreePopupImpl;
35 import com.intellij.util.ObjectUtils;
36 import com.intellij.util.ui.EmptyIcon;
37 import com.intellij.util.ui.JBUI;
38 import com.intellij.util.ui.tree.TreeUtil;
39 import org.jetbrains.annotations.ApiStatus;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import javax.swing.*;
44 import javax.swing.event.HyperlinkListener;
45 import javax.swing.tree.TreePath;
46 import java.awt.*;
47 import java.awt.event.ActionEvent;
48 import java.awt.event.InputEvent;
49 import java.awt.event.KeyEvent;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.WeakHashMap;
53 import java.util.function.Function;
54 import java.util.function.Supplier;
55
56 public class PopupFactoryImpl extends JBPopupFactory {
57
58   /**
59    * Allows to get an editor position for which a popup with auxiliary information might be shown.
60    * <p/>
61    * Primary intention for this key is to hint popup position for the non-caret location.
62    */
63   public static final Key<VisualPosition> ANCHOR_POPUP_POSITION = Key.create("popup.anchor.position");
64   /**
65    * If corresponding value is defined for an {@link Editor}, popups shown for the editor will be located at specified point. This allows to
66    * show popups for non-default locations (caret location is used by default).
67    *
68    * @see JBPopupFactory#guessBestPopupLocation(Editor)
69    */
70   public static final Key<Point> ANCHOR_POPUP_POINT = Key.create("popup.anchor.point");
71
72   private static final Logger LOG = Logger.getInstance(PopupFactoryImpl.class);
73
74   private final Map<Disposable, List<Balloon>> myStorage = new WeakHashMap<>();
75
76   @Override
77   public @NotNull <T> IPopupChooserBuilder<T> createPopupChooserBuilder(@NotNull List<? extends T> list) {
78     return new PopupChooserBuilder<>(new JBList<>(new CollectionListModel<>(list)));
79   }
80
81   @Override
82   public @NotNull ListPopup createConfirmation(@PopupTitle @Nullable String title, final Runnable onYes, int defaultOptionIndex) {
83     return createConfirmation(title, CommonBundle.getYesButtonText(), CommonBundle.getNoButtonText(), onYes, defaultOptionIndex);
84   }
85
86   @Override
87   public @NotNull ListPopup createConfirmation(@PopupTitle @Nullable String title, final String yesText, String noText, final Runnable onYes, int defaultOptionIndex) {
88     return createConfirmation(title, yesText, noText, onYes, EmptyRunnable.getInstance(), defaultOptionIndex);
89   }
90
91   @Override
92   public @NotNull JBPopup createMessage(@PopupTitle String text) {
93     return createListPopup(new BaseListPopupStep<>(null, text));
94   }
95
96   @Override
97   public Balloon getParentBalloonFor(@Nullable Component c) {
98     if (c == null) return null;
99     Component eachParent = c;
100     while (eachParent != null) {
101       if (eachParent instanceof JComponent) {
102         Object balloon = ((JComponent)eachParent).getClientProperty(Balloon.KEY);
103         if (balloon instanceof Balloon) {
104           return (Balloon)balloon;
105         }
106       }
107       eachParent = eachParent.getParent();
108     }
109
110     return null;
111   }
112
113   @Override
114   protected <T> PopupChooserBuilder.@NotNull PopupComponentAdapter<T> createPopupComponentAdapter(@NotNull PopupChooserBuilder<T> builder, @NotNull JList<T> list) {
115     return new PopupListAdapter<>(builder, list);
116   }
117
118   @Override
119   protected <T> PopupChooserBuilder.@NotNull PopupComponentAdapter<T> createPopupComponentAdapter(@NotNull PopupChooserBuilder<T> builder, @NotNull JTree tree) {
120     return new PopupTreeAdapter<>(builder, tree);
121   }
122
123   @Override
124   protected <T> PopupChooserBuilder.@NotNull PopupComponentAdapter<T> createPopupComponentAdapter(@NotNull PopupChooserBuilder<T> builder, @NotNull JTable table) {
125     return new PopupTableAdapter<>(builder, table);
126   }
127
128   @Override
129   public @NotNull ListPopup createConfirmation(@PopupTitle @Nullable String title,
130                                                @NlsContexts.Label String yesText,
131                                                @NlsContexts.Label String noText,
132                                                Runnable onYes,
133                                                Runnable onNo,
134                                                int defaultOptionIndex) {
135     final BaseListPopupStep<String> step = new BaseListPopupStep<>(title, yesText, noText) {
136       boolean myRunYes;
137       @Override
138       public PopupStep onChosen(String selectedValue, final boolean finalChoice) {
139         myRunYes = selectedValue.equals(yesText);
140         return FINAL_CHOICE;
141       }
142
143       @Override
144       public void canceled() {
145         (myRunYes ? onYes : onNo).run();
146       }
147
148       @Override
149       public boolean isMnemonicsNavigationEnabled() {
150         return true;
151       }
152     };
153     step.setDefaultOptionIndex(defaultOptionIndex);
154
155     final Application app = ApplicationManager.getApplication();
156     return app == null || !app.isUnitTestMode() ? new ListPopupImpl(step) : new MockConfirmation(step, yesText);
157   }
158
159
160   public static class ActionGroupPopup extends ListPopupImpl {
161
162     private final Runnable myDisposeCallback;
163     private final Component myComponent;
164
165     public ActionGroupPopup(@PopupTitle @Nullable String title,
166                             @NotNull ActionGroup actionGroup,
167                             @NotNull DataContext dataContext,
168                             boolean showNumbers,
169                             boolean useAlphaAsNumbers,
170                             boolean showDisabledActions,
171                             boolean honorActionMnemonics,
172                             Runnable disposeCallback,
173                             int maxRowCount,
174                             Condition<? super AnAction> preselectActionCondition,
175                             @Nullable String actionPlace) {
176       this(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, disposeCallback,
177            maxRowCount, preselectActionCondition, actionPlace, null, false);
178     }
179
180     public ActionGroupPopup(@PopupTitle @Nullable String title,
181                             @NotNull ActionGroup actionGroup,
182                             @NotNull DataContext dataContext,
183                             boolean showNumbers,
184                             boolean useAlphaAsNumbers,
185                             boolean showDisabledActions,
186                             boolean honorActionMnemonics,
187                             Runnable disposeCallback,
188                             int maxRowCount,
189                             Condition<? super AnAction> preselectActionCondition,
190                             @Nullable String actionPlace,
191                             boolean autoSelection) {
192       this(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, disposeCallback,
193            maxRowCount, preselectActionCondition, actionPlace, null, autoSelection);
194     }
195
196     public ActionGroupPopup(@PopupTitle @Nullable String title,
197                             @NotNull ActionGroup actionGroup,
198                             @NotNull DataContext dataContext,
199                             boolean showNumbers,
200                             boolean useAlphaAsNumbers,
201                             boolean showDisabledActions,
202                             boolean honorActionMnemonics,
203                             Runnable disposeCallback,
204                             int maxRowCount,
205                             Condition<? super AnAction> preselectActionCondition,
206                             @Nullable String actionPlace,
207                             @Nullable PresentationFactory presentationFactory,
208                             boolean autoSelection) {
209       this(null, createStep(title, actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics,
210                             preselectActionCondition, actionPlace, presentationFactory, autoSelection), disposeCallback, dataContext, maxRowCount);
211       UiInspectorUtil.registerProvider(getList(), () -> UiInspectorUtil.collectActionGroupInfo("Menu", actionGroup, actionPlace));
212     }
213
214     protected ActionGroupPopup(@Nullable WizardPopup aParent,
215                                @NotNull ListPopupStep step,
216                                @Nullable Runnable disposeCallback,
217                                @NotNull DataContext dataContext,
218                                int maxRowCount) {
219       super(CommonDataKeys.PROJECT.getData(dataContext), aParent, step, null);
220       setMaxRowCount(maxRowCount);
221       myDisposeCallback = disposeCallback;
222       myComponent = PlatformCoreDataKeys.CONTEXT_COMPONENT.getData(dataContext);
223
224       registerAction("handleActionToggle1", KeyEvent.VK_SPACE, 0, new AbstractAction() {
225         @Override
226         public void actionPerformed(ActionEvent e) {
227           handleToggleAction();
228         }
229       });
230
231       addListSelectionListener(e -> {
232         JList<?> list = (JList<?>)e.getSource();
233         ActionItem actionItem = (ActionItem)list.getSelectedValue();
234         if (actionItem == null) return;
235         ActionMenu.showDescriptionInStatusBar(true, myComponent, actionItem.getDescription());
236       });
237     }
238
239     protected static ListPopupStep<ActionItem> createStep(@PopupTitle @Nullable String title,
240                                                           @NotNull ActionGroup actionGroup,
241                                                           @NotNull DataContext dataContext,
242                                                           boolean showNumbers,
243                                                           boolean useAlphaAsNumbers,
244                                                           boolean showDisabledActions,
245                                                           boolean honorActionMnemonics,
246                                                           Condition<? super AnAction> preselectActionCondition,
247                                                           @Nullable String actionPlace,
248                                                           @Nullable PresentationFactory presentationFactory,
249                                                           boolean autoSelection) {
250       final Component component = PlatformCoreDataKeys.CONTEXT_COMPONENT.getData(dataContext);
251       LOG.assertTrue(component != null, "dataContext has no component for new ListPopupStep");
252
253       List<ActionItem> items = ActionPopupStep.createActionItems(
254           actionGroup, dataContext, showNumbers, useAlphaAsNumbers, showDisabledActions, honorActionMnemonics, actionPlace, presentationFactory);
255
256       return new ActionPopupStep(items, title, getComponentContextSupplier(dataContext, component), actionPlace, showNumbers || honorActionMnemonics && anyMnemonicsIn(items),
257                                  preselectActionCondition, autoSelection, showDisabledActions, presentationFactory);
258     }
259
260     @Override
261     public void dispose() {
262       if (myDisposeCallback != null) {
263         myDisposeCallback.run();
264       }
265       ActionMenu.showDescriptionInStatusBar(true, myComponent, null);
266       super.dispose();
267     }
268
269     @Override
270     public void handleSelect(boolean handleFinalChoices, InputEvent e) {
271       ActionItem item = ObjectUtils.tryCast(getList().getSelectedValue(), ActionItem.class);
272       ActionPopupStep step = ObjectUtils.tryCast(getListStep(), ActionPopupStep.class);
273       if (step != null && item != null && step.isSelectable(item) && item.isKeepPopupOpen()) {
274         step.performAction(item.getAction(), e != null ? e.getModifiers() : 0, e);
275         step.updateStepItems(() -> getList().repaint());
276       }
277       else {
278         super.handleSelect(handleFinalChoices, e);
279       }
280     }
281
282     protected void handleToggleAction() {
283       List<Object> selectedValues = getList().getSelectedValuesList();
284       ActionPopupStep step = ObjectUtils.tryCast(getListStep(), ActionPopupStep.class);
285       if (step == null) return;
286       boolean updateStep = false;
287       for (Object value : selectedValues) {
288         ActionItem item = ObjectUtils.tryCast(value, ActionItem.class);
289         if (item != null && step.isSelectable(item) && item.getAction() instanceof Toggleable) {
290           step.performAction(item.getAction(), 0);
291           updateStep = true;
292         }
293       }
294       if (updateStep) {
295         step.updateStepItems(() -> getList().repaint());
296       }
297     }
298   }
299
300   private static @NotNull Supplier<DataContext> getComponentContextSupplier(@NotNull DataContext parentDataContext,
301                                                                             @Nullable Component component) {
302     if(component == null) return () -> parentDataContext;
303     DataContext dataContext = Utils.wrapDataContext(DataManager.getInstance().getDataContext(component));
304     if (Utils.isAsyncDataContext(dataContext)) return () -> dataContext;
305     return () -> DataManager.getInstance().getDataContext(component);
306   }
307
308   @Override
309   public @NotNull ListPopup createActionGroupPopup(@PopupTitle @Nullable String title,
310                                                    @NotNull ActionGroup actionGroup,
311                                                    @NotNull DataContext dataContext,
312                                                    ActionSelectionAid aid,
313                                                    boolean showDisabledActions,
314                                                    Runnable disposeCallback,
315                                                    int maxRowCount,
316                                                    Condition<? super AnAction> preselectActionCondition,
317                                                    @Nullable String actionPlace) {
318     return new ActionGroupPopup(title,
319                                 actionGroup,
320                                 dataContext,
321                                 aid == ActionSelectionAid.ALPHA_NUMBERING || aid == ActionSelectionAid.NUMBERING,
322                                 aid == ActionSelectionAid.ALPHA_NUMBERING,
323                                 showDisabledActions,
324                                 aid == ActionSelectionAid.MNEMONICS,
325                                 disposeCallback,
326                                 maxRowCount,
327                                 preselectActionCondition,
328                                 actionPlace);
329   }
330
331   @Override
332   public @NotNull ListPopup createActionGroupPopup(@PopupTitle @Nullable String title,
333                                                    @NotNull ActionGroup actionGroup,
334                                                    @NotNull DataContext dataContext,
335                                                    boolean showNumbers,
336                                                    boolean showDisabledActions,
337                                                    boolean honorActionMnemonics,
338                                                    Runnable disposeCallback,
339                                                    int maxRowCount,
340                                                    Condition<? super AnAction> preselectActionCondition) {
341     return new ActionGroupPopup(title, actionGroup, dataContext, showNumbers, true, showDisabledActions, honorActionMnemonics,
342                                   disposeCallback, maxRowCount, preselectActionCondition, null);
343   }
344
345   @Override
346   public @NotNull ListPopupStep<ActionItem> createActionsStep(@NotNull ActionGroup actionGroup,
347                                                               @NotNull DataContext dataContext,
348                                                               @Nullable String actionPlace,
349                                                               boolean showNumbers,
350                                                               boolean showDisabledActions,
351                                                               @PopupTitle @Nullable String title,
352                                                               Component component,
353                                                               boolean honorActionMnemonics,
354                                                               int defaultOptionIndex,
355                                                               boolean autoSelectionEnabled) {
356     return ActionPopupStep.createActionsStep(
357       actionGroup, dataContext, showNumbers, true, showDisabledActions,
358       title, honorActionMnemonics, autoSelectionEnabled,
359       getComponentContextSupplier(dataContext, component),
360       actionPlace, null, defaultOptionIndex, null);
361   }
362
363   @ApiStatus.Internal
364   public static boolean anyMnemonicsIn(Iterable<? extends ActionItem> items) {
365     for (ActionItem item : items) {
366       if (item.getAction().getTemplatePresentation().getMnemonic() != 0) return true;
367     }
368
369     return false;
370   }
371
372   @Override
373   public @NotNull ListPopup createListPopup(@NotNull ListPopupStep step) {
374     return new ListPopupImpl(step);
375   }
376
377   @Override
378   public @NotNull ListPopup createListPopup(@NotNull ListPopupStep step, int maxRowCount) {
379     ListPopupImpl popup = new ListPopupImpl(step);
380     popup.setMaxRowCount(maxRowCount);
381     return popup;
382   }
383
384   @Override
385   public @NotNull ListPopup createListPopup(@NotNull Project project,
386                                             @NotNull ListPopupStep step,
387                                             @NotNull Function<ListCellRenderer, ListCellRenderer> cellRendererProducer) {
388     return new ListPopupImpl(project, step) {
389       @Override
390       protected ListCellRenderer<?> getListElementRenderer() {
391         return cellRendererProducer.apply(super.getListElementRenderer());
392       }
393     };
394   }
395
396   @Override
397   public @NotNull TreePopup createTree(JBPopup parent, @NotNull TreePopupStep aStep, Object parentValue) {
398     return new TreePopupImpl(aStep.getProject(), parent, aStep, parentValue);
399   }
400
401   @Override
402   public @NotNull TreePopup createTree(@NotNull TreePopupStep aStep) {
403     return new TreePopupImpl(aStep.getProject(), null, aStep, null);
404   }
405
406   @Override
407   public @NotNull ComponentPopupBuilder createComponentPopupBuilder(@NotNull JComponent content, JComponent preferableFocusComponent) {
408     return new ComponentPopupBuilderImpl(content, preferableFocusComponent);
409   }
410
411
412   @Override
413   public @NotNull RelativePoint guessBestPopupLocation(@NotNull DataContext dataContext) {
414     Component component = PlatformCoreDataKeys.CONTEXT_COMPONENT.getData(dataContext);
415     JComponent focusOwner = component instanceof JComponent ? (JComponent)component : null;
416
417     if (focusOwner == null) {
418       Project project = CommonDataKeys.PROJECT.getData(dataContext);
419       JFrame frame = project == null ? null : WindowManager.getInstance().getFrame(project);
420       focusOwner = frame == null ? null : frame.getRootPane();
421       if (focusOwner == null) {
422         throw new IllegalArgumentException("focusOwner cannot be null");
423       }
424     }
425
426     final Point point = PlatformDataKeys.CONTEXT_MENU_POINT.getData(dataContext);
427     if (point != null) {
428       return new RelativePoint(focusOwner, point);
429     }
430
431     Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
432     if (editor != null && focusOwner == editor.getContentComponent()) {
433       return guessBestPopupLocation(editor);
434     }
435     return guessBestPopupLocation(focusOwner);
436   }
437
438   @Override
439   public @NotNull RelativePoint guessBestPopupLocation(@NotNull JComponent component) {
440     Point popupMenuPoint = null;
441     final Rectangle visibleRect = component.getVisibleRect();
442     if (component instanceof JList) { // JList
443       JList list = (JList)component;
444       int firstVisibleIndex = list.getFirstVisibleIndex();
445       int lastVisibleIndex = list.getLastVisibleIndex();
446       int[] selectedIndices = list.getSelectedIndices();
447       for (int index : selectedIndices) {
448         if (firstVisibleIndex <= index && index <= lastVisibleIndex) {
449           Rectangle cellBounds = list.getCellBounds(index, index);
450           popupMenuPoint = new Point(visibleRect.x + visibleRect.width / 4, cellBounds.y + cellBounds.height - 1);
451           break;
452         }
453       }
454     }
455     else if (component instanceof JTree) { // JTree
456       JTree tree = (JTree)component;
457       TreePath[] paths = tree.getSelectionPaths();
458       if (paths != null && paths.length > 0) {
459         TreePath pathFound = null;
460         int distanceFound = Integer.MAX_VALUE;
461         int center = visibleRect.y + visibleRect.height / 2;
462         for (TreePath path : paths) {
463           Rectangle bounds = tree.getPathBounds(path);
464           if (bounds != null) {
465             int distance = Math.abs(bounds.y + bounds.height / 2 - center);
466             if (distance < distanceFound) {
467               popupMenuPoint = new Point(bounds.x + 2, bounds.y + bounds.height - 1);
468               distanceFound = distance;
469               pathFound = path;
470             }
471           }
472         }
473         if (pathFound != null) {
474           TreeUtil.scrollToVisible(tree, pathFound, false);
475         }
476       }
477     }
478     else if (component instanceof JTable) {
479       JTable table = (JTable)component;
480       int column = table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
481       int row = Math.max(table.getSelectionModel().getLeadSelectionIndex(), table.getSelectionModel().getAnchorSelectionIndex());
482       Rectangle rect = table.getCellRect(row, column, false);
483       if (!visibleRect.intersects(rect)) {
484         table.scrollRectToVisible(rect);
485       }
486       popupMenuPoint = new Point(rect.x, rect.y + rect.height - 1);
487     }
488     else if (component instanceof PopupOwner) {
489       popupMenuPoint = ((PopupOwner)component).getBestPopupPosition();
490     }
491     if (popupMenuPoint == null) {
492       popupMenuPoint = new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2);
493     }
494
495     return new RelativePoint(component, popupMenuPoint);
496   }
497
498   @Override
499   public boolean isBestPopupLocationVisible(@NotNull Editor editor) {
500     return getVisibleBestPopupLocation(editor) != null;
501   }
502
503   @Override
504   public @NotNull RelativePoint guessBestPopupLocation(@NotNull Editor editor) {
505     Point p = getVisibleBestPopupLocation(editor);
506     if (p == null) {
507       final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
508       p = new Point(visibleArea.x + visibleArea.width / 3, visibleArea.y + visibleArea.height / 2);
509     }
510     return new RelativePoint(editor.getContentComponent(), p);
511   }
512
513   private static @Nullable Point getVisibleBestPopupLocation(@NotNull Editor editor) {
514     int lineHeight = editor.getLineHeight();
515     Point p = editor.getUserData(ANCHOR_POPUP_POINT);
516     if (p == null) {
517       VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION);
518
519       if (visualPosition == null) {
520         CaretModel caretModel = editor.getCaretModel();
521         if (caretModel.isUpToDate()) {
522           visualPosition = caretModel.getVisualPosition();
523         }
524         else {
525           visualPosition = editor.offsetToVisualPosition(caretModel.getOffset());
526         }
527       }
528
529       p = editor.visualPositionToXY(visualPosition);
530       p.y += lineHeight;
531     }
532
533     final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
534     return !visibleArea.contains(p) && !visibleArea.contains(p.x, p.y - lineHeight) ? null : p;
535   }
536
537   @Override
538   public Point getCenterOf(JComponent container, JComponent content) {
539     return AbstractPopup.getCenterOf(container, content);
540   }
541
542   @Override
543   public @NotNull List<JBPopup> getChildPopups(@NotNull Component component) {
544     return AbstractPopup.getChildPopups(component);
545   }
546
547   @Override
548   public boolean isPopupActive() {
549   return IdeEventQueue.getInstance().isPopupActive();
550   }
551
552   @Override
553   public @NotNull BalloonBuilder createBalloonBuilder(@NotNull JComponent content) {
554     return new BalloonPopupBuilderImpl(myStorage, content);
555   }
556
557   @Override
558   public @NotNull BalloonBuilder createDialogBalloonBuilder(@NotNull JComponent content, @PopupTitle @Nullable String title) {
559     final BalloonPopupBuilderImpl builder = new BalloonPopupBuilderImpl(myStorage, content);
560     final Color bg = UIManager.getColor("Panel.background");
561     final Color borderOriginal = Color.darkGray;
562     final Color border = ColorUtil.toAlpha(borderOriginal, 75);
563     builder
564       .setDialogMode(true)
565       .setTitle(title)
566       .setAnimationCycle(200)
567       .setFillColor(bg).setBorderColor(border).setHideOnClickOutside(false)
568       .setHideOnKeyOutside(false)
569       .setHideOnAction(false)
570       .setCloseButtonEnabled(true)
571       .setShadow(true);
572
573     return builder;
574   }
575
576   @Override
577   public @NotNull BalloonBuilder createHtmlTextBalloonBuilder(@NotNull String htmlContent,
578                                                               @Nullable Icon icon,
579                                                               Color textColor,
580                                                               Color fillColor,
581                                                               @Nullable HyperlinkListener listener) {
582     JEditorPane text = IdeTooltipManager.initPane(htmlContent, new HintHint().setTextFg(textColor).setAwtTooltip(true), null);
583
584     if (listener != null) {
585       text.addHyperlinkListener(listener);
586     }
587     text.setEditable(false);
588     NonOpaquePanel.setTransparent(text);
589     text.setBorder(null);
590
591
592     JLabel label = new JLabel();
593     final JPanel content = new NonOpaquePanel(new BorderLayout((int)(label.getIconTextGap() * 1.5), (int)(label.getIconTextGap() * 1.5)));
594
595     final NonOpaquePanel textWrapper = new NonOpaquePanel(new GridBagLayout());
596     JScrollPane scrolledText = ScrollPaneFactory.createScrollPane(text, true);
597     scrolledText.setBackground(fillColor);
598     scrolledText.getViewport().setBackground(fillColor);
599     textWrapper.add(scrolledText);
600     content.add(textWrapper, BorderLayout.CENTER);
601     if (icon != null) {
602       final NonOpaquePanel north = new NonOpaquePanel(new BorderLayout());
603       north.add(new JLabel(icon), BorderLayout.NORTH);
604       content.add(north, BorderLayout.WEST);
605     }
606
607     content.setBorder(JBUI.Borders.empty(2, 4));
608
609     final BalloonBuilder builder = createBalloonBuilder(content);
610
611     builder.setFillColor(fillColor);
612
613     return builder;
614   }
615
616   @Override
617   public @NotNull BalloonBuilder createHtmlTextBalloonBuilder(@NotNull String htmlContent,
618                                                               @NotNull MessageType messageType,
619                                                               @Nullable HyperlinkListener listener) {
620     return createHtmlTextBalloonBuilder(htmlContent, messageType.getDefaultIcon(), messageType.getPopupBackground(), listener);
621   }
622
623
624   public static class ActionItem implements ShortcutProvider, AnActionHolder, NumericMnemonicItem {
625     private final AnAction myAction;
626     private @NlsActions.ActionText String myText;
627     private @NlsContexts.DetailedDescription String myDescription;
628     private @NlsContexts.DetailedDescription String myTooltip;
629     private @NlsContexts.ListItem String myValue;
630     private boolean myIsEnabled;
631     private boolean myIsPerformGroup;
632     private boolean myIsSubstepSuppressed;
633     private Icon myIcon;
634     private Icon mySelectedIcon;
635     private boolean myIsKeepPopupOpen;
636
637     private final int myMaxIconWidth;
638     private final int myMaxIconHeight;
639
640     private final Character myMnemonicChar;
641     private final boolean myMnemonicsEnabled;
642     private final boolean myHonorActionMnemonics;
643
644     private final boolean myPrependWithSeparator;
645     private final @NlsContexts.Separator String mySeparatorText;
646
647     ActionItem(@NotNull AnAction action,
648                @Nullable Character mnemonicChar,
649                boolean mnemonicsEnabled,
650                boolean honorActionMnemonics,
651                int maxIconWidth,
652                int maxIconHeight,
653                boolean prependWithSeparator,
654                @NlsContexts.Separator String separatorText) {
655       myAction = action;
656       myMnemonicChar = mnemonicChar;
657       myMnemonicsEnabled = mnemonicsEnabled;
658       myHonorActionMnemonics = honorActionMnemonics;
659       myMaxIconWidth = maxIconWidth;
660       myMaxIconHeight = maxIconHeight;
661       myPrependWithSeparator = prependWithSeparator;
662       mySeparatorText = separatorText;
663
664       myAction.getTemplatePresentation().addPropertyChangeListener(evt -> {
665         if (evt.getPropertyName() == Presentation.PROP_TEXT) {
666           myText = myAction.getTemplatePresentation().getText();
667         }
668       });
669     }
670
671     ActionItem(@NotNull AnAction action,
672                @NotNull @NlsActions.ActionText String text) {
673       myAction = action;
674       myText = text;
675
676       myMnemonicChar = null;
677       myMnemonicsEnabled = false;
678       myHonorActionMnemonics = false;
679       myMaxIconWidth = -1;
680       myMaxIconHeight = -1;
681       myPrependWithSeparator = false;
682       mySeparatorText = null;
683     }
684
685     public void updateFromPresentation(@NotNull Presentation presentation, @NotNull String actionPlace) {
686       String text = presentation.getText();
687       if (text != null && !myMnemonicsEnabled && myHonorActionMnemonics) {
688         text = TextWithMnemonic.fromPlainText(text, (char)myAction.getTemplatePresentation().getMnemonic()).toString();
689       }
690       myText = text;
691       LOG.assertTrue(text != null, "Action in `" + actionPlace + "` has no presentation: " + myAction.getClass().getName());
692
693       myDescription =  presentation.getDescription();
694       myTooltip = (String)presentation.getClientProperty(JComponent.TOOL_TIP_TEXT_KEY);
695
696       myIsEnabled = presentation.isEnabled();
697       myIsPerformGroup = myAction instanceof ActionGroup && presentation.isPerformGroup();
698       myIsSubstepSuppressed = myAction instanceof ActionGroup && Utils.isSubmenuSuppressed(presentation);
699       myIsKeepPopupOpen = myIsKeepPopupOpen || myAction instanceof KeepingPopupOpenAction || presentation.isMultipleChoice();
700
701       Couple<Icon> icons = ActionStepBuilder.calcRawIcons(myAction, presentation);
702       Icon icon = icons.first;
703       Icon selectedIcon = icons.second;
704
705       if (myMaxIconWidth != -1 && myMaxIconHeight != -1) {
706         if (icon != null) icon = new SizedIcon(icon, myMaxIconWidth, myMaxIconHeight);
707         if (selectedIcon != null) selectedIcon = new SizedIcon(selectedIcon, myMaxIconWidth, myMaxIconHeight);
708       }
709
710       if (icon == null) icon = selectedIcon != null ? selectedIcon : EmptyIcon.create(myMaxIconWidth, myMaxIconHeight);
711       myIcon = icon;
712       mySelectedIcon = selectedIcon;
713
714       myValue = presentation.getClientProperty(Presentation.PROP_VALUE);
715     }
716
717     @Override
718     public @Nullable Character getMnemonicChar() {
719       return myMnemonicChar;
720     }
721
722     @Override
723     public boolean digitMnemonicsEnabled() {
724       return myMnemonicsEnabled;
725     }
726
727     @Override
728     public @NotNull AnAction getAction() {
729       return myAction;
730     }
731
732     public @NotNull @NlsActions.ActionText String getText() {
733       return myText;
734     }
735
736     public @Nullable Icon getIcon(boolean selected) {
737       return selected && mySelectedIcon != null ? mySelectedIcon : myIcon;
738     }
739
740     public boolean isPrependWithSeparator() {
741       return myPrependWithSeparator;
742     }
743
744     public @NlsContexts.Separator String getSeparatorText() {
745       return mySeparatorText;
746     }
747
748     public boolean isEnabled() { return myIsEnabled; }
749
750     public boolean isPerformGroup() { return myIsPerformGroup; }
751
752     public boolean isSubstepSuppressed() { return myIsSubstepSuppressed; }
753
754     public boolean isKeepPopupOpen() { return myIsKeepPopupOpen; }
755
756     public @NlsContexts.DetailedDescription String getDescription() {
757       return myDescription == null ? myTooltip : myDescription;
758     }
759
760     public @NlsContexts.DetailedDescription String getTooltip() {
761       return myTooltip;
762     }
763
764     @Override
765     public @Nullable ShortcutSet getShortcut() {
766       return myAction.getShortcutSet();
767     }
768
769     @Override
770     public String toString() {
771       return myText;
772     }
773
774     public @NlsContexts.ListItem String getValue() {
775       return myValue;
776     }
777   }
778 }