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