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