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