2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com
.intellij
.ui
;
18 import com
.intellij
.openapi
.application
.Application
;
19 import com
.intellij
.openapi
.application
.ApplicationManager
;
20 import com
.intellij
.openapi
.diagnostic
.Logger
;
21 import com
.intellij
.util
.ui
.UIUtil
;
22 import org
.jetbrains
.annotations
.NotNull
;
25 import javax
.swing
.border
.Border
;
27 import java
.util
.ArrayList
;
30 * This is high performance Swing component which represents an icon
31 * with a colored text. The text consists of fragments. Each
32 * text fragment has its own color (foreground) and font style.
34 * @author Vladimir Kondratyev
36 public class SimpleColoredComponent
extends JComponent
{
37 private static final Logger LOG
= Logger
.getInstance("#com.intellij.ui.SimpleColoredComponent");
39 private final ArrayList
<String
> myFragments
;
40 private final ArrayList
<SimpleTextAttributes
> myAttributes
;
41 private ArrayList
<Object
> myFragmentTags
= null;
44 * Component's icon. It can be <code>null</code>.
50 private Insets myIpad
;
52 * Gap between icon and text. It is used only if icon is defined.
54 private int myIconTextGap
;
56 * Defines whether the focus border around the text is painted or not.
57 * For example, text can have a border if the component represents a selected item
60 private boolean myPaintFocusBorder
;
62 * Defines whether the focus border around the text extends to icon or not
64 private boolean myFocusBorderAroundIcon
;
66 * This is the border around the text. For example, text can have a border
67 * if the component represents a selected item in a focused JList.
68 * Border can be <code>null</code>.
70 private final MyBorder myBorder
;
72 private int myMainTextLastIndex
= -1;
74 private int myAlignIndex
;
75 private int myAlignWidth
;
77 private boolean myIconOpaque
= true;
79 private boolean myAutoInvalidate
= true;
81 public SimpleColoredComponent() {
82 myFragments
= new ArrayList
<String
>(3);
83 myAttributes
= new ArrayList
<SimpleTextAttributes
>(3);
84 myIpad
= new Insets(1, 2, 1, 2);
86 myBorder
= new MyBorder();
90 public final void append(@NotNull String fragment
) {
91 append(fragment
, SimpleTextAttributes
.REGULAR_ATTRIBUTES
);
95 * Appends string fragments to existing ones. Appended string
96 * will have specified <code>attributes</code>.
98 public final void append(@NotNull final String fragment
, @NotNull final SimpleTextAttributes attributes
) {
99 append(fragment
, attributes
, myMainTextLastIndex
< 0);
103 * Appends string fragments to existing ones. Appended string
104 * will have specified <code>attributes</code>.
106 public void append(@NotNull final String fragment
, @NotNull final SimpleTextAttributes attributes
, boolean isMainText
) {
107 synchronized (this) {
108 myFragments
.add(fragment
);
109 myAttributes
.add(attributes
);
111 myMainTextLastIndex
= myFragments
.size() - 1;
114 revalidateAndRepaint();
117 private void revalidateAndRepaint() {
118 if (myAutoInvalidate
) {
125 public void append(@NotNull final String fragment
, @NotNull final SimpleTextAttributes attributes
, Object tag
) {
126 synchronized (this) {
127 append(fragment
, attributes
);
128 if (myFragmentTags
== null) {
129 myFragmentTags
= new ArrayList
<Object
>();
131 while(myFragmentTags
.size() < myFragments
.size()-1) {
132 myFragmentTags
.add(null);
134 myFragmentTags
.add(tag
);
136 revalidateAndRepaint();
139 public synchronized void appendAlign(int alignWidth
) {
140 myAlignIndex
= myFragments
.size()-1;
141 myAlignWidth
= alignWidth
;
145 * Clear all special attributes of <code>SimpleColoredComponent</code>.
146 * They are icon, text fragments and their attributes, "paint focus border".
148 public void clear() {
149 synchronized (this) {
151 myPaintFocusBorder
= false;
153 myAttributes
.clear();
154 myFragmentTags
= null;
155 myMainTextLastIndex
= -1;
159 revalidateAndRepaint();
163 * @return component's icon. This method returns <code>null</code>
164 * if there is no icon.
166 public final Icon
getIcon() {
171 * Sets a new component icon
173 public final void setIcon(final Icon icon
) {
175 revalidateAndRepaint();
179 * @return "leave" (internal) internal paddings of the component
181 public Insets
getIpad() {
186 * Sets specified internal paddings
188 public void setIpad(final Insets ipad
) {
191 revalidateAndRepaint();
195 * @return gap between icon and text
197 public int getIconTextGap() {
198 return myIconTextGap
;
202 * Sets a new gap between icon and text
204 * @throws java.lang.IllegalArgumentException
205 * if the <code>iconTextGap</code>
206 * has a negative value
208 public void setIconTextGap(final int iconTextGap
) {
209 if (iconTextGap
< 0) {
210 throw new IllegalArgumentException("wrong iconTextGap: " + iconTextGap
);
212 myIconTextGap
= iconTextGap
;
214 revalidateAndRepaint();
218 * Sets whether focus border is painted or not
220 protected final void setPaintFocusBorder(final boolean paintFocusBorder
) {
221 myPaintFocusBorder
= paintFocusBorder
;
227 * Sets whether focus border extends to icon or not. If so then
228 * component also extends the selection.
230 protected final void setFocusBorderAroundIcon(final boolean focusBorderAroundIcon
) {
231 myFocusBorderAroundIcon
= focusBorderAroundIcon
;
236 public boolean isIconOpaque() {
240 public void setIconOpaque(final boolean iconOpaque
) {
241 myIconOpaque
= iconOpaque
;
246 public Dimension
getPreferredSize() {
247 return computePreferredSize(false);
251 public synchronized Object
getFragmentTag(int index
) {
252 if (myFragmentTags
!= null && index
< myFragmentTags
.size()) {
253 return myFragmentTags
.get(index
);
258 public final synchronized Dimension
computePreferredSize(final boolean mainTextOnly
) {
260 int width
= myIpad
.left
;
262 if (myIcon
!= null) {
263 width
+= myIcon
.getIconWidth() + myIconTextGap
;
266 final Insets borderInsets
= myBorder
.getBorderInsets(this);
267 width
+= borderInsets
.left
;
269 Font font
= getFont();
271 font
= UIManager
.getFont("Label.font");
274 LOG
.assertTrue(font
!= null);
276 for (int i
= 0; i
< myAttributes
.size(); i
++) {
277 SimpleTextAttributes attributes
= myAttributes
.get(i
);
278 if (font
.getStyle() != attributes
.getStyle()) { // derive font only if it is necessary
279 font
= font
.deriveFont(attributes
.getStyle());
281 final FontMetrics metrics
= getFontMetrics(font
);
282 width
+= metrics
.stringWidth(myFragments
.get(i
));
283 if (i
== myAlignIndex
&& width
< myAlignWidth
) {
284 width
= myAlignWidth
;
287 if (mainTextOnly
&& myMainTextLastIndex
>= 0 && i
== myMainTextLastIndex
) break;
289 width
+= myIpad
.right
+ borderInsets
.right
;
292 int height
= myIpad
.top
+ myIpad
.bottom
;
294 final FontMetrics metrics
= getFontMetrics(font
);
295 int textHeight
= metrics
.getHeight();
296 textHeight
+= borderInsets
.top
+ borderInsets
.bottom
;
298 if (myIcon
!= null) {
299 height
+= Math
.max(myIcon
.getIconHeight(), textHeight
);
302 height
+= textHeight
;
305 // Take into accound that the component itself can have a border
306 final Insets insets
= getInsets();
307 width
+= insets
.left
+ insets
.right
;
308 height
+= insets
.top
+ insets
.bottom
;
310 return new Dimension(width
, height
);
313 public int findFragmentAt(int x
) {
314 int curX
= myIpad
.left
;
315 if (myIcon
!= null) {
316 curX
+= myIcon
.getIconWidth() + myIconTextGap
;
319 Font font
= getFont();
320 LOG
.assertTrue(font
!= null);
322 for (int i
= 0; i
< myAttributes
.size(); i
++) {
323 SimpleTextAttributes attributes
= myAttributes
.get(i
);
324 if (font
.getStyle() != attributes
.getStyle()) { // derive font only if it is necessary
325 font
= font
.deriveFont(attributes
.getStyle());
327 final FontMetrics metrics
= getFontMetrics(font
);
328 final int curWidth
= metrics
.stringWidth(myFragments
.get(i
));
329 if (x
>= curX
&& x
< curX
+ curWidth
) {
333 if (i
== myAlignIndex
&& curX
< myAlignWidth
) {
340 protected void paintComponent(final Graphics g
) {
344 catch (RuntimeException e
) {
345 LOG
.error(logSwingPath(), e
);
350 protected synchronized void doPaint(final Graphics g
) {
354 // Paint icon and its background
355 final Icon icon
= myIcon
; // guard against concurrent modification (IDEADEV-12635)
357 final Container parent
= getParent();
358 Color iconBackgroundColor
= null;
359 if (isIconOpaque()) {
360 if (parent
!= null && !myFocusBorderAroundIcon
&& !UIUtil
.isFullRowSelectionLAF()) {
361 iconBackgroundColor
= parent
.getBackground();
364 iconBackgroundColor
= getBackground();
368 if (iconBackgroundColor
!= null) {
369 g
.setColor(iconBackgroundColor
);
370 g
.fillRect(0, 0, icon
.getIconWidth() + myIpad
.left
+ myIconTextGap
, getHeight());
373 icon
.paintIcon(this, g
, myIpad
.left
, (getHeight() - icon
.getIconHeight()) / 2);
375 xOffset
+= myIpad
.left
+ icon
.getIconWidth() + myIconTextGap
;
379 // Paint text background
380 g
.setColor(getBackground());
381 g
.fillRect(xOffset
, 0, getWidth() - xOffset
, getHeight());
384 // If there is no icon, then we have to add left internal padding
386 xOffset
= myIpad
.left
;
389 int textStart
= xOffset
;
390 xOffset
+= myBorder
.getBorderInsets(this).left
;
393 UIUtil
.applyRenderingHints(g
);
394 for (int i
= 0; i
< myFragments
.size(); i
++) {
395 final SimpleTextAttributes attributes
= myAttributes
.get(i
);
396 Font font
= getFont();
397 if (font
.getStyle() != attributes
.getStyle()) { // derive font only if it is necessary
398 font
= font
.deriveFont(attributes
.getStyle());
401 final FontMetrics metrics
= getFontMetrics(font
);
403 final String fragment
= myFragments
.get(i
);
404 final int fragmentWidth
= metrics
.stringWidth(fragment
);
406 final Color bgColor
= attributes
.getBgColor();
407 if (isOpaque() && bgColor
!= null) {
409 g
.fillRect(xOffset
, 0, fragmentWidth
, getHeight());
412 Color color
= attributes
.getFgColor();
413 if (color
== null) { // in case if color is not defined we have to get foreground color from Swing hierarchy
414 color
= getForeground();
417 color
= UIUtil
.getTextInactiveTextColor();
421 final int textBaseline
= (getHeight() - metrics
.getHeight()) / 2 + metrics
.getAscent();
422 g
.drawString(fragment
, xOffset
, textBaseline
);
424 // 1. Strikeout effect
425 if (attributes
.isStrikeout()) {
426 final int strikeOutAt
= textBaseline
+ (metrics
.getDescent() - metrics
.getAscent()) / 2;
427 UIUtil
.drawLine(g
, xOffset
, strikeOutAt
, xOffset
+ fragmentWidth
, strikeOutAt
);
430 if (attributes
.isWaved()) {
431 if (attributes
.getWaveColor() != null) {
432 g
.setColor(attributes
.getWaveColor());
434 final int wavedAt
= textBaseline
+ 1;
435 for (int x
= xOffset
; x
<= xOffset
+ fragmentWidth
; x
+= 4) {
436 UIUtil
.drawLine(g
, x
, wavedAt
, x
+ 2, wavedAt
+ 2);
437 UIUtil
.drawLine(g
, x
+ 3, wavedAt
+ 1, x
+ 4, wavedAt
);
440 if (attributes
.isUnderline()) {
441 final int underlineAt
= textBaseline
+ 1;
442 UIUtil
.drawLine(g
, xOffset
, underlineAt
, xOffset
+ fragmentWidth
, underlineAt
);
446 xOffset
+= fragmentWidth
;
447 if (i
== myAlignIndex
&& xOffset
< myAlignWidth
) {
448 xOffset
= myAlignWidth
;
452 // Paint focus border around the text and icon (if necessary)
453 if (myPaintFocusBorder
) {
454 if (myFocusBorderAroundIcon
|| icon
== null) {
455 myBorder
.paintBorder(this, g
, 0, 0, getWidth(), getHeight());
458 myBorder
.paintBorder(this, g
, textStart
, 0, getWidth() - textStart
, getHeight());
463 private static void checkCanPaint(Graphics g
) {
464 if (UIUtil
.isPrinting(g
)) return;
467 if (!isDisplayable()) {
468 LOG.assertTrue(false, logSwingPath());
471 final Application application
= ApplicationManager
.getApplication();
472 if (application
!= null) {
473 application
.assertIsDispatchThread();
475 else if (!SwingUtilities
.isEventDispatchThread()) {
476 throw new RuntimeException(Thread
.currentThread().toString());
480 private String
logSwingPath() {
481 //noinspection HardCodedStringLiteral
482 final StringBuilder buffer
= new StringBuilder("Components hierarchy:\n");
483 for (Container c
= this; c
!= null; c
= c
.getParent()) {
485 buffer
.append(c
.toString());
487 return buffer
.toString();
490 protected void setBorderInsets(Insets insets
) {
491 myBorder
.setInsets(insets
);
493 revalidateAndRepaint();
496 private static final class MyBorder
implements Border
{
497 private Insets myInsets
;
500 myInsets
= new Insets(1, 1, 1, 1);
503 public void setInsets(final Insets insets
) {
507 public void paintBorder(final Component c
, final Graphics g
, final int x
, final int y
, final int width
, final int height
) {
508 g
.setColor(Color
.BLACK
);
509 UIUtil
.drawDottedRectangle(g
, x
, y
, x
+ width
- 1, y
+ height
- 1);
512 public Insets
getBorderInsets(final Component c
) {
516 public boolean isBorderOpaque() {
522 public String
toString() {
523 StringBuffer result
= new StringBuffer();
524 for (String each
: myFragments
) {
528 return result
.toString();
532 public void change(@NotNull Runnable runnable
, boolean autoInvalidate
) {
533 boolean old
= myAutoInvalidate
;
534 myAutoInvalidate
= autoInvalidate
;
538 myAutoInvalidate
= old
;