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