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.
17 package com.intellij.codeInsight.lookup.impl;
19 import com.intellij.codeInsight.lookup.LookupElement;
20 import com.intellij.codeInsight.lookup.LookupElementPresentation;
21 import com.intellij.codeInsight.lookup.LookupValueWithUIHint;
22 import com.intellij.codeInsight.lookup.RealLookupElementPresentation;
23 import com.intellij.openapi.application.AccessToken;
24 import com.intellij.openapi.application.ReadAction;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.editor.colors.EditorColorsScheme;
27 import com.intellij.openapi.editor.colors.EditorFontType;
28 import com.intellij.openapi.editor.ex.util.EditorUtil;
29 import com.intellij.openapi.progress.ProcessCanceledException;
30 import com.intellij.openapi.util.TextRange;
31 import com.intellij.openapi.util.text.StringUtil;
32 import com.intellij.psi.codeStyle.MinusculeMatcher;
33 import com.intellij.psi.codeStyle.NameUtil;
34 import com.intellij.ui.*;
35 import com.intellij.ui.components.JBList;
36 import com.intellij.ui.speedSearch.SpeedSearchUtil;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.containers.FList;
39 import com.intellij.util.ui.EmptyIcon;
40 import com.intellij.util.ui.JBUI;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
45 import javax.swing.border.EmptyBorder;
47 import java.util.HashMap;
53 * @author Konstantin Bulenkov
55 public class LookupCellRenderer implements ListCellRenderer {
56 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupCellRenderer");
57 //TODO[kb]: move all these awesome constants to Editor's Fonts & Colors settings
58 private static final int AFTER_TAIL = 10;
59 private static final int AFTER_TYPE = 6;
60 private Icon myEmptyIcon = EmptyIcon.create(5);
61 private final Font myNormalFont;
62 private final Font myBoldFont;
63 private final FontMetrics myNormalMetrics;
64 private final FontMetrics myBoldMetrics;
66 public static final Color BACKGROUND_COLOR = new JBColor(new Color(235, 244, 254), JBColor.background());
67 private static final Color FOREGROUND_COLOR = JBColor.foreground();
68 private static final Color GRAYED_FOREGROUND_COLOR = new JBColor(Gray._160, Gray._110);
69 private static final Color SELECTED_BACKGROUND_COLOR = new Color(0, 82, 164);
70 private static final Color SELECTED_NON_FOCUSED_BACKGROUND_COLOR = new JBColor(new Color(110, 142, 162), new Color(85, 88, 90));
71 private static final Color SELECTED_FOREGROUND_COLOR = new JBColor(JBColor.WHITE, JBColor.foreground());
72 private static final Color SELECTED_GRAYED_FOREGROUND_COLOR = new JBColor(JBColor.WHITE, JBColor.foreground());
74 static final Color PREFIX_FOREGROUND_COLOR = new JBColor(new Color(176, 0, 176), new Color(209, 122, 214));
75 private static final Color SELECTED_PREFIX_FOREGROUND_COLOR = new JBColor(new Color(249, 236, 204), new Color(209, 122, 214));
77 private final LookupImpl myLookup;
79 private final SimpleColoredComponent myNameComponent;
80 private final SimpleColoredComponent myTailComponent;
81 private final SimpleColoredComponent myTypeLabel;
82 private final LookupPanel myPanel;
83 private final Map<Integer, Boolean> mySelected = new HashMap<Integer, Boolean>();
85 private static final String ELLIPSIS = "\u2026";
86 private int myMaxWidth = -1;
88 public LookupCellRenderer(LookupImpl lookup) {
89 EditorColorsScheme scheme = lookup.getTopLevelEditor().getColorsScheme();
90 myNormalFont = scheme.getFont(EditorFontType.PLAIN);
91 myBoldFont = scheme.getFont(EditorFontType.BOLD);
94 myNameComponent = new MySimpleColoredComponent();
95 myNameComponent.setIpad(JBUI.insetsLeft(2));
96 myNameComponent.setMyBorder(null);
98 myTailComponent = new MySimpleColoredComponent();
99 myTailComponent.setIpad(new Insets(0, 0, 0, 0));
101 myTypeLabel = new MySimpleColoredComponent();
102 myTypeLabel.setIpad(new Insets(0, 0, 0, 0));
104 myPanel = new LookupPanel();
105 myPanel.add(myNameComponent, BorderLayout.WEST);
106 myPanel.add(myTailComponent, BorderLayout.CENTER);
107 myTailComponent.setBorder(new EmptyBorder(0, 0, 0, AFTER_TAIL));
109 myPanel.add(myTypeLabel, BorderLayout.EAST);
110 myTypeLabel.setBorder(new EmptyBorder(0, 0, 0, AFTER_TYPE));
112 myNormalMetrics = myLookup.getTopLevelEditor().getComponent().getFontMetrics(myNormalFont);
113 myBoldMetrics = myLookup.getTopLevelEditor().getComponent().getFontMetrics(myBoldFont);
116 private boolean myIsSelected = false;
118 public Component getListCellRendererComponent(
126 boolean nonFocusedSelection = isSelected && myLookup.getFocusDegree() == LookupImpl.FocusDegree.SEMI_FOCUSED;
127 if (!myLookup.isFocused()) {
131 myIsSelected = isSelected;
132 final LookupElement item = (LookupElement)value;
133 final Color foreground = getForegroundColor(isSelected);
134 final Color background = nonFocusedSelection ? SELECTED_NON_FOCUSED_BACKGROUND_COLOR :
135 isSelected ? SELECTED_BACKGROUND_COLOR : BACKGROUND_COLOR;
137 int allowedWidth = list.getWidth() - AFTER_TAIL - AFTER_TYPE - getTextIndent();
139 FontMetrics normalMetrics = getRealFontMetrics(item, false);
140 FontMetrics boldMetrics = getRealFontMetrics(item, true);
141 final LookupElementPresentation presentation = new RealLookupElementPresentation(isSelected ? getMaxWidth() : allowedWidth,
142 normalMetrics, boldMetrics, myLookup);
143 AccessToken token = ReadAction.start();
145 if (item.isValid()) {
147 item.renderElement(presentation);
149 catch (ProcessCanceledException e) {
151 presentation.setItemTextForeground(JBColor.RED);
152 presentation.setItemText("Error occurred, see the log in Help | Show Log");
154 catch (Exception e) {
161 presentation.setItemTextForeground(JBColor.RED);
162 presentation.setItemText("Invalid");
169 myNameComponent.clear();
170 myNameComponent.setIcon(augmentIcon(presentation.getIcon(), myEmptyIcon));
171 myNameComponent.setBackground(background);
172 allowedWidth -= setItemTextLabel(item, new JBColor(isSelected ? SELECTED_FOREGROUND_COLOR : presentation.getItemTextForeground(), presentation.getItemTextForeground()), isSelected, presentation, allowedWidth);
174 Font customFont = myLookup.getCustomFont(item, false);
175 myTailComponent.setFont(customFont != null ? customFont : myNormalFont);
176 myTypeLabel.setFont(customFont != null ? customFont : myNormalFont);
179 if (allowedWidth > 0) {
180 allowedWidth -= setTypeTextLabel(item, background, foreground, presentation, isSelected ? getMaxWidth() : allowedWidth, isSelected, nonFocusedSelection, normalMetrics);
183 myTailComponent.clear();
184 myTailComponent.setBackground(background);
185 if (isSelected || allowedWidth >= 0) {
186 setTailTextLabel(isSelected, presentation, foreground, isSelected ? getMaxWidth() : allowedWidth, nonFocusedSelection,
190 if (mySelected.containsKey(index)) {
191 if (!isSelected && mySelected.get(index)) {
192 myPanel.setUpdateExtender(true);
195 mySelected.put(index, isSelected);
197 final double w = myNameComponent.getPreferredSize().getWidth() +
198 myTailComponent.getPreferredSize().getWidth() +
199 myTypeLabel.getPreferredSize().getWidth();
201 boolean useBoxLayout = isSelected && w > list.getWidth() && ((JBList)list).getExpandableItemsHandler().isEnabled();
202 if (useBoxLayout != myPanel.getLayout() instanceof BoxLayout) {
205 myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.X_AXIS));
206 myPanel.add(myNameComponent);
207 myPanel.add(myTailComponent);
208 myPanel.add(myTypeLabel);
210 myPanel.setLayout(new BorderLayout());
211 myPanel.add(myNameComponent, BorderLayout.WEST);
212 myPanel.add(myTailComponent, BorderLayout.CENTER);
213 myPanel.add(myTypeLabel, BorderLayout.EAST);
220 private static Color getForegroundColor(boolean isSelected) {
221 return isSelected ? SELECTED_FOREGROUND_COLOR : FOREGROUND_COLOR;
224 private int getMaxWidth() {
225 if (myMaxWidth < 0) {
226 final Point p = myLookup.getComponent().getLocationOnScreen();
227 final Rectangle rectangle = ScreenUtil.getScreenRectangle(p);
228 myMaxWidth = rectangle.x + rectangle.width - p.x - 111;
233 private void setTailTextLabel(boolean isSelected,
234 LookupElementPresentation presentation,
237 boolean nonFocusedSelection, FontMetrics fontMetrics) {
238 int style = getStyle(false, presentation.isStrikeout(), false);
240 for (LookupElementPresentation.TextFragment fragment : presentation.getTailFragments()) {
241 if (allowedWidth < 0) {
245 String trimmed = trimLabelText(fragment.text, allowedWidth, fontMetrics);
246 myTailComponent.append(trimmed, new SimpleTextAttributes(style, getTailTextColor(isSelected, fragment, foreground, nonFocusedSelection)));
247 allowedWidth -= RealLookupElementPresentation.getStringWidth(trimmed, fontMetrics);
251 private String trimLabelText(@Nullable String text, int maxWidth, FontMetrics metrics) {
252 if (text == null || StringUtil.isEmpty(text)) {
256 final int strWidth = RealLookupElementPresentation.getStringWidth(text, metrics);
257 if (strWidth <= maxWidth || myIsSelected) {
261 if (RealLookupElementPresentation.getStringWidth(ELLIPSIS, metrics) > maxWidth) {
266 int j = text.length();
268 int mid = (i + j) / 2;
269 final String candidate = text.substring(0, mid) + ELLIPSIS;
270 final int width = RealLookupElementPresentation.getStringWidth(candidate, metrics);
271 if (width <= maxWidth) {
278 return text.substring(0, i) + ELLIPSIS;
281 private static Color getTypeTextColor(LookupElement item, Color foreground, LookupElementPresentation presentation, boolean selected, boolean nonFocusedSelection) {
282 if (nonFocusedSelection) {
286 return presentation.isTypeGrayed() ? getGrayedForeground(selected) : item instanceof EmptyLookupItem ? JBColor.foreground() : foreground;
289 private static Color getTailTextColor(boolean isSelected, LookupElementPresentation.TextFragment fragment, Color defaultForeground, boolean nonFocusedSelection) {
290 if (nonFocusedSelection) {
291 return defaultForeground;
294 if (fragment.isGrayed()) {
295 return getGrayedForeground(isSelected);
299 final Color tailForeground = fragment.getForegroundColor();
300 if (tailForeground != null) {
301 return tailForeground;
305 return defaultForeground;
308 public static Color getGrayedForeground(boolean isSelected) {
309 return isSelected ? SELECTED_GRAYED_FOREGROUND_COLOR : GRAYED_FOREGROUND_COLOR;
312 private int setItemTextLabel(LookupElement item, final Color foreground, final boolean selected, LookupElementPresentation presentation, int allowedWidth) {
313 boolean bold = presentation.isItemTextBold();
315 Font customItemFont = myLookup.getCustomFont(item, bold);
316 myNameComponent.setFont(customItemFont != null ? customItemFont : bold ? myBoldFont : myNormalFont);
317 int style = getStyle(bold, presentation.isStrikeout(), presentation.isItemTextUnderlined());
319 final FontMetrics metrics = getRealFontMetrics(item, bold);
320 final String name = trimLabelText(presentation.getItemText(), allowedWidth, metrics);
321 int used = RealLookupElementPresentation.getStringWidth(name, metrics);
323 renderItemName(item, foreground, selected, style, name, myNameComponent);
327 private FontMetrics getRealFontMetrics(LookupElement item, boolean bold) {
328 Font customFont = myLookup.getCustomFont(item, bold);
329 if (customFont != null) {
330 return myLookup.getTopLevelEditor().getComponent().getFontMetrics(customFont);
333 return bold ? myBoldMetrics : myNormalMetrics;
336 @SimpleTextAttributes.StyleAttributeConstant
337 private static int getStyle(boolean bold, boolean strikeout, boolean underlined) {
338 int style = bold ? SimpleTextAttributes.STYLE_BOLD : SimpleTextAttributes.STYLE_PLAIN;
340 style |= SimpleTextAttributes.STYLE_STRIKEOUT;
343 style |= SimpleTextAttributes.STYLE_UNDERLINE;
348 private void renderItemName(LookupElement item,
351 @SimpleTextAttributes.StyleAttributeConstant int style,
353 final SimpleColoredComponent nameComponent) {
354 final SimpleTextAttributes base = new SimpleTextAttributes(style, foreground);
356 final String prefix = item instanceof EmptyLookupItem ? "" : myLookup.itemPattern(item);
357 if (prefix.length() > 0) {
358 Iterable<TextRange> ranges = getMatchingFragments(prefix, name);
359 if (ranges != null) {
360 SimpleTextAttributes highlighted =
361 new SimpleTextAttributes(style, selected ? SELECTED_PREFIX_FOREGROUND_COLOR : PREFIX_FOREGROUND_COLOR);
362 SpeedSearchUtil.appendColoredFragments(nameComponent, name, ranges, base, highlighted);
366 nameComponent.append(name, base);
369 public static FList<TextRange> getMatchingFragments(String prefix, String name) {
370 return new MinusculeMatcher("*" + prefix, NameUtil.MatchingCaseSensitivity.NONE).matchingFragments(name);
373 private int setTypeTextLabel(LookupElement item,
374 final Color background,
376 final LookupElementPresentation presentation,
378 boolean selected, boolean nonFocusedSelection, FontMetrics normalMetrics) {
379 final String givenText = presentation.getTypeText();
380 final String labelText = trimLabelText(StringUtil.isEmpty(givenText) ? "" : " " + givenText, allowedWidth, normalMetrics);
382 int used = RealLookupElementPresentation.getStringWidth(labelText, normalMetrics);
384 final Icon icon = presentation.getTypeIcon();
386 myTypeLabel.setIcon(icon);
387 used += icon.getIconWidth();
390 Color sampleBackground = background;
392 Object o = item.isValid() ? item.getObject() : null;
393 //noinspection deprecation
394 if (o instanceof LookupValueWithUIHint && StringUtil.isEmpty(labelText)) {
395 //noinspection deprecation
396 Color proposedBackground = ((LookupValueWithUIHint)o).getColorHint();
397 if (proposedBackground != null) {
398 sampleBackground = proposedBackground;
400 myTypeLabel.append(" ");
401 used += normalMetrics.stringWidth("WW");
403 myTypeLabel.append(labelText);
406 myTypeLabel.setBackground(sampleBackground);
407 myTypeLabel.setForeground(getTypeTextColor(item, foreground, presentation, selected, nonFocusedSelection));
411 public static Icon augmentIcon(@Nullable Icon icon, @NotNull Icon standard) {
416 if (icon.getIconHeight() < standard.getIconHeight() || icon.getIconWidth() < standard.getIconWidth()) {
417 final LayeredIcon layeredIcon = new LayeredIcon(2);
418 layeredIcon.setIcon(icon, 0, 0, (standard.getIconHeight() - icon.getIconHeight()) / 2);
419 layeredIcon.setIcon(standard, 1);
427 Font getFontAbleToDisplay(LookupElementPresentation p) {
428 String sampleString = p.getItemText() + p.getTailText() + p.getTypeText();
430 // assume a single font can display all lookup item chars
431 Set<Font> fonts = ContainerUtil.newHashSet();
432 for (int i = 0; i < sampleString.length(); i++) {
433 fonts.add(EditorUtil.fontForChar(sampleString.charAt(i), Font.PLAIN, myLookup.getTopLevelEditor()).getFont());
436 eachFont: for (Font font : fonts) {
437 if (font.equals(myNormalFont)) continue;
439 for (int i = 0; i < sampleString.length(); i++) {
440 if (!font.canDisplay(sampleString.charAt(i))) {
450 int updateMaximumWidth(final LookupElementPresentation p, LookupElement item) {
451 final Icon icon = p.getIcon();
452 if (icon != null && (icon.getIconWidth() > myEmptyIcon.getIconWidth() || icon.getIconHeight() > myEmptyIcon.getIconHeight())) {
453 myEmptyIcon = new EmptyIcon(Math.max(icon.getIconWidth(), myEmptyIcon.getIconWidth()), Math.max(icon.getIconHeight(), myEmptyIcon.getIconHeight()));
456 return RealLookupElementPresentation.calculateWidth(p, getRealFontMetrics(item, false), getRealFontMetrics(item, true)) + AFTER_TAIL + AFTER_TYPE;
459 public int getTextIndent() {
460 return myNameComponent.getIpad().left + myEmptyIcon.getIconWidth() + myNameComponent.getIconTextGap();
463 private static class MySimpleColoredComponent extends SimpleColoredComponent {
464 private MySimpleColoredComponent() {
465 setFocusBorderAroundIcon(true);
469 protected void applyAdditionalHints(@NotNull Graphics2D g) {
470 super.applyAdditionalHints(g);
474 private class LookupPanel extends JPanel {
475 boolean myUpdateExtender;
476 public LookupPanel() {
477 super(new BorderLayout());
480 public void setUpdateExtender(boolean updateExtender) {
481 myUpdateExtender = updateExtender;
485 public void paint(Graphics g){
487 if (!myLookup.isFocused() && myLookup.isCompletion()) {
490 g.setColor(ColorUtil.withAlpha(BACKGROUND_COLOR, .4));
491 g.fillRect(0, 0, getWidth(), getHeight());