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