/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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.android.tools.idea.ui;
import com.google.common.collect.Lists;
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.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.*;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntObjectHashMap;
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.lang.IllegalArgumentException;
import java.util.*;
import java.util.List;
@SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext", "FieldAccessedSynchronizedAndUnsynchronized", "UnusedDeclaration"})
public class WrapAwareColoredComponent 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;
@NotNull private final List<String> myFragments = Lists.newArrayListWithCapacity(3);
@NotNull private final List<SimpleTextAttributes> myAttributes = Lists.newArrayListWithCapacity(3);
@NotNull private final TIntObjectHashMap<TIntArrayList> myBreakOffsets = new TIntObjectHashMap<TIntArrayList>();
@NotNull private final TIntIntHashMap myLineHeights = new TIntIntHashMap();
@NotNull private final Dimension myTextDimensions = new Dimension();
@NotNull private final WrapsAwareTextHelper myTextHelper = new WrapsAwareTextHelper(this);
@NotNull private final String myLineBreakMarker;
/**
* Internal padding
*/
@NotNull private Insets myIpad = new Insets(1, 2, 1, 2);
/**
* 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>.
*/
@Nullable private Border myBorder = new MyBorder();
@Nullable private List<Object> myFragmentTags;
/**
* Component's icon. It can be <code>null</code>.
*/
@Nullable private Icon myIcon;
/**
* Holds value of the last width limit used for {@link #computeTextDimension(Font, boolean, int) calculating text dimensions}.
* <p/>
* E.g. we can safely use {@link #myTextDimensions cached text dimensions} if value of this field is not null and equals
* to the {@link #getWidth() current width}.
*/
@Nullable private Integer myLastUsedWidthLimit;
/**
* Gap between icon and text. It is used only if icon is defined.
*/
protected int myIconTextGap = 2;
/**
* 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;
private int myMainTextLastIndex = -1;
private final TIntIntHashMap myFixedWidths = new TIntIntHashMap(10);
@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;
private boolean myWrapText;
public WrapAwareColoredComponent() {
setOpaque(true);
WrapsAwareTextHelper.appendLineBreak(myFragments);
myLineBreakMarker = myFragments.get(0);
myFragments.clear();
}
@NotNull
public ColoredIterator iterator() {
return new MyIterator();
}
public boolean isIconOnTheRight() {
return myIconOnTheRight;
}
public void setIconOnTheRight(boolean iconOnTheRight) {
myIconOnTheRight = iconOnTheRight;
}
@NotNull
public WrapAwareColoredComponent appendLineBreak() {
WrapsAwareTextHelper.appendLineBreak(myFragments);
myAttributes.add(SimpleTextAttributes.REGULAR_ATTRIBUTES);
myMainTextLastIndex = myFragments.size() - 1;
resetTextLayoutCache();
return this;
}
@NotNull
public final WrapAwareColoredComponent 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;
}
resetTextLayoutCache();
}
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(@NotNull String fragment, @NotNull SimpleTextAttributes attributes, @Nullable 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();
resetTextLayoutCache();
}
public void resetTextLayoutCache() {
myLastUsedWidthLimit = null;
myBreakOffsets.clear();
}
/**
* @return component's icon. This method returns <code>null</code>
* if there is no icon.
*/
@Nullable
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 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();
}
@Nullable
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);
int height = myIpad.top + myIpad.bottom;
width += myIpad.right + borderInsets.right;
// 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
}
assert font != null;
Dimension textDimension = computeTextDimension(font, mainTextOnly, myWrapText ? getWidth() - width : 0);
width += textDimension.width;
int textHeight = textDimension.height;
textHeight += borderInsets.top + borderInsets.bottom;
if (myIcon != null) {
height += Math.max(myIcon.getIconHeight(), textHeight);
}
else {
height += textHeight;
}
return new Dimension(width, height);
}
@NotNull
private Dimension computeTextDimension(@NotNull Font font, final boolean mainTextOnly, int widthLimit) {
if (myLastUsedWidthLimit != null && widthLimit == myLastUsedWidthLimit) {
return myTextDimensions;
}
final List<String> fragmentsToUse;
final List<SimpleTextAttributes> attributesToUse;
if (mainTextOnly && myMainTextLastIndex >= 0 && myMainTextLastIndex < myFragments.size() - 1) {
fragmentsToUse = myFragments.subList(0, myMainTextLastIndex);
attributesToUse = myAttributes.subList(0, myMainTextLastIndex);
}
else {
fragmentsToUse = myFragments;
attributesToUse = myAttributes;
}
myBreakOffsets.clear();
myLineHeights.clear();
myTextHelper.wrap(fragmentsToUse, attributesToUse, font, myFixedWidths, widthLimit, myTextDimensions, myBreakOffsets, myLineHeights);
myLastUsedWidthLimit = widthLimit;
return myTextDimensions;
}
/**
* 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 y) {
// Make sure text wraps are properly calculated
computePreferredSize(false);
int curX = myIpad.left;
if (myIcon != null) {
final int iconStartX;
if (myIconOnTheRight) {
iconStartX = curX + myTextDimensions.width + myIconTextGap;
}
else {
iconStartX = curX;
curX += myIcon.getIconWidth() + myIconTextGap;
}
if (x >= iconStartX && x < iconStartX + myIcon.getIconWidth()) {
return FRAGMENT_ICON;
}
}
if (x - curX >= 0 && x - curX < myTextDimensions.width && y >= 0 && y <= myTextDimensions.height) {
return myTextHelper.mapFragment(myFragments, myAttributes, myFixedWidths, myBreakOffsets, myLineHeights, getFont(), x - curX, y);
}
else {
return -1;
}
}
@Nullable
public Object getFragmentTagAt(int x, int y) {
int index = findFragmentAt(x, y);
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>");
}
}
@NotNull
private static String convertFragment(@NotNull 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(@NotNull final Graphics g) {
try {
_doPaint(g);
}
catch (RuntimeException e) {
LOG.error(logSwingPath(), e);
throw e;
}
}
private synchronized void _doPaint(@NotNull final Graphics g) {
checkCanPaint(g);
doPaint((Graphics2D)g);
}
protected void doPaint(@NotNull 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(@NotNull Graphics2D g, int offset) {
if (isOpaque() || shouldDrawBackground()) {
paintBackground(g, offset, getWidth() - offset, getHeight());
}
}
protected void paintBackground(@NotNull 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(@NotNull Graphics2D g, int offset, boolean focusAroundIcon) {
// Force using right text dimensions.
computePreferredSize(false);
// 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;
int x = offset;
int y = 0;
int line = 0;
boolean beforePaintTextCalled = 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);
int lineHeight = myLineHeights.get(line++);
if (lineHeight <= 0) {
lineHeight = metrics.getHeight();
}
final String wholeFragmentTextToDraw = myFragments.get(i);
if (myLineBreakMarker.equals(wholeFragmentTextToDraw)) {
y += lineHeight;
x = offset;
continue;
}
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);
for (TextRange range = nextFragmentLineRange(i, null); range != null; range = nextFragmentLineRange(i, range)) {
if (range.getStartOffset() > 0) { // This is not the first fragment's part, i.e. it was long enough to be split into multiple lines.
lineHeight = myLineHeights.get(++line);
if (lineHeight <= 0) {
lineHeight = metrics.getHeight();
}
x = offset;
y += lineHeight;
}
String textToDraw = wholeFragmentTextToDraw.substring(range.getStartOffset(), range.getEndOffset());
final int textWidth = isOracleRetina ? GraphicsUtil.stringWidth(textToDraw, font) : metrics.stringWidth(textToDraw);
final int textBaseline = y + getTextBaseLine(metrics, lineHeight);
if (!beforePaintTextCalled) {
beforePaintText(g, x, textBaseline);
}
final Color bgColor = attributes.isSearchMatch() ? null : attributes.getBgColor();
if ((attributes.isOpaque() || isOpaque()) && bgColor != null) {
g.setColor(bgColor);
g.fillRect(x, y, textWidth, lineHeight);
}
if (!attributes.isSearchMatch()) {
if (shouldDrawMacShadow()) {
g.setColor(SHADOW_COLOR);
g.drawString(textToDraw, x, textBaseline + 1);
}
if (shouldDrawDimmed()) {
color = ColorUtil.dimmer(color);
}
g.setColor(color);
g.drawString(textToDraw, x, textBaseline);
}
// 1. Strikeout effect
if (attributes.isStrikeout()) {
final int strikeOutAt = textBaseline + (metrics.getDescent() - metrics.getAscent()) / 2;
UIUtil.drawLine(g, x, strikeOutAt, x + textWidth, strikeOutAt);
}
// 2. Waved effect
if (attributes.isWaved()) {
if (attributes.getWaveColor() != null) {
g.setColor(attributes.getWaveColor());
}
final int wavedAt = textBaseline + 1;
for (int waveX = x; waveX <= x + textWidth; waveX += 4) {
UIUtil.drawLine(g, waveX, wavedAt, waveX + 2, wavedAt + 2);
UIUtil.drawLine(g, waveX + 3, wavedAt + 1, waveX + 4, wavedAt);
}
}
// 3. Underline
if (attributes.isUnderline()) {
final int underlineAt = textBaseline + 1;
UIUtil.drawLine(g, x, underlineAt, x + textWidth, underlineAt);
}
// 4. Bold Dotted Line
if (attributes.isBoldDottedLine()) {
final int dottedAt = SystemInfo.isMac ? textBaseline : textBaseline + 1;
final Color lineColor = attributes.getWaveColor();
UIUtil.drawBoldDottedLine(g, x, x + textWidth, dottedAt, bgColor, lineColor, isOpaque());
}
if (attributes.isSearchMatch()) {
searchMatches.add(new Object[]{x, x + textWidth, textBaseline, textToDraw, g.getFont(), lineHeight});
}
final int fixedWidth = myFixedWidths.get(i);
if (fixedWidth > 0 && textWidth < fixedWidth) {
x += fixedWidth;
}
else {
x += textWidth;
}
}
}
// 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], (Integer)info[5]);
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;
}
protected void beforePaintText(@NotNull Graphics g, int x, int textBaseLine) {
}
/**
* There is a possible case that particular text fragment is displayed at more than one line. It's assumed that information about
* such inner fragment line break offsets is stored at the {@link #myBreakOffsets} field.
* <p/>
* This helper method assumes to be used during iterative fragment parts processing, i.e. it receives a text range within the target
* fragment (identified by it's index at the {@link #myFragments} collection) and returns text range for the next part of the fragment
* to be drawn
*
* @param fragmentIndex target fragment's index within the {@link #myFragments fragments collection}
* @param previousFragmentLineRange text range for the fragment's part used the last time (<code>null</code> value indicates that
* the fragment hasn't been used yet)
* @return text fragment for the fragment's part to be shown at new line (if any); <code>null</code> as an
* indication that the target fragment has been completely processed
*/
@Nullable
private TextRange nextFragmentLineRange(int fragmentIndex, @Nullable TextRange previousFragmentLineRange) {
TIntArrayList breakOffsets = myBreakOffsets.get(fragmentIndex);
String fragmentText = myFragments.get(fragmentIndex);
if (breakOffsets == null || breakOffsets.isEmpty()) {
if (previousFragmentLineRange == null) {
return TextRange.allOf(fragmentText);
}
else {
return null;
}
}
if (previousFragmentLineRange == null) {
return TextRange.create(0, breakOffsets.get(0));
}
for (int i = 0; i < breakOffsets.size(); i++) {
if (breakOffsets.get(i) == previousFragmentLineRange.getEndOffset()) {
if (i < breakOffsets.size() - 1) {
return TextRange.create(previousFragmentLineRange.getEndOffset(), breakOffsets.get(i + 1));
}
else {
return TextRange.create(previousFragmentLineRange.getEndOffset(), fragmentText.length());
}
}
}
return null;
}
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;
}
Dimension textDimension = computeTextDimension(font, false, myWrapText ? getWidth() : 0);
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) {
final int y;
if (myLineHeights.size() <= 1) {
// Draw icon center-aligned in case one ore less text lines.
y = (getHeight() - icon.getIconHeight()) / 2;
}
else {
// Draw icon at the first text line instead.
if (icon.getIconHeight() > myLineHeights.get(0)) {
y = myIpad.top;
}
else {
y = myIpad.top + (myLineHeights.get(0) - icon.getIconHeight()) / 2;
}
}
icon.paintIcon(this, g, offset, y);
}
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;
}
/**
* Instructs current component to display {@link #append(String) encapsulated text} in a way to avoid it to go beyond the horizontal
* visible area.
* <p/>
* Example:
* <pre>
* Say, we have a situation like below:
*
* | |
* | |<-- visible area
* | |
* |1234567|89
* | |
*
* Wrapped text is shown as follows then:
*
* | |
* |1234567|
* |89 |
* | |
* </pre>
*
* @param wrapText a flag which indicates if target text shown by the current control should be wrapped
*/
public void setWrapText(boolean wrapText) {
if (myWrapText != wrapText) {
resetTextLayoutCache();
}
myWrapText = wrapText;
}
public static int getTextBaseLine(@NotNull FontMetrics metrics, final int height) {
return (height - metrics.getHeight()) / 2 + metrics.getAscent();
}
private static void checkCanPaint(@NotNull 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(@NotNull Insets insets) {
if (myBorder instanceof MyBorder) {
((MyBorder)myBorder).setInsets(insets);
}
revalidateAndRepaint();
}
private static final class MyBorder implements Border {
@NotNull private Insets myInsets;
public MyBorder() {
myInsets = new Insets(1, 1, 1, 1);
}
public void setInsets(@NotNull 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(JBColor.BLACK);
UIUtil.drawDottedRectangle(g, x, y, x + width - 1, y + height - 1);
}
@Override
public Insets getBorderInsets(@NotNull 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, "");
}
@NotNull
@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();
}
}
}