All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.extended.label.AbstractStyledTextContent Maven / Gradle / Ivy

There is a newer version: 1.2.14
Show newest version
/*
 * 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.extended.label;

import com.alee.api.clone.behavior.OmitOnClone;
import com.alee.api.merge.behavior.OmitOnMerge;
import com.alee.painter.decoration.DecorationException;
import com.alee.painter.decoration.IDecoration;
import com.alee.painter.decoration.content.AbstractTextContent;
import com.alee.utils.*;
import com.alee.utils.general.Pair;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Abstract implementation of styled text content
 *
 * @param  component type
 * @param  decoration type
 * @param  content type
 * @author Alexandr Zernov
 * @see How to use WebStyledLabel
 */
public abstract class AbstractStyledTextContent, I extends AbstractStyledTextContent>
        extends AbstractTextContent
{
    /**
     * todo 1. Implement minimum rows count
     * todo 2. Implement custom colors for custom style elements
     * todo 3. Implement different fonts for text parts
     * todo 4. Paint shadow for all different accessories
     */

    /**
     * Whether or not should ignore style font color settings.
     */
    @XStreamAsAttribute
    protected Boolean ignoreStyleColors;

    /**
     * Script font ratio.
     */
    @XStreamAsAttribute
    protected Float scriptFontRatio;

    /**
     * Whether or not should keep hard line breaks.
     */
    @XStreamAsAttribute
    protected Boolean preserveLineBreaks;

    /**
     * Size of gaps between label rows in pixels.
     */
    @XStreamAsAttribute
    protected Integer rowGap;

    /**
     * Global text style.
     * It can be specified to add an additional {@link StyleRange} with range [0,text.length].
     * It uses the style part of the standard styled text syntax.
     * 

* Here are a few examples of what can be provided here: * 1. "u;b" - underlined bold text * 2. "b;c(red)" - bold red text * 3. "i;bg(0,255,0)" - italic text with blue background highlight * * @see Styled text syntax */ @XStreamAsAttribute protected String globalStyle; /** * Runtime variables. */ @OmitOnClone @OmitOnMerge protected transient List textRanges; @Override public void activate ( final C c, final D d ) { // Performing default actions super.activate ( c, d ); // Building initial text ranges buildTextRanges ( c, d ); } @Override public void deactivate ( final C c, final D d ) { // Clearing text ranges textRanges = null; // Performing default actions super.deactivate ( c, d ); } /** * Returns whether or not ignore style font color settings. * * @param c painted component * @param d painted decoration state * @return true if ignore style font color settings, false otherwise */ protected boolean isIgnoreColorSettings ( final C c, final D d ) { return ignoreStyleColors != null && ignoreStyleColors; } /** * Returns script font ratio. * * @param c painted component * @param d painted decoration state * @return script font ratio */ protected Float getScriptFontRatio ( final C c, final D d ) { return scriptFontRatio != null ? scriptFontRatio : 1.5f; } /** * Returns whether hard breaks are preserved or not. * * @param c painted component * @param d painted decoration state * @return true if hard breaks are preserved, false otherwise */ protected boolean isPreserveLineBreaks ( final C c, final D d ) { return preserveLineBreaks == null || preserveLineBreaks; } /** * Returns text row gap. * * @param c painted component * @param d painted decoration state * @return text row gap */ protected int getRowGap ( final C c, final D d ) { return rowGap != null ? rowGap : 0; } /** * Returns global style range. * * @param plainText plain text * @param c painted component * @param d painted decoration state @return text row gap * @return global style range */ protected StyleRange getGlobalStyle ( final String plainText, final C c, final D d ) { return !TextUtils.isEmpty ( globalStyle ) && plainText != null ? new StyleSettings ( 0, plainText.length (), globalStyle ).getStyleRange () : null; } /** * Returns list of text style ranges. * * @param c painted component * @param d painted decoration state * @return list of text style ranges */ protected abstract List getStyleRanges ( C c, D d ); /** * Returns text wrapping type. * * @param c painted component * @param d painted decoration state * @return text wrapping type */ protected abstract TextWrap getWrapType ( C c, D d ); /** * Returns maximum rows count. * * @param c painted component * @param d painted decoration state * @return maximum rows count */ protected abstract int getMaximumRows ( C c, D d ); /** * Builds text ranges based on plain text and style ranges. * * @param c painted component * @param d painted decoration state */ protected void buildTextRanges ( final C c, final D d ) { // Retrieving plain text final String plainText = getText ( c, d ); // Retrieving component style ranges List styleRanges = getStyleRanges ( c, d ); // Adding global style on top of them final StyleRange globalStyle = getGlobalStyle ( plainText, c, d ); if ( globalStyle != null ) { // We have to copy ranges list to avoid affecting the source list styleRanges = CollectionUtils.copy ( styleRanges ); styleRanges.add ( 0, globalStyle ); } textRanges = new TextRanges ( plainText, styleRanges ).getTextRanges (); } @Override public int getContentBaseline ( final C c, final D d, final Rectangle bounds ) { // todo Return baseline appropriate for styled label // todo This should refer to either first or last line of text return super.getContentBaseline ( c, d, bounds ); } @Override protected void paintText ( final Graphics2D g2d, final Rectangle bounds, final C c, final D d ) { if ( textRanges != null ) { // Text rows gap final int rg = Math.max ( 0, getRowGap ( c, d ) ); // Calculating text bounds coordinates final int x = bounds.x; int y = bounds.y; // Layout the text final List rows = layout ( c, d, bounds ); if ( !rows.isEmpty () ) { // Calculating y-axis offset final int va = getVerticalAlignment ( c, d ); if ( va != TOP ) { // Calculating total height int th = -rg; for ( final StyledTextRow row : rows ) { th += row.height + rg; } // Adjusting vertical position according to alignment if ( th < bounds.height ) { switch ( va ) { case CENTER: y += Math.ceil ( ( bounds.height - th ) / 2.0 ); break; case BOTTOM: y += bounds.height - th; break; default: throw new DecorationException ( "Incorrect vertical alignment provided: " + va ); } } } final Pair fs = getFontSize ( c, d ); y += fs.getKey (); // Painting the text for ( int i = 0; i < rows.size (); i++ ) { final StyledTextRow row = rows.get ( i ); paintRow ( c, d, g2d, bounds, x, y, row, i == rows.size () - 1 ); y += row.height + rg; } } } } /** * Performs styled text layout. * * @param c painted component * @param d painted decoration state * @param bounds painting bounds * @return List of rows to paint */ protected List layout ( final C c, final D d, final Rectangle bounds ) { final int endY = bounds.y + bounds.height; final int endX = bounds.x + bounds.width; final Pair fs = getFontSize ( c, d ); final int maxRowHeight = fs.getValue (); final int maxAscent = fs.getKey (); int x = bounds.x; int y = bounds.y + maxAscent; final int mnemonicIndex = getMnemonicIndex ( c, d ); final int maximumRows = getMaximumRows ( c, d ); final int rowGap = getRowGap ( c, d ); final TextWrap wrapType = getWrapType ( c, d ); final boolean preserveLineBreaks = isPreserveLineBreaks ( c, d ); Font font = c.getFont (); final int defaultFontSize = font.getSize (); int rowCount = 0; int charDisplayed = 0; int nextRowStartIndex = 0; StyledTextRow row = new StyledTextRow ( maxRowHeight, true ); boolean readyToPaint = false; boolean leadingRow = false; final List rows = new ArrayList (); // Painting the text for ( int i = 0; i < textRanges.size (); i++ ) { final TextRange textRange = textRanges.get ( i ); final StyleRange style = textRange.styleRange; // Updating font if need final int size = style != null && ( style.isSuperscript () || style.isSubscript () ) ? Math.round ( ( float ) defaultFontSize / getScriptFontRatio ( c, d ) ) : defaultFontSize; font = c.getFont (); if ( style != null && ( style.getStyle () != -1 && font.getStyle () != style.getStyle () || font.getSize () != size ) ) { font = FontUtils.getCachedDerivedFont ( font, style.getStyle () == -1 ? font.getStyle () : style.getStyle (), size ); } final FontMetrics cfm = c.getFontMetrics ( font ); if ( textRange.text.equals ( "\n" ) ) { if ( wrapType == TextWrap.none || preserveLineBreaks ) { if ( row.isEmpty () ) { row.append ( " ", null, cfm, -1, -1 ); } i++; charDisplayed += 1; readyToPaint = true; leadingRow = true; } } else if ( nextRowStartIndex == 0 || nextRowStartIndex < textRange.text.length () ) { String s = textRange.text.substring ( nextRowStartIndex ); int strWidth = cfm.stringWidth ( s ); final int widthLeft = endX - x; if ( wrapType != TextWrap.none && widthLeft < strWidth && widthLeft >= 0 ) { if ( ( maximumRows <= 0 || rowCount < maximumRows ) && y + maxRowHeight + Math.max ( 0, rowGap ) <= endY ) { int availLength = ( int ) ( ( long ) s.length () * widthLeft / strWidth ) + 1; // Optimistic prognoses int firstWordOffset = Math.max ( 0, TextUtils.findFirstWordFromIndex ( s, 0 ) ); int nextRowStartInSubString = 0; do { final String subStringThisRow; int lastInWordEndIndex; if ( wrapType == TextWrap.word || wrapType == TextWrap.mixed ) { final String subString = s.substring ( 0, Math.max ( 0, Math.min ( availLength, s.length () ) ) ); // Searching last word start lastInWordEndIndex = TextUtils.findLastRowWordStartIndex ( subString.trim () ); // Only one word in row left if ( lastInWordEndIndex < 0 ) { if ( wrapType == TextWrap.word ) { if ( row.isEmpty () ) { // Search last index of the first word end nextRowStartInSubString = firstWordOffset + TextUtils.findFirstRowWordEndIndex ( s.trim () ); } break; } else { if ( row.isEmpty () ) { lastInWordEndIndex = availLength - 1; firstWordOffset = 0; } } } nextRowStartInSubString = firstWordOffset + lastInWordEndIndex + 1; subStringThisRow = subString.substring ( 0, Math.min ( nextRowStartInSubString, subString.length () ) ); } else//if ( wt == WrapType.character ) { subStringThisRow = s.substring ( 0, Math.max ( 0, Math.min ( availLength, s.length () ) ) ); nextRowStartInSubString = availLength; firstWordOffset = 0; } strWidth = cfm.stringWidth ( subStringThisRow ); if ( strWidth > widthLeft ) { // todo Try to optimize availLength--; } } while ( strWidth > widthLeft && availLength > 0 ); if ( nextRowStartInSubString > 0 && ( availLength > 0 || row.isEmpty () ) ) { // Extracting wrapped text fragment s = s.substring ( 0, Math.min ( nextRowStartInSubString, s.length () ) ); strWidth = row.append ( s, style, cfm, charDisplayed, mnemonicIndex ); charDisplayed += s.length (); nextRowStartIndex += nextRowStartInSubString; } else if ( row.isEmpty () ) { // Skipping current text range i++; strWidth = row.append ( s, style, cfm, charDisplayed, mnemonicIndex ); charDisplayed += s.length (); nextRowStartIndex = 0; } else { strWidth = 0; nextRowStartIndex = 0; } } else { strWidth = row.append ( s, style, cfm, charDisplayed, mnemonicIndex ); charDisplayed += s.length (); nextRowStartIndex = 0; } readyToPaint = true; } else { strWidth = row.append ( s, style, cfm, charDisplayed, mnemonicIndex ); charDisplayed += s.length (); nextRowStartIndex = 0; } x += strWidth; } else { nextRowStartIndex = 0; } if ( readyToPaint && !row.isEmpty () ) { rows.add ( row ); rowCount++; row = new StyledTextRow ( maxRowHeight, leadingRow ); readyToPaint = false; leadingRow = false; // Setting up next row y += maxRowHeight + Math.max ( 0, rowGap ); x = bounds.x; i--; // Checking that row is last if ( y > endY || maximumRows > 0 && rowCount >= maximumRows ) { break; } } } // Painting last row if need if ( !row.isEmpty () ) { rows.add ( row ); } return rows; } /** * Paints single styled text row. * * @param c painted component * @param d painted decoration state * @param g2d graphics context * @param bounds painting bounds * @param textX text X coordinate * @param textY text Y coordinate * @param row painted row * @param isLast whether or not painted row is last */ protected void paintRow ( final C c, final D d, final Graphics2D g2d, final Rectangle bounds, final int textX, final int textY, final StyledTextRow row, final boolean isLast ) { // Painting settings final Font font = c.getFont (); final int defaultFontSize = font.getSize (); final FontMetrics fm = c.getFontMetrics ( font ); final TextWrap wt = getWrapType ( c, d ); final int ha = getAdjustedHorizontalAlignment ( c, d ); // Calculating text X coordinate int x = textX; if ( bounds.width > row.width ) { switch ( ha ) { case LEFT: break; case CENTER: x += Math.floor ( ( bounds.width - row.width ) / 2.0 ); break; case RIGHT: x += bounds.width - row.width; break; default: throw new DecorationException ( "Incorrect horizontal alignment provided: " + ha ); } } // Painting styled text fragments int charDisplayed = 0; for ( final TextRange textRange : row.fragments ) { final StyleRange style = textRange.getStyleRange (); // Updating font if need final int size = style != null && ( style.isSuperscript () || style.isSubscript () ) ? Math.round ( ( float ) defaultFontSize / getScriptFontRatio ( c, d ) ) : defaultFontSize; Font cFont = c.getFont (); if ( style != null && ( style.getStyle () != -1 && cFont.getStyle () != style.getStyle () || cFont.getSize () != size ) ) { cFont = FontUtils.getCachedDerivedFont ( cFont, style.getStyle () == -1 ? cFont.getStyle () : style.getStyle (), size ); } final FontMetrics cfm = c.getFontMetrics ( cFont ); int y = textY; String s = textRange.text; final int strWidth = cfm.stringWidth ( s ); // Checking mnemonic int mnemonicIndex = -1; if ( row.mnemonic >= 0 && row.mnemonic < charDisplayed + s.length () ) { mnemonicIndex = row.mnemonic - charDisplayed; } // Checking whether or not text should be truncated final boolean truncated; if ( isTruncate ( c, d ) ) { final int availableWidth = bounds.width + bounds.x - x; truncated = availableWidth < strWidth && ( wt == TextWrap.none || wt == TextWrap.word || isLast ); if ( truncated ) { // Clip string s = SwingUtilities.layoutCompoundLabel ( cfm, s, null, 0, ha, 0, 0, new Rectangle ( x, y, availableWidth, bounds.height ), new Rectangle (), new Rectangle (), 0 ); } } else { truncated = false; } // Starting of actual painting g2d.setFont ( cFont ); if ( style != null ) { if ( style.isSuperscript () ) { y -= fm.getHeight () - cfm.getHeight (); } else if ( style.isSubscript () ) { y += fm.getDescent () - cfm.getDescent (); } } if ( style != null && style.getBackground () != null ) { g2d.setPaint ( style.getBackground () ); g2d.fillRect ( x, y - cfm.getAscent (), strWidth, cfm.getAscent () + cfm.getDescent () ); } final boolean useStyleForeground = style != null && !isIgnoreColorSettings ( c, d ) && style.getForeground () != null; final Color textColor = useStyleForeground ? style.getForeground () : getColor ( c, d ); g2d.setPaint ( textColor ); paintStyledTextFragment ( c, d, g2d, s, x, y, mnemonicIndex, cfm, style, strWidth ); // Stop on truncated part // Otherwise we might end up having two truncated parts if ( truncated ) { break; } x += strWidth; charDisplayed += s.length (); } } /** * Returns font max height and ascent. * * @param c painted component * @param d painted decoration state * @return font max height and ascent */ protected Pair getFontSize ( final C c, final D d ) { final Font font = c.getFont (); final int defaultFontSize = font.getSize (); final FontMetrics fm = c.getFontMetrics ( font ); int maxHeight = fm.getHeight (); int maxAscent = fm.getAscent (); for ( final TextRange textRange : textRanges ) { final StyleRange style = textRange.styleRange; final int size = style != null && ( style.isSuperscript () || style.isSubscript () ) ? Math.round ( ( float ) defaultFontSize / getScriptFontRatio ( c, d ) ) : defaultFontSize; Font cFont = font; if ( style != null && ( style.getStyle () != -1 && cFont.getStyle () != style.getStyle () || cFont.getSize () != size ) ) { cFont = FontUtils.getCachedDerivedFont ( cFont, style.getStyle () == -1 ? cFont.getStyle () : style.getStyle (), size ); final FontMetrics fm2 = c.getFontMetrics ( cFont ); maxHeight = Math.max ( maxHeight, fm2.getHeight () ); maxAscent = Math.max ( maxAscent, fm2.getAscent () ); } } return new Pair ( maxAscent, maxHeight ); } /** * Actually paints styled text fragment. * * @param c painted component * @param d painted decoration state * @param g2d graphics context * @param s text fragment * @param x text X coordinate * @param y text Y coordinate * @param mnemonicIndex index of mnemonic * @param fm text fragment font metrics * @param style style of text fragment * @param strWidth text fragment width */ protected void paintStyledTextFragment ( final C c, final D d, final Graphics2D g2d, final String s, final int x, final int y, final int mnemonicIndex, final FontMetrics fm, final StyleRange style, final int strWidth ) { // This is required to properly render sub-pixel text antialias final RenderingHints rh = g2d.getRenderingHints (); // Painting text fragment paintTextFragment ( c, d, g2d, s, x, y, mnemonicIndex ); // Painting accessories if ( style != null ) { // todo Separate all these implementations into special TextAccessory interface implementations // todo Make each accessory configurable to some extent, for example to provide its color if ( style.isStrikeThrough () ) { final int lineY = y + ( fm.getDescent () - fm.getAscent () ) / 2; g2d.drawLine ( x, lineY, x + strWidth - 1, lineY ); } if ( style.isDoubleStrikeThrough () ) { final int lineY = y + ( fm.getDescent () - fm.getAscent () ) / 2; g2d.drawLine ( x, lineY - 1, x + strWidth - 1, lineY - 1 ); g2d.drawLine ( x, lineY + 1, x + strWidth - 1, lineY + 1 ); } if ( style.isUnderlined () ) { final int lineY = y + 1; g2d.drawLine ( x, lineY, x + strWidth - 1, lineY ); } if ( style.isWaved () ) { // tood Remove hardcoded red as soon as accessories are available final Paint op = GraphicsUtils.setupPaint ( g2d, Color.RED ); final int waveY = y + 1; for ( int waveX = x; waveX < x + strWidth; waveX += 4 ) { if ( waveX + 2 <= x + strWidth - 1 ) { g2d.drawLine ( waveX, waveY, waveX + 1, waveY ); } if ( waveX + 4 <= x + strWidth - 1 ) { g2d.drawLine ( waveX + 2, waveY + 1, waveX + 3, waveY + 1 ); } } GraphicsUtils.restorePaint ( g2d, op ); } } // This is required to properly render sub-pixel text antialias g2d.setRenderingHints ( rh ); } @Override protected Dimension getPreferredTextSize ( final C c, final D d, final Dimension available ) { // Preferred size for maximum possible space final Dimension vSize = getPreferredStyledTextSize ( c, d, new Dimension ( Short.MAX_VALUE, Short.MAX_VALUE ) ); // Preferred size for available space final Dimension hSize = getPreferredStyledTextSize ( c, d, available ); return SwingUtils.max ( vSize, hSize ); /** * This doesn't work for some cases and causes issue when available size expands. * It also doesn't properly scale in case of content layout or any kind of padding usage. * Some major reworks in the size calculation structure are required to make this piece of code work. */ /*// Preferred size for maximum possible space final Insets p = PainterSupport.getPadding ( c ); final int pw = SizeMethodsImpl.getPreferredWidth ( c ) - ( p != null ? p.left + p.right : 0 ); final int mw = pw >= 0 && available.width <= 0 ? pw : Short.MAX_VALUE; final int ph = SizeMethodsImpl.getPreferredHeight ( c ) - ( p != null ? p.top + p.bottom : 0 ); final int mh = ph >= 0 && available.height <= 0 ? ph : Short.MAX_VALUE; final Dimension vSize = getPreferredStyledTextSize ( c, d, new Dimension ( mw, mh ) ); // Preferred size for available space final Dimension hSize = getPreferredStyledTextSize ( c, d, available ); // Preferred contains maximum of two return SwingUtils.max ( vSize, hSize );*/ } /** * Returns preferred styled text size. * * @param c painted component * @param d painted decoration state * @param available theoretically available space for this content * @return preferred styled text size */ protected Dimension getPreferredStyledTextSize ( final C c, final D d, final Dimension available ) { final Dimension ps = new Dimension ( 0, 0 ); if ( textRanges != null ) { final List rows = layout ( c, d, new Rectangle ( 0, 0, available.width, available.height ) ); if ( !rows.isEmpty () ) { final int rg = Math.max ( 0, getRowGap ( c, d ) ); ps.height -= rg; for ( final StyledTextRow row : rows ) { ps.width = Math.max ( ps.width, row.width ); ps.height += row.height + rg; } } } return ps; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy