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