com.alee.painter.decoration.content.AbstractTextContent Maven / Gradle / Ivy
/*
* This file is part of WebLookAndFeel library.
*
* WebLookAndFeel library is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* WebLookAndFeel library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with WebLookAndFeel library. If not, see .
*/
package com.alee.painter.decoration.content;
import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.api.clone.behavior.OmitOnClone;
import com.alee.api.jdk.Objects;
import com.alee.api.merge.behavior.OmitOnMerge;
import com.alee.painter.decoration.DecorationException;
import com.alee.painter.decoration.IDecoration;
import com.alee.utils.ColorUtils;
import com.alee.utils.GraphicsUtils;
import com.alee.utils.SwingUtils;
import com.alee.utils.TextUtils;
import com.alee.utils.swing.BasicHTML;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
import javax.swing.*;
import javax.swing.text.View;
import java.awt.*;
import java.util.Map;
/**
* Abstract implementation of simple text content.
*
* @param component type
* @param decoration type
* @param content type
* @author Mikle Garin
* @author Alexandr Zernov
*/
public abstract class AbstractTextContent, I extends AbstractTextContent>
extends AbstractContent implements SwingConstants
{
/**
* todo 1. Move shadow settings into separate serializable object
*/
/**
* Preferred text antialias option.
*/
@Nullable
@XStreamAsAttribute
protected TextRasterization rasterization;
/**
* Text foreground color.
*/
@Nullable
@XStreamAsAttribute
protected Color color;
/**
* Whether or not custom color should be ignored in favor of {@link #color}, unless it is {@code null}.
* Custom color is a non-{@link javax.swing.plaf.UIResource} color specified as {@link Component}'s foreground.
*/
@Nullable
@XStreamAsAttribute
protected Boolean ignoreCustomColor;
/**
* Horizontal text alignment.
* todo Replace with BoxOrientation or new HorizontalAlignment
*/
@Nullable
@XStreamAsAttribute
protected Integer halign;
/**
* Vertical text alignment.
* todo Replace with BoxOrientation or new VerticalAlignment
*/
@Nullable
@XStreamAsAttribute
protected Integer valign;
/**
* Whether or not text should be truncated when it gets outside of the available bounds.
*/
@Nullable
@XStreamAsAttribute
protected Boolean truncate;
/**
* Whether or not should paint text shadow.
*/
@Nullable
@XStreamAsAttribute
protected Boolean shadow;
/**
* Text shadow color.
*/
@Nullable
@XStreamAsAttribute
protected Color shadowColor;
/**
* Text shadow size.
*/
@Nullable
@XStreamAsAttribute
protected Integer shadowSize;
/**
* Text shadow opacity.
*/
@Nullable
@XStreamAsAttribute
protected Float shadowOpacity;
/**
* Cached HTML {@link View} settings.
* This is runtime-only field that should not be serialized.
* It is also generated automatically on demand if missing.
*
* @see #getHtml(javax.swing.JComponent, com.alee.painter.decoration.IDecoration)
* @see #cleanupHtml(javax.swing.JComponent, com.alee.painter.decoration.IDecoration)
*/
@Nullable
@OmitOnClone
@OmitOnMerge
protected transient String htmlSettings;
/**
* Cached HTML {@link View}.
* This is runtime-only field that should not be serialized.
* It is also generated automatically on demand if missing.
*
* @see #getHtml(javax.swing.JComponent, com.alee.painter.decoration.IDecoration)
* @see #cleanupHtml(javax.swing.JComponent, com.alee.painter.decoration.IDecoration)
*/
@Nullable
@OmitOnClone
@OmitOnMerge
protected transient View htmlView;
@NotNull
@Override
public String getId ()
{
return id != null ? id : "text";
}
@Override
public boolean isEmpty ( @NotNull final C c, @NotNull final D d )
{
return TextUtils.isEmpty ( getText ( c, d ) );
}
@Override
public void deactivate ( @NotNull final C c, @NotNull final D d )
{
// Performing default actions
super.deactivate ( c, d );
// Cleaning up HTML caches
// todo This might lower HTML performance for components with multiple states like buttons
// todo Perfectly HTML should be cleaned up only when decoration is removed from possible decorations and not just deactivated
cleanupHtml ( c, d );
}
/**
* Returns preferred rasterization option.
*
* @param c painted component
* @param d painted decoration state
* @return preferred rasterization option
*/
@NotNull
public TextRasterization getRasterization ( @NotNull final C c, @NotNull final D d )
{
return rasterization != null ? rasterization : TextRasterization.subpixel;
}
/**
* Returns text font.
*
* @param c painted component
* @param d painted decoration state
* @return text font
*/
@NotNull
protected Font getFont ( @NotNull final C c, @NotNull final D d )
{
return c.getFont ();
}
/**
* Returns text font metrics.
*
* @param c painted component
* @param d painted decoration state
* @return text font metrics
*/
@NotNull
protected FontMetrics getFontMetrics ( @NotNull final C c, @NotNull final D d )
{
return c.getFontMetrics ( getFont ( c, d ) );
}
/**
* Returns text foreground color.
*
* @param c painted component
* @param d painted decoration state
* @return text foreground color
*/
@NotNull
protected Color getColor ( @NotNull final C c, @NotNull final D d )
{
// This {@link javax.swing.plaf.UIResource} check allows us to ignore such colors in favor of style ones
// But this will not ignore custom color set from the code as this component foreground unless explicitly said to do so
return color != null && ( isIgnoreCustomColor ( c, d ) || SwingUtils.isUIResource ( getCustomColor ( c, d ) ) )
? color
: getCustomColor ( c, d );
}
/**
* Returns custom text foreground color.
*
* @param c painted component
* @param d painted decoration state
* @return custom text foreground color
*/
@NotNull
protected Color getCustomColor ( @NotNull final C c, @NotNull final D d )
{
return c.getForeground ();
}
/**
* Returns whether or not custom color should be ignored in favor of {@link #color}, unless it is {@code null}.
* Custom color is a non-{@link javax.swing.plaf.UIResource} color specified as {@link Component}'s foreground.
*
* @param c painted component
* @param d painted decoration state
* @return {@code true} if custom color should be ignored in favor of {@link #color}, unless it is {@code null}, {@code false} otherwise
*/
public boolean isIgnoreCustomColor ( @NotNull final C c, @NotNull final D d )
{
return ignoreCustomColor != null && ignoreCustomColor;
}
/**
* Returns text horizontal alignment.
*
* @param c painted component
* @param d painted decoration state
* @return text horizontal alignment
*/
protected int getHorizontalAlignment ( @NotNull final C c, @NotNull final D d )
{
return halign != null ? halign : LEADING;
}
/**
* Returns text horizontal alignment adjusted according to the component orientation.
*
* @param c painted component
* @param d painted decoration state
* @return text horizontal alignment
*/
protected int getAdjustedHorizontalAlignment ( @NotNull final C c, @NotNull final D d )
{
final int adjusted;
final int alignment = getHorizontalAlignment ( c, d );
switch ( alignment )
{
case LEADING:
adjusted = isLeftToRight ( c, d ) ? LEFT : RIGHT;
break;
case TRAILING:
adjusted = isLeftToRight ( c, d ) ? RIGHT : LEFT;
break;
default:
adjusted = alignment;
break;
}
return adjusted;
}
/**
* Returns text vertical alignment.
*
* @param c painted component
* @param d painted decoration state
* @return text vertical alignment
*/
protected int getVerticalAlignment ( @NotNull final C c, @NotNull final D d )
{
return valign != null ? valign : CENTER;
}
/**
* Returns whether or not text should be truncated when it gets outside of the available bounds.
*
* @param c painted component
* @param d painted decoration state
* @return true if text should be truncated when it gets outside of the available bounds, false otherwise
*/
protected boolean isTruncate ( @NotNull final C c, @NotNull final D d )
{
return truncate == null || truncate;
}
/**
* Returns whether or not text shadow should be painted.
*
* @param c painted component
* @param d painted decoration state
* @return true if text shadow should be painted, false otherwise
*/
protected boolean isShadow ( @NotNull final C c, @NotNull final D d )
{
return shadow != null && shadow;
}
/**
* Returns shadow color.
*
* @param c painted component
* @param d painted decoration state
* @return shadow color
*/
@NotNull
protected Color getShadowColor ( @NotNull final C c, @NotNull final D d )
{
if ( shadowColor == null )
{
throw new DecorationException ( "Shadow color must be specified" );
}
return shadowColor;
}
/**
* Returns shadow size.
*
* @param c painted component
* @param d painted decoration state
* @return shadow size
*/
protected int getShadowSize ( @NotNull final C c, @NotNull final D d )
{
return shadowSize != null ? shadowSize : 2;
}
/**
* Returns shadow opacity.
*
* @param c painted component
* @param d painted decoration state
* @return shadow opacity
*/
public float getShadowOpacity ( @NotNull final C c, @NotNull final D d )
{
return shadowOpacity != null ? shadowOpacity : 0.8f;
}
/**
* Returns whether or not text contains HTML.
*
* @param c painted component
* @param d painted decoration state
* @return true if text contains HTML, false otherwise
*/
protected boolean isHtmlText ( @NotNull final C c, @NotNull final D d )
{
// Determining whether or not text contains HTML
final String text = getText ( c, d );
final boolean html = BasicHTML.isHTMLString ( c, text );
// Cleaning up HTML caches
if ( !html )
{
cleanupHtml ( c, d );
}
return html;
}
/**
* Returns HTML text view to be painted.
*
* @param c painted component
* @param d painted decoration state
* @return HTML text view to be painted
*/
@NotNull
protected View getHtml ( @NotNull final C c, @NotNull final D d )
{
// HTML content settings
final String text = getText ( c, d );
final Font font = getFont ( c, d );
final Color foreground = getColor ( c, d );
// HTML view settings
final String settings = text + ";" + font + ";" + foreground;
// Updating HTML view if needed
if ( htmlView == null || Objects.notEquals ( htmlSettings, settings ) )
{
htmlSettings = settings;
htmlView = BasicHTML.createHTMLView ( c, text, font, foreground );
}
// Return cached HTML view
return htmlView;
}
/**
* Cleans up HTML text view caches.
*
* @param c painted component
* @param d painted decoration state
*/
protected void cleanupHtml ( @NotNull final C c, @NotNull final D d )
{
htmlSettings = null;
htmlView = null;
}
/**
* Returns text to be painted.
*
* @param c painted component
* @param d painted decoration state
* @return text to be painted
*/
@Nullable
protected abstract String getText ( @NotNull C c, @NotNull D d );
/**
* Returns mnemonic index or {@code -1} if it shouldn't be displayed.
*
* @param c painted component
* @param d painted decoration state
* @return mnemonic index or {@code -1} if it shouldn't be displayed
*/
protected abstract int getMnemonicIndex ( @NotNull C c, @NotNull D d );
@Override
public boolean hasContentBaseline ( @NotNull final C c, @NotNull final D d )
{
return !isHtmlText ( c, d );
}
@Override
public int getContentBaseline ( @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds )
{
final int baseline;
if ( !isHtmlText ( c, d ) )
{
final FontMetrics fm = getFontMetrics ( c, d );
baseline = getTextY ( c, d, bounds, fm );
}
else
{
baseline = -1;
}
return baseline;
}
@NotNull
@Override
public Component.BaselineResizeBehavior getContentBaselineResizeBehavior ( @NotNull final C c, @NotNull final D d )
{
final Component.BaselineResizeBehavior result;
if ( !isHtmlText ( c, d ) )
{
switch ( getVerticalAlignment ( c, d ) )
{
case TOP:
result = Component.BaselineResizeBehavior.CONSTANT_ASCENT;
break;
case BOTTOM:
result = Component.BaselineResizeBehavior.CONSTANT_DESCENT;
break;
case CENTER:
result = Component.BaselineResizeBehavior.CENTER_OFFSET;
break;
default:
result = Component.BaselineResizeBehavior.OTHER;
break;
}
}
else
{
result = Component.BaselineResizeBehavior.OTHER;
}
return result;
}
@Override
protected void paintContent ( @NotNull final Graphics2D g2d, @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds )
{
// Ensure that text painting is allowed
if ( !isEmpty ( c, d ) )
{
// Applying graphics settings
final Font oldFont = GraphicsUtils.setupFont ( g2d, getFont ( c, d ) );
// Installing text antialias settings
final TextRasterization rasterization = getRasterization ( c, d );
final Map oldHints = SwingUtils.setupTextAntialias ( g2d, rasterization );
// Paint color
final Paint op = GraphicsUtils.setupPaint ( g2d, getColor ( c, d ) );
// Painting either HTML or plain text
if ( isHtmlText ( c, d ) )
{
paintHtml ( g2d, c, d, bounds );
}
else
{
final int shadowWidth = isShadow ( c, d ) ? getShadowSize ( c, d ) : 0;
bounds.x += shadowWidth;
bounds.width -= shadowWidth * 2;
paintText ( g2d, c, d, bounds );
}
// Restoring paint color
GraphicsUtils.restorePaint ( g2d, op );
// Restoring text antialias settings
SwingUtils.restoreTextAntialias ( g2d, oldHints );
// Restoring graphics settings
GraphicsUtils.restoreFont ( g2d, oldFont );
}
}
/**
* Paints HTML text view.
* Note that HTML text is usually not affected by the graphics paint settings as it defines its own one inside.
* This is why custom {@link com.alee.utils.swing.BasicHTML} class exists to provide proper body text color upon view creation.
*
* @param g2d graphics context
* @param c painted component
* @param d painted decoration state
* @param bounds painting bounds
*/
protected void paintHtml ( @NotNull final Graphics2D g2d, @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds )
{
getHtml ( c, d ).paint ( g2d, bounds );
}
/**
* Paints plain text view.
*
* @param g2d graphics context
* @param c painted component
* @param d painted decoration state
* @param bounds painting bounds
*/
protected void paintText ( @NotNull final Graphics2D g2d, @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds )
{
// Painting settings
final String text = getText ( c, d );
if ( TextUtils.notEmpty ( text ) )
{
final FontMetrics fontMetrics = getFontMetrics ( c, d );
final int textWidth = fontMetrics.stringWidth ( text );
final int horizontalAlignment = getAdjustedHorizontalAlignment ( c, d );
final int mnemonicIndex = getMnemonicIndex ( c, d );
// Calculating text coordinates
final int textX = getTextX ( c, d, bounds, textWidth, horizontalAlignment );
final int textY = getTextY ( c, d, bounds, fontMetrics );
// Truncating text if needed
final String paintedText = getPaintedText ( c, d, bounds, text, fontMetrics, textWidth, horizontalAlignment );
// Painting text
paintTextFragment ( c, d, g2d, paintedText, textX, textY, mnemonicIndex );
}
}
/**
* Returns text X coordinate within component bounds.
*
* @param c painted component
* @param d painted decoration state
* @param bounds painting bounds
* @param width text width
* @param alignment horizontal text alignment
* @return text X coordinate within component bounds
*/
protected int getTextX ( @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds, final int width, final int alignment )
{
int textX = bounds.x;
if ( width < bounds.width )
{
switch ( alignment )
{
case LEFT:
break;
case CENTER:
textX += Math.floor ( ( bounds.width - width ) / 2.0 );
break;
case RIGHT:
textX += bounds.width - width;
break;
default:
throw new DecorationException ( "Incorrect horizontal alignment provided: " + alignment );
}
}
return textX;
}
/**
* Returns text Y coordinate within component bounds.
*
* @param c painted component
* @param d painted decoration state
* @param bounds painting bounds
* @param fm font metrics
* @return text Y coordinate within component bounds
*/
protected int getTextY ( @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds, @NotNull final FontMetrics fm )
{
int textY = bounds.y;
// Adjusting coordinates according to vertical alignment
final int va = getVerticalAlignment ( c, d );
switch ( va )
{
case TOP:
textY += fm.getAscent ();
break;
case CENTER:
textY += Math.ceil ( ( bounds.height + fm.getAscent () - fm.getLeading () - fm.getDescent () ) / 2.0 );
break;
case BOTTOM:
textY += bounds.height - fm.getHeight ();
break;
default:
throw new DecorationException ( "Incorrect vertical alignment provided: " + va );
}
return textY;
}
/**
* Returns text that will actually be painted.
*
* @param c painted component
* @param d painted decoration state
* @param bounds painting bounds
* @param text text to paint
* @param fm font metrics
* @param width text width
* @param alignment horizontal text alignment
* @return text that will actually be painted
*/
@NotNull
protected String getPaintedText ( @NotNull final C c, @NotNull final D d, @NotNull final Rectangle bounds, @NotNull final String text,
@NotNull final FontMetrics fm, final int width, final int alignment )
{
final String paintedText;
if ( isTruncate ( c, d ) && bounds.width < width )
{
paintedText = SwingUtilities.layoutCompoundLabel (
fm, text, null, 0, alignment, 0, 0,
bounds, new Rectangle (), new Rectangle (), 0
);
}
else
{
paintedText = text;
}
return paintedText;
}
/**
* Paints text fragment.
*
* @param c painted component
* @param d painted decoration state
* @param g2d graphics context
* @param text text fragment
* @param textX text X coordinate
* @param textY text Y coordinate
* @param mnemonicIndex index of mnemonic
*/
protected void paintTextFragment ( @NotNull final C c, @NotNull final D d, @NotNull final Graphics2D g2d, @NotNull final String text,
final int textX, final int textY, final int mnemonicIndex )
{
// Painting text shadow
paintTextShadow ( c, d, g2d, text, textX, textY );
// Painting text itself
paintTextString ( c, d, g2d, text, textX, textY );
// Painting mnemonic
paintMnemonic ( c, d, g2d, text, mnemonicIndex, textX, textY );
}
/**
* Draw a string with a blur or shadow effect. The light angle is assumed to be 0 degrees, (i.e., window is illuminated from top).
* The effect is intended to be subtle to be usable in as many text components as possible. The effect is generated with multiple calls
* to draw string. This method paints the text on coordinates {@code tx}, {@code ty}. If text should be painted elsewhere, a transform
* should be applied to the graphics before passing it.
*
* @param c painted component
* @param d painted decoration state
* @param g2d graphics context
* @param text text to paint
* @param textX text X coordinate
* @param textY text Y coordinate
*/
protected void paintTextShadow ( @NotNull final C c, @NotNull final D d, @NotNull final Graphics2D g2d, @NotNull final String text,
final int textX, final int textY )
{
if ( isShadow ( c, d ) )
{
// This is required to properly render sub-pixel text antialias
final RenderingHints rh = g2d.getRenderingHints ();
// Shadow settings
final float opacity = getShadowOpacity ( c, d );
final int size = getShadowSize ( c, d );
final Color color = getShadowColor ( c, d );
final double tx = -size;
final double ty = 1 - size;
/* todo final boolean isShadow = true; - replace with shadow type? #557 */
// Configuring graphics
final Composite oldComposite = g2d.getComposite ();
final Color oldColor = g2d.getColor ();
g2d.translate ( textX + tx, textY + ty );
// Use a alpha blend smaller than 1 to prevent the effect from becoming too dark when multiple paints occur on top of each other
float preAlpha = 0.4f;
if ( oldComposite instanceof AlphaComposite )
{
final AlphaComposite alphaComposite = ( AlphaComposite ) oldComposite;
if ( alphaComposite.getRule () == AlphaComposite.SRC_OVER )
{
// Make sure alpha blend is adjusted by composite passed from above
preAlpha = alphaComposite.getAlpha () * preAlpha;
}
}
g2d.setPaint ( ColorUtils.opaque ( color ) );
// If the effect is a shadow it looks better to stop painting a bit earlier - shadow will look softer
final int maxSize = /*isShadow ?*/ size - 1 /*: size*/;
for ( int i = -size; i <= maxSize; i++ )
{
for ( int j = -size; j <= maxSize; j++ )
{
final double distance = i * i + j * j;
float alpha;
if ( distance > 0.0d )
{
alpha = ( float ) ( 1.0f / ( distance * size * opacity ) );
}
else
{
alpha = opacity;
}
alpha *= preAlpha;
if ( alpha > 1.0f )
{
alpha = 1.0f;
}
g2d.setComposite ( AlphaComposite.getInstance ( AlphaComposite.SRC_OVER, alpha ) );
g2d.drawString ( text, i + size, j + size );
}
}
// Restore graphics
g2d.translate ( -textX - tx, -textY - ty );
g2d.setComposite ( oldComposite );
g2d.setPaint ( oldColor );
// This is required to properly render sub-pixel text antialias
g2d.setRenderingHints ( rh );
}
}
/**
* Paints text string.
*
* @param c painted component
* @param d painted decoration state
* @param g2d graphics context
* @param text text to paint
* @param textX text X coordinate
* @param textY text Y coordinate
*/
protected void paintTextString ( @NotNull final C c, @NotNull final D d, @NotNull final Graphics2D g2d, @NotNull final String text,
final int textX, final int textY )
{
g2d.drawString ( text, textX, textY );
}
/**
* Paints underlined at the specified character index.
*
* @param c painted component
* @param d painted decoration state
* @param g2d graphics context
* @param text text to paint
* @param mnemonicIndex index of mnemonic
* @param textX text X coordinate
* @param textY text Y coordinate
*/
protected void paintMnemonic ( @NotNull final C c,@NotNull final D d, @NotNull final Graphics2D g2d, @NotNull final String text,
final int mnemonicIndex, final int textX, final int textY )
{
if ( mnemonicIndex >= 0 && mnemonicIndex < text.length () )
{
final FontMetrics fm = getFontMetrics ( c, d );
g2d.fillRect ( textX + fm.stringWidth ( text.substring ( 0, mnemonicIndex ) ), textY + fm.getDescent () - 1,
fm.charWidth ( text.charAt ( mnemonicIndex ) ), 1 );
}
}
@NotNull
@Override
protected Dimension getContentPreferredSize ( @NotNull final C c, @NotNull final D d, @NotNull final Dimension available )
{
final Dimension ps;
if ( !isEmpty ( c, d ) )
{
final int w;
final int h;
if ( isHtmlText ( c, d ) )
{
final View html = getHtml ( c, d );
w = ( int ) html.getPreferredSpan ( View.X_AXIS );
h = ( int ) html.getPreferredSpan ( View.Y_AXIS );
ps = new Dimension ( w, h );
}
else
{
final Dimension pts = getPreferredTextSize ( c, d, available );
pts.width += isShadow ( c, d ) ? getShadowSize ( c, d ) * 2 : 0;
ps = pts;
}
}
else
{
ps = new Dimension ( 0, 0 );
}
return ps;
}
/**
* Returns preferred text size.
*
* @param c painted component
* @param d painted decoration state
* @param available theoretically available space for this content
* @return preferred text size
*/
@NotNull
protected Dimension getPreferredTextSize ( @NotNull final C c, @NotNull final D d, @NotNull final Dimension available )
{
final String text = getText ( c, d );
final FontMetrics fm = getFontMetrics ( c, d );
final int w = SwingUtils.stringWidth ( fm, text );
final int h = fm.getHeight ();
return new Dimension ( w, h );
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy