IDEA-155000 Overlapping texts in completion popup
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / lookup / impl / LookupCellRenderer.java
1 /*
2  * Copyright 2000-2016 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.Editor;
27 import com.intellij.openapi.editor.colors.EditorColorsScheme;
28 import com.intellij.openapi.editor.colors.EditorFontType;
29 import com.intellij.openapi.editor.ex.util.EditorUtil;
30 import com.intellij.openapi.progress.ProcessCanceledException;
31 import com.intellij.openapi.util.TextRange;
32 import com.intellij.openapi.util.registry.Registry;
33 import com.intellij.openapi.util.text.StringUtil;
34 import com.intellij.psi.codeStyle.MinusculeMatcher;
35 import com.intellij.psi.codeStyle.NameUtil;
36 import com.intellij.ui.*;
37 import com.intellij.ui.components.JBList;
38 import com.intellij.ui.speedSearch.SpeedSearchUtil;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.containers.FList;
41 import com.intellij.util.ui.EmptyIcon;
42 import com.intellij.util.ui.JBUI;
43 import com.intellij.util.ui.accessibility.AccessibleContextUtil;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import javax.swing.*;
48 import javax.swing.border.Border;
49 import javax.swing.border.EmptyBorder;
50 import java.awt.*;
51 import java.util.HashMap;
52 import java.util.Map;
53 import java.util.Set;
54
55 /**
56  * @author peter
57  * @author Konstantin Bulenkov
58  */
59 public class LookupCellRenderer implements ListCellRenderer {
60   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupCellRenderer");
61   //TODO[kb]: move all these awesome constants to Editor's Fonts & Colors settings
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.getTopLevelEditor().getColorsScheme();
92     myNormalFont = scheme.getFont(EditorFontType.PLAIN);
93     myBoldFont = scheme.getFont(EditorFontType.BOLD);
94
95     myLookup = lookup;
96     myNameComponent = new MySimpleColoredComponent();
97     myNameComponent.setIpad(JBUI.insetsLeft(2));
98     myNameComponent.setMyBorder(null);
99
100     myTailComponent = new MySimpleColoredComponent();
101     myTailComponent.setIpad(new Insets(0, 0, 0, 0));
102     myTailComponent.setBorder(new EmptyBorder(0, 0, 0, JBUI.scale(10)));
103
104     myTypeLabel = new MySimpleColoredComponent();
105     myTypeLabel.setIpad(new Insets(0, 0, 0, 0));
106     myTypeLabel.setBorder(new EmptyBorder(0, 0, 0, JBUI.scale(6)));
107
108     myPanel = new LookupPanel();
109     myPanel.add(myNameComponent, BorderLayout.WEST);
110     myPanel.add(myTailComponent, BorderLayout.CENTER);
111     myPanel.add(myTypeLabel, BorderLayout.EAST);
112
113     myNormalMetrics = myLookup.getTopLevelEditor().getComponent().getFontMetrics(myNormalFont);
114     myBoldMetrics = myLookup.getTopLevelEditor().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() - calcSpacing(myNameComponent, myEmptyIcon) - calcSpacing(myTailComponent, null) - calcSpacing(myTypeLabel, null);
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.setBackground(background);
172     allowedWidth -= setItemTextLabel(item, new JBColor(isSelected ? SELECTED_FOREGROUND_COLOR : presentation.getItemTextForeground(), presentation.getItemTextForeground()), isSelected, presentation, allowedWidth);
173
174     Font font = myLookup.getCustomFont(item, false);
175     if (font == null) {
176       font = myNormalFont;
177     }
178     myTailComponent.setFont(font);
179     myTypeLabel.setFont(font);
180     myNameComponent.setIcon(augmentIcon(myLookup.getEditor(), presentation.getIcon(), myEmptyIcon));
181
182
183     myTypeLabel.clear();
184     if (allowedWidth > 0) {
185       allowedWidth -= setTypeTextLabel(item, background, foreground, presentation, isSelected ? getMaxWidth() : allowedWidth, isSelected, nonFocusedSelection, normalMetrics);
186     }
187
188     myTailComponent.clear();
189     myTailComponent.setBackground(background);
190     if (isSelected || allowedWidth >= 0) {
191       setTailTextLabel(isSelected, presentation, foreground, isSelected ? getMaxWidth() : allowedWidth, nonFocusedSelection,
192                        normalMetrics);
193     }
194
195     if (mySelected.containsKey(index)) {
196       if (!isSelected && mySelected.get(index)) {
197         myPanel.setUpdateExtender(true);
198       }
199     }
200     mySelected.put(index, isSelected);
201
202     final double w = myNameComponent.getPreferredSize().getWidth() +
203                      myTailComponent.getPreferredSize().getWidth() +
204                      myTypeLabel.getPreferredSize().getWidth();
205
206     boolean useBoxLayout = isSelected && w > list.getWidth() && ((JBList)list).getExpandableItemsHandler().isEnabled();
207     if (useBoxLayout != myPanel.getLayout() instanceof BoxLayout) {
208       myPanel.removeAll();
209       if (useBoxLayout) {
210         myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.X_AXIS));
211         myPanel.add(myNameComponent);
212         myPanel.add(myTailComponent);
213         myPanel.add(myTypeLabel);
214       } else {
215         myPanel.setLayout(new BorderLayout());
216         myPanel.add(myNameComponent, BorderLayout.WEST);
217         myPanel.add(myTailComponent, BorderLayout.CENTER);
218         myPanel.add(myTypeLabel, BorderLayout.EAST);
219       }
220     }
221
222     AccessibleContextUtil.setCombinedName(myPanel, myNameComponent, "", myTailComponent, " - ", myTypeLabel);
223     AccessibleContextUtil.setCombinedDescription(myPanel, myNameComponent, "", myTailComponent, " - ", myTypeLabel);
224     return myPanel;
225   }
226
227   private static int calcSpacing(@NotNull SimpleColoredComponent component, @Nullable Icon icon) {
228     Insets iPad = component.getIpad();
229     int width = iPad.left + iPad.right;
230     Border myBorder = component.getMyBorder();
231     if (myBorder != null) {
232       Insets insets = myBorder.getBorderInsets(component);
233       width += insets.left + insets.right;
234     }
235     Insets insets = component.getInsets();
236     if (insets != null) {
237       width += insets.left + insets.right;
238     }
239     if (icon != null) {
240       width += icon.getIconWidth() + component.getIconTextGap();
241     }
242     return width;
243   }
244
245   private static Color getForegroundColor(boolean isSelected) {
246     return isSelected ? SELECTED_FOREGROUND_COLOR : FOREGROUND_COLOR;
247   }
248
249   private int getMaxWidth() {
250     if (myMaxWidth < 0) {
251       final Point p = myLookup.getComponent().getLocationOnScreen();
252       final Rectangle rectangle = ScreenUtil.getScreenRectangle(p);
253       myMaxWidth = rectangle.x + rectangle.width - p.x - 111;
254     }
255     return myMaxWidth;
256   }
257
258   private void setTailTextLabel(boolean isSelected,
259                                 LookupElementPresentation presentation,
260                                 Color foreground,
261                                 int allowedWidth,
262                                 boolean nonFocusedSelection, FontMetrics fontMetrics) {
263     int style = getStyle(false, presentation.isStrikeout(), false);
264
265     for (LookupElementPresentation.TextFragment fragment : presentation.getTailFragments()) {
266       if (allowedWidth < 0) {
267         return;
268       }
269
270       String trimmed = trimLabelText(fragment.text, allowedWidth, fontMetrics);
271       myTailComponent.append(trimmed, new SimpleTextAttributes(style, getTailTextColor(isSelected, fragment, foreground, nonFocusedSelection)));
272       allowedWidth -= RealLookupElementPresentation.getStringWidth(trimmed, fontMetrics);
273     }
274   }
275
276   private String trimLabelText(@Nullable String text, int maxWidth, FontMetrics metrics) {
277     if (text == null || StringUtil.isEmpty(text)) {
278       return "";
279     }
280
281     final int strWidth = RealLookupElementPresentation.getStringWidth(text, metrics);
282     if (strWidth <= maxWidth || myIsSelected) {
283       return text;
284     }
285
286     if (RealLookupElementPresentation.getStringWidth(ELLIPSIS, metrics) > maxWidth) {
287       return "";
288     }
289
290     int i = 0;
291     int j = text.length();
292     while (i + 1 < j) {
293       int mid = (i + j) / 2;
294       final String candidate = text.substring(0, mid) + ELLIPSIS;
295       final int width = RealLookupElementPresentation.getStringWidth(candidate, metrics);
296       if (width <= maxWidth) {
297         i = mid;
298       } else {
299         j = mid;
300       }
301     }
302
303     return text.substring(0, i) + ELLIPSIS;
304   }
305
306   private static Color getTypeTextColor(LookupElement item, Color foreground, LookupElementPresentation presentation, boolean selected, boolean nonFocusedSelection) {
307     if (nonFocusedSelection) {
308       return foreground;
309     }
310
311     return presentation.isTypeGrayed() ? getGrayedForeground(selected) : item instanceof EmptyLookupItem ? JBColor.foreground() : foreground;
312   }
313
314   private static Color getTailTextColor(boolean isSelected, LookupElementPresentation.TextFragment fragment, Color defaultForeground, boolean nonFocusedSelection) {
315     if (nonFocusedSelection) {
316       return defaultForeground;
317     }
318
319     if (fragment.isGrayed()) {
320       return getGrayedForeground(isSelected);
321     }
322
323     if (!isSelected) {
324       final Color tailForeground = fragment.getForegroundColor();
325       if (tailForeground != null) {
326         return tailForeground;
327       }
328     }
329
330     return defaultForeground;
331   }
332
333   public static Color getGrayedForeground(boolean isSelected) {
334     return isSelected ? SELECTED_GRAYED_FOREGROUND_COLOR : GRAYED_FOREGROUND_COLOR;
335   }
336
337   private int setItemTextLabel(LookupElement item, final Color foreground, final boolean selected, LookupElementPresentation presentation, int allowedWidth) {
338     boolean bold = presentation.isItemTextBold();
339
340     Font customItemFont = myLookup.getCustomFont(item, bold);
341     myNameComponent.setFont(customItemFont != null ? customItemFont : bold ? myBoldFont : myNormalFont);
342     int style = getStyle(bold, presentation.isStrikeout(), presentation.isItemTextUnderlined());
343
344     final FontMetrics metrics = getRealFontMetrics(item, bold);
345     final String name = trimLabelText(presentation.getItemText(), allowedWidth, metrics);
346     int used = RealLookupElementPresentation.getStringWidth(name, metrics);
347
348     renderItemName(item, foreground, selected, style, name, myNameComponent);
349     return used;
350   }
351
352   private FontMetrics getRealFontMetrics(LookupElement item, boolean bold) {
353     Font customFont = myLookup.getCustomFont(item, bold);
354     if (customFont != null) {
355       return myLookup.getTopLevelEditor().getComponent().getFontMetrics(customFont);
356     }
357
358     return bold ? myBoldMetrics : myNormalMetrics;
359   }
360
361   @SimpleTextAttributes.StyleAttributeConstant
362   private static int getStyle(boolean bold, boolean strikeout, boolean underlined) {
363     int style = bold ? SimpleTextAttributes.STYLE_BOLD : SimpleTextAttributes.STYLE_PLAIN;
364     if (strikeout) {
365       style |= SimpleTextAttributes.STYLE_STRIKEOUT;
366     }
367     if (underlined) {
368       style |= SimpleTextAttributes.STYLE_UNDERLINE;
369     }
370     return style;
371   }
372
373   private void renderItemName(LookupElement item,
374                       Color foreground,
375                       boolean selected,
376                       @SimpleTextAttributes.StyleAttributeConstant int style,
377                       String name,
378                       final SimpleColoredComponent nameComponent) {
379     final SimpleTextAttributes base = new SimpleTextAttributes(style, foreground);
380
381     final String prefix = item instanceof EmptyLookupItem ? "" : myLookup.itemPattern(item);
382     if (prefix.length() > 0) {
383       Iterable<TextRange> ranges = getMatchingFragments(prefix, name);
384       if (ranges != null) {
385         SimpleTextAttributes highlighted =
386           new SimpleTextAttributes(style, selected ? SELECTED_PREFIX_FOREGROUND_COLOR : PREFIX_FOREGROUND_COLOR);
387         SpeedSearchUtil.appendColoredFragments(nameComponent, name, ranges, base, highlighted);
388         return;
389       }
390     }
391     nameComponent.append(name, base);
392   }
393
394   public static FList<TextRange> getMatchingFragments(String prefix, String name) {
395     return new MinusculeMatcher("*" + prefix, NameUtil.MatchingCaseSensitivity.NONE).matchingFragments(name);
396   }
397
398   private int setTypeTextLabel(LookupElement item,
399                                final Color background,
400                                Color foreground,
401                                final LookupElementPresentation presentation,
402                                int allowedWidth,
403                                boolean selected, boolean nonFocusedSelection, FontMetrics normalMetrics) {
404     final String givenText = presentation.getTypeText();
405     final String labelText = trimLabelText(StringUtil.isEmpty(givenText) ? "" : " " + givenText, allowedWidth, normalMetrics);
406
407     int used = RealLookupElementPresentation.getStringWidth(labelText, normalMetrics);
408
409     final Icon icon = presentation.getTypeIcon();
410     if (icon != null) {
411       myTypeLabel.setIcon(icon);
412       used += icon.getIconWidth();
413     }
414
415     Color sampleBackground = background;
416
417     Object o = item.isValid() ? item.getObject() : null;
418     //noinspection deprecation
419     if (o instanceof LookupValueWithUIHint && StringUtil.isEmpty(labelText)) {
420       //noinspection deprecation
421       Color proposedBackground = ((LookupValueWithUIHint)o).getColorHint();
422       if (proposedBackground != null) {
423         sampleBackground = proposedBackground;
424       }
425       myTypeLabel.append("  ");
426       used += normalMetrics.stringWidth("WW");
427     } else {
428       myTypeLabel.append(labelText);
429     }
430
431     myTypeLabel.setBackground(sampleBackground);
432     myTypeLabel.setForeground(getTypeTextColor(item, foreground, presentation, selected, nonFocusedSelection));
433     return used;
434   }
435
436   public static Icon augmentIcon(@Nullable Editor editor, @Nullable Icon icon, @NotNull Icon standard) {
437     if (Registry.is("editor.scale.completion.icons")) {
438       standard = EditorUtil.scaleIconAccordingEditorFont(standard, editor);
439       icon = EditorUtil.scaleIconAccordingEditorFont(icon, editor);
440     }
441     if (icon == null) {
442       return standard;
443     }
444
445     if (icon.getIconHeight() < standard.getIconHeight() || icon.getIconWidth() < standard.getIconWidth()) {
446       final LayeredIcon layeredIcon = new LayeredIcon(2);
447       layeredIcon.setIcon(icon, 0, 0, (standard.getIconHeight() - icon.getIconHeight()) / 2);
448       layeredIcon.setIcon(standard, 1);
449       return layeredIcon;
450     }
451
452     return icon;
453   }
454
455   @Nullable
456   Font getFontAbleToDisplay(LookupElementPresentation p) {
457     String sampleString = p.getItemText() + p.getTailText() + p.getTypeText();
458
459     // assume a single font can display all lookup item chars
460     Set<Font> fonts = ContainerUtil.newHashSet();
461     for (int i = 0; i < sampleString.length(); i++) {
462       fonts.add(EditorUtil.fontForChar(sampleString.charAt(i), Font.PLAIN, myLookup.getTopLevelEditor()).getFont());
463     }
464
465     eachFont: for (Font font : fonts) {
466       if (font.equals(myNormalFont)) continue;
467       
468       for (int i = 0; i < sampleString.length(); i++) {
469         if (!font.canDisplay(sampleString.charAt(i))) {
470           continue eachFont;
471         }
472       }
473       return font;
474     }
475     return null;
476   }
477
478
479   int updateMaximumWidth(final LookupElementPresentation p, LookupElement item) {
480     final Icon icon = p.getIcon();
481     if (icon != null && (icon.getIconWidth() > myEmptyIcon.getIconWidth() || icon.getIconHeight() > myEmptyIcon.getIconHeight())) {
482       myEmptyIcon = new EmptyIcon(Math.max(icon.getIconWidth(), myEmptyIcon.getIconWidth()), Math.max(icon.getIconHeight(), myEmptyIcon.getIconHeight()));
483     }
484
485     return RealLookupElementPresentation.calculateWidth(p, getRealFontMetrics(item, false), getRealFontMetrics(item, true)) +
486            calcSpacing(myTailComponent, null) + calcSpacing(myTypeLabel, null);
487   }
488
489   public int getTextIndent() {
490     return myNameComponent.getIpad().left + myEmptyIcon.getIconWidth() + myNameComponent.getIconTextGap();
491   }
492
493   private static class MySimpleColoredComponent extends SimpleColoredComponent {
494     private MySimpleColoredComponent() {
495       setFocusBorderAroundIcon(true);
496     }
497
498     @Override
499     protected void applyAdditionalHints(@NotNull Graphics2D g) {
500       super.applyAdditionalHints(g);
501     }
502   }
503
504   private class LookupPanel extends JPanel {
505     boolean myUpdateExtender;
506     public LookupPanel() {
507       super(new BorderLayout());
508     }
509
510     public void setUpdateExtender(boolean updateExtender) {
511       myUpdateExtender = updateExtender;
512     }
513
514     @Override
515     public void paint(Graphics g){
516       super.paint(g);
517       if (!myLookup.isFocused() && myLookup.isCompletion()) {
518         g = g.create();
519         try {
520           g.setColor(ColorUtil.withAlpha(BACKGROUND_COLOR, .4));
521           g.fillRect(0, 0, getWidth(), getHeight());
522         }
523         finally {
524           g.dispose();
525         }
526       }
527     }
528   }
529 }