From e8c981ae82f2bef4de126cc35705cc5efe54e302 Mon Sep 17 00:00:00 2001 From: peter Date: Mon, 21 Apr 2014 16:12:38 +0200 Subject: [PATCH] create LookupUi and move there some ui-related code from LookupImpl; skip this code in tests --- .../codeInsight/lookup/impl/LookupImpl.java | 436 +--------------- .../codeInsight/lookup/impl/LookupUi.java | 466 ++++++++++++++++++ 2 files changed, 489 insertions(+), 413 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupUi.java diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java index 0402bba74698..8df95370fdaa 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java @@ -16,7 +16,6 @@ package com.intellij.codeInsight.lookup.impl; -import com.intellij.codeInsight.CodeInsightBundle; import com.intellij.codeInsight.FileModificationService; import com.intellij.codeInsight.completion.CodeCompletionFeatures; import com.intellij.codeInsight.completion.CompletionLookupArranger; @@ -27,25 +26,19 @@ import com.intellij.codeInsight.hint.HintManager; import com.intellij.codeInsight.hint.HintManagerImpl; import com.intellij.codeInsight.lookup.*; import com.intellij.featureStatistics.FeatureUsageTracker; -import com.intellij.icons.AllIcons; -import com.intellij.ide.DataManager; import com.intellij.ide.IdeEventQueue; import com.intellij.ide.ui.UISettings; import com.intellij.lang.LangBundle; import com.intellij.openapi.Disposable; -import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.event.*; import com.intellij.openapi.editor.event.DocumentAdapter; -import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; -import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; @@ -57,20 +50,12 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.impl.DebugUtil; import com.intellij.ui.*; import com.intellij.ui.awt.RelativePoint; -import com.intellij.ui.components.JBLayeredPane; import com.intellij.ui.components.JBList; -import com.intellij.ui.components.JBScrollPane; -import com.intellij.ui.plaf.beg.BegPopupMenuBorder; import com.intellij.ui.popup.AbstractPopup; -import com.intellij.util.Alarm; import com.intellij.util.CollectConsumer; -import com.intellij.util.PlatformIcons; import com.intellij.util.containers.ConcurrentHashMap; import com.intellij.util.containers.ConcurrentWeakHashMap; import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.ui.AbstractLayoutManager; -import com.intellij.util.ui.AsyncProcessIcon; -import com.intellij.util.ui.ButtonlessScrollBarUI; import com.intellij.util.ui.update.Activatable; import com.intellij.util.ui.update.UiNotifyConnector; import org.jetbrains.annotations.NotNull; @@ -78,13 +63,11 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; 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.awt.event.KeyEvent; +import java.awt.event.MouseEvent; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -120,8 +103,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable return myExtender; } }; - private final LookupCellRenderer myCellRenderer; - private Boolean myPositionedAbove = null; + final LookupCellRenderer myCellRenderer; private final List myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); @@ -131,12 +113,10 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable private boolean myHidden = false; private boolean mySelectionTouched; private FocusDegree myFocusDegree = FocusDegree.FOCUSED; - private final AsyncProcessIcon myProcessIcon = new AsyncProcessIcon("Completion progress"); - private final JPanel myIconPanel = new JPanel(new BorderLayout()); private volatile boolean myCalculating; private final Advertiser myAdComponent; private volatile String myAdText; - private volatile int myLookupTextWidth = 50; + volatile int myLookupTextWidth = 50; private boolean myChangeGuard; private volatile LookupArranger myArranger; private LookupArranger myPresentableArranger; @@ -144,18 +124,11 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable ContainerUtil.identityStrategy()); private final Map myCustomFonts = new ConcurrentWeakHashMap( ContainerUtil.identityStrategy()); - private LookupHint myElementHint = null; - private final Alarm myHintAlarm = new Alarm(); - private final JLabel mySortingLabel = new JLabel(); - private final JScrollPane myScrollPane; - final LookupLayeredPane myLayeredPane = new LookupLayeredPane(); - private final JButton myScrollBarIncreaseButton; private boolean myStartCompletionWhenNothingMatches; - private boolean myResizePending; - private int myMaximumHeight = Integer.MAX_VALUE; + boolean myResizePending; private boolean myFinishing; - private boolean myUpdating; - private final ModalityState myModalityState; + boolean myUpdating; + private LookupUi myUi; public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger) { super(new JPanel(new BorderLayout())); @@ -169,7 +142,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable myArranger = arranger; myPresentableArranger = arranger; - myIconPanel.setVisible(false); myCellRenderer = new LookupCellRenderer(this); myList.setCellRenderer(myCellRenderer); @@ -181,36 +153,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable myList.getExpandableItemsHandler(); - myScrollBarIncreaseButton = new JButton(); - myScrollBarIncreaseButton.setFocusable(false); - myScrollBarIncreaseButton.setRequestFocusEnabled(false); - - myScrollPane = new JBScrollPane(myList); - 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; - } - }); - getComponent().add(myLayeredPane, BorderLayout.CENTER); - - //IDEA-82111 - fixMouseCheaters(); - - myLayeredPane.mainPanel.add(myScrollPane, BorderLayout.CENTER); - myScrollPane.setBorder(null); - myAdComponent = new Advertiser(); - JComponent adComponent = myAdComponent.getAdComponent(); - adComponent.setBorder(new EmptyBorder(0, 1, 1, 2 + AllIcons.Ide.LookupRelevance.getIconWidth())); - myLayeredPane.mainPanel.add(adComponent, BorderLayout.SOUTH); - getComponent().setBorder(new BegPopupMenuBorder()); - - myIconPanel.setBackground(Color.LIGHT_GRAY); - myIconPanel.add(myProcessIcon); myOffsets = new LookupOffsets(editor); @@ -218,14 +161,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable addEmptyItem(model); updateListHeight(model); - addListeners(); - - mySortingLabel.setBorder(new LineBorder(new JBColor(Color.LIGHT_GRAY, JBColor.background()))); - mySortingLabel.setOpaque(true); - new ChangeLookupSorting().installOn(mySortingLabel); - updateSorting(); - myModalityState = ModalityState.stateForComponent(getComponent()); } private CollectionListModel getListModel() { @@ -233,34 +169,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable return (CollectionListModel)myList.getModel(); } - //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() { - getComponent().addFocusListener(new FocusAdapter() { - @Override - public void focusGained(FocusEvent e) { - final ActionCallback done = IdeFocusManager.getInstance(myProject).requestFocus(myEditor.getContentComponent(), true); - IdeFocusManager.getInstance(myProject).typeAheadUntil(done); - new Alarm(LookupImpl.this).addRequest(new Runnable() { - @Override - public void run() { - if (!done.isDone()) { - done.setDone(); - } - } - }, 300, myModalityState); - } - }); - } - - 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"); - - resort(false); - } - public void setArranger(LookupArranger arranger) { myArranger = arranger; } @@ -284,22 +192,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable public void setCalculating(final boolean calculating) { myCalculating = calculating; - Runnable setVisible = new Runnable() { - @Override - public void run() { - myIconPanel.setVisible(myCalculating); - } - }; - if (myCalculating) { - new Alarm(this).addRequest(setVisible, 100, myModalityState); - } else { - setVisible.run(); - } - - if (calculating) { - myProcessIcon.resume(); - } else { - myProcessIcon.suspend(); + if (myUi != null) { + myUi.setCalculating(calculating); } } @@ -564,50 +458,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable return myMatchers.get(item); } - // in layered pane coordinate system. - private Rectangle calculatePosition() { - Dimension dim = getComponent().getPreferredSize(); - int lookupStart = getLookupStart(); - if (lookupStart < 0 || lookupStart > myEditor.getDocument().getTextLength()) { - LOG.error(lookupStart + "; offset=" + myEditor.getCaretModel().getOffset() + "; element=" + - getPsiElement()); - } - - LogicalPosition pos = myEditor.offsetToLogicalPosition(lookupStart); - Point location = myEditor.logicalPositionToXY(pos); - location.y += myEditor.getLineHeight(); - location.x -= myCellRenderer.getIconIndent() + getComponent().getInsets().left; - - SwingUtilities.convertPointToScreen(location, myEditor.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 + myEditor.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 = myEditor.getComponent().getRootPane(); - if (rootPane == null) { - LOG.error(myEditor.isDisposed() + "; shown=" + myShown + "; disposed=" + myDisposed + "; editorShowing=" + myEditor.getContentComponent().isShowing()); - } - Rectangle candidate = new Rectangle(location, dim); - ScreenUtil.cropRectangleToFitTheScreen(candidate); - - SwingUtilities.convertPointFromScreen(location, rootPane.getLayeredPane()); - return new Rectangle(location.x, location.y, dim.width, candidate.height); - } - public void finishLookup(final char completionChar) { finishLookup(completionChar, (LookupElement)myList.getSelectedValue()); } @@ -794,7 +644,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable return false; } if (isVisible()) { - updateLookupLocation(); + HintManagerImpl.updateLocation(this, myEditor, myUi.calculatePosition().getLocation()); } checkValid(); return true; @@ -835,12 +685,9 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable myAdComponent.showRandomText(); - getComponent().setBorder(null); - updateScrollbarVisibility(); - - Rectangle bounds = calculatePosition(); - myMaximumHeight = bounds.height; - Point p = bounds.getLocation(); + myUi = new LookupUi(this, myAdComponent, myList, myProject); + myUi.setCalculating(myCalculating); + Point p = myUi.calculatePosition().getLocation(); HintManagerImpl.getInstanceImpl().showEditorHint(this, myEditor, p, HintManager.HIDE_BY_ESCAPE | HintManager.UPDATE_BY_SCROLLING, 0, false, HintManagerImpl.createHintHint(myEditor, p, this, HintManager.UNDER).setAwtTooltip(false)); @@ -923,8 +770,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable @Override public void valueChanged(ListSelectionEvent e){ - myHintAlarm.cancelAllRequests(); - final LookupElement item = getCurrentItem(); if (oldItem != item && !myList.isEmpty()) { // do not update on temporary model wipe fireCurrentItemChanged(item); @@ -933,9 +778,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable } oldItem = item; } - if (item != null) { - updateHint(item); - } } }); @@ -956,53 +798,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable return true; } }.installOn(myList); - - final Alarm alarm = new Alarm(this); - myScrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() { - @Override - public void adjustmentValueChanged(AdjustmentEvent e) { - if (!myShown || myUpdating || myDisposed) return; - alarm.addRequest(new Runnable() { - @Override - public void run() { - refreshUi(false, false); - } - }, 300, myModalityState); - } - }); - } - - private void updateHint(@NotNull final LookupElement item) { - checkValid(); - if (myElementHint != null) { - myLayeredPane.remove(myElementHint); - myElementHint = null; - final JRootPane rootPane = getComponent().getRootPane(); - if (rootPane != null) { - rootPane.revalidate(); - rootPane.repaint(); - } - } - if (!isFocused()) { - return; - } - - final Collection actions = getActionsFor(item); - if (!actions.isEmpty()) { - myHintAlarm.addRequest(new Runnable() { - @Override - public void run() { - assert !myDisposed; - if (!ShowHideIntentionIconLookupAction.shouldShowLookupHint() || - ((CompletionExtender)myList.getExpandableItemsHandler()).isShowing()) { - return; - } - myElementHint = new LookupHint(); - myLayeredPane.add(myElementHint, 20, 0); - myLayeredPane.layoutHint(); - } - }, 500, myModalityState); - } } @Override @@ -1075,7 +870,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable } } - private void fireCurrentItemChanged(LookupElement item){ + void fireCurrentItemChanged(LookupElement item){ if (!myListeners.isEmpty()){ LookupEvent event = new LookupEvent(this, item, (char)0); for (LookupListener listener : myListeners) { @@ -1208,7 +1003,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable @Override public boolean isPositionedAboveCaret(){ - return myPositionedAbove != null && myPositionedAbove.booleanValue(); + return myUi != null && myUi.isPositionedAboveCaret(); } @Override @@ -1279,8 +1074,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable } myOffsets.disposeMarkers(); - Disposer.dispose(myProcessIcon); - Disposer.dispose(myHintAlarm); myDisposed = true; disposeTrace = DebugUtil.currentStackTrace() + "\n============"; //noinspection AssignmentToStaticFieldFromInstanceMethod @@ -1291,59 +1084,19 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable assert !myUpdating; myUpdating = true; try { - doRefreshUi(mayCheckReused, onExplicitAction); + final boolean reused = mayCheckReused && checkReused(); + boolean selectionVisible = isSelectionVisible(); + boolean itemsChanged = updateList(onExplicitAction, reused); + if (isVisible()) { + LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode()); + myUi.refreshUi(selectionVisible, itemsChanged, reused, onExplicitAction); + } } finally { myUpdating = false; } } - private void doRefreshUi(boolean mayCheckReused, boolean onExplicitAction) { - final boolean reused = mayCheckReused && checkReused(); - - boolean selectionVisible = isSelectionVisible(); - - boolean itemsChanged = updateList(onExplicitAction, reused); - - if (isVisible()) { - LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode()); - - if (myEditor.getComponent().getRootPane() == null) { - return; - } - - updateScrollbarVisibility(); - - if (myResizePending || itemsChanged) { - myMaximumHeight = Integer.MAX_VALUE; - } - Rectangle rectangle = calculatePosition(); - myMaximumHeight = rectangle.height; - - if (myResizePending || itemsChanged) { - myResizePending = false; - pack(); - } - HintManagerImpl.updateLocation(this, myEditor, rectangle.getLocation()); - - if (reused || selectionVisible || onExplicitAction) { - ensureSelectionVisible(false); - } - } - } - - private void updateLookupLocation() { - Rectangle rectangle = calculatePosition(); - myMaximumHeight = rectangle.height; - HintManagerImpl.updateLocation(this, myEditor, rectangle.getLocation()); - } - - private void updateScrollbarVisibility() { - boolean showSorting = isCompletion() && getListModel().getSize() >= 3; - mySortingLabel.setVisible(showSorting); - myScrollPane.setVerticalScrollBarPolicy(showSorting ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - } - public void markReused() { myAdComponent.clearAdvertisements(); synchronized (myList) { @@ -1368,7 +1121,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable if (ApplicationManager.getApplication().isDispatchThread()) { runnable.run(); } else { - ApplicationManager.getApplication().invokeLater(runnable, myModalityState); + ApplicationManager.getApplication().invokeLater(runnable); } } @@ -1406,155 +1159,12 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable return true; } - 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 = myLookupTextWidth + myCellRenderer.getIconIndent(); - int scrollBarWidth = myScrollPane.getPreferredSize().width - myScrollPane.getViewport().getPreferredSize().width; - int listWidth = Math.min(scrollBarWidth + maxCellWidth, UISettings.getInstance().MAX_LOOKUP_WIDTH2); - - Dimension adSize = myAdComponent.getAdComponent().getPreferredSize(); - - int panelHeight = myList.getPreferredScrollableViewportSize().height + adSize.height; - if (getListModel().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 (!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 != getListModel().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 = myAdComponent.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 scrollBar = myScrollPane.getVerticalScrollBar(); - scrollBar.revalidate(); - scrollBar.repaint(); - - final Dimension iconSize = myProcessIcon.getPreferredSize(); - myIconPanel.setBounds(getWidth() - iconSize.width - (scrollBar.isVisible() ? scrollBar.getWidth() : 0), 0, iconSize.width, iconSize.height); - - final Dimension sortSize = mySortingLabel.getPreferredSize(); - final Point sbLocation = SwingUtilities.convertPoint(scrollBar, 0, 0, myLayeredPane); - - final int sortHeight = Math.max(adHeight, mySortingLabel.getPreferredSize().height); - mySortingLabel.setBounds(sbLocation.x, getHeight() - sortHeight, sortSize.width, sortHeight); - } - - void layoutHint() { - if (myElementHint != null && getCurrentItem() != null) { - final Rectangle bounds = 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) { - showElementActions(); - } - } - }); - } - } - public Map getRelevanceStrings() { synchronized (myList) { return myPresentableArranger.getRelevanceStrings(); } } - 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 AnAction(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(); - } - }; - } - } - public enum FocusDegree { FOCUSED, SEMI_FOCUSED, UNFOCUSED } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupUi.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupUi.java new file mode 100644 index 000000000000..2851eb35a6c3 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupUi.java @@ -0,0 +1,466 @@ +/* + * 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()) { + return; + } + + final Collection 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 scrollBar = myScrollPane.getVerticalScrollBar(); + scrollBar.revalidate(); + scrollBar.repaint(); + + final Dimension iconSize = myProcessIcon.getPreferredSize(); + myIconPanel.setBounds(getWidth() - iconSize.width - (scrollBar.isVisible() ? scrollBar.getWidth() : 0), 0, iconSize.width, iconSize.height); + + final Dimension sortSize = mySortingLabel.getPreferredSize(); + final Point sbLocation = SwingUtilities.convertPoint(scrollBar, 0, 0, myLayeredPane); + + final int sortHeight = Math.max(adHeight, mySortingLabel.getPreferredSize().height); + mySortingLabel.setBounds(sbLocation.x, 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(); + } + }; + } + } +}