001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * --------------
028 * TextTitle.java
029 * --------------
030 * (C) Copyright 2000-2007, by David Berry and Contributors.
031 *
032 * Original Author:  David Berry;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *                   Nicolas Brodu;
035 *
036 * Changes (from 18-Sep-2001)
037 * --------------------------
038 * 18-Sep-2001 : Added standard header (DG);
039 * 07-Nov-2001 : Separated the JCommon Class Library classes, JFreeChart now 
040 *               requires jcommon.jar (DG);
041 * 09-Jan-2002 : Updated Javadoc comments (DG);
042 * 07-Feb-2002 : Changed Insets --> Spacer in AbstractTitle.java (DG);
043 * 06-Mar-2002 : Updated import statements (DG);
044 * 25-Jun-2002 : Removed redundant imports (DG);
045 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
046 * 28-Oct-2002 : Small modifications while changing JFreeChart class (DG);
047 * 13-Mar-2003 : Changed width used for relative spacing to fix bug 703050 (DG);
048 * 26-Mar-2003 : Implemented Serializable (DG);
049 * 15-Jul-2003 : Fixed null pointer exception (DG);
050 * 11-Sep-2003 : Implemented Cloneable (NB)
051 * 22-Sep-2003 : Added checks for null values and throw nullpointer 
052 *               exceptions (TM); 
053 *               Background paint was not serialized.
054 * 07-Oct-2003 : Added fix for exception caused by empty string in title (DG);
055 * 29-Oct-2003 : Added workaround for text alignment in PDF output (DG);
056 * 03-Feb-2004 : Fixed bug in getPreferredWidth() method (DG);
057 * 17-Feb-2004 : Added clone() method and fixed bug in equals() method (DG);
058 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D 
059 *               because of JDK bug 4976448 which persists on JDK 1.3.1.  Also
060 *               fixed bug in getPreferredHeight() method (DG);
061 * 29-Apr-2004 : Fixed bug in getPreferredWidth() method - see bug id 
062 *               944173 (DG);
063 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
064 *               release (DG);
065 * 08-Feb-2005 : Updated for changes in RectangleConstraint class (DG);
066 * 11-Feb-2005 : Implemented PublicCloneable (DG);
067 * 20-Apr-2005 : Added support for tooltips (DG);
068 * 26-Apr-2005 : Removed LOGGER (DG);
069 * 06-Jun-2005 : Modified equals() to handle GradientPaint (DG);
070 * 06-Jul-2005 : Added flag to control whether or not the title expands to
071 *               fit the available space (DG);
072 * 07-Oct-2005 : Added textAlignment attribute (DG);
073 * ------------- JFREECHART 1.0.x RELEASED ------------------------------------
074 * 13-Dec-2005 : Fixed bug 1379331 - incorrect drawing with LEFT or RIGHT 
075 *               title placement (DG);
076 * 
077 */
078
079package org.jfree.chart.title;
080
081import java.awt.Color;
082import java.awt.Font;
083import java.awt.Graphics2D;
084import java.awt.Paint;
085import java.awt.geom.Rectangle2D;
086import java.io.IOException;
087import java.io.ObjectInputStream;
088import java.io.ObjectOutputStream;
089import java.io.Serializable;
090
091import org.jfree.chart.block.BlockResult;
092import org.jfree.chart.block.EntityBlockParams;
093import org.jfree.chart.block.LengthConstraintType;
094import org.jfree.chart.block.RectangleConstraint;
095import org.jfree.chart.entity.ChartEntity;
096import org.jfree.chart.entity.EntityCollection;
097import org.jfree.chart.entity.StandardEntityCollection;
098import org.jfree.chart.event.TitleChangeEvent;
099import org.jfree.data.Range;
100import org.jfree.io.SerialUtilities;
101import org.jfree.text.G2TextMeasurer;
102import org.jfree.text.TextBlock;
103import org.jfree.text.TextBlockAnchor;
104import org.jfree.text.TextUtilities;
105import org.jfree.ui.HorizontalAlignment;
106import org.jfree.ui.RectangleEdge;
107import org.jfree.ui.RectangleInsets;
108import org.jfree.ui.Size2D;
109import org.jfree.ui.VerticalAlignment;
110import org.jfree.util.ObjectUtilities;
111import org.jfree.util.PaintUtilities;
112import org.jfree.util.PublicCloneable;
113
114/**
115 * A chart title that displays a text string with automatic wrapping as 
116 * required.
117 */
118public class TextTitle extends Title 
119                       implements Serializable, Cloneable, PublicCloneable {
120
121    /** For serialization. */
122    private static final long serialVersionUID = 8372008692127477443L;
123    
124    /** The default font. */
125    public static final Font DEFAULT_FONT = new Font("SansSerif", Font.BOLD, 
126            12);
127
128    /** The default text color. */
129    public static final Paint DEFAULT_TEXT_PAINT = Color.black;
130
131    /** The title text. */
132    private String text;
133
134    /** The font used to display the title. */
135    private Font font;
136    
137    /** The text alignment. */
138    private HorizontalAlignment textAlignment;
139
140    /** The paint used to display the title text. */
141    private transient Paint paint;
142
143    /** The background paint. */
144    private transient Paint backgroundPaint;
145
146    /** The tool tip text (can be <code>null</code>). */
147    private String toolTipText;
148    
149    /** The URL text (can be <code>null</code>). */
150    private String urlText;
151    
152    /** The content. */
153    private TextBlock content;
154    
155    /** 
156     * A flag that controls whether the title expands to fit the available
157     * space..
158     */
159    private boolean expandToFitSpace = false;
160    
161    /**
162     * Creates a new title, using default attributes where necessary.
163     */
164    public TextTitle() {
165        this("");
166    }
167
168    /**
169     * Creates a new title, using default attributes where necessary.
170     *
171     * @param text  the title text (<code>null</code> not permitted).
172     */
173    public TextTitle(String text) {
174        this(text, TextTitle.DEFAULT_FONT, TextTitle.DEFAULT_TEXT_PAINT,
175                Title.DEFAULT_POSITION, Title.DEFAULT_HORIZONTAL_ALIGNMENT,
176                Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING);
177    }
178
179    /**
180     * Creates a new title, using default attributes where necessary.
181     *
182     * @param text  the title text (<code>null</code> not permitted).
183     * @param font  the title font (<code>null</code> not permitted).
184     */
185    public TextTitle(String text, Font font) {
186        this(text, font, TextTitle.DEFAULT_TEXT_PAINT, Title.DEFAULT_POSITION,
187                Title.DEFAULT_HORIZONTAL_ALIGNMENT, 
188                Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING);
189    }
190
191    /**
192     * Creates a new title.
193     *
194     * @param text  the text for the title (<code>null</code> not permitted).
195     * @param font  the title font (<code>null</code> not permitted).
196     * @param paint  the title paint (<code>null</code> not permitted).
197     * @param position  the title position (<code>null</code> not permitted).
198     * @param horizontalAlignment  the horizontal alignment (<code>null</code> 
199     *                             not permitted).
200     * @param verticalAlignment  the vertical alignment (<code>null</code> not 
201     *                           permitted).
202     * @param padding  the space to leave around the outside of the title.
203     */
204    public TextTitle(String text, Font font, Paint paint, 
205                     RectangleEdge position, 
206                     HorizontalAlignment horizontalAlignment, 
207                     VerticalAlignment verticalAlignment,
208                     RectangleInsets padding) {
209
210        super(position, horizontalAlignment, verticalAlignment, padding);
211        
212        if (text == null) {
213            throw new NullPointerException("Null 'text' argument.");
214        }
215        if (font == null) {
216            throw new NullPointerException("Null 'font' argument.");
217        }
218        if (paint == null) {
219            throw new NullPointerException("Null 'paint' argument.");
220        }
221        this.text = text;
222        this.font = font;
223        this.paint = paint;
224        // the textAlignment and the horizontalAlignment are separate things,
225        // but it makes sense for the default textAlignment to match the
226        // title's horizontal alignment...
227        this.textAlignment = horizontalAlignment;
228        this.backgroundPaint = null;
229        this.content = null;
230        this.toolTipText = null;
231        this.urlText = null;
232        
233    }
234
235    /**
236     * Returns the title text.
237     *
238     * @return The text (never <code>null</code>).
239     * 
240     * @see #setText(String)
241     */
242    public String getText() {
243        return this.text;
244    }
245
246    /**
247     * Sets the title to the specified text and sends a 
248     * {@link TitleChangeEvent} to all registered listeners.
249     *
250     * @param text  the text (<code>null</code> not permitted).
251     */
252    public void setText(String text) {
253        if (text == null) {
254            throw new IllegalArgumentException("Null 'text' argument.");
255        }
256        if (!this.text.equals(text)) {
257            this.text = text;
258            notifyListeners(new TitleChangeEvent(this));
259        }
260    }
261
262    /**
263     * Returns the text alignment.  This controls how the text is aligned 
264     * within the title's bounds, whereas the title's horizontal alignment
265     * controls how the title's bounding rectangle is aligned within the 
266     * drawing space.
267     * 
268     * @return The text alignment.
269     */
270    public HorizontalAlignment getTextAlignment() {
271        return this.textAlignment;
272    }
273    
274    /**
275     * Sets the text alignment.
276     * 
277     * @param alignment  the alignment (<code>null</code> not permitted).
278     */
279    public void setTextAlignment(HorizontalAlignment alignment) {
280        if (alignment == null) {
281            throw new IllegalArgumentException("Null 'alignment' argument.");
282        }
283        this.textAlignment = alignment;
284        notifyListeners(new TitleChangeEvent(this));
285    }
286    
287    /**
288     * Returns the font used to display the title string.
289     *
290     * @return The font (never <code>null</code>).
291     * 
292     * @see #setFont(Font)
293     */
294    public Font getFont() {
295        return this.font;
296    }
297
298    /**
299     * Sets the font used to display the title string.  Registered listeners 
300     * are notified that the title has been modified.
301     *
302     * @param font  the new font (<code>null</code> not permitted).
303     * 
304     * @see #getFont()
305     */
306    public void setFont(Font font) {
307        if (font == null) {
308            throw new IllegalArgumentException("Null 'font' argument.");
309        }
310        if (!this.font.equals(font)) {
311            this.font = font;
312            notifyListeners(new TitleChangeEvent(this));
313        }
314    }
315
316    /**
317     * Returns the paint used to display the title string.
318     *
319     * @return The paint (never <code>null</code>).
320     * 
321     * @see #setPaint(Paint)
322     */
323    public Paint getPaint() {
324        return this.paint;
325    }
326
327    /**
328     * Sets the paint used to display the title string.  Registered listeners 
329     * are notified that the title has been modified.
330     *
331     * @param paint  the new paint (<code>null</code> not permitted).
332     * 
333     * @see #getPaint()
334     */
335    public void setPaint(Paint paint) {
336        if (paint == null) {
337            throw new IllegalArgumentException("Null 'paint' argument.");
338        }
339        if (!this.paint.equals(paint)) {
340            this.paint = paint;
341            notifyListeners(new TitleChangeEvent(this));
342        }
343    }
344
345    /**
346     * Returns the background paint.
347     *
348     * @return The paint (possibly <code>null</code>).
349     */
350    public Paint getBackgroundPaint() {
351        return this.backgroundPaint;
352    }
353
354    /**
355     * Sets the background paint and sends a {@link TitleChangeEvent} to all 
356     * registered listeners.  If you set this attribute to <code>null</code>, 
357     * no background is painted (which makes the title background transparent).
358     *
359     * @param paint  the background paint (<code>null</code> permitted).
360     */
361    public void setBackgroundPaint(Paint paint) {
362        this.backgroundPaint = paint;
363        notifyListeners(new TitleChangeEvent(this));
364    }
365    
366    /**
367     * Returns the tool tip text.
368     *
369     * @return The tool tip text (possibly <code>null</code>).
370     */
371    public String getToolTipText() {
372        return this.toolTipText;
373    }
374
375    /**
376     * Sets the tool tip text to the specified text and sends a 
377     * {@link TitleChangeEvent} to all registered listeners.
378     *
379     * @param text  the text (<code>null</code> permitted).
380     */
381    public void setToolTipText(String text) {
382        this.toolTipText = text;
383        notifyListeners(new TitleChangeEvent(this));
384    }
385
386    /**
387     * Returns the URL text.
388     *
389     * @return The URL text (possibly <code>null</code>).
390     */
391    public String getURLText() {
392        return this.urlText;
393    }
394
395    /**
396     * Sets the URL text to the specified text and sends a 
397     * {@link TitleChangeEvent} to all registered listeners.
398     *
399     * @param text  the text (<code>null</code> permitted).
400     */
401    public void setURLText(String text) {
402        this.urlText = text;
403        notifyListeners(new TitleChangeEvent(this));
404    }
405    
406    /**
407     * Returns the flag that controls whether or not the title expands to fit
408     * the available space.
409     * 
410     * @return The flag.
411     */
412    public boolean getExpandToFitSpace() {
413        return this.expandToFitSpace;   
414    }
415    
416    /**
417     * Sets the flag that controls whether the title expands to fit the 
418     * available space, and sends a {@link TitleChangeEvent} to all registered
419     * listeners.
420     * 
421     * @param expand  the flag.
422     */
423    public void setExpandToFitSpace(boolean expand) {
424        this.expandToFitSpace = expand;
425        notifyListeners(new TitleChangeEvent(this));        
426    }
427
428    /**
429     * Arranges the contents of the block, within the given constraints, and 
430     * returns the block size.
431     * 
432     * @param g2  the graphics device.
433     * @param constraint  the constraint (<code>null</code> not permitted).
434     * 
435     * @return The block size (in Java2D units, never <code>null</code>).
436     */
437    public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
438        RectangleConstraint cc = toContentConstraint(constraint);
439        LengthConstraintType w = cc.getWidthConstraintType();
440        LengthConstraintType h = cc.getHeightConstraintType();
441        Size2D contentSize = null;
442        if (w == LengthConstraintType.NONE) {
443            if (h == LengthConstraintType.NONE) {
444                throw new RuntimeException("Not yet implemented."); 
445            }
446            else if (h == LengthConstraintType.RANGE) {
447                throw new RuntimeException("Not yet implemented."); 
448            }
449            else if (h == LengthConstraintType.FIXED) {
450                throw new RuntimeException("Not yet implemented.");
451            }            
452        }
453        else if (w == LengthConstraintType.RANGE) {
454            if (h == LengthConstraintType.NONE) {
455                throw new RuntimeException("Not yet implemented."); 
456            }
457            else if (h == LengthConstraintType.RANGE) {
458                contentSize = arrangeRR(g2, cc.getWidthRange(), 
459                        cc.getHeightRange()); 
460            }
461            else if (h == LengthConstraintType.FIXED) {
462                throw new RuntimeException("Not yet implemented.");
463            }
464        }
465        else if (w == LengthConstraintType.FIXED) {
466            if (h == LengthConstraintType.NONE) {
467                throw new RuntimeException("Not yet implemented."); 
468            }
469            else if (h == LengthConstraintType.RANGE) {
470                throw new RuntimeException("Not yet implemented."); 
471            }
472            else if (h == LengthConstraintType.FIXED) {
473                throw new RuntimeException("Not yet implemented.");
474            }
475        }
476        return new Size2D(calculateTotalWidth(contentSize.getWidth()),
477                calculateTotalHeight(contentSize.getHeight()));
478    }
479    
480    /**
481     * Returns the content size for the title.  This will reflect the fact that
482     * a text title positioned on the left or right of a chart will be rotated
483     * 90 degrees.
484     * 
485     * @param g2  the graphics device.
486     * @param widthRange  the width range.
487     * @param heightRange  the height range.
488     * 
489     * @return The content size.
490     */
491    protected Size2D arrangeRR(Graphics2D g2, Range widthRange, 
492            Range heightRange) {
493        RectangleEdge position = getPosition();
494        if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) {
495            float maxWidth = (float) widthRange.getUpperBound();
496            g2.setFont(this.font);
497            this.content = TextUtilities.createTextBlock(this.text, this.font, 
498                    this.paint, maxWidth, new G2TextMeasurer(g2));
499            this.content.setLineAlignment(this.textAlignment);
500            Size2D contentSize = this.content.calculateDimensions(g2);
501            if (this.expandToFitSpace) {
502                return new Size2D(maxWidth, contentSize.getHeight());
503            }
504            else {
505                return contentSize;
506            }
507        }
508        else if (position == RectangleEdge.LEFT || position 
509                == RectangleEdge.RIGHT) {
510            float maxWidth = (float) heightRange.getUpperBound();
511            g2.setFont(this.font);
512            this.content = TextUtilities.createTextBlock(this.text, this.font, 
513                    this.paint, maxWidth, 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     * Draws the title on a Java 2D graphics device (such as the screen or a 
532     * printer).
533     *
534     * @param g2  the graphics device.
535     * @param area  the area allocated for the title.
536     */
537    public void draw(Graphics2D g2, Rectangle2D area) {
538        draw(g2, area, null);
539    }
540    
541    /**
542     * Draws the block within the specified area.
543     * 
544     * @param g2  the graphics device.
545     * @param area  the area.
546     * @param params  if this is an instance of {@link EntityBlockParams} it
547     *                is used to determine whether or not an 
548     *                {@link EntityCollection} is returned by this method.
549     * 
550     * @return An {@link EntityCollection} containing a chart entity for the
551     *         title, or <code>null</code>.
552     */
553    public Object draw(Graphics2D g2, Rectangle2D area, Object params) {
554        if (this.content == null) {
555            return null;   
556        }
557        area = trimMargin(area);
558        drawBorder(g2, area);
559        if (this.text.equals("")) {
560            return null;
561        }
562        ChartEntity entity = null;
563        if (params instanceof EntityBlockParams) {
564            EntityBlockParams p = (EntityBlockParams) params;
565            if (p.getGenerateEntities()) {
566                entity = new ChartEntity(area, this.toolTipText, this.urlText);
567            }
568        }
569        area = trimBorder(area);
570        if (this.backgroundPaint != null) {
571            g2.setPaint(this.backgroundPaint);
572            g2.fill(area);
573        }
574        area = trimPadding(area);
575        RectangleEdge position = getPosition();
576        if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) {
577            drawHorizontal(g2, area);
578        }
579        else if (position == RectangleEdge.LEFT 
580                 || position == RectangleEdge.RIGHT) {
581            drawVertical(g2, area);
582        }
583        BlockResult result = new BlockResult();
584        if (entity != null) {
585            StandardEntityCollection sec = new StandardEntityCollection();
586            sec.add(entity);
587            result.setEntityCollection(sec);
588        }
589        return result;
590    }
591
592    /**
593     * Draws a the title horizontally within the specified area.  This method 
594     * will be called from the {@link #draw(Graphics2D, Rectangle2D) draw} 
595     * method.
596     * 
597     * @param g2  the graphics device.
598     * @param area  the area for the title.
599     */
600    protected void drawHorizontal(Graphics2D g2, Rectangle2D area) {
601        Rectangle2D titleArea = (Rectangle2D) area.clone();
602        g2.setFont(this.font);
603        g2.setPaint(this.paint);
604        TextBlockAnchor anchor = null;
605        float x = 0.0f;
606        HorizontalAlignment horizontalAlignment = getHorizontalAlignment();
607        if (horizontalAlignment == HorizontalAlignment.LEFT) {
608            x = (float) titleArea.getX();
609            anchor = TextBlockAnchor.TOP_LEFT;
610        }
611        else if (horizontalAlignment == HorizontalAlignment.RIGHT) {
612            x = (float) titleArea.getMaxX();
613            anchor = TextBlockAnchor.TOP_RIGHT;
614        }
615        else if (horizontalAlignment == HorizontalAlignment.CENTER) {
616            x = (float) titleArea.getCenterX();
617            anchor = TextBlockAnchor.TOP_CENTER;
618        }
619        float y = 0.0f;
620        RectangleEdge position = getPosition();
621        if (position == RectangleEdge.TOP) {
622            y = (float) titleArea.getY();
623        }
624        else if (position == RectangleEdge.BOTTOM) {
625            y = (float) titleArea.getMaxY();
626            if (horizontalAlignment == HorizontalAlignment.LEFT) {
627                anchor = TextBlockAnchor.BOTTOM_LEFT;
628            }
629            else if (horizontalAlignment == HorizontalAlignment.CENTER) {
630                anchor = TextBlockAnchor.BOTTOM_CENTER;
631            }
632            else if (horizontalAlignment == HorizontalAlignment.RIGHT) {
633                anchor = TextBlockAnchor.BOTTOM_RIGHT;
634            }
635        }
636        this.content.draw(g2, x, y, anchor);
637    }
638    
639    /**
640     * Draws a the title vertically within the specified area.  This method 
641     * will be called from the {@link #draw(Graphics2D, Rectangle2D) draw} 
642     * method.
643     * 
644     * @param g2  the graphics device.
645     * @param area  the area for the title.
646     */
647    protected void drawVertical(Graphics2D g2, Rectangle2D area) {
648        Rectangle2D titleArea = (Rectangle2D) area.clone();
649        g2.setFont(this.font);
650        g2.setPaint(this.paint);
651        TextBlockAnchor anchor = null;
652        float y = 0.0f;
653        VerticalAlignment verticalAlignment = getVerticalAlignment();
654        if (verticalAlignment == VerticalAlignment.TOP) {
655            y = (float) titleArea.getY();
656            anchor = TextBlockAnchor.TOP_RIGHT;
657        }
658        else if (verticalAlignment == VerticalAlignment.BOTTOM) {
659            y = (float) titleArea.getMaxY();
660            anchor = TextBlockAnchor.TOP_LEFT;
661        }
662        else if (verticalAlignment == VerticalAlignment.CENTER) {
663            y = (float) titleArea.getCenterY();
664            anchor = TextBlockAnchor.TOP_CENTER;
665        }
666        float x = 0.0f;
667        RectangleEdge position = getPosition();
668        if (position == RectangleEdge.LEFT) {
669            x = (float) titleArea.getX();
670        }
671        else if (position == RectangleEdge.RIGHT) {
672            x = (float) titleArea.getMaxX();
673            if (verticalAlignment == VerticalAlignment.TOP) {
674                anchor = TextBlockAnchor.BOTTOM_RIGHT;
675            }
676            else if (verticalAlignment == VerticalAlignment.CENTER) {
677                anchor = TextBlockAnchor.BOTTOM_CENTER;
678            }
679            else if (verticalAlignment == VerticalAlignment.BOTTOM) {
680                anchor = TextBlockAnchor.BOTTOM_LEFT;
681            }
682        }
683        this.content.draw(g2, x, y, anchor, x, y, -Math.PI / 2.0);
684    }
685
686    /**
687     * Tests this title for equality with another object.
688     *
689     * @param obj  the object (<code>null</code> permitted).
690     *
691     * @return <code>true</code> or <code>false</code>.
692     */
693    public boolean equals(Object obj) {
694        if (obj == this) {
695            return true;
696        }
697        if (!(obj instanceof TextTitle)) {
698            return false;
699        }
700        if (!super.equals(obj)) {
701            return false;
702        }
703        TextTitle that = (TextTitle) obj;
704        if (!ObjectUtilities.equal(this.text, that.text)) {
705            return false;
706        }
707        if (!ObjectUtilities.equal(this.font, that.font)) {
708            return false;
709        }
710        if (!PaintUtilities.equal(this.paint, that.paint)) {
711            return false;
712        }
713        if (this.textAlignment != that.textAlignment) {
714            return false;
715        }
716        if (!PaintUtilities.equal(this.backgroundPaint, that.backgroundPaint)) {
717            return false;
718        }
719        return true;
720    }
721
722    /**
723     * Returns a hash code.
724     * 
725     * @return A hash code.
726     */
727    public int hashCode() {
728        int result = super.hashCode();
729        result = 29 * result + (this.text != null ? this.text.hashCode() : 0);
730        result = 29 * result + (this.font != null ? this.font.hashCode() : 0);
731        result = 29 * result + (this.paint != null ? this.paint.hashCode() : 0);
732        result = 29 * result + (this.backgroundPaint != null 
733                ? this.backgroundPaint.hashCode() : 0);
734        return result;
735    }
736
737    /**
738     * Returns a clone of this object.
739     * 
740     * @return A clone.
741     * 
742     * @throws CloneNotSupportedException never.
743     */
744    public Object clone() throws CloneNotSupportedException {
745        return super.clone();
746    }
747    
748    /**
749     * Provides serialization support.
750     *
751     * @param stream  the output stream.
752     *
753     * @throws IOException  if there is an I/O error.
754     */
755    private void writeObject(ObjectOutputStream stream) throws IOException {
756        stream.defaultWriteObject();
757        SerialUtilities.writePaint(this.paint, stream);
758        SerialUtilities.writePaint(this.backgroundPaint, stream);
759    }
760
761    /**
762     * Provides serialization support.
763     *
764     * @param stream  the input stream.
765     *
766     * @throws IOException  if there is an I/O error.
767     * @throws ClassNotFoundException  if there is a classpath problem.
768     */
769    private void readObject(ObjectInputStream stream) 
770        throws IOException, ClassNotFoundException 
771    {
772        stream.defaultReadObject();
773        this.paint = SerialUtilities.readPaint(stream);
774        this.backgroundPaint = SerialUtilities.readPaint(stream);
775    }
776
777}
778