replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-api / src / com / intellij / ui / SimpleColoredComponent.java
1 /*
2  * Copyright 2000-2017 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 package com.intellij.ui;
17
18 import com.intellij.ide.BrowserUtil;
19 import com.intellij.ide.ui.UISettings;
20 import com.intellij.openapi.application.Application;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.util.Comparing;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.ui.paint.EffectPainter;
27 import com.intellij.util.ui.JBInsets;
28 import com.intellij.util.ui.JBUI;
29 import com.intellij.util.ui.UIUtil;
30 import gnu.trove.TIntIntHashMap;
31 import org.intellij.lang.annotations.JdkConstants;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import javax.accessibility.Accessible;
36 import javax.accessibility.AccessibleContext;
37 import javax.accessibility.AccessibleRole;
38 import javax.swing.*;
39 import javax.swing.border.Border;
40 import javax.swing.tree.TreeCellRenderer;
41 import java.awt.*;
42 import java.awt.font.FontRenderContext;
43 import java.awt.font.TextAttribute;
44 import java.awt.font.TextLayout;
45 import java.text.AttributedCharacterIterator;
46 import java.text.AttributedString;
47 import java.text.CharacterIterator;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.Iterator;
51 import java.util.List;
52
53 /**
54  * This is high performance Swing component which represents an icon
55  * with a colored text. The text consists of fragments. Each
56  * text fragment has its own color (foreground) and font style.
57  *
58  * @author Vladimir Kondratyev
59  */
60 @SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext", "FieldAccessedSynchronizedAndUnsynchronized"})
61 public class SimpleColoredComponent extends JComponent implements Accessible, ColoredTextContainer {
62   private static final Logger LOG = Logger.getInstance("#com.intellij.ui.SimpleColoredComponent");
63
64   public static final Color SHADOW_COLOR = new JBColor(new Color(250, 250, 250, 140), Gray._0.withAlpha(50));
65   @SuppressWarnings("unused") public static final Color STYLE_SEARCH_MATCH_BACKGROUND = SHADOW_COLOR; //api compatibility
66   public static final int FRAGMENT_ICON = -2;
67
68   private final List<String> myFragments;
69   private final List<TextLayout> myLayouts;
70   private Font myLayoutFont;
71   private final List<SimpleTextAttributes> myAttributes;
72   private List<Object> myFragmentTags = null;
73   private TIntIntHashMap myFragmentAlignment;
74
75   /**
76    * Component's icon. It can be {@code null}.
77    */
78   private Icon myIcon;
79   /**
80    * Internal padding
81    */
82   private Insets myIpad;
83   /**
84    * Gap between icon and text. It is used only if icon is defined.
85    */
86   protected int myIconTextGap;
87   /**
88    * Defines whether the focus border around the text is painted or not.
89    * For example, text can have a border if the component represents a selected item
90    * in focused JList.
91    */
92   private boolean myPaintFocusBorder;
93   /**
94    * Defines whether the focus border around the text extends to icon or not
95    */
96   private boolean myFocusBorderAroundIcon;
97   /**
98    * This is the border around the text. For example, text can have a border
99    * if the component represents a selected item in a focused JList.
100    * Border can be {@code null}.
101    */
102   private Border myBorder;
103
104   private int myMainTextLastIndex = -1;
105
106   private final TIntIntHashMap myFragmentPadding;
107
108   @JdkConstants.HorizontalAlignment private int myTextAlign = SwingConstants.LEFT;
109
110   private boolean myIconOpaque = false;
111
112   private boolean myAutoInvalidate = !(this instanceof TreeCellRenderer);
113
114   private boolean myIconOnTheRight = false;
115   private boolean myTransparentIconBackground;
116
117   public SimpleColoredComponent() {
118     myFragments = new ArrayList<>(3);
119     myLayouts = new ArrayList<>(3);
120     myAttributes = new ArrayList<>(3);
121     myIpad = new JBInsets(1, 2, 1, 2);
122     myIconTextGap = JBUI.scale(2);
123     myBorder = new MyBorder();
124     myFragmentPadding = new TIntIntHashMap(10);
125     myFragmentAlignment = new TIntIntHashMap(10);
126     setOpaque(true);
127     updateUI();
128   }
129
130   @Override
131   public void updateUI() {
132     UISettings.setupComponentAntialiasing(this);
133   }
134
135   @NotNull
136   public ColoredIterator iterator() {
137     return new MyIterator();
138   }
139
140   @SuppressWarnings("unused")
141   public boolean isIconOnTheRight() {
142     return myIconOnTheRight;
143   }
144
145   public void setIconOnTheRight(boolean iconOnTheRight) {
146     myIconOnTheRight = iconOnTheRight;
147   }
148
149   @NotNull
150   public final SimpleColoredComponent append(@NotNull String fragment) {
151     append(fragment, SimpleTextAttributes.REGULAR_ATTRIBUTES);
152     return this;
153   }
154
155   /**
156    * Appends string fragments to existing ones. Appended string
157    * will have specified {@code attributes}.
158    *
159    * @param fragment   text fragment
160    * @param attributes text attributes
161    */
162   @Override
163   public final void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes) {
164     append(fragment, attributes, myMainTextLastIndex < 0);
165   }
166
167   /**
168    * Appends text fragment and sets it's end offset and alignment.
169    * See SimpleColoredComponent#appendTextPadding for details
170    * @param fragment text fragment
171    * @param attributes text attributes
172    * @param padding end offset of the text
173    * @param align alignment between current offset and padding
174    */
175   public final void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, int padding, @JdkConstants.HorizontalAlignment int align) {
176     append(fragment, attributes, myMainTextLastIndex < 0);
177     appendTextPadding(padding, align);
178   }
179
180   /**
181    * Appends string fragments to existing ones. Appended string
182    * will have specified {@code attributes}.
183    *
184    * @param fragment   text fragment
185    * @param attributes text attributes
186    * @param isMainText main text of not
187    */
188   public void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, boolean isMainText) {
189     _append(fragment, attributes, isMainText);
190     revalidateAndRepaint();
191   }
192
193   private synchronized void _append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, boolean isMainText) {
194     myFragments.add(fragment);
195     myAttributes.add(attributes);
196     if (isMainText) {
197       myMainTextLastIndex = myFragments.size() - 1;
198     }
199   }
200
201   void revalidateAndRepaint() {
202     if (myAutoInvalidate) {
203       revalidate();
204     }
205
206     repaint();
207   }
208
209   @Override
210   public void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, Object tag) {
211     _append(fragment, attributes, tag);
212     revalidateAndRepaint();
213   }
214
215   private synchronized void _append(String fragment, SimpleTextAttributes attributes, Object tag) {
216     append(fragment, attributes);
217     if (myFragmentTags == null) {
218       myFragmentTags = new ArrayList<>();
219     }
220     while (myFragmentTags.size() < myFragments.size() - 1) {
221       myFragmentTags.add(null);
222     }
223     myFragmentTags.add(tag);
224   }
225
226   /**
227    * fragment width isn't a right name, it is actually a padding
228    * @deprecated remove in IDEA 16
229    */
230   @Deprecated
231   public synchronized void appendFixedTextFragmentWidth(int width) {
232     appendTextPadding(width);
233   }
234
235   public synchronized void appendTextPadding(int padding) {
236     appendTextPadding(padding, SwingConstants.LEFT);
237   }
238
239   /**
240    * @param padding end offset that will be set after drawing current text fragment
241    * @param align alignment of the current text fragment, if it is SwingConstants.RIGHT
242    *              or SwingConstants.TRAILING then the text fragment will be aligned to the right at
243    *              the padding, otherwise it will be aligned to the left
244    */
245   public synchronized void appendTextPadding(int padding, @JdkConstants.HorizontalAlignment int align) {
246     final int alignIndex = myFragments.size() - 1;
247     myFragmentPadding.put(alignIndex, padding);
248     myFragmentAlignment.put(alignIndex, align);
249   }
250
251   public void setTextAlign(@JdkConstants.HorizontalAlignment int align) {
252     myTextAlign = align;
253   }
254
255   /**
256    * Clear all special attributes of {@code SimpleColoredComponent}.
257    * They are icon, text fragments and their attributes, "paint focus border".
258    */
259   public void clear() {
260     _clear();
261     revalidateAndRepaint();
262   }
263
264   private synchronized void _clear() {
265     myIcon = null;
266     myPaintFocusBorder = false;
267     myFragments.clear();
268     myLayouts.clear();
269     myAttributes.clear();
270     myFragmentTags = null;
271     myMainTextLastIndex = -1;
272     myFragmentPadding.clear();
273   }
274
275   /**
276    * @return component's icon. This method returns {@code null}
277    * if there is no icon.
278    */
279   public final Icon getIcon() {
280     return myIcon;
281   }
282
283   /**
284    * Sets a new component icon
285    *
286    * @param icon icon
287    */
288   @Override
289   public final void setIcon(@Nullable final Icon icon) {
290     myIcon = icon;
291     revalidateAndRepaint();
292   }
293
294   /**
295    * @return "leave" (internal) internal paddings of the component
296    */
297   @NotNull
298   public Insets getIpad() {
299     return myIpad;
300   }
301
302   /**
303    * Sets specified internal paddings
304    *
305    * @param ipad insets
306    */
307   public void setIpad(@NotNull Insets ipad) {
308     myIpad = ipad;
309
310     revalidateAndRepaint();
311   }
312
313   /**
314    * @return gap between icon and text
315    */
316   public int getIconTextGap() {
317     return myIconTextGap;
318   }
319
320   /**
321    * Sets a new gap between icon and text
322    *
323    * @param iconTextGap the gap between text and icon
324    * @throws IllegalArgumentException if the {@code iconTextGap}
325    *                                            has a negative value
326    */
327   public void setIconTextGap(final int iconTextGap) {
328     if (iconTextGap < 0) {
329       throw new IllegalArgumentException("wrong iconTextGap: " + iconTextGap);
330     }
331     myIconTextGap = iconTextGap;
332
333     revalidateAndRepaint();
334   }
335
336   public Border getMyBorder() {
337     return myBorder;
338   }
339
340   public void setMyBorder(@Nullable Border border) {
341     myBorder = border;
342   }
343
344   /**
345    * Sets whether focus border is painted or not
346    *
347    * @param paintFocusBorder {@code true} or {@code false}
348    */
349   protected final void setPaintFocusBorder(final boolean paintFocusBorder) {
350     myPaintFocusBorder = paintFocusBorder;
351
352     repaint();
353   }
354
355   /**
356    * Sets whether focus border extends to icon or not. If so then
357    * component also extends the selection.
358    *
359    * @param focusBorderAroundIcon {@code true} or {@code false}
360    */
361   protected final void setFocusBorderAroundIcon(final boolean focusBorderAroundIcon) {
362     myFocusBorderAroundIcon = focusBorderAroundIcon;
363
364     repaint();
365   }
366
367   public boolean isIconOpaque() {
368     return myIconOpaque;
369   }
370
371   public void setIconOpaque(final boolean iconOpaque) {
372     myIconOpaque = iconOpaque;
373
374     repaint();
375   }
376
377   @Override
378   @NotNull
379   public Dimension getPreferredSize() {
380     return computePreferredSize(false);
381   }
382
383   @Override
384   @NotNull
385   public Dimension getMinimumSize() {
386     return computePreferredSize(false);
387   }
388
389   @Nullable
390   public synchronized Object getFragmentTag(int index) {
391     if (myFragmentTags != null && index < myFragmentTags.size()) {
392       return myFragmentTags.get(index);
393     }
394     return null;
395   }
396
397   @NotNull
398   public final synchronized Dimension computePreferredSize(final boolean mainTextOnly) {
399     // Calculate width
400     int width = myIpad.left;
401
402     if (myIcon != null) {
403       width += myIcon.getIconWidth() + myIconTextGap;
404     }
405
406     final Insets borderInsets = myBorder != null ? myBorder.getBorderInsets(this) : JBUI.emptyInsets();
407     width += borderInsets.left;
408
409     Font font = getBaseFont();
410
411     width += computeTextWidth(font, mainTextOnly);
412     width += myIpad.right + borderInsets.right;
413
414     // Take into account that the component itself can have a border
415     final Insets insets = getInsets();
416     if (insets != null) {
417       width += insets.left + insets.right;
418     }
419
420     int height = computePreferredHeight();
421
422     return new Dimension(width, height);
423   }
424
425   public final synchronized int computePreferredHeight() {
426     int height = myIpad.top + myIpad.bottom;
427
428     Font font = getBaseFont();
429
430     final FontMetrics metrics = getFontMetrics(font);
431     int textHeight = Math.max(JBUI.scale(16), metrics.getHeight()); //avoid too narrow rows
432     
433     Insets borderInsets = myBorder != null ? myBorder.getBorderInsets(this) : JBUI.emptyInsets();
434     textHeight += borderInsets.top + borderInsets.bottom;
435
436     if (myIcon != null) {
437       height += Math.max(myIcon.getIconHeight(), textHeight);
438     }
439     else {
440       height += textHeight;
441     }
442
443     // Take into account that the component itself can have a border
444     final Insets insets = getInsets();
445     if (insets != null) {
446       height += insets.top + insets.bottom;
447     }
448
449     return height;
450   }
451
452   private Rectangle computePaintArea() {
453     Rectangle area = new Rectangle(getWidth(), getHeight());
454     JBInsets.removeFrom(area, getInsets());
455     JBInsets.removeFrom(area, myIpad);
456     return area;
457   }
458
459   private float computeTextWidth(@NotNull Font font, final boolean mainTextOnly) {
460     float result = 0;
461     int baseSize = font.getSize();
462     boolean wasSmaller = false;
463     for (int i = 0; i < myAttributes.size(); i++) {
464       SimpleTextAttributes attributes = myAttributes.get(i);
465       boolean isSmaller = attributes.isSmaller();
466       if (font.getStyle() != attributes.getFontStyle() || isSmaller != wasSmaller) { // derive font only if it is necessary
467         font = font.deriveFont(attributes.getFontStyle(), isSmaller ? UIUtil.getFontSize(UIUtil.FontSize.SMALL) : baseSize);
468       }
469       wasSmaller = isSmaller;
470
471       result += computeStringWidth(i, font);
472
473       final int fixedWidth = myFragmentPadding.get(i);
474       if (fixedWidth > 0 && result < fixedWidth) {
475         result = fixedWidth;
476       }
477       if (mainTextOnly && myMainTextLastIndex >= 0 && i == myMainTextLastIndex) break;
478     }
479     return result;
480   }
481
482   @NotNull
483   private Font getBaseFont() {
484     Font font = getFont();
485     if (font == null) font = UIUtil.getLabelFont();
486     return font;
487   }
488
489   private TextLayout getTextLayout(int fragmentIndex, Font font, FontRenderContext frc) {
490     if (getBaseFont() != myLayoutFont) myLayouts.clear();
491     TextLayout layout = fragmentIndex < myLayouts.size() ? myLayouts.get(fragmentIndex) : null;
492     if (layout == null && needFontFallback(font, myFragments.get(fragmentIndex))) {
493       layout = createAndCacheTextLayout(fragmentIndex, font, frc);
494     }
495     return layout;
496   }
497
498   private void doDrawString(Graphics2D g, int fragmentIndex, float x, float y) {
499     String text = myFragments.get(fragmentIndex);
500     if (StringUtil.isEmpty(text)) return;
501     TextLayout layout = getTextLayout(fragmentIndex, g.getFont(), g.getFontRenderContext());
502     if (layout != null) {
503       layout.draw(g, x, y);
504     }
505     else {
506       g.drawString(text, x, y);
507     }
508   }
509
510   private float computeStringWidth(int fragmentIndex, Font font) {
511     String text = myFragments.get(fragmentIndex);
512     if (StringUtil.isEmpty(text)) return 0;
513     FontRenderContext fontRenderContext = getFontMetrics(font).getFontRenderContext();
514     TextLayout layout = getTextLayout(fragmentIndex, font, fontRenderContext);
515     if (layout != null) {
516       return layout.getAdvance();
517     }
518     else {
519       return (float)font.getStringBounds(text, fontRenderContext).getWidth();
520     }
521   }
522
523   private TextLayout createAndCacheTextLayout(int fragmentIndex, Font basefont, FontRenderContext fontRenderContext) {
524     String text = myFragments.get(fragmentIndex);
525     AttributedString string = new AttributedString(text);
526     int start = 0;
527     int end = text.length();
528     AttributedCharacterIterator it = string.getIterator(new AttributedCharacterIterator.Attribute[0], start, end);
529     Font currentFont = basefont;
530     int currentIndex = start;
531     for(char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
532       Font font = basefont;
533       if (!font.canDisplay(c)) {
534         for (SuitableFontProvider provider : SuitableFontProvider.EP_NAME.getExtensions()) {
535           font = provider.getFontAbleToDisplay(c, basefont.getSize(), basefont.getStyle(), basefont.getFamily());
536           if (font != null) break;
537         }
538       }
539       int i = it.getIndex();
540       if (!Comparing.equal(currentFont, font)) {
541         if (i > currentIndex) {
542           string.addAttribute(TextAttribute.FONT, currentFont, currentIndex, i);
543         }
544         currentFont = font;
545         currentIndex = i;
546       }
547     }
548     if (currentIndex < end) {
549       string.addAttribute(TextAttribute.FONT, currentFont, currentIndex, end);
550     }
551     TextLayout layout = new TextLayout(string.getIterator(), fontRenderContext);
552     if (fragmentIndex >= myLayouts.size()) {
553       myLayouts.addAll(Collections.nCopies(fragmentIndex - myLayouts.size() + 1, null));
554     }
555     myLayouts.set(fragmentIndex, layout);
556     myLayoutFont = getBaseFont();
557     return layout;
558   }
559
560   private static boolean needFontFallback(Font font, String text) {
561     return font.canDisplayUpTo(text) != -1
562            && text.indexOf(CharacterIterator.DONE) == -1; // see IDEA-137517, TextLayout does not support this character
563   }
564
565   /**
566    * Returns the index of text fragment at the specified X offset.
567    *
568    * @param x the offset
569    * @return the index of the fragment, {@link #FRAGMENT_ICON} if the icon is at the offset, or -1 if nothing is there.
570    */
571   public int findFragmentAt(int x) {
572     float curX = myIpad.left;
573     if (myIcon != null && !myIconOnTheRight) {
574       final int iconRight = myIcon.getIconWidth() + myIconTextGap;
575       if (x < iconRight) {
576         return FRAGMENT_ICON;
577       }
578       curX += iconRight;
579     }
580
581     Font font = getBaseFont();
582
583     int baseSize = font.getSize();
584     boolean wasSmaller = false;
585     for (int i = 0; i < myAttributes.size(); i++) {
586       SimpleTextAttributes attributes = myAttributes.get(i);
587       boolean isSmaller = attributes.isSmaller();
588       if (font.getStyle() != attributes.getFontStyle() || isSmaller != wasSmaller) { // derive font only if it is necessary
589         font = font.deriveFont(attributes.getFontStyle(), isSmaller ? UIUtil.getFontSize(UIUtil.FontSize.SMALL) : baseSize);
590       }
591       wasSmaller = isSmaller;
592
593       final float curWidth = computeStringWidth(i, font);
594       if (x >= curX && x < curX + curWidth) {
595         return i;
596       }
597       curX += curWidth;
598       final int fragmentPadding = myFragmentPadding.get(i);
599       if (fragmentPadding > 0 && curX < fragmentPadding) {
600         curX = fragmentPadding;
601       }
602     }
603
604     if (myIcon != null && myIconOnTheRight) {
605       curX += myIconTextGap;
606       if (x >= curX && x < curX + myIcon.getIconWidth()) {
607         return FRAGMENT_ICON;
608       }
609     }
610     return -1;
611   }
612
613   @Nullable
614   public Object getFragmentTagAt(int x) {
615     int index = findFragmentAt(x);
616     return index < 0 ? null : getFragmentTag(index);
617   }
618
619   @NotNull
620   protected JLabel formatToLabel(@NotNull JLabel label) {
621     label.setIcon(myIcon);
622
623     if (!myFragments.isEmpty()) {
624       final StringBuilder text = new StringBuilder();
625       text.append("<html><body style=\"white-space:nowrap\">");
626
627       for (int i = 0; i < myFragments.size(); i++) {
628         final String fragment = myFragments.get(i);
629         final SimpleTextAttributes attributes = myAttributes.get(i);
630         final Object tag = getFragmentTag(i);
631         if (tag instanceof BrowserLauncherTag) {
632           formatLink(text, fragment, attributes, ((BrowserLauncherTag)tag).myUrl);
633         }
634         else {
635           formatText(text, fragment, attributes);
636         }
637       }
638
639       text.append("</body></html>");
640       label.setText(text.toString());
641     }
642
643     return label;
644   }
645
646   static void formatText(@NotNull StringBuilder builder, @NotNull String fragment, @NotNull SimpleTextAttributes attributes) {
647     if (!fragment.isEmpty()) {
648       builder.append("<span");
649       formatStyle(builder, attributes);
650       builder.append('>').append(convertFragment(fragment)).append("</span>");
651     }
652   }
653
654   static void formatLink(@NotNull StringBuilder builder,
655                          @NotNull String fragment,
656                          @NotNull SimpleTextAttributes attributes,
657                          @NotNull String url) {
658     if (!fragment.isEmpty()) {
659       builder.append("<a href=\"").append(StringUtil.replace(url, "\"", "%22")).append("\"");
660       formatStyle(builder, attributes);
661       builder.append('>').append(convertFragment(fragment)).append("</a>");
662     }
663   }
664
665   private static String convertFragment(String fragment) {
666     return StringUtil.escapeXml(fragment).replaceAll("\\\\n", "<br>");
667   }
668
669   private static void formatStyle(final StringBuilder builder, final SimpleTextAttributes attributes) {
670     final Color fgColor = attributes.getFgColor();
671     final Color bgColor = attributes.getBgColor();
672     final int style = attributes.getStyle();
673
674     final int pos = builder.length();
675     if (fgColor != null) {
676       builder.append("color:#").append(Integer.toString(fgColor.getRGB() & 0xFFFFFF, 16)).append(';');
677     }
678     if (bgColor != null) {
679       builder.append("background-color:#").append(Integer.toString(bgColor.getRGB() & 0xFFFFFF, 16)).append(';');
680     }
681     if ((style & SimpleTextAttributes.STYLE_BOLD) != 0) {
682       builder.append("font-weight:bold;");
683     }
684     if ((style & SimpleTextAttributes.STYLE_ITALIC) != 0) {
685       builder.append("font-style:italic;");
686     }
687     if ((style & SimpleTextAttributes.STYLE_UNDERLINE) != 0) {
688       builder.append("text-decoration:underline;");
689     }
690     else if ((style & SimpleTextAttributes.STYLE_STRIKEOUT) != 0) {
691       builder.append("text-decoration:line-through;");
692     }
693     if (builder.length() > pos) {
694       builder.insert(pos, " style=\"");
695       builder.append('"');
696     }
697   }
698
699   @Override
700   protected void paintComponent(final Graphics g) {
701     try {
702       _doPaint(g);
703     }
704     catch (RuntimeException e) {
705       LOG.error(logSwingPath(), e);
706       throw e;
707     }
708   }
709
710   private synchronized void _doPaint(final Graphics g) {
711     checkCanPaint(g);
712     doPaint((Graphics2D)g);
713   }
714
715   protected void doPaint(final Graphics2D g) {
716     int offset = 0;
717     final Icon icon = myIcon; // guard against concurrent modification (IDEADEV-12635)
718     if (icon != null && !myIconOnTheRight) {
719       doPaintIcon(g, icon, 0);
720       offset += myIpad.left + icon.getIconWidth() + myIconTextGap;
721     }
722
723     doPaintTextBackground(g, offset);
724     offset = doPaintText(g, offset, myFocusBorderAroundIcon || icon == null);
725     if (icon != null && myIconOnTheRight) {
726       doPaintIcon(g, icon, offset);
727     }
728   }
729
730   private void doPaintTextBackground(Graphics2D g, int offset) {
731     if (isOpaque() || shouldDrawBackground()) {
732       paintBackground(g, offset, getWidth() - offset, getHeight());
733     }
734   }
735
736   protected void paintBackground(Graphics2D g, int x, int width, int height) {
737     g.setColor(getBackground());
738     g.fillRect(x, 0, width, height);
739   }
740
741   protected void doPaintIcon(@NotNull Graphics2D g, @NotNull Icon icon, int offset) {
742     final Container parent = getParent();
743     Color iconBackgroundColor = null;
744     if ((isOpaque() || isIconOpaque()) && !isTransparentIconBackground()) {
745       if (parent != null && !myFocusBorderAroundIcon && !UIUtil.isFullRowSelectionLAF()) {
746         iconBackgroundColor = parent.getBackground();
747       }
748       else {
749         iconBackgroundColor = getBackground();
750       }
751     }
752
753     if (iconBackgroundColor != null) {
754       g.setColor(iconBackgroundColor);
755       g.fillRect(offset, 0, icon.getIconWidth() + myIpad.left + myIconTextGap, getHeight());
756     }
757
758     paintIcon(g, icon, offset + myIpad.left);
759   }
760
761   protected int doPaintText(Graphics2D g, int textStart, boolean focusAroundIcon) {
762     // If there is no icon, then we have to add left internal padding
763     if (textStart == 0) {
764       textStart = myIpad.left;
765     }
766
767     float offset = textStart;
768     if (myBorder != null) {
769       offset += myBorder.getBorderInsets(this).left;
770     }
771
772     final List<Object[]> searchMatches = new ArrayList<>();
773
774     applyAdditionalHints(g);
775     final Font baseFont = getBaseFont();
776     g.setFont(baseFont);
777     offset += computeTextAlignShift();
778     int baseSize = baseFont.getSize();
779     FontMetrics baseMetrics = g.getFontMetrics();
780     Rectangle area = computePaintArea();
781     final int textBaseline = area.y + getTextBaseLine(baseMetrics, area.height);
782     boolean wasSmaller = false;
783     for (int i = 0; i < myFragments.size(); i++) {
784       final SimpleTextAttributes attributes = myAttributes.get(i);
785
786       Font font = g.getFont();
787       boolean isSmaller = attributes.isSmaller();
788       if (font.getStyle() != attributes.getFontStyle() || isSmaller != wasSmaller) { // derive font only if it is necessary
789         font = font.deriveFont(attributes.getFontStyle(), isSmaller ? UIUtil.getFontSize(UIUtil.FontSize.SMALL) : baseSize);
790       }
791       wasSmaller = isSmaller;
792
793       g.setFont(font);
794       final FontMetrics metrics = g.getFontMetrics(font);
795
796       final float fragmentWidth = computeStringWidth(i, font);
797
798       final int fragmentPadding = myFragmentPadding.get(i);
799
800       final Color bgColor = attributes.isSearchMatch() ? null : attributes.getBgColor();
801       if ((attributes.isOpaque() || isOpaque()) && bgColor != null) {
802         g.setColor(bgColor);
803         g.fillRect((int)offset, 0, (int)fragmentWidth, getHeight());
804       }
805
806       Color color = attributes.getFgColor();
807       if (color == null) { // in case if color is not defined we have to get foreground color from Swing hierarchy
808         color = getForeground();
809       }
810       if (!isEnabled()) {
811         color = UIUtil.getInactiveTextColor();
812       }
813       g.setColor(color);
814
815       final int fragmentAlignment = myFragmentAlignment.get(i);
816
817       final float endOffset;
818       if (fragmentPadding > 0 &&
819           fragmentPadding > fragmentWidth) {
820         endOffset = fragmentPadding;
821         if (fragmentAlignment == SwingConstants.RIGHT || fragmentAlignment == SwingConstants.TRAILING) {
822           offset = fragmentPadding - fragmentWidth;
823         }
824       }
825       else {
826         endOffset = offset + fragmentWidth;
827       }
828
829       if (!attributes.isSearchMatch()) {
830         if (shouldDrawMacShadow()) {
831           g.setColor(SHADOW_COLOR);
832           doDrawString(g, i, offset, textBaseline + 1);
833         }
834
835         if (shouldDrawDimmed()) {
836           color = ColorUtil.dimmer(color);
837         }
838
839         g.setColor(color);
840         doDrawString(g, i, offset, textBaseline);
841       }
842
843       // for some reason strokeState here may be incorrect, resetting the stroke helps
844       g.setStroke(g.getStroke());
845
846       // 1. Strikeout effect
847       if (attributes.isStrikeout() && !attributes.isSearchMatch()) {
848         EffectPainter.STRIKE_THROUGH.paint(g, (int)offset, textBaseline, (int)fragmentWidth, getCharHeight(g), font);
849       }
850       // 2. Waved effect
851       if (attributes.isWaved()) {
852         if (attributes.getWaveColor() != null) {
853           g.setColor(attributes.getWaveColor());
854         }
855         EffectPainter.WAVE_UNDERSCORE.paint(g, (int)offset, textBaseline + 1, (int)fragmentWidth, Math.max(2, metrics.getDescent()), font);
856       }
857       // 3. Underline
858       if (attributes.isUnderline()) {
859         EffectPainter.LINE_UNDERSCORE.paint(g, (int)offset, textBaseline, (int)fragmentWidth, metrics.getDescent(), font);
860       }
861       // 4. Bold Dotted Line
862       if (attributes.isBoldDottedLine()) {
863         final int dottedAt = SystemInfo.isMac ? textBaseline : textBaseline + 1;
864         final Color lineColor = attributes.getWaveColor();
865         UIUtil.drawBoldDottedLine(g, (int)offset, (int)(offset + fragmentWidth), dottedAt, bgColor, lineColor, isOpaque());
866       }
867
868       if (attributes.isSearchMatch()) {
869         searchMatches.add(new Object[]{offset, offset + fragmentWidth, (float)textBaseline, myFragments.get(i), g.getFont(), attributes});
870       }
871
872       offset = endOffset;
873     }
874
875     // Paint focus border around the text and icon (if necessary)
876     if (myPaintFocusBorder && myBorder != null) {
877       if (focusAroundIcon) {
878         myBorder.paintBorder(this, g, 0, 0, getWidth(), getHeight());
879       }
880       else {
881         myBorder.paintBorder(this, g, textStart, 0, getWidth() - textStart, getHeight());
882       }
883     }
884
885     // draw search matches after all
886     for (final Object[] info : searchMatches) {
887       float x1 = (float)info[0];
888       float x2 = (float)info[1];
889       UIUtil.drawSearchMatch(g, x1, x2, getHeight());
890       g.setFont((Font)info[4]);
891
892       float baseline = (float)info[2];
893       String text = (String)info[3];
894       if (shouldDrawMacShadow()) {
895         g.setColor(SHADOW_COLOR);
896         g.drawString(text, x1, baseline + 1);
897       }
898
899       g.setColor(new JBColor(Gray._50, Gray._0));
900       g.drawString(text, x1, baseline);
901
902       if (((SimpleTextAttributes)info[5]).isStrikeout()) {
903         EffectPainter.STRIKE_THROUGH.paint(g, (int)x1, (int)baseline, (int)(x2 - x1), getCharHeight(g), g.getFont());
904       }
905     }
906     return (int)offset;
907   }
908
909   private static int getCharHeight(Graphics g) {
910     // magic of determining character height
911     return g.getFontMetrics().charWidth('a');
912   }
913
914   private int computeTextAlignShift() {
915     if (myTextAlign == SwingConstants.LEFT || myTextAlign == SwingConstants.LEADING) {
916       return 0;
917     }
918
919     int componentWidth = getSize().width;
920     int excessiveWidth = componentWidth - computePreferredSize(false).width;
921     if (excessiveWidth <= 0) {
922       return 0;
923     }
924
925     if (myTextAlign == SwingConstants.CENTER) {
926       return excessiveWidth / 2;
927     }
928     else if (myTextAlign == SwingConstants.RIGHT || myTextAlign == SwingConstants.TRAILING) {
929       return excessiveWidth;
930     }
931     return 0;
932   }
933
934   protected boolean shouldDrawMacShadow() {
935     return false;
936   }
937
938   protected boolean shouldDrawDimmed() {
939     return false;
940   }
941
942   protected boolean shouldDrawBackground() {
943     return false;
944   }
945
946   protected void paintIcon(@NotNull Graphics g, @NotNull Icon icon, int offset) {
947     Rectangle area = computePaintArea();
948     icon.paintIcon(this, g, offset, area.y + (area.height - icon.getIconHeight() + 1) / 2);
949   }
950
951   protected void applyAdditionalHints(@NotNull Graphics2D g) {
952     UISettings.setupAntialiasing(g);
953   }
954
955   @Override
956   public int getBaseline(int width, int height) {
957     super.getBaseline(width, height);
958     return getTextBaseLine(getFontMetrics(getFont()), height);
959   }
960
961   public boolean isTransparentIconBackground() {
962     return myTransparentIconBackground;
963   }
964
965   public void setTransparentIconBackground(boolean transparentIconBackground) {
966     myTransparentIconBackground = transparentIconBackground;
967   }
968
969   public static int getTextBaseLine(@NotNull FontMetrics metrics, final int height) {
970     // adding leading to ascent, just like in editor (leads to bad presentation for certain fonts with Oracle JDK, see IDEA-167541)
971     return (height - metrics.getHeight()) / 2 + metrics.getAscent() +
972            (SystemInfo.isJetBrainsJvm ? metrics.getLeading() : 0);
973   }
974
975   private static void checkCanPaint(Graphics g) {
976     if (UIUtil.isPrinting(g)) return;
977
978     /* wtf??
979     if (!isDisplayable()) {
980       LOG.assertTrue(false, logSwingPath());
981     }
982     */
983     final Application application = ApplicationManager.getApplication();
984     if (application != null) {
985       application.assertIsDispatchThread();
986     }
987     else if (!SwingUtilities.isEventDispatchThread()) {
988       throw new RuntimeException(Thread.currentThread().toString());
989     }
990   }
991
992   @NotNull
993   private String logSwingPath() {
994     //noinspection HardCodedStringLiteral
995     final StringBuilder buffer = new StringBuilder("Components hierarchy:\n");
996     for (Container c = this; c != null; c = c.getParent()) {
997       buffer.append('\n');
998       buffer.append(c);
999     }
1000     return buffer.toString();
1001   }
1002
1003   protected void setBorderInsets(Insets insets) {
1004     if (myBorder instanceof MyBorder) {
1005       ((MyBorder)myBorder).setInsets(insets);
1006     }
1007
1008     revalidateAndRepaint();
1009   }
1010
1011   private static final class MyBorder implements Border {
1012     private Insets myInsets;
1013
1014     public MyBorder() {
1015       myInsets = JBUI.insets(1);
1016     }
1017
1018     public void setInsets(final Insets insets) {
1019       myInsets = insets;
1020     }
1021
1022     @Override
1023     public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
1024     }
1025
1026     @Override
1027     public Insets getBorderInsets(final Component c) {
1028       return (Insets)myInsets.clone();
1029     }
1030
1031     @Override
1032     public boolean isBorderOpaque() {
1033       return false;
1034     }
1035   }
1036
1037   @NotNull
1038   public CharSequence getCharSequence(boolean mainOnly) {
1039     List<String> fragments = mainOnly && myMainTextLastIndex > -1 && myMainTextLastIndex + 1 < myFragments.size() ?
1040                              myFragments.subList(0, myMainTextLastIndex + 1) : myFragments;
1041     return StringUtil.join(fragments, "");
1042   }
1043
1044   @Override
1045   public String toString() {
1046     return getCharSequence(false).toString();
1047   }
1048
1049   public void change(@NotNull Runnable runnable, boolean autoInvalidate) {
1050     boolean old = myAutoInvalidate;
1051     myAutoInvalidate = autoInvalidate;
1052     try {
1053       runnable.run();
1054     }
1055     finally {
1056       myAutoInvalidate = old;
1057     }
1058   }
1059
1060   @Override
1061   public AccessibleContext getAccessibleContext() {
1062     if (accessibleContext == null) {
1063       accessibleContext = new AccessibleSimpleColoredComponent();
1064     }
1065     return accessibleContext;
1066   }
1067
1068   protected class AccessibleSimpleColoredComponent extends JComponent.AccessibleJComponent {
1069     @Override
1070     public String getAccessibleName() {
1071       return getCharSequence(false).toString();
1072     }
1073     @Override
1074     public AccessibleRole getAccessibleRole() {
1075       return AccessibleRole.LABEL;
1076     }
1077   }
1078
1079   public static class BrowserLauncherTag implements Runnable {
1080     private final String myUrl;
1081
1082     public BrowserLauncherTag(@NotNull String url) {
1083       myUrl = url;
1084     }
1085
1086     @Override
1087     public void run() {
1088       BrowserUtil.browse(myUrl);
1089     }
1090   }
1091
1092   public interface ColoredIterator extends Iterator<String> {
1093     int getOffset();
1094
1095     int getEndOffset();
1096
1097     @NotNull
1098     String getFragment();
1099
1100     @NotNull
1101     SimpleTextAttributes getTextAttributes();
1102
1103     int split(int offset, @NotNull SimpleTextAttributes attributes);
1104   }
1105
1106   private class MyIterator implements ColoredIterator {
1107     int myIndex = -1;
1108     int myOffset;
1109     int myEndOffset;
1110
1111     @Override
1112     public int getOffset() {
1113       return myOffset;
1114     }
1115
1116     @Override
1117     public int getEndOffset() {
1118       return myEndOffset;
1119     }
1120
1121     @NotNull
1122     @Override
1123     public String getFragment() {
1124       return myFragments.get(myIndex);
1125     }
1126
1127     @NotNull
1128     @Override
1129     public SimpleTextAttributes getTextAttributes() {
1130       return myAttributes.get(myIndex);
1131     }
1132
1133     @Override
1134     public int split(int offset, @NotNull SimpleTextAttributes attributes) {
1135       if (offset < 0 || offset > myEndOffset - myOffset) {
1136         throw new IllegalArgumentException(offset + " is not within [0, " + (myEndOffset - myOffset) + "]");
1137       }
1138       if (offset == myEndOffset - myOffset) {   // replace
1139         myAttributes.set(myIndex, attributes);
1140       }
1141       else if (offset > 0) {   // split
1142         String text = getFragment();
1143         myFragments.set(myIndex, text.substring(0, offset));
1144         myAttributes.add(myIndex, attributes);
1145         myFragments.add(myIndex + 1, text.substring(offset));
1146         if (myFragmentTags != null && myFragmentTags.size() > myIndex) {
1147           myFragmentTags.add(myIndex, myFragments.get(myIndex));
1148         }
1149         if (myIndex < myLayouts.size()) myLayouts.set(myIndex, null);
1150         if ((myIndex + 1) < myLayouts.size()) myLayouts.add(myIndex + 1, null);
1151         myIndex++;
1152       }
1153       myOffset += offset;
1154       return myOffset;
1155     }
1156
1157     @Override
1158     public boolean hasNext() {
1159       return myIndex + 1 < myFragments.size();
1160     }
1161
1162     @Override
1163     public String next() {
1164       myIndex++;
1165       myOffset = myEndOffset;
1166       String text = getFragment();
1167       myEndOffset += text.length();
1168       return text;
1169     }
1170
1171     @Override
1172     public void remove() {
1173       throw new UnsupportedOperationException();
1174     }
1175   }
1176 }