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.GraphicsUtil;
41 import com.intellij.util.ui.UIUtil;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
46 import javax.swing.border.EmptyBorder;
48 import java.awt.image.BufferedImage;
49 import java.util.HashMap;
55 * @author Konstantin Bulenkov
57 public class LookupCellRenderer implements ListCellRenderer {
58 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupCellRenderer");
59 //TODO[kb]: move all these awesome constants to Editor's Fonts & Colors settings
60 private static final int AFTER_TAIL = 10;
61 private static final int AFTER_TYPE = 6;
62 private Icon myEmptyIcon = EmptyIcon.create(5);
63 private final Font myNormalFont;
64 private final Font myBoldFont;
65 private final FontMetrics myNormalMetrics;
66 private final FontMetrics myBoldMetrics;
68 public static final Color BACKGROUND_COLOR = new JBColor(new Color(235, 244, 254), JBColor.background());
69 private static final Color FOREGROUND_COLOR = JBColor.foreground();
70 private static final Color GRAYED_FOREGROUND_COLOR = new JBColor(Gray._160, Gray._110);
71 private static final Color SELECTED_BACKGROUND_COLOR = new Color(0, 82, 164);
72 private static final Color SELECTED_NON_FOCUSED_BACKGROUND_COLOR = new JBColor(new Color(110, 142, 162), new Color(85, 88, 90));
73 private static final Color SELECTED_FOREGROUND_COLOR = new JBColor(JBColor.WHITE, JBColor.foreground());
74 private static final Color SELECTED_GRAYED_FOREGROUND_COLOR = new JBColor(JBColor.WHITE, JBColor.foreground());
76 static final Color PREFIX_FOREGROUND_COLOR = new JBColor(new Color(176, 0, 176), new Color(209, 122, 214));
77 private static final Color SELECTED_PREFIX_FOREGROUND_COLOR = new JBColor(new Color(249, 236, 204), new Color(209, 122, 214));
79 private final LookupImpl myLookup;
81 private final SimpleColoredComponent myNameComponent;
82 private final SimpleColoredComponent myTailComponent;
83 private final SimpleColoredComponent myTypeLabel;
84 private final LookupPanel myPanel;
85 private final Map<Integer, Boolean> mySelected = new HashMap<Integer, Boolean>();
87 private static final String ELLIPSIS = "\u2026";
88 private int myMaxWidth = -1;
90 public LookupCellRenderer(LookupImpl lookup) {
91 EditorColorsScheme scheme = lookup.getEditor().getColorsScheme();
92 myNormalFont = scheme.getFont(EditorFontType.PLAIN);
93 myBoldFont = scheme.getFont(EditorFontType.BOLD);
96 myNameComponent = new MySimpleColoredComponent();
97 myNameComponent.setIpad(new Insets(0, 0, 0, 0));
99 myTailComponent = new MySimpleColoredComponent();
100 myTailComponent.setIpad(new Insets(0, 0, 0, 0));
102 myTypeLabel = new MySimpleColoredComponent();
103 myTypeLabel.setIpad(new Insets(0, 0, 0, 0));
105 myPanel = new LookupPanel();
106 myPanel.add(myNameComponent, BorderLayout.WEST);
107 myPanel.add(myTailComponent, BorderLayout.CENTER);
108 myTailComponent.setBorder(new EmptyBorder(0, 0, 0, AFTER_TAIL));
110 myPanel.add(myTypeLabel, BorderLayout.EAST);
111 myTypeLabel.setBorder(new EmptyBorder(0, 0, 0, AFTER_TYPE));
113 myNormalMetrics = myLookup.getEditor().getComponent().getFontMetrics(myNormalFont);
114 myBoldMetrics = myLookup.getEditor().getComponent().getFontMetrics(myBoldFont);
117 private boolean myIsSelected = false;
119 public Component getListCellRendererComponent(
127 boolean nonFocusedSelection = isSelected && myLookup.getFocusDegree() == LookupImpl.FocusDegree.SEMI_FOCUSED;
128 if (!myLookup.isFocused()) {
132 myIsSelected = isSelected;
133 final LookupElement item = (LookupElement)value;
134 final Color foreground = getForegroundColor(isSelected);
135 final Color background = nonFocusedSelection ? SELECTED_NON_FOCUSED_BACKGROUND_COLOR :
136 isSelected ? SELECTED_BACKGROUND_COLOR : BACKGROUND_COLOR;
138 int allowedWidth = list.getWidth() - AFTER_TAIL - AFTER_TYPE - getIconIndent();
140 FontMetrics normalMetrics = getRealFontMetrics(item, false);
141 FontMetrics boldMetrics = getRealFontMetrics(item, true);
142 final LookupElementPresentation presentation = new RealLookupElementPresentation(isSelected ? getMaxWidth() : allowedWidth,
143 normalMetrics, boldMetrics, myLookup);
144 AccessToken token = ReadAction.start();
146 if (item.isValid()) {
148 item.renderElement(presentation);
150 catch (ProcessCanceledException e) {
152 presentation.setItemTextForeground(JBColor.RED);
153 presentation.setItemText("Error occurred, see the log in Help | Show Log");
155 catch (Exception e) {
162 presentation.setItemTextForeground(JBColor.RED);
163 presentation.setItemText("Invalid");
170 myNameComponent.clear();
171 myNameComponent.setIcon(augmentIcon(presentation.getIcon(), myEmptyIcon));
172 myNameComponent.setBackground(background);
173 allowedWidth -= setItemTextLabel(item, new JBColor(isSelected ? SELECTED_FOREGROUND_COLOR : presentation.getItemTextForeground(), presentation.getItemTextForeground()), isSelected, presentation, allowedWidth);
175 Font customFont = myLookup.getCustomFont(item, false);
176 myTailComponent.setFont(customFont != null ? customFont : myNormalFont);
177 myTypeLabel.setFont(customFont != null ? customFont : myNormalFont);
180 if (allowedWidth > 0) {
181 allowedWidth -= setTypeTextLabel(item, background, foreground, presentation, isSelected ? getMaxWidth() : allowedWidth, isSelected, nonFocusedSelection, normalMetrics);
184 myTailComponent.clear();
185 myTailComponent.setBackground(background);
186 if (isSelected || allowedWidth >= 0) {
187 setTailTextLabel(isSelected, presentation, foreground, isSelected ? getMaxWidth() : allowedWidth, nonFocusedSelection,
191 if (mySelected.containsKey(index)) {
192 if (!isSelected && mySelected.get(index)) {
193 myPanel.setUpdateExtender(true);
196 mySelected.put(index, isSelected);
198 final double w = myNameComponent.getPreferredSize().getWidth() +
199 myTailComponent.getPreferredSize().getWidth() +
200 myTypeLabel.getPreferredSize().getWidth();
202 boolean useBoxLayout = isSelected && w > list.getWidth() && ((JBList)list).getExpandableItemsHandler().isEnabled();
203 if (useBoxLayout != myPanel.getLayout() instanceof BoxLayout) {
206 myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.X_AXIS));
207 myPanel.add(myNameComponent);
208 myPanel.add(myTailComponent);
209 myPanel.add(myTypeLabel);
211 myPanel.setLayout(new BorderLayout());
212 myPanel.add(myNameComponent, BorderLayout.WEST);
213 myPanel.add(myTailComponent, BorderLayout.CENTER);
214 myPanel.add(myTypeLabel, BorderLayout.EAST);
221 private static Color getForegroundColor(boolean isSelected) {
222 return isSelected ? SELECTED_FOREGROUND_COLOR : FOREGROUND_COLOR;
225 private int getMaxWidth() {
226 if (myMaxWidth < 0) {
227 final Point p = myLookup.getComponent().getLocationOnScreen();
228 final Rectangle rectangle = ScreenUtil.getScreenRectangle(p);
229 myMaxWidth = rectangle.x + rectangle.width - p.x - 111;
234 private void setTailTextLabel(boolean isSelected,
235 LookupElementPresentation presentation,
238 boolean nonFocusedSelection, FontMetrics fontMetrics) {
239 int style = getStyle(false, presentation.isStrikeout(), false);
241 for (LookupElementPresentation.TextFragment fragment : presentation.getTailFragments()) {
242 if (allowedWidth < 0) {
246 String trimmed = trimLabelText(fragment.text, allowedWidth, fontMetrics);
247 myTailComponent.append(trimmed, new SimpleTextAttributes(style, getTailTextColor(isSelected, fragment, foreground, nonFocusedSelection)));
248 allowedWidth -= RealLookupElementPresentation.getStringWidth(trimmed, fontMetrics);
252 private String trimLabelText(@Nullable String text, int maxWidth, FontMetrics metrics) {
253 if (text == null || StringUtil.isEmpty(text)) {
257 final int strWidth = RealLookupElementPresentation.getStringWidth(text, metrics);
258 if (strWidth <= maxWidth || myIsSelected) {
262 if (RealLookupElementPresentation.getStringWidth(ELLIPSIS, metrics) > maxWidth) {
267 int j = text.length();
269 int mid = (i + j) / 2;
270 final String candidate = text.substring(0, mid) + ELLIPSIS;
271 final int width = RealLookupElementPresentation.getStringWidth(candidate, metrics);
272 if (width <= maxWidth) {
279 return text.substring(0, i) + ELLIPSIS;
282 private static Color getTypeTextColor(LookupElement item, Color foreground, LookupElementPresentation presentation, boolean selected, boolean nonFocusedSelection) {
283 if (nonFocusedSelection) {
287 return presentation.isTypeGrayed() ? getGrayedForeground(selected) : item instanceof EmptyLookupItem ? JBColor.foreground() : foreground;
290 private static Color getTailTextColor(boolean isSelected, LookupElementPresentation.TextFragment fragment, Color defaultForeground, boolean nonFocusedSelection) {
291 if (nonFocusedSelection) {
292 return defaultForeground;
295 if (fragment.isGrayed()) {
296 return getGrayedForeground(isSelected);
300 final Color tailForeground = fragment.getForegroundColor();
301 if (tailForeground != null) {
302 return tailForeground;
306 return defaultForeground;
309 public static Color getGrayedForeground(boolean isSelected) {
310 return isSelected ? SELECTED_GRAYED_FOREGROUND_COLOR : GRAYED_FOREGROUND_COLOR;
313 private int setItemTextLabel(LookupElement item, final Color foreground, final boolean selected, LookupElementPresentation presentation, int allowedWidth) {
314 boolean bold = presentation.isItemTextBold();
316 Font customItemFont = myLookup.getCustomFont(item, bold);
317 myNameComponent.setFont(customItemFont != null ? customItemFont : bold ? myBoldFont : myNormalFont);
318 int style = getStyle(bold, presentation.isStrikeout(), presentation.isItemTextUnderlined());
320 final FontMetrics metrics = getRealFontMetrics(item, bold);
321 final String name = trimLabelText(presentation.getItemText(), allowedWidth, metrics);
322 int used = RealLookupElementPresentation.getStringWidth(name, metrics);
324 renderItemName(item, foreground, selected, style, name, myNameComponent);
328 private FontMetrics getRealFontMetrics(LookupElement item, boolean bold) {
329 Font customFont = myLookup.getCustomFont(item, bold);
330 if (customFont != null) {
331 return myLookup.getEditor().getComponent().getFontMetrics(customFont);
334 return bold ? myBoldMetrics : myNormalMetrics;
337 @SimpleTextAttributes.StyleAttributeConstant
338 private static int getStyle(boolean bold, boolean strikeout, boolean underlined) {
339 int style = bold ? SimpleTextAttributes.STYLE_BOLD : SimpleTextAttributes.STYLE_PLAIN;
341 style |= SimpleTextAttributes.STYLE_STRIKEOUT;
344 style |= SimpleTextAttributes.STYLE_UNDERLINE;
349 private void renderItemName(LookupElement item,
352 @SimpleTextAttributes.StyleAttributeConstant int style,
354 final SimpleColoredComponent nameComponent) {
355 final SimpleTextAttributes base = new SimpleTextAttributes(style, foreground);
357 final String prefix = item instanceof EmptyLookupItem ? "" : myLookup.itemPattern(item);
358 if (prefix.length() > 0) {
359 Iterable<TextRange> ranges = getMatchingFragments(prefix, name);
360 if (ranges != null) {
361 SimpleTextAttributes highlighted =
362 new SimpleTextAttributes(style, selected ? SELECTED_PREFIX_FOREGROUND_COLOR : PREFIX_FOREGROUND_COLOR);
363 SpeedSearchUtil.appendColoredFragments(nameComponent, name, ranges, base, highlighted);
367 nameComponent.append(name, base);
370 public static FList<TextRange> getMatchingFragments(String prefix, String name) {
371 return new MinusculeMatcher("*" + prefix, NameUtil.MatchingCaseSensitivity.NONE).matchingFragments(name);
374 private int setTypeTextLabel(LookupElement item,
375 final Color background,
377 final LookupElementPresentation presentation,
379 boolean selected, boolean nonFocusedSelection, FontMetrics normalMetrics) {
380 final String givenText = presentation.getTypeText();
381 final String labelText = trimLabelText(StringUtil.isEmpty(givenText) ? "" : " " + givenText, allowedWidth, normalMetrics);
383 int used = RealLookupElementPresentation.getStringWidth(labelText, normalMetrics);
385 final Icon icon = presentation.getTypeIcon();
387 myTypeLabel.setIcon(icon);
388 used += icon.getIconWidth();
391 Color sampleBackground = background;
393 Object o = item.isValid() ? item.getObject() : null;
394 //noinspection deprecation
395 if (o instanceof LookupValueWithUIHint && StringUtil.isEmpty(labelText)) {
396 //noinspection deprecation
397 Color proposedBackground = ((LookupValueWithUIHint)o).getColorHint();
398 if (proposedBackground != null) {
399 sampleBackground = proposedBackground;
401 myTypeLabel.append(" ");
402 used += normalMetrics.stringWidth("WW");
404 myTypeLabel.append(labelText);
407 myTypeLabel.setBackground(sampleBackground);
408 myTypeLabel.setForeground(getTypeTextColor(item, foreground, presentation, selected, nonFocusedSelection));
412 public static Icon augmentIcon(@Nullable Icon icon, @NotNull Icon standard) {
417 if (icon.getIconHeight() < standard.getIconHeight() || icon.getIconWidth() < standard.getIconWidth()) {
418 final LayeredIcon layeredIcon = new LayeredIcon(2);
419 layeredIcon.setIcon(icon, 0, 0, (standard.getIconHeight() - icon.getIconHeight()) / 2);
420 layeredIcon.setIcon(standard, 1);
428 Font getFontAbleToDisplay(LookupElementPresentation p) {
429 String sampleString = p.getItemText() + p.getTailText() + p.getTypeText();
431 // assume a single font can display all lookup item chars
432 Set<Font> fonts = ContainerUtil.newHashSet();
433 for (int i = 0; i < sampleString.length(); i++) {
434 fonts.add(EditorUtil.fontForChar(sampleString.charAt(i), Font.PLAIN, myLookup.getEditor()).getFont());
437 eachFont: for (Font font : fonts) {
438 if (font.equals(myNormalFont)) continue;
440 for (int i = 0; i < sampleString.length(); i++) {
441 if (!font.canDisplay(sampleString.charAt(i))) {
451 int updateMaximumWidth(final LookupElementPresentation p, LookupElement item) {
452 final Icon icon = p.getIcon();
453 if (icon != null && (icon.getIconWidth() > myEmptyIcon.getIconWidth() || icon.getIconHeight() > myEmptyIcon.getIconHeight())) {
454 myEmptyIcon = new EmptyIcon(Math.max(icon.getIconWidth(), myEmptyIcon.getIconWidth()), Math.max(icon.getIconHeight(), myEmptyIcon.getIconHeight()));
457 return RealLookupElementPresentation.calculateWidth(p, getRealFontMetrics(item, false), getRealFontMetrics(item, true)) + AFTER_TAIL + AFTER_TYPE;
460 public int getIconIndent() {
461 return myNameComponent.getIconTextGap() + myEmptyIcon.getIconWidth();
465 private static class MySimpleColoredComponent extends SimpleColoredComponent {
466 private MySimpleColoredComponent() {
467 setFocusBorderAroundIcon(true);
471 protected void applyAdditionalHints(@NotNull Graphics2D g) {
472 GraphicsUtil.setupAntialiasing(g);
476 private class LookupPanel extends JPanel {
477 boolean myUpdateExtender;
478 public LookupPanel() {
479 super(new BorderLayout());
482 public void setUpdateExtender(boolean updateExtender) {
483 myUpdateExtender = updateExtender;
487 public void paint(Graphics g){
488 if (!myLookup.isFocused() && myLookup.isCompletion()) {
489 ((Graphics2D)g).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f));
491 // sub-pixel antialiasing does not work with alpha composite, so we workaround this by painting to RGB image first
492 BufferedImage image = UIUtil.createImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
493 Graphics2D imageGraphics = image.createGraphics();
494 super.paint(imageGraphics);
495 imageGraphics.dispose();
496 UIUtil.drawImage(g, image, 0, 0, null);