tabs: fixing invalidation on tab property changes
[fedora-idea.git] / platform / platform-api / src / com / intellij / ui / SimpleColoredComponent.java
blobaa6a588d7416b9f2953c60fd868be92dfd57fa53
1 /*
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;
24 import javax.swing.*;
25 import javax.swing.border.Border;
26 import java.awt.*;
27 import java.util.ArrayList;
29 /**
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;
43 /**
44 * Component's icon. It can be <code>null</code>.
46 private Icon myIcon;
47 /**
48 * Internal padding
50 private Insets myIpad;
51 /**
52 * Gap between icon and text. It is used only if icon is defined.
54 private int myIconTextGap;
55 /**
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
58 * in focused JList.
60 private boolean myPaintFocusBorder;
61 /**
62 * Defines whether the focus border around the text extends to icon or not
64 private boolean myFocusBorderAroundIcon;
65 /**
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);
85 myIconTextGap = 2;
86 myBorder = new MyBorder();
87 setOpaque(true);
90 public final void append(@NotNull String fragment) {
91 append(fragment, SimpleTextAttributes.REGULAR_ATTRIBUTES);
94 /**
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);
110 if (isMainText) {
111 myMainTextLastIndex = myFragments.size() - 1;
114 revalidateAndRepaint();
117 private void revalidateAndRepaint() {
118 if (myAutoInvalidate) {
119 revalidate();
122 repaint();
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) {
150 myIcon = null;
151 myPaintFocusBorder = false;
152 myFragments.clear();
153 myAttributes.clear();
154 myFragmentTags = null;
155 myMainTextLastIndex = -1;
156 myAlignIndex = -1;
157 myAlignWidth = -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() {
167 return myIcon;
171 * Sets a new component icon
173 public final void setIcon(final Icon icon) {
174 myIcon = icon;
175 revalidateAndRepaint();
179 * @return "leave" (internal) internal paddings of the component
181 public Insets getIpad() {
182 return myIpad;
186 * Sets specified internal paddings
188 public void setIpad(final Insets ipad) {
189 myIpad = 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;
223 repaint();
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;
233 repaint();
236 public boolean isIconOpaque() {
237 return myIconOpaque;
240 public void setIconOpaque(final boolean iconOpaque) {
241 myIconOpaque = iconOpaque;
243 repaint();
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);
255 return null;
258 public final synchronized Dimension computePreferredSize(final boolean mainTextOnly) {
259 // Calculate width
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();
270 if (font == null) {
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;
291 // Calculate height
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);
301 else {
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) {
330 return i;
332 curX += curWidth;
333 if (i == myAlignIndex && curX < myAlignWidth) {
334 curX = myAlignWidth;
337 return -1;
340 protected void paintComponent(final Graphics g) {
341 try {
342 doPaint(g);
344 catch (RuntimeException e) {
345 LOG.error(logSwingPath(), e);
346 throw e;
350 protected synchronized void doPaint(final Graphics g) {
351 checkCanPaint(g);
352 int xOffset = 0;
354 // Paint icon and its background
355 final Icon icon = myIcon; // guard against concurrent modification (IDEADEV-12635)
356 if (icon != null) {
357 final Container parent = getParent();
358 Color iconBackgroundColor = null;
359 if (isIconOpaque()) {
360 if (parent != null && !myFocusBorderAroundIcon && !UIUtil.isFullRowSelectionLAF()) {
361 iconBackgroundColor = parent.getBackground();
363 else {
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;
378 if (isOpaque()) {
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
385 if (xOffset == 0) {
386 xOffset = myIpad.left;
389 int textStart = xOffset;
390 xOffset += myBorder.getBorderInsets(this).left;
392 // Paint text
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());
400 g.setFont(font);
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) {
408 g.setColor(bgColor);
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();
416 if (!isEnabled()) {
417 color = UIUtil.getTextInactiveTextColor();
419 g.setColor(color);
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);
429 // 2. Waved effect
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());
457 else {
458 myBorder.paintBorder(this, g, textStart, 0, getWidth() - textStart, getHeight());
463 private static void checkCanPaint(Graphics g) {
464 if (UIUtil.isPrinting(g)) return;
466 /* wtf??
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()) {
484 buffer.append('\n');
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;
499 public MyBorder() {
500 myInsets = new Insets(1, 1, 1, 1);
503 public void setInsets(final Insets insets) {
504 myInsets = 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) {
513 return myInsets;
516 public boolean isBorderOpaque() {
517 return true;
521 @Override
522 public String toString() {
523 StringBuffer result = new StringBuffer();
524 for (String each : myFragments) {
525 result.append(each);
528 return result.toString();
532 public void change(@NotNull Runnable runnable, boolean autoInvalidate) {
533 boolean old = myAutoInvalidate;
534 myAutoInvalidate = autoInvalidate;
535 try {
536 runnable.run();
537 } finally {
538 myAutoInvalidate = old;