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