/*
 * @(#)TextPaintable.java    1.0  24 October 2004
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.awt.font.*;

/**
 * <p>A <code>TextPaintable</code> creates a <code>Paintable</code>
 * using a string, a font, and other related data.</p>
 *
 * <p>In 2.3.2, the method <code>getBounds2D</code> was modified to use the
 * default Bounds2D rectangle if that rectangle is non-<code>null</code>
 * before making any other tests or computations.</p>
 *
 * @author  Richard Rasala
 * @version 2.3.2
 * @since   2.3
 */
public class TextPaintable extends AbstractPaintable
{

    /** Bound property name for set string. */
    public static final String SET_STRING          = "set.string";
        
    /** Bound property name for set font. */
    public static final String SET_FONT            = "set.font";
        
    /** Bound property name for set fill paint. */
    public static final String SET_FILL_PAINT      = "set.fill.paint";
    
    /** Bound property name for set anchor locator. */
    public static final String SET_ANCHOR_LOCATOR  = "set.anchor.locator";
        
    /** Bound property name for set anchor position. */
    public static final String SET_ANCHOR_POSITION = "set.anchor.position";
    
    /** Bound property name for set bounds strategy. */
    public static final String SET_BOUNDS_STRATEGY = "set.bounds.strategy";
    
    
    /**
     * The standard font render context defined using the identity
     * transform, anti-aliasing on, and fractional-metrics off.
     */
    public static final FontRenderContext standardFRC
        = new FontRenderContext(null, true, false);
    
    
    /** The text string. */
    private String string = null;
    
    /** The font. */
    private Font font = null;
    
    /** The fill paint. */
    private Paint fillpaint = null;
    
    /** The text bounds strategy. */
    private TextBounds.Strategy strategy = null;
    
    /** The text anchor locator. */
    private TextAnchor.Locator locator = null;
    
    /** The x anchor position. */
    private float anchorX = 0;
    
    /** The y anchor position. */
    private float anchorY = 0;
    
