26bdf8e56955e690d2c15b70ddbf8ae6bf7439af
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / lookup / impl / LookupUi.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.codeInsight.lookup.impl;
3
4 import com.intellij.codeInsight.CodeInsightBundle;
5 import com.intellij.codeInsight.CodeInsightSettings;
6 import com.intellij.codeInsight.completion.CodeCompletionFeatures;
7 import com.intellij.codeInsight.completion.ShowHideIntentionIconLookupAction;
8 import com.intellij.codeInsight.hint.HintManagerImpl;
9 import com.intellij.codeInsight.lookup.LookupElement;
10 import com.intellij.codeInsight.lookup.LookupElementAction;
11 import com.intellij.featureStatistics.FeatureUsageTracker;
12 import com.intellij.icons.AllIcons;
13 import com.intellij.ide.IdeEventQueue;
14 import com.intellij.ide.ui.UISettings;
15 import com.intellij.idea.ActionsBundle;
16 import com.intellij.injected.editor.EditorWindow;
17 import com.intellij.openapi.actionSystem.*;
18 import com.intellij.openapi.actionSystem.impl.ActionButton;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.application.ModalityState;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.editor.Editor;
23 import com.intellij.openapi.editor.LogicalPosition;
24 import com.intellij.openapi.project.DumbAwareAction;
25 import com.intellij.openapi.util.Disposer;
26 import com.intellij.openapi.util.registry.Registry;
27 import com.intellij.ui.ComponentUtil;
28 import com.intellij.ui.ScreenUtil;
29 import com.intellij.ui.ScrollPaneFactory;
30 import com.intellij.ui.components.JBLayeredPane;
31 import com.intellij.ui.components.JBList;
32 import com.intellij.ui.components.JBScrollPane;
33 import com.intellij.ui.components.panels.NonOpaquePanel;
34 import com.intellij.util.Alarm;
35 import com.intellij.util.PlatformIcons;
36 import com.intellij.util.ui.AbstractLayoutManager;
37 import com.intellij.util.ui.AsyncProcessIcon;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import javax.swing.*;
42 import javax.swing.event.ListSelectionEvent;
43 import javax.swing.event.ListSelectionListener;
44 import java.awt.*;
45 import java.awt.event.MouseEvent;
46 import java.util.Collection;
47
48 /**
49  * @author peter
50  */
51 class LookupUi {
52   private static final Logger LOG = Logger.getInstance(LookupUi.class);
53
54   @NotNull
55   private final LookupImpl myLookup;
56   private final Advertiser myAdvertiser;
57   private final JBList myList;
58   private final ModalityState myModalityState;
59   private final Alarm myHintAlarm = new Alarm();
60   private final JScrollPane myScrollPane;
61   private final AsyncProcessIcon myProcessIcon = new AsyncProcessIcon("Completion progress");
62   private final ActionButton myMenuButton;
63   private final ActionButton myHintButton;
64   private final JComponent myBottomPanel;
65
66   private int myMaximumHeight = Integer.MAX_VALUE;
67   private Boolean myPositionedAbove = null;
68
69   LookupUi(@NotNull LookupImpl lookup, Advertiser advertiser, JBList list) {
70     myLookup = lookup;
71     myAdvertiser = advertiser;
72     myList = list;
73
74     myProcessIcon.setVisible(false);
75     myLookup.resort(false);
76
77     MenuAction menuAction = new MenuAction();
78     menuAction.add(new ChangeSortingAction());
79     menuAction.add(new DelegatedAction(ActionManager.getInstance().getAction(IdeActions.ACTION_QUICK_JAVADOC)){
80       @Override
81       public void update(@NotNull AnActionEvent e) {
82         e.getPresentation().setVisible(!CodeInsightSettings.getInstance().AUTO_POPUP_JAVADOC_INFO);
83       }
84     });
85     menuAction.add(new DelegatedAction(ActionManager.getInstance().getAction(IdeActions.ACTION_QUICK_IMPLEMENTATIONS)));
86
87     Presentation presentation = new Presentation();
88     presentation.setIcon(AllIcons.Actions.More);
89     presentation.putClientProperty(ActionButton.HIDE_DROPDOWN_ICON, Boolean.TRUE);
90
91     myMenuButton = new ActionButton(menuAction, presentation, ActionPlaces.EDITOR_POPUP, ActionToolbar.NAVBAR_MINIMUM_BUTTON_SIZE);
92
93     AnAction hintAction = new HintAction();
94     myHintButton = new ActionButton(hintAction, hintAction.getTemplatePresentation(),
95                                     ActionPlaces.EDITOR_POPUP, ActionToolbar.NAVBAR_MINIMUM_BUTTON_SIZE);
96     myHintButton.setVisible(false);
97
98     myBottomPanel = new NonOpaquePanel(new LookupBottomLayout());
99     myBottomPanel.add(myAdvertiser.getAdComponent());
100     myBottomPanel.add(myProcessIcon);
101     myBottomPanel.add(myHintButton);
102     myBottomPanel.add(myMenuButton);
103
104     LookupLayeredPane layeredPane = new LookupLayeredPane();
105     layeredPane.mainPanel.add(myBottomPanel, BorderLayout.SOUTH);
106
107     myScrollPane = ScrollPaneFactory.createScrollPane(lookup.getList(), true);
108     myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
109     ComponentUtil.putClientProperty(myScrollPane.getVerticalScrollBar(), JBScrollPane.IGNORE_SCROLLBAR_IN_INSETS, true);
110
111     lookup.getComponent().add(layeredPane, BorderLayout.CENTER);
112
113     layeredPane.mainPanel.add(myScrollPane, BorderLayout.CENTER);
114
115     myModalityState = ModalityState.stateForComponent(lookup.getTopLevelEditor().getComponent());
116
117     addListeners();
118
119     Disposer.register(lookup, myProcessIcon);
120     Disposer.register(lookup, myHintAlarm);
121   }
122
123   private void addListeners() {
124     myList.addListSelectionListener(new ListSelectionListener() {
125       @Override
126       public void valueChanged(ListSelectionEvent e) {
127         if (myLookup.isLookupDisposed()) return;
128
129         myHintAlarm.cancelAllRequests();
130         updateHint();
131       }
132     });
133   }
134
135   private void updateHint() {
136     myLookup.checkValid();
137     if (myHintButton.isVisible()) {
138       myHintButton.setVisible(false);
139     }
140
141     LookupElement item = myLookup.getCurrentItem();
142     if (item != null && item.isValid()) {
143       Collection<LookupElementAction> actions = myLookup.getActionsFor(item);
144       if (!actions.isEmpty()) {
145         myHintAlarm.addRequest(() -> {
146           if (ShowHideIntentionIconLookupAction.shouldShowLookupHint() &&
147               !((CompletionExtender)myList.getExpandableItemsHandler()).isShowing() &&
148               !myProcessIcon.isVisible()) {
149             myHintButton.setVisible(true);
150           }
151         }, 500, myModalityState);
152       }
153     }
154   }
155
156   void setCalculating(boolean calculating) {
157     Runnable iconUpdater = () -> {
158       if (calculating && myHintButton.isVisible()) {
159         myHintButton.setVisible(false);
160       }
161       myProcessIcon.setVisible(calculating);
162
163       ApplicationManager.getApplication().invokeLater(() -> {
164         if (!calculating && !myLookup.isLookupDisposed()) {
165           updateHint();
166         }
167       }, myModalityState);
168     };
169
170     if (calculating) {
171       myProcessIcon.resume();
172     } else {
173       myProcessIcon.suspend();
174     }
175     new Alarm(myLookup).addRequest(iconUpdater, 100, myModalityState);
176   }
177
178   void refreshUi(boolean selectionVisible, boolean itemsChanged, boolean reused, boolean onExplicitAction) {
179     Editor editor = myLookup.getTopLevelEditor();
180     if (editor.getComponent().getRootPane() == null || editor instanceof EditorWindow && !((EditorWindow)editor).isValid()) {
181       return;
182     }
183
184     if (myLookup.myResizePending || itemsChanged) {
185       myMaximumHeight = Integer.MAX_VALUE;
186     }
187     Rectangle rectangle = calculatePosition();
188     myMaximumHeight = rectangle.height;
189
190     if (myLookup.myResizePending || itemsChanged) {
191       myLookup.myResizePending = false;
192       myLookup.pack();
193       rectangle = calculatePosition();
194     }
195     HintManagerImpl.updateLocation(myLookup, editor, rectangle.getLocation());
196
197     if (reused || selectionVisible || onExplicitAction) {
198       myLookup.ensureSelectionVisible(false);
199     }
200   }
201
202   boolean isPositionedAboveCaret() {
203     return myPositionedAbove != null && myPositionedAbove.booleanValue();
204   }
205
206   // in layered pane coordinate system.
207   Rectangle calculatePosition() {
208     final JComponent lookupComponent = myLookup.getComponent();
209     Dimension dim = lookupComponent.getPreferredSize();
210     int lookupStart = myLookup.getLookupStart();
211     Editor editor = myLookup.getTopLevelEditor();
212     if (lookupStart < 0 || lookupStart > editor.getDocument().getTextLength()) {
213       LOG.error(lookupStart + "; offset=" + editor.getCaretModel().getOffset() + "; element=" +
214                 myLookup.getPsiElement());
215     }
216
217     LogicalPosition pos = editor.offsetToLogicalPosition(lookupStart);
218     Point location = editor.logicalPositionToXY(pos);
219     location.y += editor.getLineHeight();
220     location.x -= myLookup.myCellRenderer.getTextIndent();
221     // extra check for other borders
222     final Window window = ComponentUtil.getWindow(lookupComponent);
223     if (window != null) {
224       final Point point = SwingUtilities.convertPoint(lookupComponent, 0, 0, window);
225       location.x -= point.x;
226     }
227
228     SwingUtilities.convertPointToScreen(location, editor.getContentComponent());
229     final Rectangle screenRectangle = ScreenUtil.getScreenRectangle(editor.getContentComponent());
230
231     if (!isPositionedAboveCaret()) {
232       int shiftLow = screenRectangle.y + screenRectangle.height - (location.y + dim.height);
233       myPositionedAbove = shiftLow < 0 && shiftLow < location.y - dim.height && location.y >= dim.height;
234     }
235     if (isPositionedAboveCaret()) {
236       location.y -= dim.height + editor.getLineHeight();
237     }
238
239     if (!screenRectangle.contains(location)) {
240       location = ScreenUtil.findNearestPointOnBorder(screenRectangle, location);
241     }
242
243     Rectangle candidate = new Rectangle(location, dim);
244     ScreenUtil.cropRectangleToFitTheScreen(candidate);
245
246     if (isPositionedAboveCaret()) {
247       // need to crop as well at bottom if lookup overlaps current line
248       Point caretLocation = editor.logicalPositionToXY(pos);
249       SwingUtilities.convertPointToScreen(caretLocation, editor.getContentComponent());
250       int offset = location.y + dim.height - caretLocation.y;
251       if (offset > 0) {
252         candidate.height -= offset;
253       }
254     }
255
256     JRootPane rootPane = editor.getComponent().getRootPane();
257     if (rootPane != null) {
258       SwingUtilities.convertPointFromScreen(location, rootPane.getLayeredPane());
259     }
260     else {
261       LOG.error("editor.disposed=" + editor.isDisposed() + "; lookup.disposed=" + myLookup.isLookupDisposed() + "; editorShowing=" + editor.getContentComponent().isShowing());
262     }
263
264     myMaximumHeight = candidate.height;
265     return new Rectangle(location.x, location.y, dim.width, candidate.height);
266   }
267
268   private final class LookupLayeredPane extends JBLayeredPane {
269     final JPanel mainPanel = new JPanel(new BorderLayout());
270
271     private LookupLayeredPane() {
272       mainPanel.setBackground(LookupCellRenderer.BACKGROUND_COLOR);
273       add(mainPanel, 0, 0);
274
275       setLayout(new AbstractLayoutManager() {
276         @Override
277         public Dimension preferredLayoutSize(@Nullable Container parent) {
278           int maxCellWidth = myLookup.myCellRenderer.getLookupTextWidth() + myLookup.myCellRenderer.getTextIndent();
279           int scrollBarWidth = myScrollPane.getVerticalScrollBar().getWidth();
280           int listWidth = Math.min(scrollBarWidth + maxCellWidth, UISettings.getInstance().getMaxLookupWidth());
281
282           Dimension bottomPanelSize = myBottomPanel.getPreferredSize();
283
284           int panelHeight = myScrollPane.getPreferredSize().height + bottomPanelSize.height;
285           int width = Math.max(listWidth, bottomPanelSize.width);
286           width = Math.min(width, Registry.intValue("ide.completion.max.width"));
287           int height = Math.min(panelHeight, myMaximumHeight);
288
289           return new Dimension(width, height);
290         }
291
292         @Override
293         public void layoutContainer(Container parent) {
294           Dimension size = getSize();
295           mainPanel.setSize(size);
296           mainPanel.validate();
297
298           if (IdeEventQueue.getInstance().getTrueCurrentEvent().getID() == MouseEvent.MOUSE_DRAGGED) {
299             Dimension preferredSize = preferredLayoutSize(null);
300             if (preferredSize.width != size.width) {
301               UISettings.getInstance().setMaxLookupWidth(Math.max(500, size.width));
302             }
303
304             int listHeight = myList.getLastVisibleIndex() - myList.getFirstVisibleIndex() + 1;
305             if (listHeight != myList.getModel().getSize() && listHeight != myList.getVisibleRowCount() && preferredSize.height != size.height) {
306               UISettings.getInstance().setMaxLookupListHeight(Math.max(5, listHeight));
307             }
308           }
309
310           myList.setFixedCellWidth(myScrollPane.getViewport().getWidth());
311         }
312       });
313     }
314   }
315
316   private final class HintAction extends DumbAwareAction {
317     private HintAction() {
318       super(AllIcons.Actions.IntentionBulb);
319
320       AnAction showIntentionAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS);
321       if (showIntentionAction != null) {
322         copyShortcutFrom(showIntentionAction);
323         getTemplatePresentation().setText(CodeInsightBundle.messagePointer("action.presentation.LookupUi.text"));
324       }
325     }
326
327     @Override
328     public void actionPerformed(@NotNull AnActionEvent e) {
329       myLookup.showElementActions(e.getInputEvent());
330     }
331   }
332
333   private static final class MenuAction extends DefaultActionGroup implements HintManagerImpl.ActionToIgnore {
334     private MenuAction() {
335       setPopup(true);
336     }
337   }
338
339   private final class ChangeSortingAction extends DumbAwareAction implements HintManagerImpl.ActionToIgnore {
340     private ChangeSortingAction() {
341       super(ActionsBundle.messagePointer("action.ChangeSortingAction.text"));
342     }
343
344     @Override
345     public void actionPerformed(@NotNull AnActionEvent e) {
346       FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CHANGE_SORTING);
347       UISettings settings = UISettings.getInstance();
348       settings.setSortLookupElementsLexicographically(!settings.getSortLookupElementsLexicographically());
349       myLookup.resort(false);
350     }
351
352     @Override
353     public void update(@NotNull AnActionEvent e) {
354       e.getPresentation().setIcon(UISettings.getInstance().getSortLookupElementsLexicographically() ? PlatformIcons.CHECK_ICON : null);
355     }
356   }
357
358   private static class DelegatedAction extends DumbAwareAction implements HintManagerImpl.ActionToIgnore {
359     private final AnAction delegateAction;
360     private DelegatedAction(AnAction action) {
361       delegateAction = action;
362       getTemplatePresentation().setText(delegateAction.getTemplateText(), true);
363       copyShortcutFrom(delegateAction);
364     }
365
366     @Override
367     public void actionPerformed(@NotNull AnActionEvent e) {
368       if (e.getPlace() == ActionPlaces.EDITOR_POPUP) {
369         delegateAction.actionPerformed(e);
370       }
371     }
372   }
373
374   private class LookupBottomLayout implements LayoutManager {
375     @Override
376     public void addLayoutComponent(String name, Component comp) {}
377
378     @Override
379     public void removeLayoutComponent(Component comp) {}
380
381     @Override
382     public Dimension preferredLayoutSize(Container parent) {
383       Dimension adSize = myAdvertiser.getAdComponent().getPreferredSize();
384       Dimension hintButtonSize = myHintButton.getPreferredSize();
385       Dimension menuButtonSize = myMenuButton.getPreferredSize();
386
387       return new Dimension(adSize.width + hintButtonSize.width + menuButtonSize.width,
388                            Math.max(adSize.height, menuButtonSize.height));
389     }
390
391     @Override
392     public Dimension minimumLayoutSize(Container parent) {
393       Dimension adSize = myAdvertiser.getAdComponent().getMinimumSize();
394       Dimension hintButtonSize = myHintButton.getMinimumSize();
395       Dimension menuButtonSize = myMenuButton.getMinimumSize();
396
397       return new Dimension(adSize.width + hintButtonSize.width + menuButtonSize.width,
398                            Math.max(adSize.height, menuButtonSize.height));
399     }
400
401     @Override
402     public void layoutContainer(Container parent) {
403       Dimension size = parent.getSize();
404
405       Dimension menuButtonSize = myMenuButton.getPreferredSize();
406       int x = size.width - menuButtonSize.width;
407       int y = (size.height - menuButtonSize.height) / 2;
408
409       myMenuButton.setBounds(x, y, menuButtonSize.width, menuButtonSize.height);
410
411       Dimension myHintButtonSize = myHintButton.getPreferredSize();
412       if (myHintButton.isVisible() && !myProcessIcon.isVisible()) {
413         x -= myHintButtonSize.width;
414         y = (size.height - myHintButtonSize.height) / 2;
415         myHintButton.setBounds(x, y, myHintButtonSize.width, myHintButtonSize.height);
416       }
417       else if (!myHintButton.isVisible() && myProcessIcon.isVisible()) {
418         Dimension myProcessIconSize = myProcessIcon.getPreferredSize();
419         x -= myProcessIconSize.width;
420         y = (size.height - myProcessIconSize.height) / 2;
421         myProcessIcon.setBounds(x, y, myProcessIconSize.width, myProcessIconSize.height);
422       }
423       else if (!myHintButton.isVisible() && !myProcessIcon.isVisible()) {
424         x -= myHintButtonSize.width;
425       }
426       else {
427         throw new IllegalStateException("Can't show both process icon and hint button");
428       }
429
430       Dimension adSize = myAdvertiser.getAdComponent().getPreferredSize();
431       y = (size.height - adSize.height) / 2;
432       myAdvertiser.getAdComponent().setBounds(0, y, x, adSize.height);
433     }
434   }
435 }