/* * Copyright 2000-2017 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.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; import com.intellij.codeInsight.lookup.LookupValueWithUIHint; import com.intellij.codeInsight.lookup.RealLookupElementPresentation; import com.intellij.openapi.application.AccessToken; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.editor.colors.FontPreferences; import com.intellij.openapi.editor.ex.util.EditorUIUtil; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.codeStyle.NameUtil; import com.intellij.ui.*; import com.intellij.ui.components.JBList; import com.intellij.ui.speedSearch.SpeedSearchUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.FList; import com.intellij.util.ui.EmptyIcon; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.accessibility.AccessibleContextUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * @author peter * @author Konstantin Bulenkov */ public class LookupCellRenderer implements ListCellRenderer { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupCellRenderer"); //TODO[kb]: move all these awesome constants to Editor's Fonts & Colors settings private Icon myEmptyIcon = JBUI.scale(EmptyIcon.create(5)); private final Font myNormalFont; private final Font myBoldFont; private final FontMetrics myNormalMetrics; private final FontMetrics myBoldMetrics; public static final Color BACKGROUND_COLOR = new JBColor(() -> (JBColor.isBright() ? new Color(235, 244, 254) : JBColor.background())); public static final Color FOREGROUND_COLOR = JBColor.foreground(); private static final Color GRAYED_FOREGROUND_COLOR = new JBColor(Gray._160, Gray._110); private static final Color SELECTED_BACKGROUND_COLOR = new Color(0, 82, 164); public static final Color SELECTED_NON_FOCUSED_BACKGROUND_COLOR = new JBColor(0x6e8ea2, 0x55585a); public static final Color SELECTED_FOREGROUND_COLOR = new JBColor(() -> (JBColor.isBright() ? JBColor.WHITE : JBColor.foreground())); private static final Color SELECTED_GRAYED_FOREGROUND_COLOR = new JBColor(() -> (JBColor.isBright() ? JBColor.WHITE: JBColor.foreground())); static final Color PREFIX_FOREGROUND_COLOR = new JBColor(0xb000b0, 0xd17ad6); private static final Color SELECTED_PREFIX_FOREGROUND_COLOR = new JBColor(0xf9eccc, 0xd17ad6); private final LookupImpl myLookup; private final SimpleColoredComponent myNameComponent; private final SimpleColoredComponent myTailComponent; private final SimpleColoredComponent myTypeLabel; private final LookupPanel myPanel; private final Map<Integer, Boolean> mySelected = new HashMap<>(); private static final String ELLIPSIS = "\u2026"; private int myMaxWidth = -1; public LookupCellRenderer(LookupImpl lookup) { EditorColorsScheme scheme = lookup.getTopLevelEditor().getColorsScheme(); myNormalFont = scheme.getFont(EditorFontType.PLAIN); myBoldFont = scheme.getFont(EditorFontType.BOLD); myLookup = lookup; myNameComponent = new MySimpleColoredComponent(); myNameComponent.setIpad(JBUI.insetsLeft(2)); myNameComponent.setMyBorder(null); myTailComponent = new MySimpleColoredComponent(); myTailComponent.setIpad(JBUI.emptyInsets()); myTailComponent.setBorder(JBUI.Borders.emptyRight(10)); myTypeLabel = new MySimpleColoredComponent(); myTypeLabel.setIpad(JBUI.emptyInsets()); myTypeLabel.setBorder(JBUI.Borders.emptyRight(6)); myPanel = new LookupPanel(); myPanel.add(myNameComponent, BorderLayout.WEST); myPanel.add(myTailComponent, BorderLayout.CENTER); myPanel.add(myTypeLabel, BorderLayout.EAST); myNormalMetrics = myLookup.getTopLevelEditor().getComponent().getFontMetrics(myNormalFont); myBoldMetrics = myLookup.getTopLevelEditor().getComponent().getFontMetrics(myBoldFont); } private boolean myIsSelected = false; @Override public Component getListCellRendererComponent( final JList list, Object value, int index, boolean isSelected, boolean hasFocus) { boolean nonFocusedSelection = isSelected && myLookup.getFocusDegree() == LookupImpl.FocusDegree.SEMI_FOCUSED; if (!myLookup.isFocused()) { isSelected = false; } myIsSelected = isSelected; final LookupElement item = (LookupElement)value; final Color foreground = getForegroundColor(isSelected); final Color background = nonFocusedSelection ? SELECTED_NON_FOCUSED_BACKGROUND_COLOR : isSelected ? SELECTED_BACKGROUND_COLOR : BACKGROUND_COLOR; int allowedWidth = list.getWidth() - calcSpacing(myNameComponent, myEmptyIcon) - calcSpacing(myTailComponent, null) - calcSpacing(myTypeLabel, null); FontMetrics normalMetrics = getRealFontMetrics(item, false); FontMetrics boldMetrics = getRealFontMetrics(item, true); final LookupElementPresentation presentation = new RealLookupElementPresentation(isSelected ? getMaxWidth() : allowedWidth, normalMetrics, boldMetrics, myLookup); AccessToken token = ReadAction.start(); try { if (item.isValid()) { try { item.renderElement(presentation); //In Darcula: default monospaced bold fonts are very similar to their regular versions. //We need to tune foreground colors here to tell bold elements from regular if (presentation.isItemTextBold() && UIUtil.isUnderDarcula()) { presentation.setItemTextForeground(ColorUtil.brighter(presentation.getItemTextForeground(), 2)); } } catch (ProcessCanceledException e) { LOG.info(e); presentation.setItemTextForeground(JBColor.RED); presentation.setItemText("Error occurred, see the log in Help | Show Log"); } catch (Exception | Error e) { LOG.error(e); } } else { presentation.setItemTextForeground(JBColor.RED); presentation.setItemText("Invalid"); } } finally { token.finish(); } myNameComponent.clear(); myNameComponent.setBackground(background); allowedWidth -= setItemTextLabel(item, new JBColor(isSelected ? SELECTED_FOREGROUND_COLOR : presentation.getItemTextForeground(), presentation.getItemTextForeground()), isSelected, presentation, allowedWidth); Font font = myLookup.getCustomFont(item, false); if (font == null) { font = myNormalFont; } myTailComponent.setFont(font); myTypeLabel.setFont(font); myNameComponent.setIcon(augmentIcon(myLookup.getEditor(), presentation.getIcon(), myEmptyIcon)); myTypeLabel.clear(); if (allowedWidth > 0) { allowedWidth -= setTypeTextLabel(item, background, foreground, presentation, isSelected ? getMaxWidth() : allowedWidth, isSelected, nonFocusedSelection, normalMetrics); } myTailComponent.clear(); myTailComponent.setBackground(background); if (isSelected || allowedWidth >= 0) { setTailTextLabel(isSelected, presentation, foreground, isSelected ? getMaxWidth() : allowedWidth, nonFocusedSelection, normalMetrics); } if (mySelected.containsKey(index)) { if (!isSelected && mySelected.get(index)) { myPanel.setUpdateExtender(true); } } mySelected.put(index, isSelected); final double w = myNameComponent.getPreferredSize().getWidth() + myTailComponent.getPreferredSize().getWidth() + myTypeLabel.getPreferredSize().getWidth(); boolean useBoxLayout = isSelected && w > list.getWidth() && ((JBList)list).getExpandableItemsHandler().isEnabled(); if (useBoxLayout != myPanel.getLayout() instanceof BoxLayout) { myPanel.removeAll(); if (useBoxLayout) { myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.X_AXIS)); myPanel.add(myNameComponent); myPanel.add(myTailComponent); myPanel.add(myTypeLabel); } else { myPanel.setLayout(new BorderLayout()); myPanel.add(myNameComponent, BorderLayout.WEST); myPanel.add(myTailComponent, BorderLayout.CENTER); myPanel.add(myTypeLabel, BorderLayout.EAST); } } AccessibleContextUtil.setCombinedName(myPanel, myNameComponent, "", myTailComponent, " - ", myTypeLabel); AccessibleContextUtil.setCombinedDescription(myPanel, myNameComponent, "", myTailComponent, " - ", myTypeLabel); return myPanel; } private static int calcSpacing(@NotNull SimpleColoredComponent component, @Nullable Icon icon) { Insets iPad = component.getIpad(); int width = iPad.left + iPad.right; Border myBorder = component.getMyBorder(); if (myBorder != null) { Insets insets = myBorder.getBorderInsets(component); width += insets.left + insets.right; } Insets insets = component.getInsets(); if (insets != null) { width += insets.left + insets.right; } if (icon != null) { width += icon.getIconWidth() + component.getIconTextGap(); } return width; } private static Color getForegroundColor(boolean isSelected) { return isSelected ? SELECTED_FOREGROUND_COLOR : FOREGROUND_COLOR; } private int getMaxWidth() { if (myMaxWidth < 0) { final Point p = myLookup.getComponent().getLocationOnScreen(); final Rectangle rectangle = ScreenUtil.getScreenRectangle(p); myMaxWidth = rectangle.x + rectangle.width - p.x - 111; } return myMaxWidth; } private void setTailTextLabel(boolean isSelected, LookupElementPresentation presentation, Color foreground, int allowedWidth, boolean nonFocusedSelection, FontMetrics fontMetrics) { int style = getStyle(false, presentation.isStrikeout(), false); for (LookupElementPresentation.TextFragment fragment : presentation.getTailFragments()) { if (allowedWidth < 0) { return; } String trimmed = trimLabelText(fragment.text, allowedWidth, fontMetrics); int fragmentStyle = fragment.isItalic() ? style | SimpleTextAttributes.STYLE_ITALIC : style; myTailComponent.append(trimmed, new SimpleTextAttributes(fragmentStyle, getTailTextColor(isSelected, fragment, foreground, nonFocusedSelection))); allowedWidth -= RealLookupElementPresentation.getStringWidth(trimmed, fontMetrics); } } private String trimLabelText(@Nullable String text, int maxWidth, FontMetrics metrics) { if (text == null || StringUtil.isEmpty(text)) { return ""; } final int strWidth = RealLookupElementPresentation.getStringWidth(text, metrics); if (strWidth <= maxWidth || myIsSelected) { return text; } if (RealLookupElementPresentation.getStringWidth(ELLIPSIS, metrics) > maxWidth) { return ""; } int i = 0; int j = text.length(); while (i + 1 < j) { int mid = (i + j) / 2; final String candidate = text.substring(0, mid) + ELLIPSIS; final int width = RealLookupElementPresentation.getStringWidth(candidate, metrics); if (width <= maxWidth) { i = mid; } else { j = mid; } } return text.substring(0, i) + ELLIPSIS; } private static Color getTypeTextColor(LookupElement item, Color foreground, LookupElementPresentation presentation, boolean selected, boolean nonFocusedSelection) { if (nonFocusedSelection) { return foreground; } return presentation.isTypeGrayed() ? getGrayedForeground(selected) : item instanceof EmptyLookupItem ? JBColor.foreground() : foreground; } private static Color getTailTextColor(boolean isSelected, LookupElementPresentation.TextFragment fragment, Color defaultForeground, boolean nonFocusedSelection) { if (nonFocusedSelection) { return defaultForeground; } if (fragment.isGrayed()) { return getGrayedForeground(isSelected); } if (!isSelected) { final Color tailForeground = fragment.getForegroundColor(); if (tailForeground != null) { return tailForeground; } } return defaultForeground; } public static Color getGrayedForeground(boolean isSelected) { return isSelected ? SELECTED_GRAYED_FOREGROUND_COLOR : GRAYED_FOREGROUND_COLOR; } private int setItemTextLabel(LookupElement item, final Color foreground, final boolean selected, LookupElementPresentation presentation, int allowedWidth) { boolean bold = presentation.isItemTextBold(); Font customItemFont = myLookup.getCustomFont(item, bold); myNameComponent.setFont(customItemFont != null ? customItemFont : bold ? myBoldFont : myNormalFont); int style = getStyle(bold, presentation.isStrikeout(), presentation.isItemTextUnderlined()); final FontMetrics metrics = getRealFontMetrics(item, bold); final String name = trimLabelText(presentation.getItemText(), allowedWidth, metrics); int used = RealLookupElementPresentation.getStringWidth(name, metrics); renderItemName(item, foreground, selected, style, name, myNameComponent); return used; } private FontMetrics getRealFontMetrics(LookupElement item, boolean bold) { Font customFont = myLookup.getCustomFont(item, bold); if (customFont != null) { return myLookup.getTopLevelEditor().getComponent().getFontMetrics(customFont); } return bold ? myBoldMetrics : myNormalMetrics; } @SimpleTextAttributes.StyleAttributeConstant private static int getStyle(boolean bold, boolean strikeout, boolean underlined) { int style = bold ? SimpleTextAttributes.STYLE_BOLD : SimpleTextAttributes.STYLE_PLAIN; if (strikeout) { style |= SimpleTextAttributes.STYLE_STRIKEOUT; } if (underlined) { style |= SimpleTextAttributes.STYLE_UNDERLINE; } return style; } private void renderItemName(LookupElement item, Color foreground, boolean selected, @SimpleTextAttributes.StyleAttributeConstant int style, String name, final SimpleColoredComponent nameComponent) { final SimpleTextAttributes base = new SimpleTextAttributes(style, foreground); final String prefix = item instanceof EmptyLookupItem ? "" : myLookup.itemPattern(item); if (prefix.length() > 0) { Iterable<TextRange> ranges = getMatchingFragments(prefix, name); if (ranges != null) { SimpleTextAttributes highlighted = new SimpleTextAttributes(style, selected ? SELECTED_PREFIX_FOREGROUND_COLOR : PREFIX_FOREGROUND_COLOR); SpeedSearchUtil.appendColoredFragments(nameComponent, name, ranges, base, highlighted); return; } } nameComponent.append(name, base); } public static FList<TextRange> getMatchingFragments(String prefix, String name) { return NameUtil.buildMatcher("*" + prefix).build().matchingFragments(name); } private int setTypeTextLabel(LookupElement item, final Color background, Color foreground, final LookupElementPresentation presentation, int allowedWidth, boolean selected, boolean nonFocusedSelection, FontMetrics normalMetrics) { final String givenText = presentation.getTypeText(); final String labelText = trimLabelText(StringUtil.isEmpty(givenText) ? "" : " " + givenText, allowedWidth, normalMetrics); int used = RealLookupElementPresentation.getStringWidth(labelText, normalMetrics); final Icon icon = presentation.getTypeIcon(); if (icon != null) { myTypeLabel.setIcon(icon); used += icon.getIconWidth(); } Color sampleBackground = background; Object o = item.isValid() ? item.getObject() : null; //noinspection deprecation if (o instanceof LookupValueWithUIHint && StringUtil.isEmpty(labelText)) { //noinspection deprecation Color proposedBackground = ((LookupValueWithUIHint)o).getColorHint(); if (proposedBackground != null) { sampleBackground = proposedBackground; } myTypeLabel.append(" "); used += normalMetrics.stringWidth("WW"); } else { myTypeLabel.append(labelText); } myTypeLabel.setBackground(sampleBackground); myTypeLabel.setForeground(getTypeTextColor(item, foreground, presentation, selected, nonFocusedSelection)); return used; } public static Icon augmentIcon(@Nullable Editor editor, @Nullable Icon icon, @NotNull Icon standard) { if (Registry.is("editor.scale.completion.icons")) { standard = EditorUtil.scaleIconAccordingEditorFont(standard, editor); icon = EditorUtil.scaleIconAccordingEditorFont(icon, editor); } if (icon == null) { return standard; } if (icon.getIconHeight() < standard.getIconHeight() || icon.getIconWidth() < standard.getIconWidth()) { final LayeredIcon layeredIcon = new LayeredIcon(2); layeredIcon.setIcon(icon, 0, 0, (standard.getIconHeight() - icon.getIconHeight()) / 2); layeredIcon.setIcon(standard, 1); return layeredIcon; } return icon; } @Nullable Font getFontAbleToDisplay(LookupElementPresentation p) { String sampleString = p.getItemText() + p.getTailText() + p.getTypeText(); // assume a single font can display all lookup item chars Set<Font> fonts = ContainerUtil.newHashSet(); FontPreferences fontPreferences = myLookup.getFontPreferences(); for (int i = 0; i < sampleString.length(); i++) { fonts.add(ComplementaryFontsRegistry.getFontAbleToDisplay(sampleString.charAt(i), Font.PLAIN, fontPreferences, null).getFont()); } eachFont: for (Font font : fonts) { if (font.equals(myNormalFont)) continue; for (int i = 0; i < sampleString.length(); i++) { if (!font.canDisplay(sampleString.charAt(i))) { continue eachFont; } } return font; } return null; } int updateMaximumWidth(final LookupElementPresentation p, LookupElement item) { final Icon icon = p.getIcon(); if (icon != null && (icon.getIconWidth() > myEmptyIcon.getIconWidth() || icon.getIconHeight() > myEmptyIcon.getIconHeight())) { myEmptyIcon = EmptyIcon.create(Math.max(icon.getIconWidth(), myEmptyIcon.getIconWidth()), Math.max(icon.getIconHeight(), myEmptyIcon.getIconHeight())); } return RealLookupElementPresentation.calculateWidth(p, getRealFontMetrics(item, false), getRealFontMetrics(item, true)) + calcSpacing(myTailComponent, null) + calcSpacing(myTypeLabel, null); } public int getTextIndent() { return myNameComponent.getIpad().left + myEmptyIcon.getIconWidth() + myNameComponent.getIconTextGap(); } private static class MySimpleColoredComponent extends SimpleColoredComponent { private MySimpleColoredComponent() { setFocusBorderAroundIcon(true); } @Override protected void applyAdditionalHints(@NotNull Graphics2D g) { EditorUIUtil.setupAntialiasing(g); } } private class LookupPanel extends JPanel { boolean myUpdateExtender; public LookupPanel() { super(new BorderLayout()); } public void setUpdateExtender(boolean updateExtender) { myUpdateExtender = updateExtender; } @Override public void paint(Graphics g){ super.paint(g); if (!myLookup.isFocused() && myLookup.isCompletion()) { g = g.create(); try { g.setColor(ColorUtil.withAlpha(BACKGROUND_COLOR, .4)); g.fillRect(0, 0, getWidth(), getHeight()); } finally { g.dispose(); } } } } }