blob: 9d8e1de807ef07871cb8d4897d87fbddf7a31394 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ui;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TIntIntHashMap;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.accessibility.AccessibleStateSet;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.tree.TreeCellRenderer;
import java.awt.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
/**
* This is high performance Swing component which represents an icon
* with a colored text. The text consists of fragments. Each
* text fragment has its own color (foreground) and font style.
*
* @author Vladimir Kondratyev
*/
@SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext", "FieldAccessedSynchronizedAndUnsynchronized", "UnusedDeclaration"})
public class SimpleColoredComponent extends JComponent implements Accessible, ColoredTextContainer {
private static final boolean isOracleRetina = UIUtil.isRetina() && SystemInfo.isOracleJvm;
private static final Logger LOG = Logger.getInstance("#com.intellij.ui.SimpleColoredComponent");
public static final Color SHADOW_COLOR = new JBColor(new Color(250, 250, 250, 140), Gray._0.withAlpha(50));
public static final Color STYLE_SEARCH_MATCH_BACKGROUND = SHADOW_COLOR; //api compatibility
public static final int FRAGMENT_ICON = -2;
private final List<String> myFragments;
private final List<SimpleTextAttributes> myAttributes;
private List<Object> myFragmentTags = null;
/**
* Component's icon. It can be <code>null</code>.
*/
private Icon myIcon;
/**
* Internal padding
*/
private Insets myIpad;
/**
* Gap between icon and text. It is used only if icon is defined.
*/
protected int myIconTextGap;
/**
* Defines whether the focus border around the text is painted or not.
* For example, text can have a border if the component represents a selected item
* in focused JList.
*/
private boolean myPaintFocusBorder;
/**
* Defines whether the focus border around the text extends to icon or not
*/
private boolean myFocusBorderAroundIcon;
/**
* This is the border around the text. For example, text can have a border
* if the component represents a selected item in a focused JList.
* Border can be <code>null</code>.
*/
private Border myBorder;
private int myMainTextLastIndex = -1;
private final TIntIntHashMap myFixedWidths;
@JdkConstants.HorizontalAlignment private int myTextAlign = SwingConstants.LEFT;
private boolean myIconOpaque = false;
private boolean myAutoInvalidate = !(this instanceof TreeCellRenderer);
private final AccessibleContext myContext = new MyAccessibleContext();
private boolean myIconOnTheRight = false;
private boolean myTransparentIconBackground;
public SimpleColoredComponent() {
myFragments = new ArrayList<String>(3);
myAttributes = new ArrayList<SimpleTextAttributes>(3);
myIpad = new Insets(1, 2, 1, 2);
myIconTextGap = 2;
myBorder = new MyBorder();
myFixedWidths = new TIntIntHashMap(10);
setOpaque(true);
}
@NotNull
public ColoredIterator iterator() {
return new MyIterator();
}
public boolean isIconOnTheRight() {
return myIconOnTheRight;
}
public void setIconOnTheRight(boolean iconOnTheRight) {
myIconOnTheRight = iconOnTheRight;
}
@NotNull
public final SimpleColoredComponent append(@NotNull String fragment) {
append(fragment, SimpleTextAttributes.REGULAR_ATTRIBUTES);
return this;
}
/**
* Appends string fragments to existing ones. Appended string
* will have specified <code>attributes</code>.
* @param fragment text fragment
* @param attributes text attributes
*/
@Override
public final void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes) {
append(fragment, attributes, myMainTextLastIndex < 0);
}
/**
* Appends string fragments to existing ones. Appended string
* will have specified <code>attributes</code>.
* @param fragment text fragment
* @param attributes text attributes
* @param isMainText main text of not
*/
public void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, boolean isMainText) {
_append(fragment, attributes, isMainText);
revalidateAndRepaint();
}
private synchronized void _append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, boolean isMainText) {
myFragments.add(fragment);
myAttributes.add(attributes);
if (isMainText) {
myMainTextLastIndex = myFragments.size() - 1;
}
}
private void revalidateAndRepaint() {
if (myAutoInvalidate) {
revalidate();
}
repaint();
}
@Override
public void append(@NotNull final String fragment, @NotNull final SimpleTextAttributes attributes, Object tag) {
_append(fragment, attributes, tag);
revalidateAndRepaint();
}
private synchronized void _append(String fragment, SimpleTextAttributes attributes, Object tag) {
append(fragment, attributes);
if (myFragmentTags == null) {
myFragmentTags = new ArrayList<Object>();
}
while (myFragmentTags.size() < myFragments.size() - 1) {
myFragmentTags.add(null);
}
myFragmentTags.add(tag);
}
public synchronized void appendFixedTextFragmentWidth(int width) {
final int alignIndex = myFragments.size()-1;
myFixedWidths.put(alignIndex, width);
}
public void setTextAlign(@JdkConstants.HorizontalAlignment int align) {
myTextAlign = align;
}
/**
* Clear all special attributes of <code>SimpleColoredComponent</code>.
* They are icon, text fragments and their attributes, "paint focus border".
*/
public void clear() {
_clear();
revalidateAndRepaint();
}
private synchronized void _clear() {
myIcon = null;
myPaintFocusBorder = false;
myFragments.clear();
myAttributes.clear();
myFragmentTags = null;
myMainTextLastIndex = -1;
myFixedWidths.clear();
}
/**
* @return component's icon. This method returns <code>null</code>
* if there is no icon.
*/
public final Icon getIcon() {
return myIcon;
}
/**
* Sets a new component icon
* @param icon icon
*/
@Override
public final void setIcon(@Nullable final Icon icon) {
myIcon = icon;
revalidateAndRepaint();
}
/**
* @return "leave" (internal) internal paddings of the component
*/
@NotNull
public Insets getIpad() {
return myIpad;
}
/**
* Sets specified internal paddings
* @param ipad insets
*/
public void setIpad(@NotNull Insets ipad) {
myIpad = ipad;
revalidateAndRepaint();
}
/**
* @return gap between icon and text
*/
public int getIconTextGap() {
return myIconTextGap;
}
/**
* Sets a new gap between icon and text
*
* @param iconTextGap the gap between text and icon
* @throws java.lang.IllegalArgumentException
* if the <code>iconTextGap</code>
* has a negative value
*/
public void setIconTextGap(final int iconTextGap) {
if (iconTextGap < 0) {
throw new IllegalArgumentException("wrong iconTextGap: " + iconTextGap);
}
myIconTextGap = iconTextGap;
revalidateAndRepaint();
}
public Border getMyBorder() {
return myBorder;
}
public void setMyBorder(@Nullable Border border) {
myBorder = border;
}
/**
* Sets whether focus border is painted or not
* @param paintFocusBorder <code>true</code> or <code>false</code>
*/
protected final void setPaintFocusBorder(final boolean paintFocusBorder) {
myPaintFocusBorder = paintFocusBorder;
repaint();
}
/**
* Sets whether focus border extends to icon or not. If so then
* component also extends the selection.
* @param focusBorderAroundIcon <code>true</code> or <code>false</code>
*/
protected final void setFocusBorderAroundIcon(final boolean focusBorderAroundIcon) {
myFocusBorderAroundIcon = focusBorderAroundIcon;
repaint();
}
public boolean isIconOpaque() {
return myIconOpaque;
}
public void setIconOpaque(final boolean iconOpaque) {
myIconOpaque = iconOpaque;
repaint();
}
@Override
@NotNull
public Dimension getPreferredSize() {
return computePreferredSize(false);
}
@Override
@NotNull
public Dimension getMinimumSize() {
return computePreferredSize(false);
}
@Nullable
public synchronized Object getFragmentTag(int index) {
if (myFragmentTags != null && index < myFragmentTags.size()) {
return myFragmentTags.get(index);
}
return null;
}
@NotNull
public final synchronized Dimension computePreferredSize(final boolean mainTextOnly) {
// Calculate width
int width = myIpad.left;
if (myIcon != null) {
width += myIcon.getIconWidth() + myIconTextGap;
}
final Insets borderInsets = myBorder != null ? myBorder.getBorderInsets(this) : new Insets(0,0,0,0);
width += borderInsets.left;
Font font = getFont();
if (font == null) {
font = UIUtil.getLabelFont();
}
LOG.assertTrue(font != null);
width += computeTextWidth(font, mainTextOnly);
width += myIpad.right + borderInsets.right;
// Calculate height
int height = myIpad.top + myIpad.bottom;
final FontMetrics metrics = getFontMetrics(font);
int textHeight = metrics.getHeight();
textHeight += borderInsets.top + borderInsets.bottom;
if (myIcon != null) {
height += Math.max(myIcon.getIconHeight(), textHeight);
}
else {
height += textHeight;
}
// Take into account that the component itself can have a border
final Insets insets = getInsets();
if (insets != null) {
width += insets.left + insets.right;
height += insets.top + insets.bottom;
}
if (isOracleRetina) {
width++; //todo[kb] remove when IDEA-108760 will be fixed
}
return new Dimension(width, height);
}
private int computeTextWidth(@NotNull Font font, final boolean mainTextOnly) {
int result = 0;
int baseSize = font.getSize();
boolean wasSmaller = false;
for (int i = 0; i < myAttributes.size(); i++) {
SimpleTextAttributes attributes = myAttributes.get(i);
boolean isSmaller = attributes.isSmaller();
if (font.getStyle() != attributes.getFontStyle() || isSmaller != wasSmaller) { // derive font only if it is necessary
font = font.deriveFont(attributes.getFontStyle(), isSmaller ? UIUtil.getFontSize(UIUtil.FontSize.SMALL) : baseSize);
}
wasSmaller = isSmaller;
final String text = myFragments.get(i);
result += isOracleRetina ? GraphicsUtil.stringWidth(text, font) : getFontMetrics(font).stringWidth(text);
final int fixedWidth = myFixedWidths.get(i);
if (fixedWidth > 0 && result < fixedWidth) {
result = fixedWidth;
}
if (mainTextOnly && myMainTextLastIndex >= 0 && i == myMainTextLastIndex) break;
}
return result;
}
/**
* Returns the index of text fragment at the specified X offset.
*
* @param x the offset
* @return the index of the fragment, {@link #FRAGMENT_ICON} if the icon is at the offset, or -1 if nothing is there.
*/
public int findFragmentAt(int x) {
int curX = myIpad.left;
if (myIcon != null && !myIconOnTheRight) {
final int iconRight = myIcon.getIconWidth() + myIconTextGap;
if (x < iconRight) {
return FRAGMENT_ICON;
}
curX += iconRight;
}
Font font = getFont();
if (font == null) {
font = UIUtil.getLabelFont();
}
int baseSize = font.getSize();
boolean wasSmaller = false;
for (int i = 0; i < myAttributes.size(); i++) {
SimpleTextAttributes attributes = myAttributes.get(i);
boolean isSmaller = attributes.isSmaller();
if (font.getStyle() != attributes.getFontStyle() || isSmaller != wasSmaller) { // derive font only if it is necessary
font = font.deriveFont(attributes.getFontStyle(), isSmaller ? UIUtil.getFontSize(UIUtil.FontSize.SMALL) : baseSize);
}
wasSmaller = isSmaller;
final String text = myFragments.get(i);
final int curWidth = isOracleRetina ? GraphicsUtil.stringWidth(text, font) : getFontMetrics(font).stringWidth(text);
if (x >= curX && x < curX + curWidth) {
return i;
}
curX += curWidth;
final int fixedWidth = myFixedWidths.get(i);
if (fixedWidth > 0 && curX < fixedWidth) {
curX = fixedWidth;
}
}
if (myIcon != null && myIconOnTheRight) {
curX += myIconTextGap;
if (x >= curX && x < curX + myIcon.getIconWidth()) {
return FRAGMENT_ICON;
}
}
return -1;
}
@Nullable
public Object getFragmentTagAt(int x) {
int index = findFragmentAt(x);
return index < 0 ? null : getFragmentTag(index);
}
@NotNull
protected JLabel formatToLabel(@NotNull JLabel label) {
label.setIcon(myIcon);
if (!myFragments.isEmpty()) {
final StringBuilder text = new StringBuilder();
text.append("<html><body style=\"white-space:nowrap\">");
for (int i = 0; i < myFragments.size(); i++) {
final String fragment = myFragments.get(i);
final SimpleTextAttributes attributes = myAttributes.get(i);
final Object tag = getFragmentTag(i);
if (tag instanceof BrowserLauncherTag) {
formatLink(text, fragment, attributes, ((BrowserLauncherTag)tag).myUrl);
}
else {
formatText(text, fragment, attributes);
}
}
text.append("</body></html>");
label.setText(text.toString());
}
return label;
}
static void formatText(@NotNull StringBuilder builder, @NotNull String fragment, @NotNull SimpleTextAttributes attributes) {
if (!fragment.isEmpty()) {
builder.append("<span");
formatStyle(builder, attributes);
builder.append('>').append(convertFragment(fragment)).append("</span>");
}
}
static void formatLink(@NotNull StringBuilder builder, @NotNull String fragment, @NotNull SimpleTextAttributes attributes, @NotNull String url) {
if (!fragment.isEmpty()) {
builder.append("<a href=\"").append(StringUtil.replace(url, "\"", "%22")).append("\"");
formatStyle(builder, attributes);
builder.append('>').append(convertFragment(fragment)).append("</a>");
}
}
private static String convertFragment(String fragment) {
return StringUtil.escapeXml(fragment).replaceAll("\\\\n", "<br>");
}
private static void formatStyle(final StringBuilder builder, final SimpleTextAttributes attributes) {
final Color fgColor = attributes.getFgColor();
final Color bgColor = attributes.getBgColor();
final int style = attributes.getStyle();
final int pos = builder.length();
if (fgColor != null) {
builder.append("color:#").append(Integer.toString(fgColor.getRGB() & 0xFFFFFF, 16)).append(';');
}
if (bgColor != null) {
builder.append("background-color:#").append(Integer.toString(bgColor.getRGB() & 0xFFFFFF, 16)).append(';');
}
if ((style & SimpleTextAttributes.STYLE_BOLD) != 0) {
builder.append("font-weight:bold;");
}
if ((style & SimpleTextAttributes.STYLE_ITALIC) != 0) {
builder.append("font-style:italic;");
}
if ((style & SimpleTextAttributes.STYLE_UNDERLINE) != 0) {
builder.append("text-decoration:underline;");
}
else if ((style & SimpleTextAttributes.STYLE_STRIKEOUT) != 0) {
builder.append("text-decoration:line-through;");
}
if (builder.length() > pos) {
builder.insert(pos, " style=\"");
builder.append('"');
}
}
@Override
protected void paintComponent(final Graphics g) {
try {
_doPaint(g);
}
catch (RuntimeException e) {
LOG.error(logSwingPath(), e);
throw e;
}
}
private synchronized void _doPaint(final Graphics g) {
checkCanPaint(g);
doPaint((Graphics2D)g);
}
protected void doPaint(final Graphics2D g) {
int offset = 0;
final Icon icon = myIcon; // guard against concurrent modification (IDEADEV-12635)
if (icon != null && !myIconOnTheRight) {
doPaintIcon(g, icon, 0);
offset += myIpad.left + icon.getIconWidth() + myIconTextGap;
}
doPaintTextBackground(g, offset);
offset = doPaintText(g, offset, myFocusBorderAroundIcon || icon == null);
if (icon != null && myIconOnTheRight) {
doPaintIcon(g, icon, offset);
}
}
private void doPaintTextBackground(Graphics2D g, int offset) {
if (isOpaque() || shouldDrawBackground()) {
paintBackground(g, offset, getWidth() - offset, getHeight());
}
}
protected void paintBackground(Graphics2D g, int x, int width, int height) {
g.setColor(getBackground());
g.fillRect(x, 0, width, height);
}
protected void doPaintIcon(@NotNull Graphics2D g, @NotNull Icon icon, int offset) {
final Container parent = getParent();
Color iconBackgroundColor = null;
if ((isOpaque() || isIconOpaque()) && !isTransparentIconBackground()) {
if (parent != null && !myFocusBorderAroundIcon && !UIUtil.isFullRowSelectionLAF()) {
iconBackgroundColor = parent.getBackground();
}
else {
iconBackgroundColor = getBackground();
}
}
if (iconBackgroundColor != null) {
g.setColor(iconBackgroundColor);
g.fillRect(offset, 0, icon.getIconWidth() + myIpad.left + myIconTextGap, getHeight());
}
paintIcon(g, icon, offset + myIpad.left);
}
protected int doPaintText(Graphics2D g, int offset, boolean focusAroundIcon) {
// If there is no icon, then we have to add left internal padding
if (offset == 0) {
offset = myIpad.left;
}
int textStart = offset;
if (myBorder != null) {
offset += myBorder.getBorderInsets(this).left;
}
final List<Object[]> searchMatches = new ArrayList<Object[]>();
UIUtil.applyRenderingHints(g);
applyAdditionalHints(g);
final Font ownFont = getFont();
if (ownFont != null) {
offset += computeTextAlignShift(ownFont);
}
int baseSize = ownFont != null ? ownFont.getSize() : g.getFont().getSize();
boolean wasSmaller = false;
for (int i = 0; i < myFragments.size(); i++) {
final SimpleTextAttributes attributes = myAttributes.get(i);
Font font = g.getFont();
boolean isSmaller = attributes.isSmaller();
if (font.getStyle() != attributes.getFontStyle() || isSmaller != wasSmaller) { // derive font only if it is necessary
font = font.deriveFont(attributes.getFontStyle(), isSmaller ? UIUtil.getFontSize(UIUtil.FontSize.SMALL) : baseSize);
}
wasSmaller = isSmaller;
g.setFont(font);
final FontMetrics metrics = g.getFontMetrics(font);
final String fragment = myFragments.get(i);
final int fragmentWidth = isOracleRetina ? GraphicsUtil.stringWidth(fragment, font) : metrics.stringWidth(fragment);
final Color bgColor = attributes.isSearchMatch() ? null : attributes.getBgColor();
if ((attributes.isOpaque() || isOpaque()) && bgColor != null) {
g.setColor(bgColor);
g.fillRect(offset, 0, fragmentWidth, getHeight());
}
Color color = attributes.getFgColor();
if (color == null) { // in case if color is not defined we have to get foreground color from Swing hierarchy
color = getForeground();
}
if (!isEnabled()) {
color = UIUtil.getInactiveTextColor();
}
g.setColor(color);
final int textBaseline = getTextBaseLine(metrics, getHeight());
if (!attributes.isSearchMatch()) {
if (shouldDrawMacShadow()) {
g.setColor(SHADOW_COLOR);
g.drawString(fragment, offset, textBaseline + 1);
}
if (shouldDrawDimmed()) {
color = ColorUtil.dimmer(color);
}
g.setColor(color);
g.drawString(fragment, offset, textBaseline);
}
// 1. Strikeout effect
if (attributes.isStrikeout()) {
final int strikeOutAt = textBaseline + (metrics.getDescent() - metrics.getAscent()) / 2;
UIUtil.drawLine(g, offset, strikeOutAt, offset + fragmentWidth, strikeOutAt);
}
// 2. Waved effect
if (attributes.isWaved()) {
if (attributes.getWaveColor() != null) {
g.setColor(attributes.getWaveColor());
}
final int wavedAt = textBaseline + 1;
for (int x = offset; x <= offset + fragmentWidth; x += 4) {
UIUtil.drawLine(g, x, wavedAt, x + 2, wavedAt + 2);
UIUtil.drawLine(g, x + 3, wavedAt + 1, x + 4, wavedAt);
}
}
// 3. Underline
if (attributes.isUnderline()) {
final int underlineAt = textBaseline + 1;
UIUtil.drawLine(g, offset, underlineAt, offset + fragmentWidth, underlineAt);
}
// 4. Bold Dotted Line
if (attributes.isBoldDottedLine()) {
final int dottedAt = SystemInfo.isMac ? textBaseline : textBaseline + 1;
final Color lineColor = attributes.getWaveColor();
UIUtil.drawBoldDottedLine(g, offset, offset + fragmentWidth, dottedAt, bgColor, lineColor, isOpaque());
}
if (attributes.isSearchMatch()) {
searchMatches.add(new Object[] {offset, offset + fragmentWidth, textBaseline, fragment, g.getFont()});
}
final int fixedWidth = myFixedWidths.get(i);
if (fixedWidth > 0 && fragmentWidth < fixedWidth) {
offset = fixedWidth;
} else {
offset += fragmentWidth;
}
}
// Paint focus border around the text and icon (if necessary)
if (myPaintFocusBorder && myBorder != null) {
if (focusAroundIcon) {
myBorder.paintBorder(this, g, 0, 0, getWidth(), getHeight());
} else {
myBorder.paintBorder(this, g, textStart, 0, getWidth() - textStart, getHeight());
}
}
// draw search matches after all
for (final Object[] info: searchMatches) {
UIUtil.drawSearchMatch(g, (Integer) info[0], (Integer) info[1], getHeight());
g.setFont((Font) info[4]);
if (shouldDrawMacShadow()) {
g.setColor(SHADOW_COLOR);
g.drawString((String) info[3], (Integer) info[0], (Integer) info[2] + 1);
}
g.setColor(new JBColor(Gray._50, Gray._0));
g.drawString((String) info[3], (Integer) info[0], (Integer) info[2]);
}
return offset;
}
private int computeTextAlignShift(@NotNull Font font) {
if (myTextAlign == SwingConstants.LEFT || myTextAlign == SwingConstants.LEADING) {
return 0;
}
int componentWidth = getSize().width;
int excessiveWidth = componentWidth - computePreferredSize(false).width;
if (excessiveWidth <= 0) {
return 0;
}
int textWidth = computeTextWidth(font, false);
if (myTextAlign == SwingConstants.CENTER) {
return excessiveWidth / 2;
}
else if (myTextAlign == SwingConstants.RIGHT || myTextAlign == SwingConstants.TRAILING) {
return excessiveWidth;
}
return 0;
}
protected boolean shouldDrawMacShadow() {
return false;
}
protected boolean shouldDrawDimmed() {
return false;
}
protected boolean shouldDrawBackground() {
return false;
}
protected void paintIcon(@NotNull Graphics g, @NotNull Icon icon, int offset) {
icon.paintIcon(this, g, offset, (getHeight() - icon.getIconHeight()) / 2);
}
protected void applyAdditionalHints(@NotNull Graphics g) {
}
@Override
public int getBaseline(int width, int height) {
super.getBaseline(width, height);
return getTextBaseLine(getFontMetrics(getFont()), height);
}
public boolean isTransparentIconBackground() {
return myTransparentIconBackground;
}
public void setTransparentIconBackground(boolean transparentIconBackground) {
myTransparentIconBackground = transparentIconBackground;
}
public static int getTextBaseLine(@NotNull FontMetrics metrics, final int height) {
return (height - metrics.getHeight()) / 2 + metrics.getAscent();
}
private static void checkCanPaint(Graphics g) {
if (UIUtil.isPrinting(g)) return;
/* wtf??
if (!isDisplayable()) {
LOG.assertTrue(false, logSwingPath());
}
*/
final Application application = ApplicationManager.getApplication();
if (application != null) {
application.assertIsDispatchThread();
}
else if (!SwingUtilities.isEventDispatchThread()) {
throw new RuntimeException(Thread.currentThread().toString());
}
}
@NotNull
private String logSwingPath() {
//noinspection HardCodedStringLiteral
final StringBuilder buffer = new StringBuilder("Components hierarchy:\n");
for (Container c = this; c != null; c = c.getParent()) {
buffer.append('\n');
buffer.append(c);
}
return buffer.toString();
}
protected void setBorderInsets(Insets insets) {
if (myBorder instanceof MyBorder) {
((MyBorder)myBorder).setInsets(insets);
}
revalidateAndRepaint();
}
private static final class MyBorder implements Border {
private Insets myInsets;
public MyBorder() {
myInsets = new Insets(1, 1, 1, 1);
}
public void setInsets(final Insets insets) {
myInsets = insets;
}
@Override
public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
g.setColor(Color.BLACK);
UIUtil.drawDottedRectangle(g, x, y, x + width - 1, y + height - 1);
}
@Override
public Insets getBorderInsets(final Component c) {
return myInsets;
}
@Override
public boolean isBorderOpaque() {
return true;
}
}
@NotNull
public CharSequence getCharSequence(boolean mainOnly) {
List<String> fragments = mainOnly && myMainTextLastIndex > -1 && myMainTextLastIndex + 1 < myFragments.size()?
myFragments.subList(0, myMainTextLastIndex + 1) : myFragments;
return StringUtil.join(fragments, "");
}
@Override
public String toString() {
return getCharSequence(false).toString();
}
public void change(@NotNull Runnable runnable, boolean autoInvalidate) {
boolean old = myAutoInvalidate;
myAutoInvalidate = autoInvalidate;
try {
runnable.run();
} finally {
myAutoInvalidate = old;
}
}
@Override
public AccessibleContext getAccessibleContext() {
return myContext;
}
private static class MyAccessibleContext extends AccessibleContext {
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.AWT_COMPONENT;
}
@Override
public AccessibleStateSet getAccessibleStateSet() {
return new AccessibleStateSet();
}
@Override
public int getAccessibleIndexInParent() {
return 0;
}
@Override
public int getAccessibleChildrenCount() {
return 0;
}
@Nullable
@Override
public Accessible getAccessibleChild(int i) {
return null;
}
@Override
public Locale getLocale() throws IllegalComponentStateException {
return Locale.getDefault();
}
}
public static class BrowserLauncherTag implements Runnable {
private final String myUrl;
public BrowserLauncherTag(@NotNull String url) {
myUrl = url;
}
@Override
public void run() {
BrowserUtil.browse(myUrl);
}
}
public interface ColoredIterator extends Iterator<String> {
int getOffset();
int getEndOffset();
@NotNull
String getFragment();
@NotNull
SimpleTextAttributes getTextAttributes();
int split(int offset, @NotNull SimpleTextAttributes attributes);
}
private class MyIterator implements ColoredIterator {
int myIndex = -1;
int myOffset;
int myEndOffset;
@Override
public int getOffset() {
return myOffset;
}
@Override
public int getEndOffset() {
return myEndOffset;
}
@NotNull
@Override
public String getFragment() {
return myFragments.get(myIndex);
}
@NotNull
@Override
public SimpleTextAttributes getTextAttributes() {
return myAttributes.get(myIndex);
}
@Override
public int split(int offset, @NotNull SimpleTextAttributes attributes) {
if (offset < 0 || offset > myEndOffset - myOffset) {
throw new IllegalArgumentException(offset + " is not within [0, " + (myEndOffset - myOffset) + "]");
}
if (offset == myEndOffset - myOffset) { // replace
myAttributes.set(myIndex, attributes);
}
else if (offset > 0) { // split
String text = getFragment();
myFragments.set(myIndex, text.substring(0, offset));
myAttributes.add(myIndex, attributes);
myFragments.add(myIndex + 1, text.substring(offset));
if (myFragmentTags != null && myFragmentTags.size() > myIndex) {
myFragmentTags.add(myIndex, myFragments.get(myIndex));
}
myIndex ++;
}
myOffset += offset;
return myOffset;
}
@Override
public boolean hasNext() {
return myIndex + 1 < myFragments.size();
}
@Override
public String next() {
myIndex ++;
myOffset = myEndOffset;
String text = getFragment();
myEndOffset += text.length();
return text;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}