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

com.alee.extended.label.WebMultiLineLabelUI 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 .
 */

/*
 * The MIT License
 *
 * Copyright (c) 2009 Samuel Sjoberg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.alee.extended.label;

import com.alee.laf.label.WebLabelStyle;
import com.alee.utils.LafUtils;
import com.alee.utils.SwingUtils;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicLabelUI;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Label UI delegate that supports multiple lines and line wrapping. Hard line breaks (\n) are preserved. If the dimensions of
 * the label is too small to fit all content, the string will be clipped and "..." appended to the end of the visible text (similar to the
 * default behavior of JLabel).
 *
 * @author Samuel Sjoberg, http://samuelsjoberg.com
 */

public class WebMultiLineLabelUI extends BasicLabelUI implements ComponentListener
{
    /**
     * Client property key used to store the calculated wrapped lines on the JLabel.
     */
    public static final String PROPERTY_KEY = "WrappedText";

    // Static references to avoid heap allocations.
    protected static Rectangle paintIconR = new Rectangle ();
    protected static Rectangle paintTextR = new Rectangle ();
    protected static Rectangle paintViewR = new Rectangle ();
    protected static Insets paintViewInsets = new Insets ( 0, 0, 0, 0 );

    // Variables
    private static int defaultSize = 4;
    private FontMetrics metrics;

    // View settings
    private boolean drawShade = WebLabelStyle.drawShade;
    private Color shadeColor = WebLabelStyle.shadeColor;

    /**
     * UI instance creation.
     *
     * @param c the component about to be installed
     * @return the shared UI delegate instance
     */
    @SuppressWarnings ("UnusedParameters")
    public static ComponentUI createUI ( final JComponent c )
    {
        return new WebMultiLineLabelUI ();
    }

