use JBList builtin busy icon
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / lookup / impl / LookupUi.java
1 /*
2  * Copyright 2000-2015 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.codeInsight.lookup.impl;
17
18 import com.intellij.codeInsight.CodeInsightBundle;
19 import com.intellij.codeInsight.completion.CodeCompletionFeatures;
20 import com.intellij.codeInsight.completion.ShowHideIntentionIconLookupAction;
21 import com.intellij.codeInsight.hint.HintManagerImpl;
22 import com.intellij.codeInsight.lookup.LookupElement;
23 import com.intellij.codeInsight.lookup.LookupElementAction;
24 import com.intellij.featureStatistics.FeatureUsageTracker;
25 import com.intellij.icons.AllIcons;
26 import com.intellij.ide.DataManager;
27 import com.intellij.ide.ui.UISettings;
28 import com.intellij.injected.editor.EditorWindow;
29 import com.intellij.openapi.actionSystem.*;
30 import com.intellij.openapi.application.ModalityState;
31 import com.intellij.openapi.diagnostic.Logger;
32 import com.intellij.openapi.editor.Editor;
33 import com.intellij.openapi.editor.LogicalPosition;
34 import com.intellij.openapi.keymap.KeymapUtil;
35 import com.intellij.openapi.project.DumbAwareAction;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.ui.popup.JBPopupFactory;
38 import com.intellij.openapi.util.ActionCallback;
39 import com.intellij.openapi.util.Disposer;
40 import com.intellij.openapi.wm.IdeFocusManager;
41 import com.intellij.ui.ClickListener;
42 import com.intellij.ui.JBColor;
43 import com.intellij.ui.ScreenUtil;
44 import com.intellij.ui.components.JBLayeredPane;
45 import com.intellij.ui.components.JBList;
46 import com.intellij.ui.components.JBScrollPane;
47 import com.intellij.util.Alarm;
48 import com.intellij.util.PlatformIcons;
49 import com.intellij.util.ui.AbstractLayoutManager;
50 import com.intellij.util.ui.ButtonlessScrollBarUI;
51 import com.intellij.util.ui.JBUI;
52 import org.jetbrains.annotations.NotNull;
53 import org.jetbrains.annotations.Nullable;
54
55 import javax.swing.*;
56 import javax.swing.border.Border;
57 import javax.swing.border.EmptyBorder;
58 import javax.swing.border.LineBorder;
59 import javax.swing.event.ListSelectionEvent;
60 import javax.swing.event.ListSelectionListener;
61 import java.awt.*;
62 import java.awt.event.*;
63 import java.util.Collection;
64
65 /**
66  * @author peter
67  */
68 class LookupUi {
69   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupUi");
70   private final LookupImpl myLookup;
71   private final Advertiser myAdvertiser;
72   private final JBList myList;
73   private final Project myProject;
74   private final ModalityState myModalityState;
75   private final Alarm myHintAlarm = new Alarm();
76   private final JLabel mySortingLabel = new JLabel();
77   private final JScrollPane myScrollPane;
78   private final JButton myScrollBarIncreaseButton;
79   private final LookupLayeredPane myLayeredPane = new LookupLayeredPane();
80
81   private LookupHint myElementHint = null;
82   private int myMaximumHeight = Integer.MAX_VALUE;
83   private Boolean myPositionedAbove = null;
84
85   LookupUi(LookupImpl lookup, Advertiser advertiser, JBList list, Project project) {
86     myLookup = lookup;
87     myAdvertiser = advertiser;
88     myList = list;
89     myProject = project;
90
91     JComponent adComponent = advertiser.getAdComponent();
92     adComponent.setBorder(new EmptyBorder(0, 1, 1, 2 + AllIcons.Ide.LookupRelevance.getIconWidth()));
93     myLayeredPane.mainPanel.add(adComponent, BorderLayout.SOUTH);
94
95     myScrollBarIncreaseButton = new JButton();
96     myScrollBarIncreaseButton.setFocusable(false);
97     myScrollBarIncreaseButton.setRequestFocusEnabled(false);
98
99     myScrollPane = new JBScrollPane(lookup.getList());
100     myScrollPane.setViewportBorder(JBUI.Borders.empty());
101     myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
102     myScrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(13, -1));
103     myScrollPane.getVerticalScrollBar().setUI(new ButtonlessScrollBarUI() {
104       @Override
105       protected JButton createIncreaseButton(int orientation) {
106         return myScrollBarIncreaseButton;
107       }
108     });
109     lookup.getComponent().add(myLayeredPane, BorderLayout.CENTER);
110
111     //IDEA-82111
112     fixMouseCheaters();
113
114     myLayeredPane.mainPanel.add(myScrollPane, BorderLayout.CENTER);
115     myScrollPane.setBorder(null);
116
117     mySortingLabel.setBorder(new LineBorder(new JBColor(Color.LIGHT_GRAY, JBColor.background())));
118     mySortingLabel.setOpaque(true);
119     new ChangeLookupSorting().installOn(mySortingLabel);
120     updateSorting();
121     myModalityState = ModalityState.stateForComponent(lookup.getEditor().getComponent());
122
123     addListeners();
124
125     updateScrollbarVisibility();
126
127     Disposer.register(lookup, myHintAlarm);
128   }
129
130   private void addListeners() {
131     myList.addListSelectionListener(new ListSelectionListener() {
132       @Override
133       public void valueChanged(ListSelectionEvent e) {
134         if (myLookup.isLookupDisposed()) return;
135         
136         myHintAlarm.cancelAllRequests();
137
138         final LookupElement item = myLookup.getCurrentItem();
139         if (item != null) {
140           updateHint(item);
141         }
142       }
143     });
144
145     final Alarm alarm = new Alarm(myLookup);
146     myScrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
147       @Override
148       public void adjustmentValueChanged(AdjustmentEvent e) {
149         if (myLookup.myUpdating || myLookup.isLookupDisposed()) return;
150         alarm.addRequest(new Runnable() {
151           @Override
152           public void run() {
153             myLookup.refreshUi(false, false);
154           }
155         }, 300, myModalityState);
156       }
157     });
158   }
159
160   private void updateScrollbarVisibility() {
161     boolean showSorting = myLookup.isCompletion() && myList.getModel().getSize() >= 3;
162     mySortingLabel.setVisible(showSorting);
163     myScrollPane.setVerticalScrollBarPolicy(showSorting ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
164   }
165
166   private void updateHint(@NotNull final LookupElement item) {
167     myLookup.checkValid();
168     if (myElementHint != null) {
169       myLayeredPane.remove(myElementHint);
170       myElementHint = null;
171       final JRootPane rootPane = myLookup.getComponent().getRootPane();
172       if (rootPane != null) {
173         rootPane.revalidate();
174         rootPane.repaint();
175       }
176     }
177     if (!item.isValid()) {
178       return;
179     }
180
181     final Collection<LookupElementAction> actions = myLookup.getActionsFor(item);
182     if (!actions.isEmpty()) {
183       myHintAlarm.addRequest(new Runnable() {
184         @Override
185         public void run() {
186           if (!ShowHideIntentionIconLookupAction.shouldShowLookupHint() ||
187               ((CompletionExtender)myList.getExpandableItemsHandler()).isShowing()) {
188             return;
189           }
190           myElementHint = new LookupHint();
191           myLayeredPane.add(myElementHint, 20, 0);
192           myLayeredPane.layoutHint();
193         }
194       }, 500, myModalityState);
195     }
196   }
197
198   //Yes, it's possible to move focus to the hint. It's inconvenient, it doesn't make sense, but it's possible.
199   // This fix is for those jerks
200   private void fixMouseCheaters() {
201     myLookup.getComponent().addFocusListener(new FocusAdapter() {
202       @Override
203       public void focusGained(FocusEvent e) {
204         final ActionCallback done = IdeFocusManager.getInstance(myProject).requestFocus(myLookup.getEditor().getContentComponent(), true);
205         IdeFocusManager.getInstance(myProject).typeAheadUntil(done);
206         new Alarm(myLookup).addRequest(new Runnable() {
207           @Override
208           public void run() {
209             if (!done.isDone()) {
210               done.setDone();
211             }
212           }
213         }, 300, myModalityState);
214       }
215     });
216   }
217
218   void setCalculating(final boolean calculating) {
219     Runnable setVisible = new Runnable() {
220       @Override
221       public void run() {
222         myList.setPaintBusy(myLookup.isCalculating());
223       }
224     };
225     if (myLookup.isCalculating()) {
226       new Alarm(myLookup).addRequest(setVisible, 100, myModalityState);
227     } else {
228       setVisible.run();
229     }
230   }
231
232   private void updateSorting() {
233     final boolean lexi = UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
234     mySortingLabel.setIcon(lexi ? AllIcons.Ide.LookupAlphanumeric : AllIcons.Ide.LookupRelevance);
235     mySortingLabel.setToolTipText(lexi ? "Click to sort variants by relevance" : "Click to sort variants alphabetically");
236
237     myLookup.resort(false);
238   }
239
240   void refreshUi(boolean selectionVisible, boolean itemsChanged, boolean reused, boolean onExplicitAction) {
241     Editor editor = myLookup.getEditor();
242     if (editor.getComponent().getRootPane() == null || editor instanceof EditorWindow && !((EditorWindow)editor).isValid()) {
243       return;
244     }
245
246     updateScrollbarVisibility();
247
248     if (myLookup.myResizePending || itemsChanged) {
249       myMaximumHeight = Integer.MAX_VALUE;
250     }
251     Rectangle rectangle = calculatePosition();
252     myMaximumHeight = rectangle.height;
253
254     if (myLookup.myResizePending || itemsChanged) {
255       myLookup.myResizePending = false;
256       myLookup.pack();
257     }
258     HintManagerImpl.updateLocation(myLookup, editor, rectangle.getLocation());
259
260     if (reused || selectionVisible || onExplicitAction) {
261       myLookup.ensureSelectionVisible(false);
262     }
263   }
264
265   boolean isPositionedAboveCaret() {
266     return myPositionedAbove != null && myPositionedAbove.booleanValue();
267   }
268
269   // in layered pane coordinate system.
270   Rectangle calculatePosition() {
271     Dimension dim = myLookup.getComponent().getPreferredSize();
272     int lookupStart = myLookup.getLookupStart();
273     Editor editor = myLookup.getEditor();
274     if (lookupStart < 0 || lookupStart > editor.getDocument().getTextLength()) {
275       LOG.error(lookupStart + "; offset=" + editor.getCaretModel().getOffset() + "; element=" +
276                 myLookup.getPsiElement());
277     }
278
279     LogicalPosition pos = editor.offsetToLogicalPosition(lookupStart);
280     Point location = editor.logicalPositionToXY(pos);
281     location.y += editor.getLineHeight();
282     location.x -= myLookup.myCellRenderer.getIconIndent() + myLookup.getComponent().getInsets().left;
283
284     SwingUtilities.convertPointToScreen(location, editor.getContentComponent());
285     final Rectangle screenRectangle = ScreenUtil.getScreenRectangle(location);
286
287     if (!isPositionedAboveCaret()) {
288       int shiftLow = screenRectangle.height - (location.y + dim.height);
289       myPositionedAbove = shiftLow < 0 && shiftLow < location.y - dim.height && location.y >= dim.height;
290     }
291     if (isPositionedAboveCaret()) {
292       location.y -= dim.height + editor.getLineHeight();
293       if (pos.line == 0) {
294         location.y += 1;
295         //otherwise the lookup won't intersect with the editor and every editor's resize (e.g. after typing in console) will close the lookup
296       }
297     }
298
299     if (!screenRectangle.contains(location)) {
300       location = ScreenUtil.findNearestPointOnBorder(screenRectangle, location);
301     }
302
303     final JRootPane rootPane = editor.getComponent().getRootPane();
304     if (rootPane == null) {
305       LOG.error("editor.disposed=" + editor.isDisposed() + "; lookup.disposed=" + myLookup.isLookupDisposed() + "; editorShowing=" + editor.getContentComponent().isShowing());
306     }
307     Rectangle candidate = new Rectangle(location, dim);
308     ScreenUtil.cropRectangleToFitTheScreen(candidate);
309
310     SwingUtilities.convertPointFromScreen(location, rootPane.getLayeredPane());
311     myMaximumHeight = candidate.height;
312     return new Rectangle(location.x, location.y, dim.width, candidate.height);
313   }
314
315   private class LookupLayeredPane extends JBLayeredPane {
316     final JPanel mainPanel = new JPanel(new BorderLayout());
317
318     private LookupLayeredPane() {
319       add(mainPanel, 0, 0);
320       add(mySortingLabel, 10, 0);
321
322       setLayout(new AbstractLayoutManager() {
323         @Override
324         public Dimension preferredLayoutSize(@Nullable Container parent) {
325           int maxCellWidth = myLookup.myLookupTextWidth + myLookup.myCellRenderer.getIconIndent();
326           int scrollBarWidth = myScrollPane.getPreferredSize().width - myScrollPane.getViewport().getPreferredSize().width;
327           int listWidth = Math.min(scrollBarWidth + maxCellWidth, UISettings.getInstance().MAX_LOOKUP_WIDTH2);
328
329           Dimension adSize = myAdvertiser.getAdComponent().getPreferredSize();
330
331           int panelHeight = myList.getPreferredScrollableViewportSize().height + adSize.height;
332           if (myList.getModel().getSize() > myList.getVisibleRowCount() && myList.getVisibleRowCount() >= 5) {
333             panelHeight -= myList.getFixedCellHeight() / 2;
334           }
335           return new Dimension(Math.max(listWidth, adSize.width), Math.min(panelHeight, myMaximumHeight));
336         }
337
338         @Override
339         public void layoutContainer(Container parent) {
340           Dimension size = getSize();
341           mainPanel.setSize(size);
342           mainPanel.validate();
343
344           if (!myLookup.myResizePending) {
345             Dimension preferredSize = preferredLayoutSize(null);
346             if (preferredSize.width != size.width) {
347               UISettings.getInstance().MAX_LOOKUP_WIDTH2 = Math.max(500, size.width);
348             }
349
350             int listHeight = myList.getLastVisibleIndex() - myList.getFirstVisibleIndex() + 1;
351             if (listHeight != myList.getModel().getSize() && listHeight != myList.getVisibleRowCount() && preferredSize.height != size.height) {
352               UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT = Math.max(5, listHeight);
353             }
354           }
355
356           myList.setFixedCellWidth(myScrollPane.getViewport().getWidth());
357           layoutStatusIcons();
358           layoutHint();
359         }
360       });
361     }
362
363     private void layoutStatusIcons() {
364       int adHeight = myAdvertiser.getAdComponent().getPreferredSize().height;
365       Dimension buttonSize = adHeight > 0 || !mySortingLabel.isVisible() ? new Dimension(0, 0) : new Dimension(
366         AllIcons.Ide.LookupRelevance.getIconWidth(), AllIcons.Ide.LookupRelevance.getIconHeight());
367       myScrollBarIncreaseButton.setPreferredSize(buttonSize);
368       myScrollBarIncreaseButton.setMinimumSize(buttonSize);
369       myScrollBarIncreaseButton.setMaximumSize(buttonSize);
370       JScrollBar vScrollBar = myScrollPane.getVerticalScrollBar();
371       vScrollBar.revalidate();
372       vScrollBar.repaint();
373       
374       final Dimension sortSize = mySortingLabel.getPreferredSize();
375       final int sortWidth = vScrollBar.isVisible() ? vScrollBar.getWidth() : sortSize.width;
376       final int sortHeight = Math.max(sortSize.height, adHeight);
377       mySortingLabel.setBounds(getWidth() - sortWidth, getHeight() - sortHeight, sortSize.width, sortHeight);
378     }
379
380     void layoutHint() {
381       if (myElementHint != null && myLookup.getCurrentItem() != null) {
382         final Rectangle bounds = myLookup.getCurrentItemBounds();
383         myElementHint.setSize(myElementHint.getPreferredSize());
384
385         JScrollBar sb = myScrollPane.getVerticalScrollBar();
386         int x = bounds.x + bounds.width - myElementHint.getWidth() + (sb.isVisible() ? sb.getWidth() : 0);
387         x = Math.min(x, getWidth() - myElementHint.getWidth());
388         myElementHint.setLocation(new Point(x, bounds.y));
389       }
390     }
391   }
392
393   private class LookupHint extends JLabel {
394     private final Border INACTIVE_BORDER = BorderFactory.createEmptyBorder(2, 2, 2, 2);
395     private final Border ACTIVE_BORDER = BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.BLACK, 1), BorderFactory.createEmptyBorder(1, 1, 1, 1));
396     private LookupHint() {
397       setOpaque(false);
398       setBorder(INACTIVE_BORDER);
399       setIcon(AllIcons.Actions.IntentionBulb);
400       String acceleratorsText = KeymapUtil.getFirstKeyboardShortcutText(
401         ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS));
402       if (acceleratorsText.length() > 0) {
403         setToolTipText(CodeInsightBundle.message("lightbulb.tooltip", acceleratorsText));
404       }
405
406       addMouseListener(new MouseAdapter() {
407         @Override
408         public void mouseEntered(MouseEvent e) {
409           setBorder(ACTIVE_BORDER);
410         }
411
412         @Override
413         public void mouseExited(MouseEvent e) {
414           setBorder(INACTIVE_BORDER);
415         }
416         @Override
417         public void mousePressed(MouseEvent e) {
418           if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
419             myLookup.showElementActions();
420           }
421         }
422       });
423     }
424   }
425
426   private class ChangeLookupSorting extends ClickListener {
427
428     @Override
429     public boolean onClick(@NotNull MouseEvent e, int clickCount) {
430       DataContext context = DataManager.getInstance().getDataContext(mySortingLabel);
431       DefaultActionGroup group = new DefaultActionGroup();
432       group.add(createSortingAction(true));
433       group.add(createSortingAction(false));
434       JBPopupFactory.getInstance().createActionGroupPopup("Change sorting", group, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false).showInBestPositionFor(
435         context);
436       return true;
437     }
438
439     private AnAction createSortingAction(boolean checked) {
440       boolean currentSetting = UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
441       final boolean newSetting = checked ? currentSetting : !currentSetting;
442       return new DumbAwareAction(newSetting ? "Sort lexicographically" : "Sort by relevance", null, checked ? PlatformIcons.CHECK_ICON : null) {
443         @Override
444         public void actionPerformed(AnActionEvent e) {
445           FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CHANGE_SORTING);
446           UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY = newSetting;
447           updateSorting();
448         }
449       };
450     }
451   }
452 }