    /** The TextLayout for bounds computation and text rendering. */
    private TextLayout textLayout = null;
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * all defaults.</p>
     *
     * <p>The default anchor position is (0, 0) which must be changed to
     * a more useful value by the caller.</p>
     *
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable(String)
     * @see #TextPaintable(String, Font)
     * @see #TextPaintable(String, Font, Paint)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator, float, float)
     */
    public TextPaintable()
    {
        this(null, null, null, null, null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string.</p>
     *
     * <p>The default anchor position is (0, 0) which must be changed to
     * a more useful value by the caller.</p>
     *
     * @param string    the string to display
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable()
     * @see #TextPaintable(String, Font)
     * @see #TextPaintable(String, Font, Paint)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator, float, float)
     */
    public TextPaintable(String string)
    {
        this(string, null, null, null, null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string and font.</p>
     *
     * <p>The default anchor position is (0, 0) which must be changed to
     * a more useful value by the caller.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable()
     * @see #TextPaintable(String)
     * @see #TextPaintable(String, Font, Paint)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator, float, float)
     */
    public TextPaintable(String string, Font font)
    {
        this(string, font, null, null, null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, and fill paint.</p>
     *
     * <p>The default anchor position is (0, 0) which must be changed to
     * a more useful value by the caller.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable()
     * @see #TextPaintable(String)
     * @see #TextPaintable(String, Font)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator, float, float)
     */
    public TextPaintable(String string, Font font, Paint fillpaint)
    {
        this(string, font, fillpaint, null, null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, fill paint, and bounds strategy.</p>
     *
     * <p>The default anchor position is (0, 0) which must be changed to
     * a more useful value by the caller.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @param strategy  the strategy to use to compute the paintable bounds
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable()
     * @see #TextPaintable(String)
     * @see #TextPaintable(String, Font)
     * @see #TextPaintable(String, Font, Paint)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator, float, float)
     */
    public TextPaintable(
        String              string,
        Font                font,
        Paint               fillpaint,
        TextBounds.Strategy strategy)
    {
        this(string, font, fillpaint, strategy, null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, fill paint, bounds strategy, and anchor
     * locator.</p>
     *
     * <p>The default anchor position is (0, 0) which must be changed to
     * a more useful value by the caller.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @param strategy  the strategy to use to compute the paintable bounds
     * @param locator   the locator for the anchor position
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable()
     * @see #TextPaintable(String)
     * @see #TextPaintable(String, Font)
     * @see #TextPaintable(String, Font, Paint)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator, float, float)
     */
    public TextPaintable(
        String              string,
        Font                font,
        Paint               fillpaint,
        TextBounds.Strategy strategy,
        TextAnchor.Locator  locator)
    {
        this(string, font, fillpaint, strategy, locator, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, fill paint, bounds strategy, anchor locator,
     * and anchor position.</p>
     *
     * <p>Parameters that are <code>null</code> are set to the following
     * defaults:</p>
     *
     * <ul>
     *   <li><code>string:    ""                       </code></li>
     *   <li><code>font:      getDefaultFont()         </code></li>
     *   <li><code>fillpaint: Color.black              </code></li>
     *   <li><code>strategy:  TextBounds.LOOSE         </code></li>
     *   <li><code>locator:   TextAnchor.LEFT_BASELINE </code></li>
     * </ul>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @param fillpaint the paint to use to fill the string
     * @param strategy  the strategy to use to compute the paintable bounds
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #setString(String)
     * @see #setFont(Font)
     * @see #setFillPaint(Paint)
     * @see #setBoundsStrategy(TextBounds.Strategy)
     * @see #setAnchorLocator(TextAnchor.Locator)
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     * @see #TextPaintable()
     * @see #TextPaintable(String)
     * @see #TextPaintable(String, Font)
     * @see #TextPaintable(String, Font, Paint)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy)
     * @see #TextPaintable(String, Font, Paint, TextBounds.Strategy, TextAnchor.Locator)
     */
    public TextPaintable(
        String              string,
        Font                font,
        Paint               fillpaint,
        TextBounds.Strategy strategy,
        TextAnchor.Locator  locator,
        float               anchorX,
        float               anchorY)
    {
        // temporarily set internal string to a valid value
        // to avoid null pointer exception in setFont
        this.string = "";
        
        setFont(font);
        setString(string);
        setFillPaint(fillpaint);
        setBoundsStrategy(strategy);
        setAnchorLocator(locator);
        setAnchorPosition(anchorX, anchorY);
        
        resetTextLayout();
    }
    
    
    /**
     * <p>Paints onto a <code>Graphics</code> context g using information
     * from this object.</p>
     *
     * <p>Does nothing if the encapsulated text string has zero length.</p>
     *
     * @param g the graphics context on which to paint
     */
    public final void paint(Graphics g) {
        if ((string.length() == 0) || (g == null) || !isVisible())
            return;
        
        Graphics2D h = getPreparedGraphics2D(g);
        
        h.setFont(font);
        h.setPaint(fillpaint);
        
        textLayout.draw(h, getLeftX(), getBaseLineY());
    }
    
    
    /**
     * <p>Returns the internal <code>TextLayout</code> that encapsulates
     * the text string and the font.</p>
     *
     * <p>If the encapsulated text string has zero length, then returns
     * <code>null</code>.</p>
     *
     * @return the internal <code>TextLayout</code> object
     */
    public TextLayout getTextLayout() {
        return textLayout;
    }
    
    
    /**
     * <p>Returns a copy of the 2-dimensional bounds of the paint region
     * as computed using the <code>getStringBounds</code> method of the
     * <code>Font</code> class together with the anchor locator.</p>
     *
     * <p>In the vertical direction, these bounds extend from the ascent
     * line to the leading line.  Thus, the height of these bounds do
     * not depend on the particular string being rendered.</p>
     *
     * <p>In the horizontal direction, these bounds extend from the left
     * edge of string being painted and will include allowances for both
     * leading and trailing white space.  Unfortunately, however, if the
     * string has no leading and trailing whitespace, it is possible for
     * pixels to be painted outside of these bounds due to serifs or to
     * the slant of an italic font.</p>
     * 
     * <p>If the encapsulated text string has zero length, then this method
     * returns a rectangle at the anchor position of zero width and height.</p>
     *
     * @return a copy of the 2-dimensional bounds of the paint region
     * @see #getTightBounds()
     * @see #getLooseBounds()
     * @see #getBounds2D()
     */
    public final Rectangle2D getStringBounds() {
        if (string.length() == 0)
            return new Rectangle2D.Double(anchorX, anchorY, 0, 0);
        
        return locator.getBounds2D(string, font, anchorX, anchorY);
    }
    
    
    /**
     * <p>Returns a copy of the 2-dimensional bounds of the paint region
     * as computed using the <code>getBounds</code> method of the
     * <code>TextLayout</code> class together with the anchor locator.</p>
     *
     * <p>These bounds return a <I>tight</I> rectangle that includes all
     * pixel positions that may be modified when the string is painted.
     * The only exception to this rule is that anti-aliasing may cause
     * some pixels adjacent to this rectangle to be rendered also. There
     * is no Java code that is capable of testing for this effect.</p>
     *
     * <p>The height of these bounds will vary from string to string
     * depending on the height of the ascenders and descenders of the
     * particular characters in the particular string.</p>
     *
     * <p>These bounds do not include leading and trailing whitespace.</p>
     *
     * <p>Be aware that if you have set the logical left edge of the paint
     * area for a string to some value <I>x</I> then it is still possible
     * for pixels to the left of <I>x</I> to be modified during rendering
     * of the string due to the presence of serifs.  The bounds returned
     * by this method will include adjustments for such effects and will
     * be as accurate as possible.</p>
     * 
     * <p>If the encapsulated text string has zero length, then this method
     * returns a rectangle at the anchor position of zero width and height.</p>
     *
     * @return a copy of the tight bounds of the paint region
     * @see #getStringBounds()
     * @see #getLooseBounds()
     * @see #getBounds2D()
     */
    public final Rectangle2D getTightBounds() {
        if (string.length() == 0)
            return new Rectangle2D.Double(anchorX, anchorY, 0, 0);
        
        // find absolute bounds
        Rectangle2D area = textLayout.getBounds();
        
        float x = (float) area.getX();
        float y = (float) area.getY();
        float w = (float) area.getWidth();
        float h = (float) area.getHeight();
        
        // check for zero width or height
        if ((w <= 0) || (h <= 0))
            return new Rectangle2D.Double(anchorX, anchorY, 0, 0);
        
        // get offset for string position
        float x0 = getLeftX();
        float y0 = getBaseLineY();
        
        // offset area
        area.setRect(x + x0, y + y0, w, h);
        
        return area;
    }
    
    
    /**
     * <p>Returns the smallest rectangle that contains the pair of rectangles
     * <code>getStringBounds()</code> and <code>getTightBounds()</code>.</p>
     *
     * <p>These bounds return a <I>loose</I> rectangle that includes all
     * pixel positions that may be modified when the string is painted plus
     * allowances for additional horizontal and vertical white space.</p>
     *
     * <p>If the encapsulated text string has zero length, then this method
     * returns a rectangle at the anchor position of zero width and height.</p>
     *
     * @return a copy of the tight bounds of the paint region
     * @see #getStringBounds()
     * @see #getTightBounds()
     * @see #getBounds2D()
     */
    public final Rectangle2D getLooseBounds() {
        Rectangle2D area = getStringBounds();
        area.add(getTightBounds());
        
        return area;
    }
    
    
    /**
     * <p>Returns the bounds of the paintable based on the default settings or on
     * more detailed computations.</p>
     *
     * <p>If the value of <code>getDefaultBounds2D</code> is non-<code>null</code>,
     * then this value is returned.</p>
     *
     * <p>Otherwise, if the encapsulated text string has zero length, then returns
     * a rectangle of zero width and height located at the anchor location.</p>
     *
     * <p>Otherwise, returns a copy of the 2-dimensional bounds of the paint
     * region affected by the <code>paint</code> method according to the
     * current bounds strategy.</p>
     *
     * @return a copy of the 2-dimensional bounds of the paint region
     * @see #getStringBounds()
     * @see #getTightBounds()
     * @see #getLooseBounds()
     */
    public final Rectangle2D getBounds2D() {
        // get the default bounds
        Rectangle2D bounds = getDefaultBounds2D();
        
        if (bounds != null)
            return bounds;
        
        // string has length 0
        if (string.length() == 0)
            return new Rectangle2D.Double(anchorX, anchorY, 0, 0);
        
        // compute bounds
        return strategy.getBounds2D(this);
    }
    
    
    /**
     * <p>Returns a copy of the center of the paint region affected
     * by the <code>paint</code> method.</p>
     *
     * <p>The center is computed as the center of the base line.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return a copy of the center of the paint region
     */
    public final Point2D getCenter() {
        if (string.length() == 0)
            return new Point2D.Double(anchorX, anchorY);
        
        return locator.getCenter(string, font, anchorX, anchorY);
    }
    

    /**
     * <p>Returns the <code>Shape</code> that represents the outline of
     * the rendered text string in its font at its location.</p>
     *
     * <p>Experimental tests show that the <code>Shape</code> returned
     * by this method is not 100% accurate.  This is due to problems
     * in the <code>getOutline</code> method of the Java class
     * <code>TextLayout</code>.</p>
     *
     * <p>If the encapsulated text string has zero length, then returns
     * an empty <code>Shape</code>.</p>
     *
     * @return the <code>Shape</code> object
     */
    public Shape getOutline() {
        return (string.length() == 0)
            ? new GeneralPath()
            : textLayout.getOutline
                (TransformFactory.translate(getLeftX(), getBaseLineY()));
    }
    
    
    /**
     * <p>Tests if a point specified by coordinates is inside the paintable.</p>
     *
     * <p>This method returns <code>false</code> if one or more of the following
     * conditions occurs:</p>
     *
     * <ul>
     *   <li>The point is not in the rectangle <code>getBounds2D</code>.</li>
     *   <li>The method <code>isVisible</code> returns <code>false</code>.</li>
     * </ul>
     *
     * @param  x the x-coordinate of the point
     * @param  y the y-coordinate of the point
     * @return whether or not a specified point is inside the paintable
     */
    public boolean contains(double x, double y) {
        if (!possiblyContains(x, y))
            return false;
        
        return getOutline().contains(x, y);
    }
    
    
    /**
     * <p>Sets the string for this <code>TextPaintable</code>.</p>
     *
     * <p>If the given string is <code>null</code>, it is set to
     * the zero length string.</p>
     *
     * <p>Does nothing if the string is equal to the current string.</p>
     *
     * <p>Fires property change: SET_STRING.</p>
     *
     * @param string the string to display
     * @see #getString()
     */
    public final void setString(String string) {
        if (string == null)
            string = "";
        
        if (string.equals(this.string))
            return;
        
        this.string = string;
        resetTextLayout();
        
        firePropertyChange(SET_STRING, null, null);
    }
    
    
    /**
     * Returns the string for this <code>TextPaintable</code>.
     *
     * @return the string to display
     * @see #setString(String)
     */
    public final String getString() {
        return string;
    }
    
    
    /**
     * <p>Sets the font for this <code>TextPaintable</code>.</p>
     *
     * <p>If the given font is <code>null</code> then it is set to
     * <code>getDefaultFont()</code>.</p>
     *
     * <p>Does nothing if the font is equal to the current font.</p>
     *
     * <p>Fires property change: SET_FONT.</p>
     *
     * @param font the font to use to display the string
     * @see #getFont()
     */
    public final void setFont(Font font) {
        if (font == null)
            font = getDefaultFont();
        
        if (font.equals(this.font))
            return;
        
        removeAndAddForwardingListener(this.font, font);
        
        this.font = font;
        resetTextLayout();
        
        firePropertyChange(SET_FONT, null, null);
    }
    
    
    /**
     * Returns the font for this <code>TextPaintable</code>.
     *
     * @return the font to use to display the string
     * @see #setFont(Font)
     */
    public final Font getFont() {
        return font;
    }
    
    
    /**
     * <p>Sets the fill paint for this <code>TextPaintable</code>.</p>
     *
     * <p>If the given paint is <code>null</code>, it is set to
     * <code>Color.black</code>.</p>
     *
     * <p>Fires property change: SET_FILL_PAINT.</p>
     *
     * @param fillpaint the paint to use to fill the string
     * @see #getFillPaint()
     */
    public final void setFillPaint(Paint fillpaint) {
        if (fillpaint == null)
            fillpaint = Color.black;
        
        if (fillpaint.equals(this.fillpaint))
            return;
        
        removeAndAddForwardingListener(this.fillpaint, fillpaint);
        
        this.fillpaint = fillpaint;
        
        firePropertyChange(SET_FILL_PAINT, null, null);
    }
    
    
    /**
     * Returns the fill paint for this <code>TextPaintable</code>.
     *
     * @return the paint to use to fill the string
     * @see #setFillPaint(Paint)
     */
    public final Paint getFillPaint() {
        return fillpaint;
    }
    
    
    /**
     * <p>Sets the bounds strategy for this <code>TextPaintable</code>.</p>
     *
     * <p>If the given strategy is <code>null</code>, it is set to
     * <code>TextBounds.LOOSE</code>.</p>
     *
     * <p>Fires property change: SET_BOUNDS_STRATEGY.</p>
     *
     * @param strategy the strategy to use to compute the paintable bounds
     * @see #getBoundsStrategy()
     */
    public final void setBoundsStrategy(TextBounds.Strategy strategy) {
        if (strategy == null)
            strategy = TextBounds.LOOSE;
        
        if (strategy.equals(this.strategy))
            return;
        
        this.strategy = strategy;
        
        firePropertyChange(SET_BOUNDS_STRATEGY, null, null);
    }
    
    
    /**
     * Returns the bounds strategy for this <code>TextPaintable</code>.
     *
     * @return the strategy to use to compute the paintable bounds
     * @see #setFillPaint(Paint)
     */
    public final TextBounds.Strategy getBoundsStrategy() {
        return strategy;
    }
    
    
    /**
     * <p>Sets the <code>TextAnchor.Locator</code> for this
     * <code>TextPaintable</code>.</p>
     *
     * <p>The following constants are provided in the class
     * <code>TextAnchor</code> as possible options for the
     * <code>TextAnchor.Locator</code> parameter:</p>
     *
     * <ul>
     *   <li><code>TextAnchor.LEFT_BASELINE</code></li>
     *   <li><code>TextAnchor.RIGHT_BASELINE</code></li>
     *   <li><code>TextAnchor.CENTER_BASELINE</code></li>
     *   <li><code>TextAnchor.LEFT_ASCENTLINE</code></li>
     *   <li><code>TextAnchor.RIGHT_ASCENTLINE</code></li>
     *   <li><code>TextAnchor.CENTER_ASCENTLINE</code></li>
     *   <li><code>TextAnchor.LEFT_DESCENTLINE</code></li>
     *   <li><code>TextAnchor.RIGHT_DESCENTLINE</code></li>
     *   <li><code>TextAnchor.CENTER_DESCENTLINE</code></li>
     *   <li><code>TextAnchor.LEFT_LEADINGLINE</code></li>
     *   <li><code>TextAnchor.RIGHT_LEADINGLINE</code></li>
     *   <li><code>TextAnchor.CENTER_LEADINGLINE</code></li>
     * </ul>
     *
     * <p>If the given locator is <code>null</code>, it is set to
     * <code>TextAnchor.LEFT_BASELINE</code>.</p>
     *
     * <p>Fires property change: SET_ANCHOR_LOCATOR.</p>
     *
     * @param locator the locator for the anchor position
     * @see #getAnchorLocator()
     */
    public final void setAnchorLocator(TextAnchor.Locator locator) {
        if (locator == null)
            locator = TextAnchor.LEFT_BASELINE;
        
        if (locator.equals(this.locator))
            return;
        
        this.locator = locator;
        
        firePropertyChange(SET_ANCHOR_LOCATOR, null, null);
    }
    
    
    /**
     * <p>Returns the <code>TextAnchor.Locator</code> for this
     * <code>TextPaintable</code>.</p>
     *
     * @return the locator for the anchor position
     * @see #setAnchorLocator(TextAnchor.Locator)
     */
    public final TextAnchor.Locator getAnchorLocator() {
        return locator;
    }
    
    
    /**
     * <p>Sets the anchor position for this <code>TextPaintable</code>.</p>
     *
     * <p>Fires property change: SET_ANCHOR_POSITION.</p>
     *
     * @param anchorX the anchor x-coordinate
     * @param anchorY the anchor y-coordinate
     * @see #setAnchorPosition(float[])
     * @see #getAnchorPosition()
     */
    public final void setAnchorPosition(float anchorX, float anchorY) {
        if ((anchorX == this.anchorX) && (anchorY == this.anchorY))
            return;
        
        this.anchorX = anchorX;
        this.anchorY = anchorY;
        
        firePropertyChange(SET_ANCHOR_POSITION, null, null);
    }
    
    
    /**
     * <p>Sets the anchor position for this <code>TextPaintable</code>
     * using an array <code>float[2]</code>.</p>
     *
     * <p>A <code>null</code> parameter is ignored.</p>
     *
     * <p>Fires property change: SET_ANCHOR_POSITION.</p>
     *
     * @param position the anchor position
     * @see #setAnchorPosition(float, float)
     * @see #getAnchorPosition()
     */
    public final void setAnchorPosition(float[] position) {
        if ((position == null) || (position.length != 2))
            return;
        
        setAnchorPosition(position[0], position[1]);
    }
    
    
    /**
     * Returns a copy of the anchor position for this <code>TextPaintable</code>
     * as an array <code>float[2]</code>.
     *
     * @return the anchor position
     * @see #setAnchorPosition(float, float)
     * @see #setAnchorPosition(float[])
     */
    public final float[] getAnchorPosition() {
        return new float[] { anchorX, anchorY };
    }
    
    
    /**
     * <p>Returns the left x-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return the left x-coordinate of this text paintable
     */
    public final float getLeftX() {
        if (string.length() == 0)
            return anchorX;
        
        return locator.getLeftX(string, font, anchorX);
    }
    
    
    /**
     * <p>Returns the right x-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return the right x-coordinate of this text paintable
     */
    public final float getRightX() {
        if (string.length() == 0)
            return anchorX;
        
        return locator.getRightX(string, font, anchorX);
    }
    
    
    /**
     * <p>Returns the center x-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return the center x-coordinate of this text paintable
     */
    public final float getCenterX() {
        if (string.length() == 0)
            return anchorX;
        
        return locator.getCenterX(string, font, anchorX);
    }
    
    
    /**
     * <p>Returns the base line y-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return the base line y-coordinate of this text paintable
     */
    public final float getBaseLineY() {
        if (string.length() == 0)
            return anchorY;
        
        return locator.getBaseLineY(string, font, anchorY);
    }
    
    
    /**
     * <p>Returns the ascent line y-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return the ascent line y-coordinate of this text paintable
     */
    public final float getAscentLineY() {
        if (string.length() == 0)
            return anchorY;
        
        return locator.getAscentLineY(string, font, anchorY);
    }
    
    
    /**
     * <p>Returns the descent line y-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the anchor locator.</p>
     *
     * @return the descent line y-coordinate of this text paintable
     */
    public final float getDescentLineY() {
        if (string.length() == 0)
            return anchorY;
        
        return locator.getDescentLineY(string, font, anchorY);
    }
    
    
    /**
     * <p>Returns the leading line y-coordinate of this <code>TextPaintable</code>.</p>
     *
     * <p>This methods uses the anchor locator.</p>
     *
     * @return the leading line y-coordinate of this text paintable
     */
    public final float getLeadingLineY() {
        if (string.length() == 0)
            return anchorY;
        
        return locator.getLeadingLineY(string, font, anchorY);
    }
    
    
    /**
     * <p>Returns the width of the area that will be occupied by this
     * <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the method <code>getBounds2D</code>.
     *
     * @return the width of this text paintable
     */
    public final float getWidth() {
        return (float) getBounds2D().getWidth();
    }
    
    
    /**
     * <p>Returns the height of the area that will be occupied by this
     * <code>TextPaintable</code>.</p>
     *
     * <p>This method uses the method <code>getBounds2D</code>.
     *
     * @return the height of this text paintable
     */
    public final float getHeight() {
        return (float) getBounds2D().getHeight();
    }
    
    
    /**
     * <p>Returns the default font for a <code>JLabel</code> in the
     * current UI settings.  This font is used in constructing a 
     * <code>TextPaintable</code> if the user does not specify a
     * font.</p>
     *
     * @return the current <code>JLabel</code> default font
     */
    public static Font getDefaultFont() {
        JLabel label = new JLabel();
        return label.getFont();
    }
    
    
    /**
     * Resets the text layout on string or font change.
     */
    private void resetTextLayout() {
        textLayout = (string.length() == 0)
            ? null
            : new TextLayout(string, font, standardFRC);
    }
    
}
