2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.codeInsight.lookup.impl;
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;
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;
64 import java.awt.event.*;
65 import java.util.Collection;
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();
85 private LookupHint myElementHint = null;
86 private int myMaximumHeight = Integer.MAX_VALUE;
87 private Boolean myPositionedAbove = null;
89 LookupUi(LookupImpl lookup, Advertiser advertiser, JBList list, Project project) {
91 myAdvertiser = advertiser;
95 myIconPanel.setVisible(false);
96 myIconPanel.setBackground(Color.LIGHT_GRAY);
97 myIconPanel.add(myProcessIcon);
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);
103 myScrollBarIncreaseButton = new JButton();
104 myScrollBarIncreaseButton.setFocusable(false);
105 myScrollBarIncreaseButton.setRequestFocusEnabled(false);
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() {
113 protected JButton createIncreaseButton(int orientation) {
114 return myScrollBarIncreaseButton;
117 lookup.getComponent().add(myLayeredPane, BorderLayout.CENTER);
122 myLayeredPane.mainPanel.add(myScrollPane, BorderLayout.CENTER);
123 myScrollPane.setBorder(null);
125 mySortingLabel.setBorder(new LineBorder(new JBColor(Color.LIGHT_GRAY, JBColor.background())));
126 mySortingLabel.setOpaque(true);
127 new ChangeLookupSorting().installOn(mySortingLabel);
129 myModalityState = ModalityState.stateForComponent(lookup.getTopLevelEditor().getComponent());
133 updateScrollbarVisibility();
135 Disposer.register(lookup, myProcessIcon);
136 Disposer.register(lookup, myHintAlarm);
139 private void addListeners() {
140 myList.addListSelectionListener(new ListSelectionListener() {
142 public void valueChanged(ListSelectionEvent e) {
143 if (myLookup.isLookupDisposed()) return;
145 myHintAlarm.cancelAllRequests();
147 final LookupElement item = myLookup.getCurrentItem();
154 final Alarm alarm = new Alarm(myLookup);
155 myScrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
157 public void adjustmentValueChanged(AdjustmentEvent e) {
158 if (myLookup.myUpdating || myLookup.isLookupDisposed()) return;
159 alarm.addRequest(new Runnable() {
162 myLookup.refreshUi(false, false);
164 }, 300, myModalityState);
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);
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();
186 if (!item.isValid()) {
190 final Collection<LookupElementAction> actions = myLookup.getActionsFor(item);
191 if (!actions.isEmpty()) {
192 myHintAlarm.addRequest(new Runnable() {
195 if (!ShowHideIntentionIconLookupAction.shouldShowLookupHint() ||
196 ((CompletionExtender)myList.getExpandableItemsHandler()).isShowing()) {
199 myElementHint = new LookupHint();
200 myLayeredPane.add(myElementHint, 20, 0);
201 myLayeredPane.layoutHint();
203 }, 500, myModalityState);
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() {
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() {
218 if (!done.isDone()) {
222 }, 300, myModalityState);
227 void setCalculating(final boolean calculating) {
228 Runnable setVisible = new Runnable() {
231 myIconPanel.setVisible(myLookup.isCalculating());
234 if (myLookup.isCalculating()) {
235 new Alarm(myLookup).addRequest(setVisible, 100, myModalityState);
241 myProcessIcon.resume();
243 myProcessIcon.suspend();
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");
252 myLookup.resort(false);
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()) {
261 updateScrollbarVisibility();
263 if (myLookup.myResizePending || itemsChanged) {
264 myMaximumHeight = Integer.MAX_VALUE;
266 Rectangle rectangle = calculatePosition();
267 myMaximumHeight = rectangle.height;
269 if (myLookup.myResizePending || itemsChanged) {
270 myLookup.myResizePending = false;
273 HintManagerImpl.updateLocation(myLookup, editor, rectangle.getLocation());
275 if (reused || selectionVisible || onExplicitAction) {
276 myLookup.ensureSelectionVisible(false);
280 boolean isPositionedAboveCaret() {
281 return myPositionedAbove != null && myPositionedAbove.booleanValue();
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());
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;
306 SwingUtilities.convertPointToScreen(location, editor.getContentComponent());
307 final Rectangle screenRectangle = ScreenUtil.getScreenRectangle(location);
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;
313 if (isPositionedAboveCaret()) {
314 location.y -= dim.height + editor.getLineHeight();
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
321 if (!screenRectangle.contains(location)) {
322 location = ScreenUtil.findNearestPointOnBorder(screenRectangle, location);
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());
329 Rectangle candidate = new Rectangle(location, dim);
330 ScreenUtil.cropRectangleToFitTheScreen(candidate);
332 SwingUtilities.convertPointFromScreen(location, rootPane.getLayeredPane());
333 myMaximumHeight = candidate.height;
334 return new Rectangle(location.x, location.y, dim.width, candidate.height);
337 private class LookupLayeredPane extends JBLayeredPane {
338 final JPanel mainPanel = new JPanel(new BorderLayout());
340 private LookupLayeredPane() {
341 add(mainPanel, 0, 0);
342 add(myIconPanel, 42, 0);
343 add(mySortingLabel, 10, 0);
345 setLayout(new AbstractLayoutManager() {
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);
352 Dimension adSize = myAdvertiser.getAdComponent().getPreferredSize();
354 int panelHeight = myList.getPreferredScrollableViewportSize().height + adSize.height;
355 if (myList.getModel().getSize() > myList.getVisibleRowCount() && myList.getVisibleRowCount() >= 5) {
356 panelHeight -= myList.getFixedCellHeight() / 2;
358 return new Dimension(Math.max(listWidth, adSize.width), Math.min(panelHeight, myMaximumHeight));
362 public void layoutContainer(Container parent) {
363 Dimension size = getSize();
364 mainPanel.setSize(size);
365 mainPanel.validate();
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);
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);
379 myList.setFixedCellWidth(myScrollPane.getViewport().getWidth());
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();
397 final Dimension iconSize = myProcessIcon.getPreferredSize();
398 myIconPanel.setBounds(getWidth() - iconSize.width - (vScrollBar.isVisible() ? vScrollBar.getWidth() : 0), 0, iconSize.width,
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);
408 if (myElementHint != null && myLookup.getCurrentItem() != null) {
409 final Rectangle bounds = myLookup.getCurrentItemBounds();
410 myElementHint.setSize(myElementHint.getPreferredSize());
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));
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() {
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));
433 addMouseListener(new MouseAdapter() {
435 public void mouseEntered(MouseEvent e) {
436 setBorder(ACTIVE_BORDER);
440 public void mouseExited(MouseEvent e) {
441 setBorder(INACTIVE_BORDER);
444 public void mousePressed(MouseEvent e) {
445 if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
446 myLookup.showElementActions();
453 private class ChangeLookupSorting extends ClickListener {
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(
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) {
471 public void actionPerformed(AnActionEvent e) {
472 FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CHANGE_SORTING);
473 UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY = newSetting;