blob: b1b0c8434ebb52c31f3e5022a1df38685b7d3292 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.lookup.impl;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.completion.CodeCompletionFeatures;
import com.intellij.codeInsight.completion.ShowHideIntentionIconLookupAction;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementAction;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.ClickListener;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.Alarm;
import com.intellij.util.PlatformIcons;
import com.intellij.util.ui.AbstractLayoutManager;
import com.intellij.util.ui.AsyncProcessIcon;
import com.intellij.util.ui.ButtonlessScrollBarUI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.util.Collection;
/**
* @author peter
*/
class LookupUi {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupUi");
private final LookupImpl myLookup;
private final Advertiser myAdvertiser;
private final JBList myList;
private final Project myProject;
private final ModalityState myModalityState;
private final Alarm myHintAlarm = new Alarm();
private final JLabel mySortingLabel = new JLabel();
private final JScrollPane myScrollPane;
private final JButton myScrollBarIncreaseButton;
private final AsyncProcessIcon myProcessIcon = new AsyncProcessIcon("Completion progress");
private final JPanel myIconPanel = new JPanel(new BorderLayout());
private final LookupLayeredPane myLayeredPane = new LookupLayeredPane();
private LookupHint myElementHint = null;
private int myMaximumHeight = Integer.MAX_VALUE;
private Boolean myPositionedAbove = null;
LookupUi(LookupImpl lookup, Advertiser advertiser, JBList list, Project project) {
myLookup = lookup;
myAdvertiser = advertiser;
myList = list;
myProject = project;
myIconPanel.setVisible(false);
myIconPanel.setBackground(Color.LIGHT_GRAY);
myIconPanel.add(myProcessIcon);
JComponent adComponent = advertiser.getAdComponent();
adComponent.setBorder(new EmptyBorder(0, 1, 1, 2 + AllIcons.Ide.LookupRelevance.getIconWidth()));
myLayeredPane.mainPanel.add(adComponent, BorderLayout.SOUTH);
myScrollBarIncreaseButton = new JButton();
myScrollBarIncreaseButton.setFocusable(false);
myScrollBarIncreaseButton.setRequestFocusEnabled(false);
myScrollPane = new JBScrollPane(lookup.getList());
myScrollPane.setViewportBorder(new EmptyBorder(0, 0, 0, 0));
myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
myScrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(13, -1));
myScrollPane.getVerticalScrollBar().setUI(new ButtonlessScrollBarUI() {
@Override
protected JButton createIncreaseButton(int orientation) {
return myScrollBarIncreaseButton;
}
});
lookup.getComponent().add(myLayeredPane, BorderLayout.CENTER);
//IDEA-82111
fixMouseCheaters();
myLayeredPane.mainPanel.add(myScrollPane, BorderLayout.CENTER);
myScrollPane.setBorder(null);
mySortingLabel.setBorder(new LineBorder(new JBColor(Color.LIGHT_GRAY, JBColor.background())));
mySortingLabel.setOpaque(true);
new ChangeLookupSorting().installOn(mySortingLabel);
updateSorting();
myModalityState = ModalityState.stateForComponent(myLookup.getComponent());
addListeners();
updateScrollbarVisibility();
Disposer.register(lookup, myProcessIcon);
Disposer.register(lookup, myHintAlarm);
}
private void addListeners() {
myList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
myHintAlarm.cancelAllRequests();
final LookupElement item = myLookup.getCurrentItem();
if (item != null) {
updateHint(item);
}
}
});
final Alarm alarm = new Alarm(myLookup);
myScrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
@Override
public void adjustmentValueChanged(AdjustmentEvent e) {
if (myLookup.myUpdating || myLookup.isLookupDisposed()) return;
alarm.addRequest(new Runnable() {
@Override
public void run() {
myLookup.refreshUi(false, false);
}
}, 300, myModalityState);
}
});
}
private void updateScrollbarVisibility() {
boolean showSorting = myLookup.isCompletion() && myList.getModel().getSize() >= 3;
mySortingLabel.setVisible(showSorting);
myScrollPane.setVerticalScrollBarPolicy(showSorting ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
}
private void updateHint(@NotNull final LookupElement item) {
myLookup.checkValid();
if (myElementHint != null) {
myLayeredPane.remove(myElementHint);
myElementHint = null;
final JRootPane rootPane = myLookup.getComponent().getRootPane();
if (rootPane != null) {
rootPane.revalidate();
rootPane.repaint();
}
}
if (!myLookup.isFocused() || !item.isValid()) {
return;
}
final Collection<LookupElementAction> actions = myLookup.getActionsFor(item);
if (!actions.isEmpty()) {
myHintAlarm.addRequest(new Runnable() {
@Override
public void run() {
if (!ShowHideIntentionIconLookupAction.shouldShowLookupHint() ||
((CompletionExtender)myList.getExpandableItemsHandler()).isShowing()) {
return;
}
myElementHint = new LookupHint();
myLayeredPane.add(myElementHint, 20, 0);
myLayeredPane.layoutHint();
}
}, 500, myModalityState);
}
}
//Yes, it's possible to move focus to the hint. It's inconvenient, it doesn't make sense, but it's possible.
// This fix is for those jerks
private void fixMouseCheaters() {
myLookup.getComponent().addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
final ActionCallback done = IdeFocusManager.getInstance(myProject).requestFocus(myLookup.getEditor().getContentComponent(), true);
IdeFocusManager.getInstance(myProject).typeAheadUntil(done);
new Alarm(myLookup).addRequest(new Runnable() {
@Override
public void run() {
if (!done.isDone()) {
done.setDone();
}
}
}, 300, myModalityState);
}
});
}
void setCalculating(final boolean calculating) {
Runnable setVisible = new Runnable() {
@Override
public void run() {
myIconPanel.setVisible(myLookup.isCalculating());
}
};
if (myLookup.isCalculating()) {
new Alarm(myLookup).addRequest(setVisible, 100, myModalityState);
} else {
setVisible.run();
}
if (calculating) {
myProcessIcon.resume();
} else {
myProcessIcon.suspend();
}
}
private void updateSorting() {
final boolean lexi = UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
mySortingLabel.setIcon(lexi ? AllIcons.Ide.LookupAlphanumeric : AllIcons.Ide.LookupRelevance);
mySortingLabel.setToolTipText(lexi ? "Click to sort variants by relevance" : "Click to sort variants alphabetically");
myLookup.resort(false);
}
void refreshUi(boolean selectionVisible, boolean itemsChanged, boolean reused, boolean onExplicitAction) {
if (myLookup.getEditor().getComponent().getRootPane() == null) {
return;
}
updateScrollbarVisibility();
if (myLookup.myResizePending || itemsChanged) {
myMaximumHeight = Integer.MAX_VALUE;
}
Rectangle rectangle = calculatePosition();
myMaximumHeight = rectangle.height;
if (myLookup.myResizePending || itemsChanged) {
myLookup.myResizePending = false;
myLookup.pack();
}
HintManagerImpl.updateLocation(myLookup, myLookup.getEditor(), rectangle.getLocation());
if (reused || selectionVisible || onExplicitAction) {
myLookup.ensureSelectionVisible(false);
}
}
boolean isPositionedAboveCaret() {
return myPositionedAbove != null && myPositionedAbove.booleanValue();
}
// in layered pane coordinate system.
Rectangle calculatePosition() {
Dimension dim = myLookup.getComponent().getPreferredSize();
int lookupStart = myLookup.getLookupStart();
Editor editor = myLookup.getEditor();
if (lookupStart < 0 || lookupStart > editor.getDocument().getTextLength()) {
LOG.error(lookupStart + "; offset=" + editor.getCaretModel().getOffset() + "; element=" +
myLookup.getPsiElement());
}
LogicalPosition pos = editor.offsetToLogicalPosition(lookupStart);
Point location = editor.logicalPositionToXY(pos);
location.y += editor.getLineHeight();
location.x -= myLookup.myCellRenderer.getIconIndent() + myLookup.getComponent().getInsets().left;
SwingUtilities.convertPointToScreen(location, editor.getContentComponent());
final Rectangle screenRectangle = ScreenUtil.getScreenRectangle(location);
if (!isPositionedAboveCaret()) {
int shiftLow = screenRectangle.height - (location.y + dim.height);
myPositionedAbove = shiftLow < 0 && shiftLow < location.y - dim.height && location.y >= dim.height;
}
if (isPositionedAboveCaret()) {
location.y -= dim.height + editor.getLineHeight();
if (pos.line == 0) {
location.y += 1;
//otherwise the lookup won't intersect with the editor and every editor's resize (e.g. after typing in console) will close the lookup
}
}
if (!screenRectangle.contains(location)) {
location = ScreenUtil.findNearestPointOnBorder(screenRectangle, location);
}
final JRootPane rootPane = editor.getComponent().getRootPane();
if (rootPane == null) {
LOG.error("editor.disposed=" + editor.isDisposed() + "; lookup.disposed=" + myLookup.isLookupDisposed() + "; editorShowing=" + editor.getContentComponent().isShowing());
}
Rectangle candidate = new Rectangle(location, dim);
ScreenUtil.cropRectangleToFitTheScreen(candidate);
SwingUtilities.convertPointFromScreen(location, rootPane.getLayeredPane());
myMaximumHeight = candidate.height;
return new Rectangle(location.x, location.y, dim.width, candidate.height);
}
private class LookupLayeredPane extends JBLayeredPane {
final JPanel mainPanel = new JPanel(new BorderLayout());
private LookupLayeredPane() {
add(mainPanel, 0, 0);
add(myIconPanel, 42, 0);
add(mySortingLabel, 10, 0);
setLayout(new AbstractLayoutManager() {
@Override
public Dimension preferredLayoutSize(@Nullable Container parent) {
int maxCellWidth = myLookup.myLookupTextWidth + myLookup.myCellRenderer.getIconIndent();
int scrollBarWidth = myScrollPane.getPreferredSize().width - myScrollPane.getViewport().getPreferredSize().width;
int listWidth = Math.min(scrollBarWidth + maxCellWidth, UISettings.getInstance().MAX_LOOKUP_WIDTH2);
Dimension adSize = myAdvertiser.getAdComponent().getPreferredSize();
int panelHeight = myList.getPreferredScrollableViewportSize().height + adSize.height;
if (myList.getModel().getSize() > myList.getVisibleRowCount() && myList.getVisibleRowCount() >= 5) {
panelHeight -= myList.getFixedCellHeight() / 2;
}
return new Dimension(Math.max(listWidth, adSize.width), Math.min(panelHeight, myMaximumHeight));
}
@Override
public void layoutContainer(Container parent) {
Dimension size = getSize();
mainPanel.setSize(size);
mainPanel.validate();
if (!myLookup.myResizePending) {
Dimension preferredSize = preferredLayoutSize(null);
if (preferredSize.width != size.width) {
UISettings.getInstance().MAX_LOOKUP_WIDTH2 = Math.max(500, size.width);
}
int listHeight = myList.getLastVisibleIndex() - myList.getFirstVisibleIndex() + 1;
if (listHeight != myList.getModel().getSize() && listHeight != myList.getVisibleRowCount() && preferredSize.height != size.height) {
UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT = Math.max(5, listHeight);
}
}
myList.setFixedCellWidth(myScrollPane.getViewport().getWidth());
layoutStatusIcons();
layoutHint();
}
});
}
private void layoutStatusIcons() {
int adHeight = myAdvertiser.getAdComponent().getPreferredSize().height;
Dimension buttonSize = adHeight > 0 || !mySortingLabel.isVisible() ? new Dimension(0, 0) : new Dimension(
AllIcons.Ide.LookupRelevance.getIconWidth(), AllIcons.Ide.LookupRelevance.getIconHeight());
myScrollBarIncreaseButton.setPreferredSize(buttonSize);
myScrollBarIncreaseButton.setMinimumSize(buttonSize);
myScrollBarIncreaseButton.setMaximumSize(buttonSize);
JScrollBar vScrollBar = myScrollPane.getVerticalScrollBar();
vScrollBar.revalidate();
vScrollBar.repaint();
final Dimension iconSize = myProcessIcon.getPreferredSize();
myIconPanel.setBounds(getWidth() - iconSize.width - (vScrollBar.isVisible() ? vScrollBar.getWidth() : 0), 0, iconSize.width,
iconSize.height);
final Dimension sortSize = mySortingLabel.getPreferredSize();
final int sortWidth = vScrollBar.isVisible() ? vScrollBar.getWidth() : sortSize.width;
final int sortHeight = Math.max(sortSize.height, adHeight);
mySortingLabel.setBounds(getWidth() - sortWidth, getHeight() - sortHeight, sortSize.width, sortHeight);
}
void layoutHint() {
if (myElementHint != null && myLookup.getCurrentItem() != null) {
final Rectangle bounds = myLookup.getCurrentItemBounds();
myElementHint.setSize(myElementHint.getPreferredSize());
JScrollBar sb = myScrollPane.getVerticalScrollBar();
int x = bounds.x + bounds.width - myElementHint.getWidth() + (sb.isVisible() ? sb.getWidth() : 0);
x = Math.min(x, getWidth() - myElementHint.getWidth());
myElementHint.setLocation(new Point(x, bounds.y));
}
}
}
private class LookupHint extends JLabel {
private final Border INACTIVE_BORDER = BorderFactory.createEmptyBorder(2, 2, 2, 2);
private final Border ACTIVE_BORDER = BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.BLACK, 1), BorderFactory.createEmptyBorder(1, 1, 1, 1));
private LookupHint() {
setOpaque(false);
setBorder(INACTIVE_BORDER);
setIcon(AllIcons.Actions.IntentionBulb);
String acceleratorsText = KeymapUtil.getFirstKeyboardShortcutText(
ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS));
if (acceleratorsText.length() > 0) {
setToolTipText(CodeInsightBundle.message("lightbulb.tooltip", acceleratorsText));
}
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
setBorder(ACTIVE_BORDER);
}
@Override
public void mouseExited(MouseEvent e) {
setBorder(INACTIVE_BORDER);
}
@Override
public void mousePressed(MouseEvent e) {
if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
myLookup.showElementActions();
}
}
});
}
}
private class ChangeLookupSorting extends ClickListener {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
DataContext context = DataManager.getInstance().getDataContext(mySortingLabel);
DefaultActionGroup group = new DefaultActionGroup();
group.add(createSortingAction(true));
group.add(createSortingAction(false));
JBPopupFactory.getInstance().createActionGroupPopup("Change sorting", group, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false).showInBestPositionFor(
context);
return true;
}
private AnAction createSortingAction(boolean checked) {
boolean currentSetting = UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
final boolean newSetting = checked ? currentSetting : !currentSetting;
return new DumbAwareAction(newSetting ? "Sort lexicographically" : "Sort by relevance", null, checked ? PlatformIcons.CHECK_ICON : null) {
@Override
public void actionPerformed(AnActionEvent e) {
FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CHANGE_SORTING);
UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY = newSetting;
updateSorting();
}
};
}
}
}