59934194f1eb743a4175006064c9cabf7ac1a8fc
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / lookup / impl / LookupCellRenderer.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.intellij.codeInsight.lookup.impl;
18
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;
44
45 import javax.swing.*;
46 import javax.swing.border.EmptyBorder;
47 import java.awt.*;
48 import java.awt.image.BufferedImage;
49 import java.util.HashMap;
50 import java.util.Map;
51 import java.util.Set;
52
53 /**
54  * @author peter
55  * @author Konstantin Bulenkov
56  */
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;
67
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());
75
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));
78
79   private final LookupImpl myLookup;
80
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>();
86
87   private static final String ELLIPSIS = "\u2026";
88   private int myMaxWidth = -1;
89
90   public LookupCellRenderer(LookupImpl lookup) {
91     EditorColorsScheme scheme = lookup.getEditor().getColorsScheme();
92     myNormalFont = scheme.getFont(EditorFontType.PLAIN);
93     myBoldFont = scheme.getFont(EditorFontType.BOLD);
94
95     myLookup = lookup;
96     myNameComponent = new MySimpleColoredComponent();
97     myNameComponent.setIpad(new Insets(0, 0, 0, 0));
98
99     myTailComponent = new MySimpleColoredComponent();
100     myTailComponent.setIpad(new Insets(0, 0, 0, 0));
101
102     myTypeLabel = new MySimpleColoredComponent();
103     myTypeLabel.setIpad(new Insets(0, 0, 0, 0));
104
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));
109
110     myPanel.add(myTypeLabel, BorderLayout.EAST);
111     myTypeLabel.setBorder(new EmptyBorder(0, 0, 0, AFTER_TYPE));
112
113     myNormalMetrics = myLookup.getEditor().getComponent().getFontMetrics(myNormalFont);
114     myBoldMetrics = myLookup.getEditor().getComponent().getFontMetrics(myBoldFont);
115   }
116
117   private boolean myIsSelected = false;
118   @Override
119   public Component getListCellRendererComponent(
120       final JList list,
121       Object value,
122       int index,
123       boolean isSelected,
124       boolean hasFocus) {
125
126
127     boolean nonFocusedSelection = isSelected && myLookup.getFocusDegree() == LookupImpl.FocusDegree.SEMI_FOCUSED;
128     if (!myLookup.isFocused()) {
129       isSelected = false;
130     }
131
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;
137
138     int allowedWidth = list.getWidth() - AFTER_TAIL - AFTER_TYPE - getIconIndent();
139
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();
145     try {
146       if (item.isValid()) {
147         try {
148           item.renderElement(presentation);
149         }
150         catch (ProcessCanceledException e) {
151           LOG.info(e);
152           presentation.setItemTextForeground(JBColor.RED);
153           presentation.setItemText("Error occurred, see the log in Help | Show Log");
154         }
155         catch (Exception e) {
156           LOG.error(e);
157         }
158         catch (Error e) {
159           LOG.error(e);
160         }
161       } else {
162         presentation.setItemTextForeground(JBColor.RED);
163         presentation.setItemText("Invalid");
164       }
165     }
166     finally {
167       token.finish();
168     }
169
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);
174
175     Font customFont = myLookup.getCustomFont(item, false);
176     myTailComponent.setFont(customFont != null ? customFont : myNormalFont);
177     myTypeLabel.setFont(customFont != null ? customFont : myNormalFont);
178
179     myTypeLabel.clear();
180     if (allowedWidth > 0) {
181       allowedWidth -= setTypeTextLabel(item, background, foreground, presentation, isSelected ? getMaxWidth() : allowedWidth, isSelected, nonFocusedSelection, normalMetrics);
182     }
183
184     myTailComponent.clear();
185     myTailComponent.setBackground(background);
186     if (isSelected || allowedWidth >= 0) {
187       setTailTextLabel(isSelected, presentation, foreground, isSelected ? getMaxWidth() : allowedWidth, nonFocusedSelection,
188                        normalMetrics);
189     }
190
191     if (mySelected.containsKey(index)) {
192       if (!isSelected && mySelected.get(index)) {
193         myPanel.setUpdateExtender(true);
194       }
195     }
196     mySelected.put(index, isSelected);
197
198     final double w = myNameComponent.getPreferredSize().getWidth() +
199                      myTailComponent.getPreferredSize().getWidth() +
200                      myTypeLabel.getPreferredSize().getWidth();
201
202     boolean useBoxLayout = isSelected && w > list.getWidth() && ((JBList)list).getExpandableItemsHandler().isEnabled();
203     if (useBoxLayout != myPanel.getLayout() instanceof BoxLayout) {
204       myPanel.removeAll();
205       if (useBoxLayout) {
206         myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.X_AXIS));
207         myPanel.add(myNameComponent);
208         myPanel.add(myTailComponent);
209         myPanel.add(myTypeLabel);
210       } else {
211         myPanel.setLayout(new BorderLayout());
212         myPanel.add(myNameComponent, BorderLayout.WEST);
213         myPanel.add(myTailComponent, BorderLayout.CENTER);
214         myPanel.add(myTypeLabel, BorderLayout.EAST);
215       }
216     }
217
218     return myPanel;
219   }
220
221   private static Color getForegroundColor(boolean isSelected) {
222     return isSelected ? SELECTED_FOREGROUND_COLOR : FOREGROUND_COLOR;
223   }
224
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;
230     }
231     return myMaxWidth;
232   }
233
234   private void setTailTextLabel(boolean isSelected,
235                                 LookupElementPresentation presentation,
236                                 Color foreground,
237                                 int allowedWidth,
238                                 boolean nonFocusedSelection, FontMetrics fontMetrics) {
239     int style = getStyle(false, presentation.isStrikeout(), false);
240
241     for (LookupElementPresentation.TextFragment fragment : presentation.getTailFragments()) {
242       if (allowedWidth < 0) {
243         return;
244       }
245
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);
249     }
250   }
251
252   private String trimLabelText(@Nullable String text, int maxWidth, FontMetrics metrics) {
253     if (text == null || StringUtil.isEmpty(text)) {
254       return "";
255     }
256
257     final int strWidth = RealLookupElementPresentation.getStringWidth(text, metrics);
258     if (strWidth <= maxWidth || myIsSelected) {
259       return text;
260     }
261
262     if (RealLookupElementPresentation.getStringWidth(ELLIPSIS, metrics) > maxWidth) {
263       return "";
264     }
265
266     int i = 0;
267     int j = text.length();
268     while (i + 1 < j) {
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) {
273         i = mid;
274       } else {
275         j = mid;
276       }
277     }
278
279     return text.substring(0, i) + ELLIPSIS;
280   }
281
282   private static Color getTypeTextColor(LookupElement item, Color foreground, LookupElementPresentation presentation, boolean selected, boolean nonFocusedSelection) {
283     if (nonFocusedSelection) {
284       return foreground;
285     }
286
287     return presentation.isTypeGrayed() ? getGrayedForeground(selected) : item instanceof EmptyLookupItem ? JBColor.foreground() : foreground;
288   }
289
290   private static Color getTailTextColor(boolean isSelected, LookupElementPresentation.TextFragment fragment, Color defaultForeground, boolean nonFocusedSelection) {
291     if (nonFocusedSelection) {
292       return defaultForeground;
293     }
294
295     if (fragment.isGrayed()) {
296       return getGrayedForeground(isSelected);
297     }
298
299     if (!isSelected) {
300       final Color tailForeground = fragment.getForegroundColor();
301       if (tailForeground != null) {
302         return tailForeground;
303       }
304     }
305
306     return defaultForeground;
307   }
308
309   public static Color getGrayedForeground(boolean isSelected) {
310     return isSelected ? SELECTED_GRAYED_FOREGROUND_COLOR : GRAYED_FOREGROUND_COLOR;
311   }
312
313   private int setItemTextLabel(LookupElement item, final Color foreground, final boolean selected, LookupElementPresentation presentation, int allowedWidth) {
314     boolean bold = presentation.isItemTextBold();
315
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());
319
320     final FontMetrics metrics = getRealFontMetrics(item, bold);
321     final String name = trimLabelText(presentation.getItemText(), allowedWidth, metrics);
322     int used = RealLookupElementPresentation.getStringWidth(name, metrics);
323
324     renderItemName(item, foreground, selected, style, name, myNameComponent);
325     return used;
326   }
327
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);
332     }
333
334     return bold ? myBoldMetrics : myNormalMetrics;
335   }
336
337   @SimpleTextAttributes.StyleAttributeConstant
338   private static int getStyle(boolean bold, boolean strikeout, boolean underlined) {
339     int style = bold ? SimpleTextAttributes.STYLE_BOLD : SimpleTextAttributes.STYLE_PLAIN;
340     if (strikeout) {
341       style |= SimpleTextAttributes.STYLE_STRIKEOUT;
342     }
343     if (underlined) {
344       style |= SimpleTextAttributes.STYLE_UNDERLINE;
345     }
346     return style;
347   }
348
349   private void renderItemName(LookupElement item,
350                       Color foreground,
351                       boolean selected,
352                       @SimpleTextAttributes.StyleAttributeConstant int style,
353                       String name,
354                       final SimpleColoredComponent nameComponent) {
355     final SimpleTextAttributes base = new SimpleTextAttributes(style, foreground);
356
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);
364         return;
365       }
366     }
367     nameComponent.append(name, base);
368   }
369
370   public static FList<TextRange> getMatchingFragments(String prefix, String name) {
371     return new MinusculeMatcher("*" + prefix, NameUtil.MatchingCaseSensitivity.NONE).matchingFragments(name);
372   }
373
374   private int setTypeTextLabel(LookupElement item,
375                                final Color background,
376                                Color foreground,
377                                final LookupElementPresentation presentation,
378                                int allowedWidth,
379                                boolean selected, boolean nonFocusedSelection, FontMetrics normalMetrics) {
380     final String givenText = presentation.getTypeText();
381     final String labelText = trimLabelText(StringUtil.isEmpty(givenText) ? "" : " " + givenText, allowedWidth, normalMetrics);
382
383     int used = RealLookupElementPresentation.getStringWidth(labelText, normalMetrics);
384
385     final Icon icon = presentation.getTypeIcon();
386     if (icon != null) {
387       myTypeLabel.setIcon(icon);
388       used += icon.getIconWidth();
389     }
390
391     Color sampleBackground = background;
392
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;
400       }
401       myTypeLabel.append("  ");
402       used += normalMetrics.stringWidth("WW");
403     } else {
404       myTypeLabel.append(labelText);
405     }
406
407     myTypeLabel.setBackground(sampleBackground);
408     myTypeLabel.setForeground(getTypeTextColor(item, foreground, presentation, selected, nonFocusedSelection));
409     return used;
410   }
411
412   public static Icon augmentIcon(@Nullable Icon icon, @NotNull Icon standard) {
413     if (icon == null) {
414       return standard;
415     }
416
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);
421       return layeredIcon;
422     }
423
424     return icon;
425   }
426
427   @Nullable
428   Font getFontAbleToDisplay(LookupElementPresentation p) {
429     String sampleString = p.getItemText() + p.getTailText() + p.getTypeText();
430
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());
435     }
436
437     eachFont: for (Font font : fonts) {
438       if (font.equals(myNormalFont)) continue;
439       
440       for (int i = 0; i < sampleString.length(); i++) {
441         if (!font.canDisplay(sampleString.charAt(i))) {
442           continue eachFont;
443         }
444       }
445       return font;
446     }
447     return null;
448   }
449
450
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()));
455     }
456
457     return RealLookupElementPresentation.calculateWidth(p, getRealFontMetrics(item, false), getRealFontMetrics(item, true)) + AFTER_TAIL + AFTER_TYPE;
458   }
459
460   public int getIconIndent() {
461     return myNameComponent.getIconTextGap() + myEmptyIcon.getIconWidth();
462   }
463
464
465   private static class MySimpleColoredComponent extends SimpleColoredComponent {
466     private MySimpleColoredComponent() {
467       setFocusBorderAroundIcon(true);
468     }
469
470     @Override
471     protected void applyAdditionalHints(@NotNull Graphics2D g) {
472       GraphicsUtil.setupAntialiasing(g);
473     }
474   }
475
476   private class LookupPanel extends JPanel {
477     boolean myUpdateExtender;
478     public LookupPanel() {
479       super(new BorderLayout());
480     }
481
482     public void setUpdateExtender(boolean updateExtender) {
483       myUpdateExtender = updateExtender;
484     }
485
486     @Override
487     public void paint(Graphics g){
488       if (!myLookup.isFocused() && myLookup.isCompletion()) {
489         ((Graphics2D)g).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f));
490
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);
497       }
498       else {
499         super.paint(g);
500       }
501     }
502   }
503 }