001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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 * Title.java
029 * ----------
030 * (C) Copyright 2000-2008, 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 21-Aug-2001)
037 * --------------------------
038 * 21-Aug-2001 : Added standard header (DG);
039 * 18-Sep-2001 : Updated header (DG);
040 * 14-Nov-2001 : Package com.jrefinery.common.ui.* changed to
041 *               com.jrefinery.ui.* (DG);
042 * 07-Feb-2002 : Changed blank space around title from Insets --> Spacer, to
043 *               allow for relative or absolute spacing (DG);
044 * 25-Jun-2002 : Removed unnecessary imports (DG);
045 * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG);
046 * 14-Oct-2002 : Changed the event listener storage structure (DG);
047 * 11-Sep-2003 : Took care of listeners while cloning (NB);
048 * 22-Sep-2003 : Spacer cannot be null. Added nullpointer checks for this (TM);
049 * 08-Jan-2003 : Renamed AbstractTitle --> Title and moved to separate
050 *               package (DG);
051 * 26-Oct-2004 : Refactored to implement Block interface, and removed redundant
052 *               constants (DG);
053 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
054 *               release (DG);
055 * 02-Feb-2005 : Changed Spacer --> RectangleInsets for padding (DG);
056 * 03-May-2005 : Fixed problem in equals() method (DG);
057 * 19-Sep-2008 : Added visibility flag (DG);
058 *
059 */
060
061package org.jfree.chart.title;
062
063import java.awt.Graphics2D;
064import java.awt.geom.Rectangle2D;
065import java.io.IOException;
066import java.io.ObjectInputStream;
067import java.io.ObjectOutputStream;
068import java.io.Serializable;
069
070import javax.swing.event.EventListenerList;
071
072import org.jfree.chart.block.AbstractBlock;
073import org.jfree.chart.block.Block;
074import org.jfree.chart.event.TitleChangeEvent;
075import org.jfree.chart.event.TitleChangeListener;
076import org.jfree.ui.HorizontalAlignment;
077import org.jfree.ui.RectangleEdge;
078import org.jfree.ui.RectangleInsets;
079import org.jfree.ui.VerticalAlignment;
080import org.jfree.util.ObjectUtilities;
081
082/**
083 * The base class for all chart titles.  A chart can have multiple titles,
084 * appearing at the top, bottom, left or right of the chart.
085 * <P>
086 * Concrete implementations of this class will render text and images, and
087 * hence do the actual work of drawing titles.
088 */
089public abstract class Title extends AbstractBlock
090            implements Block, Cloneable, Serializable {
091
092    /** For serialization. */
093    private static final long serialVersionUID = -6675162505277817221L;
094
095    /** The default title position. */
096    public static final RectangleEdge DEFAULT_POSITION = RectangleEdge.TOP;
097
098    /** The default horizontal alignment. */
099    public static final HorizontalAlignment
100            DEFAULT_HORIZONTAL_ALIGNMENT = HorizontalAlignment.CENTER;
101
102    /** The default vertical alignment. */
103    public static final VerticalAlignment
104            DEFAULT_VERTICAL_ALIGNMENT = VerticalAlignment.CENTER;
105
106    /** Default title padding. */
107    public static final RectangleInsets DEFAULT_PADDING = new RectangleInsets(
108            1, 1, 1, 1);
109
110    /**
111     * A flag that controls whether or not the title is visible.
112     *
113     * @since 1.0.11
114     */
115    public boolean visible;
116
117    /** The title position. */
118    private RectangleEdge position;
119
120    /** The horizontal alignment of the title content. */
121    private HorizontalAlignment horizontalAlignment;
122
123    /** The vertical alignment of the title content. */
124    private VerticalAlignment verticalAlignment;
125
126    /** Storage for registered change listeners. */
127    private transient EventListenerList listenerList;
128
129    /**
130     * A flag that can be used to temporarily disable the listener mechanism.
131     */
132    private boolean notify;
133
134    /**
135     * Creates a new title, using default attributes where necessary.
136     */
137    protected Title() {
138        this(Title.DEFAULT_POSITION,
139                Title.DEFAULT_HORIZONTAL_ALIGNMENT,
140                Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING);
141    }
142
143    /**
144     * Creates a new title, using default attributes where necessary.
145     *
146     * @param position  the position of the title (<code>null</code> not
147     *                  permitted).
148     * @param horizontalAlignment  the horizontal alignment of the title
149     *                             (<code>null</code> not permitted).
150     * @param verticalAlignment  the vertical alignment of the title
151     *                           (<code>null</code> not permitted).
152     */
153    protected Title(RectangleEdge position,
154                    HorizontalAlignment horizontalAlignment,
155                    VerticalAlignment verticalAlignment) {
156
157        this(position, horizontalAlignment, verticalAlignment,
158                Title.DEFAULT_PADDING);
159
160    }
161
162    /**
163     * Creates a new title.
164     *
165     * @param position  the position of the title (<code>null</code> not
166     *                  permitted).
167     * @param horizontalAlignment  the horizontal alignment of the title (LEFT,
168     *                             CENTER or RIGHT, <code>null</code> not
169     *                             permitted).
170     * @param verticalAlignment  the vertical alignment of the title (TOP,
171     *                           MIDDLE or BOTTOM, <code>null</code> not
172     *                           permitted).
173     * @param padding  the amount of space to leave around the outside of the
174     *                 title (<code>null</code> not permitted).
175     */
176    protected Title(RectangleEdge position,
177                    HorizontalAlignment horizontalAlignment,
178                    VerticalAlignment verticalAlignment,
179                    RectangleInsets padding) {
180
181        // check arguments...
182        if (position == null) {
183            throw new IllegalArgumentException("Null 'position' argument.");
184        }
185        if (horizontalAlignment == null) {
186            throw new IllegalArgumentException(
187                    "Null 'horizontalAlignment' argument.");
188        }
189
190        if (verticalAlignment == null) {
191            throw new IllegalArgumentException(
192                    "Null 'verticalAlignment' argument.");
193        }
194        if (padding == null) {
195            throw new IllegalArgumentException("Null 'spacer' argument.");
196        }
197
198        this.visible = true;
199        this.position = position;
200        this.horizontalAlignment = horizontalAlignment;
201        this.verticalAlignment = verticalAlignment;
202        setPadding(padding);
203        this.listenerList = new EventListenerList();
204        this.notify = true;
205
206    }
207
208    /**
209     * Returns a flag that controls whether or not the title should be
210     * drawn.  The default value is <code>true</code>.
211     *
212     * @return A boolean.
213     *
214     * @see #setVisible(boolean)
215     *
216     * @since 1.0.11
217     */
218    public boolean isVisible() {
219        return this.visible;
220    }
221
222    /**
223     * Sets a flag that controls whether or not the title should be drawn, and
224     * sends a {@link TitleChangeEvent} to all registered listeners.
225     *
226     * @param visible  the new flag value.
227     *
228     * @see #isVisible()
229     *
230     * @since 1.0.11
231     */
232    public void setVisible(boolean visible) {
233        this.visible = visible;
234        notifyListeners(new TitleChangeEvent(this));
235    }
236
237    /**
238     * Returns the position of the title.
239     *
240     * @return The title position (never <code>null</code>).
241     */
242    public RectangleEdge getPosition() {
243        return this.position;
244    }
245
246    /**
247     * Sets the position for the title and sends a {@link TitleChangeEvent} to
248     * all registered listeners.
249     *
250     * @param position  the position (<code>null</code> not permitted).
251     */
252    public void setPosition(RectangleEdge position) {
253        if (position == null) {
254            throw new IllegalArgumentException("Null 'position' argument.");
255        }
256        if (this.position != position) {
257            this.position = position;
258            notifyListeners(new TitleChangeEvent(this));
259        }
260    }
261
262    /**
263     * Returns the horizontal alignment of the title.
264     *
265     * @return The horizontal alignment (never <code>null</code>).
266     */
267    public HorizontalAlignment getHorizontalAlignment() {
268        return this.horizontalAlignment;
269    }
270
271    /**
272     * Sets the horizontal alignment for the title and sends a
273     * {@link TitleChangeEvent} to all registered listeners.
274     *
275     * @param alignment  the horizontal alignment (<code>null</code> not
276     *                   permitted).
277     */
278    public void setHorizontalAlignment(HorizontalAlignment alignment) {
279        if (alignment == null) {
280            throw new IllegalArgumentException("Null 'alignment' argument.");
281        }
282        if (this.horizontalAlignment != alignment) {
283            this.horizontalAlignment = alignment;
284            notifyListeners(new TitleChangeEvent(this));
285        }
286    }
287
288    /**
289     * Returns the vertical alignment of the title.
290     *
291     * @return The vertical alignment (never <code>null</code>).
292     */
293    public VerticalAlignment getVerticalAlignment() {
294        return this.verticalAlignment;
295    }
296
297    /**
298     * Sets the vertical alignment for the title, and notifies any registered
299     * listeners of the change.
300     *
301     * @param alignment  the new vertical alignment (TOP, MIDDLE or BOTTOM,
302     *                   <code>null</code> not permitted).
303     */
304    public void setVerticalAlignment(VerticalAlignment alignment) {
305        if (alignment == null) {
306            throw new IllegalArgumentException("Null 'alignment' argument.");
307        }
308        if (this.verticalAlignment != alignment) {
309            this.verticalAlignment = alignment;
310            notifyListeners(new TitleChangeEvent(this));
311        }
312    }
313
314    /**
315     * Returns the flag that indicates whether or not the notification
316     * mechanism is enabled.
317     *
318     * @return The flag.
319     */
320    public boolean getNotify() {
321        return this.notify;
322    }
323
324    /**
325     * Sets the flag that indicates whether or not the notification mechanism
326     * is enabled.  There are certain situations (such as cloning) where you
327     * want to turn notification off temporarily.
328     *
329     * @param flag  the new value of the flag.
330     */
331    public void setNotify(boolean flag) {
332        this.notify = flag;
333        if (flag) {
334            notifyListeners(new TitleChangeEvent(this));
335        }
336    }
337
338    /**
339     * Draws the title on a Java 2D graphics device (such as the screen or a
340     * printer).
341     *
342     * @param g2  the graphics device.
343     * @param area  the area allocated for the title (subclasses should not
344     *              draw outside this area).
345     */
346    public abstract void draw(Graphics2D g2, Rectangle2D area);
347
348    /**
349     * Returns a clone of the title.
350     * <P>
351     * One situation when this is useful is when editing the title properties -
352     * you can edit a clone, and then it is easier to cancel the changes if
353     * necessary.
354     *
355     * @return A clone of the title.
356     *
357     * @throws CloneNotSupportedException not thrown by this class, but it may
358     *         be thrown by subclasses.
359     */
360    public Object clone() throws CloneNotSupportedException {
361        Title duplicate = (Title) super.clone();
362        duplicate.listenerList = new EventListenerList();
363        // RectangleInsets is immutable => same reference in clone OK
364        return duplicate;
365    }
366
367    /**
368     * Registers an object for notification of changes to the title.
369     *
370     * @param listener  the object that is being registered.
371     */
372    public void addChangeListener(TitleChangeListener listener) {
373        this.listenerList.add(TitleChangeListener.class, listener);
374    }
375
376    /**
377     * Unregisters an object for notification of changes to the chart title.
378     *
379     * @param listener  the object that is being unregistered.
380     */
381    public void removeChangeListener(TitleChangeListener listener) {
382        this.listenerList.remove(TitleChangeListener.class, listener);
383    }
384
385    /**
386     * Notifies all registered listeners that the chart title has changed in
387     * some way.
388     *
389     * @param event  an object that contains information about the change to
390     *               the title.
391     */
392    protected void notifyListeners(TitleChangeEvent event) {
393        if (this.notify) {
394            Object[] listeners = this.listenerList.getListenerList();
395            for (int i = listeners.length - 2; i >= 0; i -= 2) {
396                if (listeners[i] == TitleChangeListener.class) {
397                    ((TitleChangeListener) listeners[i + 1]).titleChanged(
398                            event);
399                }
400            }
401        }
402    }
403
404    /**
405     * Tests an object for equality with this title.
406     *
407     * @param obj  the object (<code>null</code> not permitted).
408     *
409     * @return <code>true</code> or <code>false</code>.
410     */
411    public boolean equals(Object obj) {
412        if (obj == this) {
413            return true;
414        }
415        if (!(obj instanceof Title)) {
416            return false;
417        }
418        Title that = (Title) obj;
419        if (this.visible != that.visible) {
420            return false;
421        }
422        if (this.position != that.position) {
423            return false;
424        }
425        if (this.horizontalAlignment != that.horizontalAlignment) {
426            return false;
427        }
428        if (this.verticalAlignment != that.verticalAlignment) {
429            return false;
430        }
431        if (this.notify != that.notify) {
432            return false;
433        }
434        return super.equals(obj);
435    }
436
437    /**
438     * Returns a hashcode for the title.
439     *
440     * @return The hashcode.
441     */
442    public int hashCode() {
443        int result = 193;
444        result = 37 * result + ObjectUtilities.hashCode(this.position);
445        result = 37 * result
446                + ObjectUtilities.hashCode(this.horizontalAlignment);
447        result = 37 * result + ObjectUtilities.hashCode(this.verticalAlignment);
448        return result;
449    }
450
451    /**
452     * Provides serialization support.
453     *
454     * @param stream  the output stream.
455     *
456     * @throws IOException  if there is an I/O error.
457     */
458    private void writeObject(ObjectOutputStream stream) throws IOException {
459        stream.defaultWriteObject();
460    }
461
462    /**
463     * Provides serialization support.
464     *
465     * @param stream  the input stream.
466     *
467     * @throws IOException  if there is an I/O error.
468     * @throws ClassNotFoundException  if there is a classpath problem.
469     */
470    private void readObject(ObjectInputStream stream)
471        throws IOException, ClassNotFoundException {
472        stream.defaultReadObject();
473        this.listenerList = new EventListenerList();
474    }
475
476}