001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------
028 * TextTitle.java
029 * --------------
030 * (C) Copyright 2000-2021, by David Berry and Contributors.
031 *
032 * Original Author:  David Berry;
033 * Contributor(s):   David Gilbert;
034 *                   Nicolas Brodu;
035 *                   Peter Kolb - patch 2603321;
036 */
037
038package org.jfree.chart.title;
039
040import java.awt.Color;
041import java.awt.Font;
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.geom.Rectangle2D;
045import java.io.IOException;
046import java.io.ObjectInputStream;
047import java.io.ObjectOutputStream;
048import java.io.Serializable;
049import java.util.Objects;
050
051import org.jfree.chart.block.BlockResult;
052import org.jfree.chart.block.EntityBlockParams;
053import org.jfree.chart.block.LengthConstraintType;
054import org.jfree.chart.block.RectangleConstraint;
055import org.jfree.chart.entity.ChartEntity;
056import org.jfree.chart.entity.EntityCollection;
057import org.jfree.chart.entity.StandardEntityCollection;
058import org.jfree.chart.entity.TitleEntity;
059import org.jfree.chart.event.TitleChangeEvent;
060import org.jfree.chart.text.G2TextMeasurer;
061import org.jfree.chart.text.TextBlock;
062import org.jfree.chart.text.TextBlockAnchor;
063import org.jfree.chart.text.TextUtils;
064import org.jfree.chart.api.HorizontalAlignment;
065import org.jfree.chart.api.RectangleEdge;
066import org.jfree.chart.api.RectangleInsets;
067import org.jfree.chart.block.Size2D;
068import org.jfree.chart.api.VerticalAlignment;
069import org.jfree.chart.internal.PaintUtils;
070import org.jfree.chart.internal.Args;
071import org.jfree.chart.api.PublicCloneable;
072import org.jfree.chart.internal.SerialUtils;
073import org.jfree.data.Range;
074
075/**
076 * A chart title that displays a text string with automatic wrapping as
077 * required.
078 */
079public class TextTitle extends Title implements Serializable, Cloneable, 
080        PublicCloneable {
081
082    /** For serialization. */
083    private static final long serialVersionUID = 8372008692127477443L;
084
085    /** The default font. */
086    public static final Font DEFAULT_FONT = new Font("SansSerif", Font.BOLD,
087            12);
088
089    /** The default text color. */
090    public static final Paint DEFAULT_TEXT_PAINT = Color.BLACK;
091
092    /** The title text. */
093    private String text;
094
095    /** The font used to display the title. */
096    private Font font;
097
098    /** The text alignment. */
099    private HorizontalAlignment textAlignment;
100
101    /** The paint used to display the title text. */
102    private transient Paint paint;
103
104    /** The background paint. */
105    private transient Paint backgroundPaint;
106
107    /** The tool tip text (can be {@code null}). */
108    private String toolTipText;
109
110    /** The URL text (can be {@code null}). */
111    private String urlText;
112
113    /** The content. */
114    private TextBlock content;
115
116    /**
117     * A flag that controls whether the title expands to fit the available
118     * space..
119     */
120    private boolean expandToFitSpace = false;
121
122    /**
123     * The maximum number of lines to display.
124     */
125    private int maximumLinesToDisplay = Integer.MAX_VALUE;
126
127    /**
128     * Creates a new title, using default attributes where necessary.
129     */
130    public TextTitle() {
131        this("");
132    }
133
134    /**
135     * Creates a new title, using default attributes where necessary.
136     *
137     * @param text  the title text ({@code null} not permitted).
138     */
139    public TextTitle(String text) {
140        this(text, TextTitle.DEFAULT_FONT, TextTitle.DEFAULT_TEXT_PAINT,
141                Title.DEFAULT_POSITION, Title.DEFAULT_HORIZONTAL_ALIGNMENT,
142                Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING);
143    }
144
145    /**
146     * Creates a new title, using default attributes where necessary.
147     *
148     * @param text  the title text ({@code null} not permitted).
149     * @param font  the title font ({@code null} not permitted).
150     */
151    public TextTitle(String text, Font font) {
152        this(text, font, TextTitle.DEFAULT_TEXT_PAINT, Title.DEFAULT_POSITION,
153                Title.DEFAULT_HORIZONTAL_ALIGNMENT,
154                Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING);
155    }
156
157    /**
158     * Creates a new title with the specified attributes.
159     *
160     * @param text  the text for the title ({@code null} not permitted).
161     * @param font  the title font ({@code null} not permitted).
162     * @param paint  the title paint ({@code null} not permitted).
163     * @param position  the title position ({@code null} not permitted).
164     * @param horizontalAlignment  the horizontal alignment ({@code null}
165     *                             not permitted).
166     * @param verticalAlignment  the vertical alignment ({@code null} not
167     *                           permitted).
168     * @param padding  the space to leave around the outside of the title.
169     */
170    public TextTitle(String text, Font font, Paint paint,
171                     RectangleEdge position,
172                     HorizontalAlignment horizontalAlignment,
173                     VerticalAlignment verticalAlignment,
174                     RectangleInsets padding) {
175        super(position, horizontalAlignment, verticalAlignment, padding);
176        Args.nullNotPermitted(text, "text");
177        Args.nullNotPermitted(font, "font");
178        Args.nullNotPermitted(paint, "paint");
179        this.text = text;
180        this.font = font;
181        this.paint = paint;
182        // the textAlignment and the horizontalAlignment are separate things,
183        // but it makes sense for the default textAlignment to match the
184        // title's horizontal alignment...
185        this.textAlignment = horizontalAlignment;
186        this.backgroundPaint = null;
187        this.content = null;
188        this.toolTipText = null;
189        this.urlText = null;
190    }
191
192    /**
193     * Returns the title text.
194     *
195     * @return The text (never {@code null}).
196     *
197     * @see #setText(String)
198     */
199    public String getText() {
200        return this.text;
201    }
202
203    /**
204     * Sets the title to the specified text and sends a
205     * {@link TitleChangeEvent} to all registered listeners.
206     *
207     * @param text  the text ({@code null} not permitted).
208     */
209    public void setText(String text) {
210        Args.nullNotPermitted(text, "text");
211        if (!this.text.equals(text)) {
212            this.text = text;
213            notifyListeners(new TitleChangeEvent(this));
214        }
215    }
216
217    /**
218     * Returns the text alignment.  This controls how the text is aligned
219     * within the title's bounds, whereas the title's horizontal alignment
220     * controls how the title's bounding rectangle is aligned within the
221     * drawing space.
222     *
223     * @return The text alignment.
224     */
225    public HorizontalAlignment getTextAlignment() {
226        return this.textAlignment;
227    }
228
229    /**
230     * Sets the text alignment and sends a {@link TitleChangeEvent} to
231     * all registered listeners.
232     *
233     * @param alignment  the alignment ({@code null} not permitted).
234     */
235    public void setTextAlignment(HorizontalAlignment alignment) {
236        Args.nullNotPermitted(alignment, "alignment");
237        this.textAlignment = alignment;
238        notifyListeners(new TitleChangeEvent(this));
239    }
240
241    /**
242     * Returns the font used to display the title string.
243     *
244     * @return The font (never {@code null}).
245     *
246     * @see #setFont(Font)
247     */
248    public Font getFont() {
249        return this.font;
250    }
251
252    /**
253     * Sets the font used to display the title string.  Registered listeners
254     * are notified that the title has been modified.
255     *
256     * @param font  the new font ({@code null} not permitted).
257     *
258     * @see #getFont()
259     */
260    public void setFont(Font font) {
261        Args.nullNotPermitted(font, "font");
262        if (!this.font.equals(font)) {
263            this.font = font;
264            notifyListeners(new TitleChangeEvent(this));
265        }
266    }
267
268    /**
269     * Returns the paint used to display the title string.
270     *
271     * @return The paint (never {@code null}).
272     *
273     * @see #setPaint(Paint)
274     */
275    public Paint getPaint() {
276        return this.paint;
277    }
278
279    /**
280     * Sets the paint used to display the title string.  Registered listeners
281     * are notified that the title has been modified.
282     *
283     * @param paint  the new paint ({@code null} not permitted).
284     *
285     * @see #getPaint()
286     */
287    public void setPaint(Paint paint) {
288        Args.nullNotPermitted(paint, "paint");
289        if (!this.paint.equals(paint)) {
290            this.paint = paint;
291            notifyListeners(new TitleChangeEvent(this));
292        }
293    }
294
295    /**
296     * Returns the background paint (defaults to {@code null} which makes the 
297     * background transparent).
298     *
299     * @return The paint (possibly {@code null}).
300     */
301    public Paint getBackgroundPaint() {
302        return this.backgroundPaint;
303    }
304
305    /**
306     * Sets the background paint and sends a {@link TitleChangeEvent} to all
307     * registered listeners.  If you set this attribute to {@code null},
308     * no background is painted (which makes the title background transparent).
309     *
310     * @param paint  the background paint ({@code null} permitted).
311     */
312    public void setBackgroundPaint(Paint paint) {
313        this.backgroundPaint = paint;
314        notifyListeners(new TitleChangeEvent(this));
315    }
316
317    /**
318     * Returns the tool tip text.
319     *
320     * @return The tool tip text (possibly {@code null}).
321     */
322    public String getToolTipText() {
323        return this.toolTipText;
324    }
325
326    /**
327     * Sets the tool tip text to the specified text and sends a
328     * {@link TitleChangeEvent} to all registered listeners.
329     *
330     * @param text  the text ({@code null} permitted).
331     */
332    public void setToolTipText(String text) {
333        this.toolTipText = text;
334        notifyListeners(new TitleChangeEvent(this));
335    }
336
337    /**
338     * Returns the URL text.
339     *
340     * @return The URL text (possibly {@code null}).
341     */
342    public String getURLText() {
343        return this.urlText;
344    }
345
346    /**
347     * Sets the URL text to the specified text and sends a
348     * {@link TitleChangeEvent} to all registered listeners.
349     *
350     * @param text  the text ({@code null} permitted).
351     */
352    public void setURLText(String text) {
353        this.urlText = text;
354        notifyListeners(new TitleChangeEvent(this));
355    }
356
357    /**
358     * Returns the flag that controls whether or not the title expands to fit
359     * the available space.
360     *
361     * @return The flag.
362     */
363    public boolean getExpandToFitSpace() {
364        return this.expandToFitSpace;
365    }
366
367    /**
368     * Sets the flag that controls whether the title expands to fit the
369     * available space, and sends a {@link TitleChangeEvent} to all registered
370     * listeners.
371     *
372     * @param expand  the flag.
373     */
374    public void setExpandToFitSpace(boolean expand) {
375        this.expandToFitSpace = expand;
376        notifyListeners(new TitleChangeEvent(this));
377    }
378
379    /**
380     * Returns the maximum number of lines to display.
381     *
382     * @return The maximum.
383     *
384     * @see #setMaximumLinesToDisplay(int)
385     */
386    public int getMaximumLinesToDisplay() {
387        return this.maximumLinesToDisplay;
388    }
389
390    /**
391     * Sets the maximum number of lines to display and sends a
392     * {@link TitleChangeEvent} to all registered listeners.
393     *
394     * @param max  the maximum.
395     *
396     * @see #getMaximumLinesToDisplay()
397     */
398    public void setMaximumLinesToDisplay(int max) {
399        this.maximumLinesToDisplay = max;
400        notifyListeners(new TitleChangeEvent(this));
401    }
402
403    /**
404     * Arranges the contents of the block, within the given constraints, and
405     * returns the block size.
406     *
407     * @param g2  the graphics device.
408     * @param constraint  the constraint ({@code null} not permitted).
409     *
410     * @return The block size (in Java2D units, never {@code null}).
411     */
412    @Override
413    public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
414        RectangleConstraint cc = toContentConstraint(constraint);
415        LengthConstraintType w = cc.getWidthConstraintType();
416        LengthConstraintType h = cc.getHeightConstraintType();
417        Size2D contentSize = null;
418        if (w == LengthConstraintType.NONE) {
419            if (h == LengthConstraintType.NONE) {
420                contentSize = arrangeNN(g2);
421            }
422            else if (h == LengthConstraintType.RANGE) {
423                throw new RuntimeException("Not yet implemented.");
424            }
425            else if (h == LengthConstraintType.FIXED) {
426                throw new RuntimeException("Not yet implemented.");
427            }
428        }
429        else if (w == LengthConstraintType.RANGE) {
430            if (h == LengthConstraintType.NONE) {
431                contentSize = arrangeRN(g2, cc.getWidthRange());
432            }
433            else if (h == LengthConstraintType.RANGE) {
434                contentSize = arrangeRR(g2, cc.getWidthRange(),
435                        cc.getHeightRange());
436            }
437            else if (h == LengthConstraintType.FIXED) {
438                throw new RuntimeException("Not yet implemented.");
439            }
440        }
441        else if (w == LengthConstraintType.FIXED) {
442            if (h == LengthConstraintType.NONE) {
443                contentSize = arrangeFN(g2, cc.getWidth());
444            }
445            else if (h == LengthConstraintType.RANGE) {
446                throw new RuntimeException("Not yet implemented.");
447            }
448            else if (h == LengthConstraintType.FIXED) {
449                throw new RuntimeException("Not yet implemented.");
450            }
451        }
452        assert contentSize != null; // suppress compiler warning
453        return new Size2D(calculateTotalWidth(contentSize.getWidth()),
454                calculateTotalHeight(contentSize.getHeight()));
455    }
456
457    /**
458     * Arranges the content for this title assuming no bounds on the width
459     * or the height, and returns the required size.  This will reflect the
460     * fact that a text title positioned on the left or right of a chart will
461     * be rotated by 90 degrees.
462     *
463     * @param g2  the graphics target.
464     *
465     * @return The content size.
466     */
467    protected Size2D arrangeNN(Graphics2D g2) {
468        Range max = new Range(0.0, Float.MAX_VALUE);
469        return arrangeRR(g2, max, max);
470    }
471
472    /**
473     * Arranges the content for this title assuming a fixed width and no bounds
474     * on the height, and returns the required size.  This will reflect the
475     * fact that a text title positioned on the left or right of a chart will
476     * be rotated by 90 degrees.
477     *
478     * @param g2  the graphics target.
479     * @param w  the width.
480     *
481     * @return The content size.
482     */
483    protected Size2D arrangeFN(Graphics2D g2, double w) {
484        RectangleEdge position = getPosition();
485        if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) {
486            float maxWidth = (float) w;
487            g2.setFont(this.font);
488            this.content = TextUtils.createTextBlock(this.text, this.font,
489                    this.paint, maxWidth, this.maximumLinesToDisplay,
490                    new G2TextMeasurer(g2));
491            this.content.setLineAlignment(this.textAlignment);
492            Size2D contentSize = this.content.calculateDimensions(g2);
493            if (this.expandToFitSpace) {
494                return new Size2D(maxWidth, contentSize.getHeight());
495            }
496            else {
497                return contentSize;
498            }
499        }
500        else if (position == RectangleEdge.LEFT || position
501                == RectangleEdge.RIGHT) {
502            float maxWidth = Float.MAX_VALUE;
503            g2.setFont(this.font);
504            this.content = TextUtils.createTextBlock(this.text, this.font,
505                    this.paint, maxWidth, this.maximumLinesToDisplay,
506                    new G2TextMeasurer(g2));
507            this.content.setLineAlignment(this.textAlignment);
508            Size2D contentSize = this.content.calculateDimensions(g2);
509
510            // transpose the dimensions, because the title is rotated
511            if (this.expandToFitSpace) {
512                return new Size2D(contentSize.getHeight(), maxWidth);
513            }
514            else {
515                return new Size2D(contentSize.height, contentSize.width);
516            }
517        }
518        else {
519            throw new RuntimeException("Unrecognised exception.");
520        }
521    }
522
523    /**
524     * Arranges the content for this title assuming a range constraint for the
525     * width and no bounds on the height, and returns the required size.  This
526     * will reflect the fact that a text title positioned on the left or right
527     * of a chart will be rotated by 90 degrees.
528     *
529     * @param g2  the graphics target.
530     * @param widthRange  the range for the width.
531     *
532     * @return The content size.
533     */
534    protected Size2D arrangeRN(Graphics2D g2, Range widthRange) {
535        Size2D s = arrangeNN(g2);
536        if (widthRange.contains(s.getWidth())) {
537            return s;
538        }
539        double ww = widthRange.constrain(s.getWidth());
540        return arrangeFN(g2, ww);
541    }
542
543    /**
544     * Returns the content size for the title.  This will reflect the fact that
545     * a text title positioned on the left or right of a chart will be rotated
546     * 90 degrees.
547     *
548     * @param g2  the graphics device.
549     * @param widthRange  the width range.
550     * @param heightRange  the height range.
551     *
552     * @return The content size.
553     */
554    protected Size2D arrangeRR(Graphics2D g2, Range widthRange,
555            Range heightRange) {
556        RectangleEdge position = getPosition();
557        if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) {
558            float maxWidth = (float) widthRange.getUpperBound();
559            g2.setFont(this.font);
560            this.content = TextUtils.createTextBlock(this.text, this.font,
561                    this.paint, maxWidth, this.maximumLinesToDisplay,
562                    new G2TextMeasurer(g2));
563            this.content.setLineAlignment(this.textAlignment);
564            Size2D contentSize = this.content.calculateDimensions(g2);
565            if (this.expandToFitSpace) {
566                return new Size2D(maxWidth, contentSize.getHeight());
567            }
568            else {
569                return contentSize;
570            }
571        }
572        else if (position == RectangleEdge.LEFT || position
573                == RectangleEdge.RIGHT) {
574            float maxWidth = (float) heightRange.getUpperBound();
575            g2.setFont(this.font);
576            this.content = TextUtils.createTextBlock(this.text, this.font,
577                    this.paint, maxWidth, this.maximumLinesToDisplay,
578                    new G2TextMeasurer(g2));
579            this.content.setLineAlignment(this.textAlignment);
580            Size2D contentSize = this.content.calculateDimensions(g2);
581
582            // transpose the dimensions, because the title is rotated
583            if (this.expandToFitSpace) {
584                return new Size2D(contentSize.getHeight(), maxWidth);
585            }
586            else {
587                return new Size2D(contentSize.height, contentSize.width);
588            }
589        }
590        else {
591            throw new RuntimeException("Unrecognised exception.");
592        }
593    }
594
595    /**
596     * Draws the title on a Java 2D graphics device (such as the screen or a
597     * printer).
598     *
599     * @param g2  the graphics device.
600     * @param area  the area allocated for the title.
601     */
602    @Override
603    public void draw(Graphics2D g2, Rectangle2D area) {
604        draw(g2, area, null);
605    }
606
607    /**
608     * Draws the block within the specified area.
609     *
610     * @param g2  the graphics device.
611     * @param area  the area.
612     * @param params  if this is an instance of {@link EntityBlockParams} it
613     *                is used to determine whether or not an
614     *                {@link EntityCollection} is returned by this method.
615     *
616     * @return An {@link EntityCollection} containing a chart entity for the
617     *         title, or {@code null}.
618     */
619    @Override
620    public Object draw(Graphics2D g2, Rectangle2D area, Object params) {
621        if (this.content == null) {
622            return null;
623        }
624        area = trimMargin(area);
625        drawBorder(g2, area);
626        if (this.text.equals("")) {
627            return null;
628        }
629        ChartEntity entity = null;
630        if (params instanceof EntityBlockParams) {
631            EntityBlockParams p = (EntityBlockParams) params;
632            if (p.getGenerateEntities()) {
633                entity = new TitleEntity(area, this, this.toolTipText,
634                        this.urlText);
635            }
636        }
637        area = trimBorder(area);
638        if (this.backgroundPaint != null) {
639            g2.setPaint(this.backgroundPaint);
640            g2.fill(area);
641        }
642        area = trimPadding(area);
643        RectangleEdge position = getPosition();
644        if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) {
645            drawHorizontal(g2, area);
646        }
647        else if (position == RectangleEdge.LEFT
648                 || position == RectangleEdge.RIGHT) {
649            drawVertical(g2, area);
650        }
651        BlockResult result = new BlockResult();
652        if (entity != null) {
653            StandardEntityCollection sec = new StandardEntityCollection();
654            sec.add(entity);
655            result.setEntityCollection(sec);
656        }
657        return result;
658    }
659
660    /**
661     * Draws a the title horizontally within the specified area.  This method
662     * will be called from the {@link #draw(Graphics2D, Rectangle2D) draw}
663     * method.
664     *
665     * @param g2  the graphics device.
666     * @param area  the area for the title.
667     */
668    protected void drawHorizontal(Graphics2D g2, Rectangle2D area) {
669        Rectangle2D titleArea = (Rectangle2D) area.clone();
670        g2.setFont(this.font);
671        g2.setPaint(this.paint);
672        TextBlockAnchor anchor = null;
673        float x = 0.0f;
674        HorizontalAlignment horizontalAlignment = getHorizontalAlignment();
675        if (horizontalAlignment == HorizontalAlignment.LEFT) {
676            x = (float) titleArea.getX();
677            anchor = TextBlockAnchor.TOP_LEFT;
678        }
679        else if (horizontalAlignment == HorizontalAlignment.RIGHT) {
680            x = (float) titleArea.getMaxX();
681            anchor = TextBlockAnchor.TOP_RIGHT;
682        }
683        else if (horizontalAlignment == HorizontalAlignment.CENTER) {
684            x = (float) titleArea.getCenterX();
685            anchor = TextBlockAnchor.TOP_CENTER;
686        }
687        float y = 0.0f;
688        RectangleEdge position = getPosition();
689        if (position == RectangleEdge.TOP) {
690            y = (float) titleArea.getY();
691        }
692        else if (position == RectangleEdge.BOTTOM) {
693            y = (float) titleArea.getMaxY();
694            if (horizontalAlignment == HorizontalAlignment.LEFT) {
695                anchor = TextBlockAnchor.BOTTOM_LEFT;
696            }
697            else if (horizontalAlignment == HorizontalAlignment.CENTER) {
698                anchor = TextBlockAnchor.BOTTOM_CENTER;
699            }
700            else if (horizontalAlignment == HorizontalAlignment.RIGHT) {
701                anchor = TextBlockAnchor.BOTTOM_RIGHT;
702            }
703        }
704        this.content.draw(g2, x, y, anchor);
705    }
706
707    /**
708     * Draws a the title vertically within the specified area.  This method
709     * will be called from the {@link #draw(Graphics2D, Rectangle2D) draw}
710     * method.
711     *
712     * @param g2  the graphics device.
713     * @param area  the area for the title.
714     */
715    protected void drawVertical(Graphics2D g2, Rectangle2D area) {
716        Rectangle2D titleArea = (Rectangle2D) area.clone();
717        g2.setFont(this.font);
718        g2.setPaint(this.paint);
719        TextBlockAnchor anchor = null;
720        float y = 0.0f;
721        VerticalAlignment verticalAlignment = getVerticalAlignment();
722        if (verticalAlignment == VerticalAlignment.TOP) {
723            y = (float) titleArea.getY();
724            anchor = TextBlockAnchor.TOP_RIGHT;
725        }
726        else if (verticalAlignment == VerticalAlignment.BOTTOM) {
727            y = (float) titleArea.getMaxY();
728            anchor = TextBlockAnchor.TOP_LEFT;
729        }
730        else if (verticalAlignment == VerticalAlignment.CENTER) {
731            y = (float) titleArea.getCenterY();
732            anchor = TextBlockAnchor.TOP_CENTER;
733        }
734        float x = 0.0f;
735        RectangleEdge position = getPosition();
736        if (position == RectangleEdge.LEFT) {
737            x = (float) titleArea.getX();
738        }
739        else if (position == RectangleEdge.RIGHT) {
740            x = (float) titleArea.getMaxX();
741            if (verticalAlignment == VerticalAlignment.TOP) {
742                anchor = TextBlockAnchor.BOTTOM_RIGHT;
743            }
744            else if (verticalAlignment == VerticalAlignment.CENTER) {
745                anchor = TextBlockAnchor.BOTTOM_CENTER;
746            }
747            else if (verticalAlignment == VerticalAlignment.BOTTOM) {
748                anchor = TextBlockAnchor.BOTTOM_LEFT;
749            }
750        }
751        this.content.draw(g2, x, y, anchor, x, y, -Math.PI / 2.0);
752    }
753
754    /**
755     * Tests this title for equality with another object.
756     *
757     * @param obj  the object ({@code null} permitted).
758     *
759     * @return {@code true} or {@code false}.
760     */
761    @Override
762    public boolean equals(Object obj) {
763        if (obj == this) {
764            return true;
765        }
766        if (!(obj instanceof TextTitle)) {
767            return false;
768        }
769        TextTitle that = (TextTitle) obj;
770        if (!Objects.equals(this.text, that.text)) {
771            return false;
772        }
773        if (!Objects.equals(this.font, that.font)) {
774            return false;
775        }
776        if (!PaintUtils.equal(this.paint, that.paint)) {
777            return false;
778        }
779        if (this.textAlignment != that.textAlignment) {
780            return false;
781        }
782        if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) {
783            return false;
784        }
785        if (this.maximumLinesToDisplay != that.maximumLinesToDisplay) {
786            return false;
787        }
788        if (this.expandToFitSpace != that.expandToFitSpace) {
789            return false;
790        }
791        if (!Objects.equals(this.toolTipText, that.toolTipText)) {
792            return false;
793        }
794        if (!Objects.equals(this.urlText, that.urlText)) {
795            return false;
796        }
797        return super.equals(obj);
798    }
799
800    /**
801     * Returns a hash code.
802     *
803     * @return A hash code.
804     */
805    @Override
806    public int hashCode() {
807        int result = super.hashCode();
808        result = 29 * result + (this.text != null ? this.text.hashCode() : 0);
809        result = 29 * result + (this.font != null ? this.font.hashCode() : 0);
810        result = 29 * result + (this.paint != null ? this.paint.hashCode() : 0);
811        result = 29 * result + (this.backgroundPaint != null
812                ? this.backgroundPaint.hashCode() : 0);
813        return result;
814    }
815
816    /**
817     * Returns a clone of this object.
818     *
819     * @return A clone.
820     *
821     * @throws CloneNotSupportedException never.
822     */
823    @Override
824    public Object clone() throws CloneNotSupportedException {
825        return super.clone();
826    }
827
828    /**
829     * Provides serialization support.
830     *
831     * @param stream  the output stream.
832     *
833     * @throws IOException  if there is an I/O error.
834     */
835    private void writeObject(ObjectOutputStream stream) throws IOException {
836        stream.defaultWriteObject();
837        SerialUtils.writePaint(this.paint, stream);
838        SerialUtils.writePaint(this.backgroundPaint, stream);
839    }
840
841    /**
842     * Provides serialization support.
843     *
844     * @param stream  the input stream.
845     *
846     * @throws IOException  if there is an I/O error.
847     * @throws ClassNotFoundException  if there is a classpath problem.
848     */
849    private void readObject(ObjectInputStream stream)
850            throws IOException, ClassNotFoundException {
851        stream.defaultReadObject();
852        this.paint = SerialUtils.readPaint(stream);
853        this.backgroundPaint = SerialUtils.readPaint(stream);
854    }
855
856}
857