    @Override
    public void installUI ( final JComponent c )
    {
        super.installUI ( c );

        // Default settings
        SwingUtils.setOrientation ( c );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void uninstallDefaults ( final JLabel c )
    {
        super.uninstallDefaults ( c );
        clearCache ( c );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void installListeners ( final JLabel c )
    {
        super.installListeners ( c );
        c.addComponentListener ( this );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void uninstallListeners ( final JLabel c )
    {
        super.uninstallListeners ( c );
        c.removeComponentListener ( this );
    }

    /**
     * View settings
     */

    public boolean isDrawShade ()
    {
        return drawShade;
    }

    public void setDrawShade ( final boolean drawShade )
    {
        this.drawShade = drawShade;
    }

    public Color getShadeColor ()
    {
        return shadeColor;
    }

    public void setShadeColor ( final Color shadeColor )
    {
        this.shadeColor = shadeColor;
    }

    /**
     * Clear the wrapped line cache.
     *
     * @param l the label containing a cached value
     */
    protected void clearCache ( final JLabel l )
    {
        l.putClientProperty ( PROPERTY_KEY, null );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void propertyChange ( final PropertyChangeEvent e )
    {
        super.propertyChange ( e );
        final String name = e.getPropertyName ();
        if ( name.equals ( "text" ) || "font".equals ( name ) )
        {
            clearCache ( ( JLabel ) e.getSource () );
        }
    }

    /**
     * Calculate the paint rectangles for the icon and text for the passed label.
     *
     * @param l      a label
     * @param fm     the font metrics to use, or null to get the font metrics from the label
     * @param width  label width
     * @param height label height
     */
    protected void updateLayout ( final JLabel l, FontMetrics fm, final int width, final int height )
    {
        if ( fm == null )
        {
            fm = l.getFontMetrics ( l.getFont () );
        }
        metrics = fm;

        final String text = l.getText ();
        final Icon icon = l.getIcon ();
        final Insets insets = l.getInsets ( paintViewInsets );

        paintViewR.x = insets.left;
        paintViewR.y = insets.top;
        paintViewR.width = width - ( insets.left + insets.right );
        paintViewR.height = height - ( insets.top + insets.bottom );

        paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0;
        paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0;

        layoutCL ( l, fm, text, icon, paintViewR, paintIconR, paintTextR );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void paint ( final Graphics g, final JComponent c )
    {
        final Map hints = SwingUtils.setupTextAntialias ( g );

        final JLabel label = ( JLabel ) c;
        final String text = label.getText ();
        final Icon icon = ( label.isEnabled () ) ? label.getIcon () : label.getDisabledIcon ();

        if ( ( icon == null ) && ( text == null ) )
        {
            return;
        }

        final FontMetrics fm = g.getFontMetrics ();

        updateLayout ( label, fm, c.getWidth (), c.getHeight () );

        if ( icon != null )
        {
            icon.paintIcon ( c, g, paintIconR.x, paintIconR.y );
        }

        if ( text != null )
        {
            final View v = ( View ) c.getClientProperty ( "html" );
            if ( v != null )
            {
                // HTML view disables multi-line painting.
                v.paint ( g, paintTextR );
            }
            else
            {
                // Paint the multi line text
                paintTextLines ( g, label, fm );
            }
        }

        SwingUtils.restoreTextAntialias ( g, hints );
    }

    /**
     * Paint the wrapped text lines.
     *
     * @param g     graphics component to paint on
     * @param label the label being painted
     * @param fm    font metrics for current font
     */
    protected void paintTextLines ( final Graphics g, final JLabel label, final FontMetrics fm )
    {
        final List lines = getTextLines ( label );

        // Available component height to paint on.
        final int height = getAvailableHeight ( label );

        int textHeight = lines.size () * fm.getHeight ();
        while ( textHeight > height )
        {
            // Remove one line until no. of visible lines is found.
            textHeight -= fm.getHeight ();
        }
        paintTextR.height = Math.min ( textHeight, height );
        paintTextR.y = alignmentY ( label, fm, paintTextR );

        final int textX = paintTextR.x;
        int textY = paintTextR.y;

        for ( Iterator it = lines.iterator (); it.hasNext () && paintTextR.contains ( textX, textY + getAscent ( fm ) );
              textY += fm.getHeight () )
        {

            String text = it.next ().trim ();

            if ( it.hasNext () && !paintTextR.contains ( textX, textY + fm.getHeight () + getAscent ( fm ) ) )
            {
                // The last visible row, add a clip indication.
                text = clip ( text );
            }

            final int x = alignmentX ( label, fm, text );

            if ( label.isEnabled () )
            {
                paintEnabledText ( label, g, text, x, textY );
            }
            else
            {
                paintDisabledText ( label, g, text, x, textY );
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void paintEnabledText ( final JLabel l, final Graphics g, final String s, final int textX, final int textY )
    {
        if ( drawShade )
        {
            g.setColor ( l.getForeground () );
            paintShadowText ( g, s, textX, textY );
        }
        else
        {
            final int mnemIndex = l.getDisplayedMnemonicIndex ();
            g.setColor ( l.getForeground () );
            SwingUtils.drawStringUnderlineCharAt ( g, s, mnemIndex, textX, textY );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void paintDisabledText ( final JLabel l, final Graphics g, final String s, final int textX, final int textY )
    {
        if ( drawShade )
        {
            g.setColor ( l.getBackground ().darker () );
            paintShadowText ( g, s, textX, textY );
        }
        else
        {
            final int mnemIndex = l.getDisplayedMnemonicIndex ();
            g.setColor ( l.getForeground () );
            SwingUtils.drawStringUnderlineCharAt ( g, s, mnemIndex, textX, textY );
        }
    }

    /**
     * Paint the text with a text effect.
     *
     * @param g     graphics component used to paint on
     * @param s     the string to paint
     * @param textX the x coordinate
     * @param textY the y coordinate
     */
    private void paintShadowText ( final Graphics g, final String s, final int textX, final int textY )
    {
        g.translate ( textX, textY );
        LafUtils.paintTextShadow ( ( Graphics2D ) g, s, shadeColor );
        g.translate ( -textX, -textY );
    }

    /**
     * Returns the available height to paint text on. This is the height of the passed component with insets subtracted.
     *
     * @param l a component
     * @return the available height
     */
    protected int getAvailableHeight ( final JLabel l )
    {
        l.getInsets ( paintViewInsets );
        return l.getHeight () - paintViewInsets.top - paintViewInsets.bottom;
    }

    /**
     * Add a clip indication to the string. It is important that the string length does not exceed the length or the original string.
     *
     * @param text the to be painted
     * @return the clipped string
     */
    protected String clip ( final String text )
    {
        // Fast and lazy way to insert a clip indication is to simply replace
        // the last characters in the string with the clip indication.
        // A better way would be to use metrics and calculate how many (if any)
        // characters that need to be replaced.
        if ( text.length () < 3 )
        {
            return "...";
        }
        return text.substring ( 0, text.length () - 3 ) + "...";
    }

    /**
     * Establish the vertical text alignment. The default alignment is to center the text in the label.
     *
     * @param label  the label to paint
     * @param fm     font metrics
     * @param bounds the text bounds rectangle
     * @return the vertical text alignment, defaults to CENTER.
     */
    protected int alignmentY ( final JLabel label, final FontMetrics fm, final Rectangle bounds )
    {
        final int height = getAvailableHeight ( label );
        final int textHeight = bounds.height;

        final int align = label.getVerticalAlignment ();
        switch ( align )
        {
            case JLabel.TOP:
                return getAscent ( fm ) + paintViewInsets.top;
            case JLabel.BOTTOM:
                return getAscent ( fm ) + height - paintViewInsets.top + paintViewInsets.bottom - textHeight;
            default:
        }

        // Center alignment
        final int textY = paintViewInsets.top + ( height - textHeight ) / 2 + getAscent ( fm );
        return Math.max ( textY, getAscent ( fm ) + paintViewInsets.top );
    }

    private static int getAscent ( final FontMetrics fm )
    {
        return fm.getAscent () + fm.getLeading ();
    }

    /**
     * Establish the horizontal text alignment. The default alignment is left aligned text.
     *
     * @param label the label to paint
     * @param fm    font metrics
     * @param s     the string to paint
     * @return the x-coordinate to use when painting for proper alignment
     */
    protected int alignmentX ( final JLabel label, final FontMetrics fm, final String s )
    {
        final boolean ltr = label.getComponentOrientation ().isLeftToRight ();
        final int align = label.getHorizontalAlignment ();
        if ( align == JLabel.RIGHT || align == JLabel.TRAILING && ltr ||
                align == JLabel.LEADING && !ltr )
        {
            return paintViewR.width - fm.stringWidth ( s );
        }
        else if ( align == JLabel.CENTER )
        {
            return paintViewR.width / 2 - fm.stringWidth ( s ) / 2;
        }
        else
        {
            return paintViewR.x;
        }
    }

    /**
     * Check the given string to see if it should be rendered as HTML. Code based on implementation found in
     * BasicHTML.isHTMLString(String) in future JDKs.
     *
     * @param s the string
     * @return true if string is HTML, otherwise false
     */
    private static boolean isHTMLString ( final String s )
    {
        if ( s != null )
        {
            if ( ( s.length () >= 6 ) && ( s.charAt ( 0 ) == '<' ) && ( s.charAt ( 5 ) == '>' ) )
            {
                final String tag = s.substring ( 1, 5 );
                return tag.equalsIgnoreCase ( "html" );
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Dimension getPreferredSize ( final JComponent c )
    {
        final Dimension d = super.getPreferredSize ( c );
        final JLabel label = ( JLabel ) c;

        if ( isHTMLString ( label.getText () ) )
        {
            return d; // HTML overrides everything and we don't need to process
        }

        // Width calculated by super is OK. The preferred width is the width of
        // the unwrapped content as long as it does not exceed the width of the
        // parent container.

        if ( c.getParent () != null )
        {
            // Ensure that preferred width never exceeds the available width
            // (including its border insets) of the parent container.
            final Insets insets = c.getParent ().getInsets ();
            final Dimension size = c.getParent ().getSize ();
            if ( size.width > 0 )
            {
                // If width isn't set component shouldn't adjust.
                d.width = size.width - insets.left - insets.right;
            }
        }

        updateLayout ( label, null, d.width, d.height );

        // The preferred height is either the preferred height of the text
        // lines, or the height of the icon.
        d.height = Math.max ( d.height, getPreferredHeight ( label ) );

        return d;
    }

    /**
     * The preferred height of the label is the height of the lines with added top and bottom insets.
     *
     * @param label the label
     * @return the preferred height of the wrapped lines.
     */
    protected int getPreferredHeight ( final JLabel label )
    {
        final int numOfLines = getTextLines ( label ).size ();
        final Insets insets = label.getInsets ( paintViewInsets );
        return numOfLines * metrics.getHeight () + insets.top + insets.bottom;
    }

    /**
     * Get the lines of text contained in the text label. The prepared lines is cached as a client property, accessible via {@link
     * #PROPERTY_KEY}.
     *
     * @param l the label
     * @return the text lines of the label.
     */
    @SuppressWarnings ("unchecked")
    protected List getTextLines ( final JLabel l )
    {
        List lines = ( List ) l.getClientProperty ( PROPERTY_KEY );
        if ( lines == null )
        {
            lines = prepareLines ( l );
            l.putClientProperty ( PROPERTY_KEY, lines );
        }
        return lines;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void componentHidden ( final ComponentEvent e )
    {
        // Don't care
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void componentMoved ( final ComponentEvent e )
    {
        // Don't care
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void componentResized ( final ComponentEvent e )
    {
        clearCache ( ( JLabel ) e.getSource () );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void componentShown ( final ComponentEvent e )
    {
        // Don't care
    }

    /**
     * Prepare the text lines for rendering. The lines are wrapped to fit in the current available space for text. Explicit line breaks are
     * preserved.
     *
     * @param l the label to render
     * @return a list of text lines to render
     */
    protected List prepareLines ( final JLabel l )
    {
        final List lines = new ArrayList ( defaultSize );
        final String text = l.getText ();
        if ( text == null )
        {
            return null; // Null guard
        }
        final PlainDocument doc = new PlainDocument ();
        try
        {
            doc.insertString ( 0, text, null );
        }
        catch ( BadLocationException e )
        {
            return null;
        }
        final Element root = doc.getDefaultRootElement ();
        for ( int i = 0, j = root.getElementCount (); i < j; i++ )
        {
            wrap ( lines, root.getElement ( i ) );
        }
        return lines;
    }

    /**
     * If necessary, wrap the text into multiple lines.
     *
     * @param lines line array in which to store the wrapped lines
     * @param elem  the document element containing the text content
     */
    protected void wrap ( final List lines, final Element elem )
    {
        final int p1 = elem.getEndOffset ();
        final Document doc = elem.getDocument ();
        for ( int p0 = elem.getStartOffset (); p0 < p1; )
        {
            final int p = calculateBreakPosition ( doc, p0, p1 );
            try
            {
                lines.add ( doc.getText ( p0, p - p0 ) );
            }
            catch ( BadLocationException e )
            {
                throw new Error ( "Can't get line text. p0=" + p0 + " p=" + p );
            }
            p0 = ( p == p0 ) ? p1 : p;
        }
    }

    /**
     * Calculate the position on which to break (wrap) the line.
     *
     * @param doc the document
     * @param p0  start position
     * @param p1  end position
     * @return the actual end position, will be p1 if content does not need to wrap, otherwise it will be less than
     * p1.
     */
    protected int calculateBreakPosition ( final Document doc, final int p0, final int p1 )
    {
        final Segment segment = SegmentCache.getSegment ();
        try
        {
            doc.getText ( p0, p1 - p0, segment );
        }
        catch ( BadLocationException e )
        {
            throw new Error ( "Can't get line text" );
        }

        final int width = paintTextR.width;
        final int p = p0 + Utilities.getBreakLocation ( segment, metrics, 0, width, null, p0 );
        SegmentCache.releaseSegment ( segment );
        return p;
    }

    /**
     * Static singleton {@link Segment} cache.
     *
     * @author Samuel Sjoberg
     * @see javax.swing.text.SegmentCache
     */
    protected static final class SegmentCache
    {
        /**
         * Reused segments.
         */
        private ArrayList segments = new ArrayList ( 2 );

        /**
         * Singleton instance.
         */
        private static SegmentCache cache = new SegmentCache ();

        /**
         * Private constructor.
         */
        private SegmentCache ()
        {
            //
        }

        /**
         * Returns a Segment. When done, the Segment should be recycled by invoking {@link
         * #releaseSegment(Segment)}.
         *
         * @return a Segment.
         */
        public static Segment getSegment ()
        {
            final int size = cache.segments.size ();
            if ( size > 0 )
            {
                return cache.segments.remove ( size - 1 );
            }
            return new Segment ();
        }

        /**
         * Releases a Segment. A segment should not be used after it is released, and a segment should never be released more
         * than once.
         */
        public static void releaseSegment ( final Segment segment )
        {
            segment.array = null;
            segment.count = 0;
            cache.segments.add ( segment );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy