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