001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ----------
028 * Title.java
029 * ----------
030 * (C) Copyright 2000-present, by David Berry and Contributors.
031 *
032 * Original Author:  David Berry;
033 * Contributor(s):   David Gilbert;
034 *                   Nicolas Brodu;
035 *                   Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier);
036 *
037 */
038
039package org.jfree.chart.title;
040
041import java.awt.Graphics2D;
042import java.awt.geom.Rectangle2D;
043import java.io.IOException;
044import java.io.ObjectInputStream;
045import java.io.ObjectOutputStream;
046import java.io.Serializable;
047import java.util.Objects;
048
049import javax.swing.event.EventListenerList;
050
051import org.jfree.chart.block.AbstractBlock;
052import org.jfree.chart.block.Block;
053import org.jfree.chart.event.TitleChangeEvent;
054import org.jfree.chart.event.TitleChangeListener;
055import org.jfree.chart.ui.HorizontalAlignment;
056import org.jfree.chart.ui.RectangleEdge;
057import org.jfree.chart.ui.RectangleInsets;
058import org.jfree.chart.ui.VerticalAlignment;
059import org.jfree.chart.util.Args;
060
061/**
062 * The base class for all chart titles.  A chart can have multiple titles,
063 * appearing at the top, bottom, left or right of the chart.
064 * <P>
065 * Concrete implementations of this class will render text and images, and
066 * hence do the actual work of drawing titles.
067 */
068public abstract class Title extends AbstractBlock
069            implements Block, Cloneable, Serializable {
070
071    /** For serialization. */
072    private static final long serialVersionUID = -6675162505277817221L;
073
074    /** The default title position. */
075    public static final RectangleEdge DEFAULT_POSITION = RectangleEdge.TOP;
076
077    /** The default horizontal alignment. */
078    public static final HorizontalAlignment
079            DEFAULT_HORIZONTAL_ALIGNMENT = HorizontalAlignment.CENTER;
080
081    /** The default vertical alignment. */
082    public static final VerticalAlignment
083            DEFAULT_VERTICAL_ALIGNMENT = VerticalAlignment.CENTER;
084
085    /** Default title padding. */
086    public static final RectangleInsets DEFAULT_PADDING = new RectangleInsets(
087            1, 1, 1, 1);
088
089    /**
090     * A flag that controls whether or not the title is visible.
091     */
092    public boolean visible;
093
094    /** The title position. */
095    private RectangleEdge position;
096
097    /** The horizontal alignment of the title content. */
098    private HorizontalAlignment horizontalAlignment;
099
100    /** The vertical alignment of the title content. */
101    private VerticalAlignment verticalAlignment;
102
103    /** Storage for registered change listeners. */
104    private transient EventListenerList listenerList;
105
106    /**
107     * A flag that can be used to temporarily disable the listener mechanism.
108     */
109    private boolean notify;
110
111    /**
112     * Creates a new title, using default attributes where necessary.
113     */
114    protected Title() {
115        this(Title.DEFAULT_POSITION,
116                Title.DEFAULT_HORIZONTAL_ALIGNMENT,
117                Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING);
118    }
119
120    /**
121     * Creates a new title, using default attributes where necessary.
122     *
123     * @param position  the position of the title ({@code null} not
124     *                  permitted).
125     * @param horizontalAlignment  the horizontal alignment of the title
126     *                             ({@code null} not permitted).
127     * @param verticalAlignment  the vertical alignment of the title
128     *                           ({@code null} not permitted).
129     */
130    protected Title(RectangleEdge position,
131                    HorizontalAlignment horizontalAlignment,
132                    VerticalAlignment verticalAlignment) {
133
134        this(position, horizontalAlignment, verticalAlignment,
135                Title.DEFAULT_PADDING);
136
137    }
138
139    /**
140     * Creates a new title.
141     *
142     * @param position  the position of the title ({@code null} not
143     *                  permitted).
144     * @param horizontalAlignment  the horizontal alignment of the title (LEFT,
145     *                             CENTER or RIGHT, {@code null} not
146     *                             permitted).
147     * @param verticalAlignment  the vertical alignment of the title (TOP,
148     *                           MIDDLE or BOTTOM, {@code null} not
149     *                           permitted).
150     * @param padding  the amount of space to leave around the outside of the
151     *                 title ({@code null} not permitted).
152     */
153    protected Title(RectangleEdge position, 
154            HorizontalAlignment horizontalAlignment, 
155            VerticalAlignment verticalAlignment, RectangleInsets padding) {
156
157        Args.nullNotPermitted(position, "position");
158        Args.nullNotPermitted(horizontalAlignment, "horizontalAlignment");
159        Args.nullNotPermitted(verticalAlignment, "verticalAlignment");
160        Args.nullNotPermitted(padding, "padding");
161
162        this.visible = true;
163        this.position = position;
164        this.horizontalAlignment = horizontalAlignment;
165        this.verticalAlignment = verticalAlignment;
166        setPadding(padding);
167        this.listenerList = new EventListenerList();
168        this.notify = true;
169    }
170
171    /**
172     * Returns a flag that controls whether or not the title should be
173     * drawn.  The default value is {@code true}.
174     *
175     * @return A boolean.
176     *
177     * @see #setVisible(boolean)
178     */
179    public boolean isVisible() {
180        return this.visible;
181    }
182
183    /**
184     * Sets a flag that controls whether or not the title should be drawn, and
185     * sends a {@link TitleChangeEvent} to all registered listeners.
186     *
187     * @param visible  the new flag value.
188     *
189     * @see #isVisible()
190     */
191    public void setVisible(boolean visible) {
192        this.visible = visible;
193        notifyListeners(new TitleChangeEvent(this));
194    }
195
196    /**
197     * Returns the position of the title.
198     *
199     * @return The title position (never {@code null}).
200     */
201    public RectangleEdge getPosition() {
202        return this.position;
203    }
204
205    /**
206     * Sets the position for the title and sends a {@link TitleChangeEvent} to
207     * all registered listeners.
208     *
209     * @param position  the position ({@code null} not permitted).
210     */
211    public void setPosition(RectangleEdge position) {
212        Args.nullNotPermitted(position, "position");
213        if (this.position != position) {
214            this.position = position;
215            notifyListeners(new TitleChangeEvent(this));
216        }
217    }
218
219    /**
220     * Returns the horizontal alignment of the title.
221     *
222     * @return The horizontal alignment (never {@code null}).
223     */
224    public HorizontalAlignment getHorizontalAlignment() {
225        return this.horizontalAlignment;
226    }
227
228    /**
229     * Sets the horizontal alignment for the title and sends a
230     * {@link TitleChangeEvent} to all registered listeners.
231     *
232     * @param alignment  the horizontal alignment ({@code null} not
233     *                   permitted).
234     */
235    public void setHorizontalAlignment(HorizontalAlignment alignment) {
236        Args.nullNotPermitted(alignment, "alignment");
237        if (this.horizontalAlignment != alignment) {
238            this.horizontalAlignment = alignment;
239            notifyListeners(new TitleChangeEvent(this));
240        }
241    }
242
243    /**
244     * Returns the vertical alignment of the title.
245     *
246     * @return The vertical alignment (never {@code null}).
247     */
248    public VerticalAlignment getVerticalAlignment() {
249        return this.verticalAlignment;
250    }
251
252    /**
253     * Sets the vertical alignment for the title, and notifies any registered
254     * listeners of the change.
255     *
256     * @param alignment  the new vertical alignment (TOP, MIDDLE or BOTTOM,
257     *                   {@code null} not permitted).
258     */
259    public void setVerticalAlignment(VerticalAlignment alignment) {
260        Args.nullNotPermitted(alignment, "alignment");
261        if (this.verticalAlignment != alignment) {
262            this.verticalAlignment = alignment;
263            notifyListeners(new TitleChangeEvent(this));
264        }
265    }
266
267    /**
268     * Returns the flag that indicates whether or not the notification
269     * mechanism is enabled.
270     *
271     * @return The flag.
272     */
273    public boolean getNotify() {
274        return this.notify;
275    }
276
277    /**
278     * Sets the flag that indicates whether or not the notification mechanism
279     * is enabled.  There are certain situations (such as cloning) where you
280     * want to turn notification off temporarily.
281     *
282     * @param flag  the new value of the flag.
283     */
284    public void setNotify(boolean flag) {
285        this.notify = flag;
286        if (flag) {
287            notifyListeners(new TitleChangeEvent(this));
288        }
289    }
290
291    /**
292     * Draws the title on a Java 2D graphics device (such as the screen or a
293     * printer).
294     *
295     * @param g2  the graphics device.
296     * @param area  the area allocated for the title (subclasses should not
297     *              draw outside this area).
298     */
299    @Override
300    public abstract void draw(Graphics2D g2, Rectangle2D area);
301
302    /**
303     * Returns a clone of the title.
304     * <P>
305     * One situation when this is useful is when editing the title properties -
306     * you can edit a clone, and then it is easier to cancel the changes if
307     * necessary.
308     *
309     * @return A clone of the title.
310     *
311     * @throws CloneNotSupportedException not thrown by this class, but it may
312     *         be thrown by subclasses.
313     */
314    @Override
315    public Object clone() throws CloneNotSupportedException {
316        Title duplicate = (Title) super.clone();
317        duplicate.listenerList = new EventListenerList();
318        // RectangleInsets is immutable => same reference in clone OK
319        return duplicate;
320    }
321
322    /**
323     * Registers an object for notification of changes to the title.
324     *
325     * @param listener  the object that is being registered.
326     */
327    public void addChangeListener(TitleChangeListener listener) {
328        this.listenerList.add(TitleChangeListener.class, listener);
329    }
330
331    /**
332     * Unregisters an object for notification of changes to the chart title.
333     *
334     * @param listener  the object that is being unregistered.
335     */
336    public void removeChangeListener(TitleChangeListener listener) {
337        this.listenerList.remove(TitleChangeListener.class, listener);
338    }
339
340    /**
341     * Notifies all registered listeners that the chart title has changed in
342     * some way.
343     *
344     * @param event  an object that contains information about the change to
345     *               the title.
346     */
347    protected void notifyListeners(TitleChangeEvent event) {
348        if (this.notify) {
349            Object[] listeners = this.listenerList.getListenerList();
350            for (int i = listeners.length - 2; i >= 0; i -= 2) {
351                if (listeners[i] == TitleChangeListener.class) {
352                    ((TitleChangeListener) listeners[i + 1]).titleChanged(
353                            event);
354                }
355            }
356        }
357    }
358
359    /**
360     * Tests an object for equality with this title.
361     *
362     * @param obj  the object ({@code null} not permitted).
363     *
364     * @return {@code true} or {@code false}.
365     */
366    @Override
367    public boolean equals(Object obj) {
368        if (obj == this) {
369            return true;
370        }
371        if (!(obj instanceof Title)) {
372            return false;
373        }
374        Title that = (Title) obj;
375        if (this.visible != that.visible) {
376            return false;
377        }
378        if (!Objects.equals(this.position, that.position)) {
379            return false;
380        }
381        if (!Objects.equals(this.horizontalAlignment, that.horizontalAlignment)) {
382            return false;
383        }
384        if (!Objects.equals(this.verticalAlignment, that.verticalAlignment)) {
385            return false;
386        }
387        if (this.notify != that.notify) {
388            return false;
389        }
390        return super.equals(obj);
391    }
392
393    /**
394     * Ensures symmetry between super/subclass implementations of equals. For
395     * more detail, see http://jqno.nl/equalsverifier/manual/inheritance.
396     *
397     * @param other Object
398     * 
399     * @return true ONLY if the parameter is THIS class type
400     */
401    @Override
402    public boolean canEqual(Object other) {
403        // fix the "equals not symmetric" problem
404        return (other instanceof Title);
405    }
406
407    /**
408     * Returns a hashcode for the title.
409     *
410     * @return The hashcode.
411     */
412    @Override
413    public int hashCode() {
414        int hash = super.hashCode(); // equals calls superclass, hashCode must also
415        hash = 97 * hash + (this.visible ? 1 : 0);
416        hash = 97 * hash + Objects.hashCode(this.position);
417        hash = 97 * hash + Objects.hashCode(this.horizontalAlignment);
418        hash = 97 * hash + Objects.hashCode(this.verticalAlignment);
419        hash = 97 * hash + (this.notify ? 1 : 0);
420        return hash;
421    }
422
423    /**
424     * Provides serialization support.
425     *
426     * @param stream  the output stream.
427     *
428     * @throws IOException  if there is an I/O error.
429     */
430    private void writeObject(ObjectOutputStream stream) throws IOException {
431        stream.defaultWriteObject();
432    }
433
434    /**
435     * Provides serialization support.
436     *
437     * @param stream  the input stream.
438     *
439     * @throws IOException  if there is an I/O error.
440     * @throws ClassNotFoundException  if there is a classpath problem.
441     */
442    private void readObject(ObjectInputStream stream)
443        throws IOException, ClassNotFoundException {
444        stream.defaultReadObject();
445        this.listenerList = new EventListenerList();
446    }
447
448}