/*
 * @(#)TextPaintable.java    2.6.0   7 June 2007
 *
 * Copyright 2007
 * 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 edu.neu.ccs.*;

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.5, the class was refactored to be consistent with
 * the new <code>Paintable</code> interface and
 * the new <code>AbstractPaintable</code> class.
 *
 * <p>In 2.4.0, the default anchor locator was changed from
 * <code>TextAnchor.LEFT_BASELINE</code> to
 * <code>TextAnchor.LEFT_ASCENTLINE</code>.  The rationale for
 * this change is to allow text with the default anchor locator
 * and default anchor position of (0, 0) to be visible in a
 * panel or window whose upper left corner is also (0, 0).  In
 * practice, a user is likely to want to set the anchor locator
 * and anchor position explicitly but this new default makes it
 * easier to do quick testing.</p>
 *
 * <p>In 2.4.0, several constructors were added that permit the
 * anchor locator and anchor position to be set without
 * specifying other parameters.</p>
 *
 * <p>In 2.4.0, this class was also updated to be consistent with
 * refinements to the <code>Paintable</code> interface.</p>
 * 
 * <p>In 2.6.0, changed <code>getOriginalCenter()</code> to return
 * a point equal to the anchor position unless the caller has set
 * a specific default original position.</p>
 * 
 * <p>In 2.6.0, changed <code>setAnchorPostion</code> by calling a
 * new method <code>shiftAnchorPostion</code>.  This method makes
 * an adjustment to the internal mutator so that the net effect is
 * visually the same as if the shift had been applied by calling
 * the <code>move</code> method on the mutated paintable.</p>
 * 
 * <p>In 2.6.0, removed final designations on methods to allow for
 * overrides.</p>
 *
 * @author  Richard Rasala
 * @version 2.6.0
 * @since   2.3
 * @see     MultiLineTextPaintable
 */
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>
     *
     * <ul>
     *   <li><pre>string:    ""                        </pre></li>
     *   <li><pre>font:      getDefaultFont()          </pre></li>
     *   <li><pre>fillpaint: Color.black               </pre></li>
     *   <li><pre>strategy:  TextBounds.LOOSE          </pre></li>
     *   <li><pre>locator:   TextAnchor.LEFT_ASCENTLINE</pre></li>
     *   <li><pre>anchorX:   0                         </pre></li>
     *   <li><pre>anchorY:   0                         </pre></li>
     * </ul>
     */
    public TextPaintable()
    {
        this(null, null, null, null, null, 0, 0);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string
     * and with the defaults set as described in the default constructor.</p>
     *
     * @param string    the string to display
     * @see #TextPaintable()
     */
    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 anchor position
     * and with the defaults set as described in the default constructor.</p>
     *
     * @param string    the string to display
     * @param anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(String string, float anchorX, float anchorY)
    {
        this(string, null, null, null, null, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, anchor locator, and anchor position
     * and with the defaults set as described in the default constructor.</p>
     *
     * @param string    the string to display
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(
        String             string,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        this(string, null, null, null, locator, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string and font
     * and with the defaults set as described in the default constructor.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @see #TextPaintable()
     */
    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 anchor position
     * and with the defaults set as described in the default constructor.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @param anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(String string, Font font, float anchorX, float anchorY)
    {
        this(string, font, null, null, null, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, anchor locator, and anchor position
     * and with the defaults set as described in the default constructor.</p>
     *
     * @param string    the string to display
     * @param font      the font to use to display the string
     * @param locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(
        String             string,
        Font               font,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        this(string, font, null, null, locator, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, and fill paint
     * and with the defaults set as described in the default constructor.</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 #TextPaintable()
     */
    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 anchor position
     * and with the defaults set as described in the default constructor.</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 anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(
        String string,
        Font   font,
        Paint  fillpaint,
        float  anchorX,
        float  anchorY)
    {
        this(string, font, fillpaint, null, null, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, fill paint, anchor locator, and anchor position
     * and with the defaults set as described in the default constructor.</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 locator   the locator for the anchor position
     * @param anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(
        String             string,
        Font               font,
        Paint              fillpaint,
        TextAnchor.Locator locator,
        float              anchorX,
        float              anchorY)
    {
        this(string, font, fillpaint, null, locator, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, fill paint, and bounds strategy
     * and with the defaults set as described in the default constructor.</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 #TextPaintable()
     */
    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
     * position
     * and with the defaults set as described in the default constructor.</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 anchorX   the anchor x-coordinate
     * @param anchorY   the anchor y-coordinate
     * @see #TextPaintable()
     */
    public TextPaintable(
        String              string,
        Font                font,
        Paint               fillpaint,
        TextBounds.Strategy strategy,
        float               anchorX,
        float               anchorY)
    {
        this(string, font, fillpaint, strategy, null, anchorX, anchorY);
    }
    
    
    /**
     * <p>The constructor that creates a <code>TextPaintable</code> with
     * the given string, font, fill paint, bounds strategy, and anchor
     * locator
     * and with the defaults set as described in the default constructor.</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 #TextPaintable()
     */
    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 as in the default constructor:</p>
     *
     * <ul>
     *   <li><pre>string:    ""                        </pre></li>
     *   <li><pre>font:      getDefaultFont()          </pre></li>
     *   <li><pre>fillpaint: Color.black               </pre></li>
     *   <li><pre>strategy:  TextBounds.LOOSE          </pre></li>
     *   <li><pre>locator:   TextAnchor.LEFT_ASCENTLINE</pre></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 #TextPaintable()
     */
    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 using information
     * from this object but without the use of the mutator transform.</P>
     *
     * <p>Does nothing if the encapsulated text string has zero length
     * or the graphics context is <code>null</code>.</p>
     *
     * @param g the graphics context on which to paint
     */
    public void originalPaint(Graphics g) {
        if ((string.length() == 0) || (g == null) || !isVisible())
            return;
        
        Graphics2D h = (Graphics2D) g.create();
        
        h.setFont(font);
        h.setPaint(fillpaint);
        
        textLayout.draw(h, getLeftX(), getBaseLineY());
    }
    
    
    /**
     * <p>Returns the actual bounds of the original paintable or
     * <code>null</code> if the paintable is effectively empty.</p>
     */
    public XRect getActualBounds2D() {
        if (string.length() == 0)
            return null;
        
        return strategy.getBounds2D(this);
    }
    
    
    /**
     * <p>Returns a copy of the center of the paint region affected
     * by the <code>originalPaint</code> method.</p>
     *
     * <p>If the user has set a default original position then that
     * is returned.  Otherwise, we define the original center to be
     * the anchor position.  This will usually not be the geometric
     * center.</p>
     *
     * @return a copy of the center of the original paint region
     */
    public XPoint2D getOriginalCenter() {
        XPoint2D center = getDefaultOriginalCenter();
        
        if (center != null)
            return center;
        
        return new XPoint2D(anchorX, anchorY);
    }
    

    /**
     * <p>Tests if a point specified by coordinates is inside the
     * original paintable without mutation.</p>
     *
     * @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
     *         original paintable
     */
    public boolean originalContains(double x, double y) {
        if (string.length() == 0)
            return false;
        
        if (!possiblyContains(x, y))
            return false;
        
        return getOutline().contains(x, y);
    }
    
    
    /**
     * <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 original
     * 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
     */
    public XRect getStringBounds() {
        if (string.length() == 0)
            return new XRect(anchorX, anchorY, 0, 0);
        
        return locator.getBounds2D(string, font, anchorX, anchorY);
    }
    
    
    /**
     * <p>Returns a copy of the 2-dimensional bounds of the original
     * 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
     */
    public XRect getTightBounds() {
        if (string.length() == 0)
            return new XRect(anchorX, anchorY, 0, 0);
        
        // find absolute bounds
        XRect area = new XRect(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 XRect(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
     */
    public XRect getLooseBounds() {
        XRect area = getStringBounds();
        return area.createUnionRect(getTightBounds());
    }
    
    
    /**
     * <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>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
     */
    public 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
     */
    public 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
     */
    public 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
     */
    public 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
     */
    public 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
     */
    public 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
     */
    public 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
     */
    public 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_ASCENTLINE</code>.</p>
     *
     * <p>Fires property change: SET_ANCHOR_LOCATOR.</p>
     *
     * @param locator the locator for the anchor position
     */
    public void setAnchorLocator(TextAnchor.Locator locator) {
        if (locator == null)
            locator = TextAnchor.LEFT_ASCENTLINE;
        
        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
     */
    public 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
     */
    public void setAnchorPosition(float anchorX, float anchorY) {
        float dx = anchorX - this.anchorX;
        float dy = anchorY - this.anchorY;
        
        shiftAnchorPosition(dx, dy);
    }
    
    
    /**
     * <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
     */
    public void setAnchorPosition(float[] position) {
        if ((position == null) || (position.length != 2))
            return;
        
        setAnchorPosition(position[0], position[1]);
    }
    
    
    /**
     * <p>Shift the anchor position by the given dx and dy.</p>
     * 
     * <p>Fires property change: SET_ANCHOR_POSITION.</p>
     *
     * @param dx the shift in the anchor x position
     * @param dy the shift in the anchor y position
     */
    public void shiftAnchorPosition(float dx, float dy) {
        if ((dx == 0) && (dy == 0))
            return;
        
        anchorX += dx;
        anchorY += dy;
        
        addPreMutation (TransformFactory.translate(-dx, -dy));
        addPostMutation(TransformFactory.translate( dx,  dy));
        
        firePropertyChange(SET_ANCHOR_POSITION, null, null);
    }
    
    
    /**
     * Returns a copy of the anchor position for this <code>TextPaintable</code>
     * as an array <code>float[2]</code>.
     *
     * @return the anchor position
     */
    public 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 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 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 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 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 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 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 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 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 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);
    }
    
}
