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    
079    package org.jfree.chart.title;
080    
081    import java.awt.Color;
082    import java.awt.Font;
083    import java.awt.Graphics2D;
084    import java.awt.Paint;
085    import java.awt.geom.Rectangle2D;
086    import java.io.IOException;
087    import java.io.ObjectInputStream;
088    import java.io.ObjectOutputStream;
089    import java.io.Serializable;
090    
091    import org.jfree.chart.block.BlockResult;
092    import org.jfree.chart.block.EntityBlockParams;
093    import org.jfree.chart.block.LengthConstraintType;
094    import org.jfree.chart.block.RectangleConstraint;
095    import org.jfree.chart.entity.ChartEntity;
096    import org.jfree.chart.entity.EntityCollection;
097    import org.jfree.chart.entity.StandardEntityCollection;
098    import org.jfree.chart.event.TitleChangeEvent;
099    import org.jfree.data.Range;
100    import org.jfree.io.SerialUtilities;
101    import org.jfree.text.G2TextMeasurer;
102    import org.jfree.text.TextBlock;
103    import org.jfree.text.TextBlockAnchor;
104    import org.jfree.text.TextUtilities;
105    import org.jfree.ui.HorizontalAlignment;
106    import org.jfree.ui.RectangleEdge;
107    import org.jfree.ui.RectangleInsets;
108    import org.jfree.ui.Size2D;
109    import org.jfree.ui.VerticalAlignment;
110    import org.jfree.util.ObjectUtilities;
111    import org.jfree.util.PaintUtilities;
112    import org.jfree.util.PublicCloneable;
113    
114    /**
115     * A chart title that displays a text string with automatic wrapping as 
116     * required.
117     */
118    public 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