001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2021, 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 * [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 * TextTitle.java 029 * -------------- 030 * (C) Copyright 2000-2021, by David Berry and Contributors. 031 * 032 * Original Author: David Berry; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Nicolas Brodu; 035 * Peter Kolb - patch 2603321; 036 * 037 */ 038 039package org.jfree.chart.title; 040 041import java.awt.Color; 042import java.awt.Font; 043import java.awt.Graphics2D; 044import java.awt.Paint; 045import java.awt.geom.Rectangle2D; 046import java.io.IOException; 047import java.io.ObjectInputStream; 048import java.io.ObjectOutputStream; 049import java.io.Serializable; 050import java.util.Objects; 051 052import org.jfree.chart.block.BlockResult; 053import org.jfree.chart.block.EntityBlockParams; 054import org.jfree.chart.block.LengthConstraintType; 055import org.jfree.chart.block.RectangleConstraint; 056import org.jfree.chart.entity.ChartEntity; 057import org.jfree.chart.entity.EntityCollection; 058import org.jfree.chart.entity.StandardEntityCollection; 059import org.jfree.chart.entity.TitleEntity; 060import org.jfree.chart.event.TitleChangeEvent; 061import org.jfree.chart.text.G2TextMeasurer; 062import org.jfree.chart.text.TextBlock; 063import org.jfree.chart.text.TextBlockAnchor; 064import org.jfree.chart.text.TextUtils; 065import org.jfree.chart.ui.HorizontalAlignment; 066import org.jfree.chart.ui.RectangleEdge; 067import org.jfree.chart.ui.RectangleInsets; 068import org.jfree.chart.ui.Size2D; 069import org.jfree.chart.ui.VerticalAlignment; 070import org.jfree.chart.util.PaintUtils; 071import org.jfree.chart.util.Args; 072import org.jfree.chart.util.PublicCloneable; 073import org.jfree.chart.util.SerialUtils; 074import org.jfree.data.Range; 075 076/** 077 * A chart title that displays a text string with automatic wrapping as 078 * required. 079 */ 080public class TextTitle extends Title implements Serializable, Cloneable, PublicCloneable { 081 082 /** For serialization. */ 083 private static final long serialVersionUID = 8372008692127477443L; 084 085 /** The default font. */ 086 public static final Font DEFAULT_FONT = new Font("SansSerif", Font.BOLD, 12); 087 088 /** The default text color. */ 089 public static final Paint DEFAULT_TEXT_PAINT = Color.BLACK; 090 091 /** The title text. */ 092 private String text; 093 094 /** The font used to display the title. */ 095 private Font font; 096 097 /** The text alignment. */ 098 private HorizontalAlignment textAlignment; 099 100 /** The paint used to display the title text. */ 101 private transient Paint paint; 102 103 /** The background paint. */ 104 private transient Paint backgroundPaint; 105 106 /** The tool tip text (can be {@code null}). */ 107 private String toolTipText; 108 109 /** The URL text (can be {@code null}). */ 110 private String urlText; 111 112 /** The content. */ 113 private TextBlock content; 114 115 /** 116 * A flag that controls whether the title expands to fit the available 117 * space.. 118 */ 119 private boolean expandToFitSpace = false; 120 121 /** 122 * The maximum number of lines to display. 123 */ 124 private int maximumLinesToDisplay = Integer.MAX_VALUE; 125 126 /** 127 * Creates a new title, using default attributes where necessary. 128 */ 129 public TextTitle() { 130 this(""); 131 } 132 133 /** 134 * Creates a new title, using default attributes where necessary. 135 * 136 * @param text the title text ({@code null} not permitted). 137 */ 138 public TextTitle(String text) { 139 this(text, TextTitle.DEFAULT_FONT, TextTitle.DEFAULT_TEXT_PAINT, 140 Title.DEFAULT_POSITION, Title.DEFAULT_HORIZONTAL_ALIGNMENT, 141 Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING); 142 } 143 144 /** 145 * Creates a new title, using default attributes where necessary. 146 * 147 * @param text the title text ({@code null} not permitted). 148 * @param font the title font ({@code null} not permitted). 149 */ 150 public TextTitle(String text, Font font) { 151 this(text, font, TextTitle.DEFAULT_TEXT_PAINT, Title.DEFAULT_POSITION, 152 Title.DEFAULT_HORIZONTAL_ALIGNMENT, 153 Title.DEFAULT_VERTICAL_ALIGNMENT, Title.DEFAULT_PADDING); 154 } 155 156 /** 157 * Creates a new title. 158 * 159 * @param text the text for the title ({@code null} not permitted). 160 * @param font the title font ({@code null} not permitted). 161 * @param paint the title paint ({@code null} not permitted). 162 * @param position the title position ({@code null} not permitted). 163 * @param horizontalAlignment the horizontal alignment ({@code null} 164 * not permitted). 165 * @param verticalAlignment the vertical alignment ({@code null} not 166 * permitted). 167 * @param padding the space to leave around the outside of the title. 168 */ 169 public TextTitle(String text, Font font, Paint paint, 170 RectangleEdge position, 171 HorizontalAlignment horizontalAlignment, 172 VerticalAlignment verticalAlignment, 173 RectangleInsets padding) { 174 175 super(position, horizontalAlignment, verticalAlignment, padding); 176 177 if (text == null) { 178 throw new NullPointerException("Null 'text' argument."); 179 } 180 if (font == null) { 181 throw new NullPointerException("Null 'font' argument."); 182 } 183 if (paint == null) { 184 throw new NullPointerException("Null 'paint' argument."); 185 } 186 this.text = text; 187 this.font = font; 188 this.paint = paint; 189 // the textAlignment and the horizontalAlignment are separate things, 190 // but it makes sense for the default textAlignment to match the 191 // title's horizontal alignment... 192 this.textAlignment = horizontalAlignment; 193 this.backgroundPaint = null; 194 this.content = null; 195 this.toolTipText = null; 196 this.urlText = null; 197 198 } 199 200 /** 201 * Returns the title text. 202 * 203 * @return The text (never {@code null}). 204 * 205 * @see #setText(String) 206 */ 207 public String getText() { 208 return this.text; 209 } 210 211 /** 212 * Sets the title to the specified text and sends a 213 * {@link TitleChangeEvent} to all registered listeners. 214 * 215 * @param text the text ({@code null} not permitted). 216 */ 217 public void setText(String text) { 218 Args.nullNotPermitted(text, "text"); 219 if (!this.text.equals(text)) { 220 this.text = text; 221 notifyListeners(new TitleChangeEvent(this)); 222 } 223 } 224 225 /** 226 * Returns the text alignment. This controls how the text is aligned 227 * within the title's bounds, whereas the title's horizontal alignment 228 * controls how the title's bounding rectangle is aligned within the 229 * drawing space. 230 * 231 * @return The text alignment. 232 */ 233 public HorizontalAlignment getTextAlignment() { 234 return this.textAlignment; 235 } 236 237 /** 238 * Sets the text alignment and sends a {@link TitleChangeEvent} to 239 * all registered listeners. 240 * 241 * @param alignment the alignment ({@code null} not permitted). 242 */ 243 public void setTextAlignment(HorizontalAlignment alignment) { 244 Args.nullNotPermitted(alignment, "alignment"); 245 this.textAlignment = alignment; 246 notifyListeners(new TitleChangeEvent(this)); 247 } 248 249 /** 250 * Returns the font used to display the title string. 251 * 252 * @return The font (never {@code null}). 253 * 254 * @see #setFont(Font) 255 */ 256 public Font getFont() { 257 return this.font; 258 } 259 260 /** 261 * Sets the font used to display the title string. Registered listeners 262 * are notified that the title has been modified. 263 * 264 * @param font the new font ({@code null} not permitted). 265 * 266 * @see #getFont() 267 */ 268 public void setFont(Font font) { 269 Args.nullNotPermitted(font, "font"); 270 if (!this.font.equals(font)) { 271 this.font = font; 272 notifyListeners(new TitleChangeEvent(this)); 273 } 274 } 275 276 /** 277 * Returns the paint used to display the title string. 278 * 279 * @return The paint (never {@code null}). 280 * 281 * @see #setPaint(Paint) 282 */ 283 public Paint getPaint() { 284 return this.paint; 285 } 286 287 /** 288 * Sets the paint used to display the title string. Registered listeners 289 * are notified that the title has been modified. 290 * 291 * @param paint the new paint ({@code null} not permitted). 292 * 293 * @see #getPaint() 294 */ 295 public void setPaint(Paint paint) { 296 Args.nullNotPermitted(paint, "paint"); 297 if (!this.paint.equals(paint)) { 298 this.paint = paint; 299 notifyListeners(new TitleChangeEvent(this)); 300 } 301 } 302 303 /** 304 * Returns the background paint. 305 * 306 * @return The paint (possibly {@code null}). 307 */ 308 public Paint getBackgroundPaint() { 309 return this.backgroundPaint; 310 } 311 312 /** 313 * Sets the background paint and sends a {@link TitleChangeEvent} to all 314 * registered listeners. If you set this attribute to {@code null}, 315 * no background is painted (which makes the title background transparent). 316 * 317 * @param paint the background paint ({@code null} permitted). 318 */ 319 public void setBackgroundPaint(Paint paint) { 320 this.backgroundPaint = paint; 321 notifyListeners(new TitleChangeEvent(this)); 322 } 323 324 /** 325 * Returns the tool tip text. 326 * 327 * @return The tool tip text (possibly {@code null}). 328 */ 329 public String getToolTipText() { 330 return this.toolTipText; 331 } 332 333 /** 334 * Sets the tool tip text to the specified text and sends a 335 * {@link TitleChangeEvent} to all registered listeners. 336 * 337 * @param text the text ({@code null} permitted). 338 */ 339 public void setToolTipText(String text) { 340 this.toolTipText = text; 341 notifyListeners(new TitleChangeEvent(this)); 342 } 343 344 /** 345 * Returns the URL text. 346 * 347 * @return The URL text (possibly {@code null}). 348 */ 349 public String getURLText() { 350 return this.urlText; 351 } 352 353 /** 354 * Sets the URL text to the specified text and sends a 355 * {@link TitleChangeEvent} to all registered listeners. 356 * 357 * @param text the text ({@code null} permitted). 358 */ 359 public void setURLText(String text) { 360 this.urlText = text; 361 notifyListeners(new TitleChangeEvent(this)); 362 } 363 364 /** 365 * Returns the flag that controls whether or not the title expands to fit 366 * the available space. 367 * 368 * @return The flag. 369 */ 370 public boolean getExpandToFitSpace() { 371 return this.expandToFitSpace; 372 } 373 374 /** 375 * Sets the flag that controls whether the title expands to fit the 376 * available space, and sends a {@link TitleChangeEvent} to all registered 377 * listeners. 378 * 379 * @param expand the flag. 380 */ 381 public void setExpandToFitSpace(boolean expand) { 382 this.expandToFitSpace = expand; 383 notifyListeners(new TitleChangeEvent(this)); 384 } 385 386 /** 387 * Returns the maximum number of lines to display. 388 * 389 * @return The maximum. 390 * 391 * @see #setMaximumLinesToDisplay(int) 392 */ 393 public int getMaximumLinesToDisplay() { 394 return this.maximumLinesToDisplay; 395 } 396 397 /** 398 * Sets the maximum number of lines to display and sends a 399 * {@link TitleChangeEvent} to all registered listeners. 400 * 401 * @param max the maximum. 402 * 403 * @see #getMaximumLinesToDisplay() 404 */ 405 public void setMaximumLinesToDisplay(int max) { 406 this.maximumLinesToDisplay = max; 407 notifyListeners(new TitleChangeEvent(this)); 408 } 409 410 /** 411 * Arranges the contents of the block, within the given constraints, and 412 * returns the block size. 413 * 414 * @param g2 the graphics device. 415 * @param constraint the constraint ({@code null} not permitted). 416 * 417 * @return The block size (in Java2D units, never {@code null}). 418 */ 419 @Override 420 public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) { 421 RectangleConstraint cc = toContentConstraint(constraint); 422 LengthConstraintType w = cc.getWidthConstraintType(); 423 LengthConstraintType h = cc.getHeightConstraintType(); 424 Size2D contentSize = null; 425 if (w == LengthConstraintType.NONE) { 426 if (h == LengthConstraintType.NONE) { 427 contentSize = arrangeNN(g2); 428 } 429 else if (h == LengthConstraintType.RANGE) { 430 throw new RuntimeException("Not yet implemented."); 431 } 432 else if (h == LengthConstraintType.FIXED) { 433 throw new RuntimeException("Not yet implemented."); 434 } 435 } 436 else if (w == LengthConstraintType.RANGE) { 437 if (h == LengthConstraintType.NONE) { 438 contentSize = arrangeRN(g2, cc.getWidthRange()); 439 } 440 else if (h == LengthConstraintType.RANGE) { 441 contentSize = arrangeRR(g2, cc.getWidthRange(), 442 cc.getHeightRange()); 443 } 444 else if (h == LengthConstraintType.FIXED) { 445 throw new RuntimeException("Not yet implemented."); 446 } 447 } 448 else if (w == LengthConstraintType.FIXED) { 449 if (h == LengthConstraintType.NONE) { 450 contentSize = arrangeFN(g2, cc.getWidth()); 451 } 452 else if (h == LengthConstraintType.RANGE) { 453 throw new RuntimeException("Not yet implemented."); 454 } 455 else if (h == LengthConstraintType.FIXED) { 456 throw new RuntimeException("Not yet implemented."); 457 } 458 } 459 assert contentSize != null; // suppress compiler warning 460 return new Size2D(calculateTotalWidth(contentSize.getWidth()), 461 calculateTotalHeight(contentSize.getHeight())); 462 } 463 464 /** 465 * Arranges the content for this title assuming no bounds on the width 466 * or the height, and returns the required size. This will reflect the 467 * fact that a text title positioned on the left or right of a chart will 468 * be rotated by 90 degrees. 469 * 470 * @param g2 the graphics target. 471 * 472 * @return The content size. 473 */ 474 protected Size2D arrangeNN(Graphics2D g2) { 475 Range max = new Range(0.0, Float.MAX_VALUE); 476 return arrangeRR(g2, max, max); 477 } 478 479 /** 480 * Arranges the content for this title assuming a fixed width and no bounds 481 * on the height, and returns the required size. This will reflect the 482 * fact that a text title positioned on the left or right of a chart will 483 * be rotated by 90 degrees. 484 * 485 * @param g2 the graphics target. 486 * @param w the width. 487 * 488 * @return The content size. 489 */ 490 protected Size2D arrangeFN(Graphics2D g2, double w) { 491 RectangleEdge position = getPosition(); 492 if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) { 493 float maxWidth = (float) w; 494 g2.setFont(this.font); 495 this.content = TextUtils.createTextBlock(this.text, this.font, 496 this.paint, maxWidth, this.maximumLinesToDisplay, 497 new G2TextMeasurer(g2)); 498 this.content.setLineAlignment(this.textAlignment); 499 Size2D contentSize = this.content.calculateDimensions(g2); 500 if (this.expandToFitSpace) { 501 return new Size2D(maxWidth, contentSize.getHeight()); 502 } 503 else { 504 return contentSize; 505 } 506 } 507 else if (position == RectangleEdge.LEFT || position 508 == RectangleEdge.RIGHT) { 509 float maxWidth = Float.MAX_VALUE; 510 g2.setFont(this.font); 511 this.content = TextUtils.createTextBlock(this.text, this.font, 512 this.paint, maxWidth, this.maximumLinesToDisplay, 513 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 * Arranges the content for this title assuming a range constraint for the 532 * width and no bounds on the height, and returns the required size. This 533 * will reflect the fact that a text title positioned on the left or right 534 * of a chart will be rotated by 90 degrees. 535 * 536 * @param g2 the graphics target. 537 * @param widthRange the range for the width. 538 * 539 * @return The content size. 540 */ 541 protected Size2D arrangeRN(Graphics2D g2, Range widthRange) { 542 Size2D s = arrangeNN(g2); 543 if (widthRange.contains(s.getWidth())) { 544 return s; 545 } 546 double ww = widthRange.constrain(s.getWidth()); 547 return arrangeFN(g2, ww); 548 } 549 550 /** 551 * Returns the content size for the title. This will reflect the fact that 552 * a text title positioned on the left or right of a chart will be rotated 553 * 90 degrees. 554 * 555 * @param g2 the graphics device. 556 * @param widthRange the width range. 557 * @param heightRange the height range. 558 * 559 * @return The content size. 560 */ 561 protected Size2D arrangeRR(Graphics2D g2, Range widthRange, 562 Range heightRange) { 563 RectangleEdge position = getPosition(); 564 if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) { 565 float maxWidth = (float) widthRange.getUpperBound(); 566 g2.setFont(this.font); 567 this.content = TextUtils.createTextBlock(this.text, this.font, 568 this.paint, maxWidth, this.maximumLinesToDisplay, 569 new G2TextMeasurer(g2)); 570 this.content.setLineAlignment(this.textAlignment); 571 Size2D contentSize = this.content.calculateDimensions(g2); 572 if (this.expandToFitSpace) { 573 return new Size2D(maxWidth, contentSize.getHeight()); 574 } 575 else { 576 return contentSize; 577 } 578 } 579 else if (position == RectangleEdge.LEFT || position 580 == RectangleEdge.RIGHT) { 581 float maxWidth = (float) heightRange.getUpperBound(); 582 g2.setFont(this.font); 583 this.content = TextUtils.createTextBlock(this.text, this.font, 584 this.paint, maxWidth, this.maximumLinesToDisplay, 585 new G2TextMeasurer(g2)); 586 this.content.setLineAlignment(this.textAlignment); 587 Size2D contentSize = this.content.calculateDimensions(g2); 588 589 // transpose the dimensions, because the title is rotated 590 if (this.expandToFitSpace) { 591 return new Size2D(contentSize.getHeight(), maxWidth); 592 } 593 else { 594 return new Size2D(contentSize.height, contentSize.width); 595 } 596 } 597 else { 598 throw new RuntimeException("Unrecognised exception."); 599 } 600 } 601 602 /** 603 * Draws the title on a Java 2D graphics device (such as the screen or a 604 * printer). 605 * 606 * @param g2 the graphics device. 607 * @param area the area allocated for the title. 608 */ 609 @Override 610 public void draw(Graphics2D g2, Rectangle2D area) { 611 draw(g2, area, null); 612 } 613 614 /** 615 * Draws the block within the specified area. 616 * 617 * @param g2 the graphics device. 618 * @param area the area. 619 * @param params if this is an instance of {@link EntityBlockParams} it 620 * is used to determine whether or not an 621 * {@link EntityCollection} is returned by this method. 622 * 623 * @return An {@link EntityCollection} containing a chart entity for the 624 * title, or {@code null}. 625 */ 626 @Override 627 public Object draw(Graphics2D g2, Rectangle2D area, Object params) { 628 if (this.content == null) { 629 return null; 630 } 631 area = trimMargin(area); 632 drawBorder(g2, area); 633 if (this.text.equals("")) { 634 return null; 635 } 636 ChartEntity entity = null; 637 if (params instanceof EntityBlockParams) { 638 EntityBlockParams p = (EntityBlockParams) params; 639 if (p.getGenerateEntities()) { 640 entity = new TitleEntity(area, this, this.toolTipText, 641 this.urlText); 642 } 643 } 644 area = trimBorder(area); 645 if (this.backgroundPaint != null) { 646 g2.setPaint(this.backgroundPaint); 647 g2.fill(area); 648 } 649 area = trimPadding(area); 650 RectangleEdge position = getPosition(); 651 if (position == RectangleEdge.TOP || position == RectangleEdge.BOTTOM) { 652 drawHorizontal(g2, area); 653 } 654 else if (position == RectangleEdge.LEFT 655 || position == RectangleEdge.RIGHT) { 656 drawVertical(g2, area); 657 } 658 BlockResult result = new BlockResult(); 659 if (entity != null) { 660 StandardEntityCollection sec = new StandardEntityCollection(); 661 sec.add(entity); 662 result.setEntityCollection(sec); 663 } 664 return result; 665 } 666 667 /** 668 * Draws a the title horizontally within the specified area. This method 669 * will be called from the {@link #draw(Graphics2D, Rectangle2D) draw} 670 * method. 671 * 672 * @param g2 the graphics device. 673 * @param area the area for the title. 674 */ 675 protected void drawHorizontal(Graphics2D g2, Rectangle2D area) { 676 Rectangle2D titleArea = (Rectangle2D) area.clone(); 677 g2.setFont(this.font); 678 g2.setPaint(this.paint); 679 TextBlockAnchor anchor = null; 680 float x = 0.0f; 681 HorizontalAlignment horizontalAlignment = getHorizontalAlignment(); 682 if (horizontalAlignment == HorizontalAlignment.LEFT) { 683 x = (float) titleArea.getX(); 684 anchor = TextBlockAnchor.TOP_LEFT; 685 } 686 else if (horizontalAlignment == HorizontalAlignment.RIGHT) { 687 x = (float) titleArea.getMaxX(); 688 anchor = TextBlockAnchor.TOP_RIGHT; 689 } 690 else if (horizontalAlignment == HorizontalAlignment.CENTER) { 691 x = (float) titleArea.getCenterX(); 692 anchor = TextBlockAnchor.TOP_CENTER; 693 } 694 float y = 0.0f; 695 RectangleEdge position = getPosition(); 696 if (position == RectangleEdge.TOP) { 697 y = (float) titleArea.getY(); 698 } 699 else if (position == RectangleEdge.BOTTOM) { 700 y = (float) titleArea.getMaxY(); 701 if (horizontalAlignment == HorizontalAlignment.LEFT) { 702 anchor = TextBlockAnchor.BOTTOM_LEFT; 703 } 704 else if (horizontalAlignment == HorizontalAlignment.CENTER) { 705 anchor = TextBlockAnchor.BOTTOM_CENTER; 706 } 707 else if (horizontalAlignment == HorizontalAlignment.RIGHT) { 708 anchor = TextBlockAnchor.BOTTOM_RIGHT; 709 } 710 } 711 this.content.draw(g2, x, y, anchor); 712 } 713 714 /** 715 * Draws a the title vertically within the specified area. This method 716 * will be called from the {@link #draw(Graphics2D, Rectangle2D) draw} 717 * method. 718 * 719 * @param g2 the graphics device. 720 * @param area the area for the title. 721 */ 722 protected void drawVertical(Graphics2D g2, Rectangle2D area) { 723 Rectangle2D titleArea = (Rectangle2D) area.clone(); 724 g2.setFont(this.font); 725 g2.setPaint(this.paint); 726 TextBlockAnchor anchor = null; 727 float y = 0.0f; 728 VerticalAlignment verticalAlignment = getVerticalAlignment(); 729 if (verticalAlignment == VerticalAlignment.TOP) { 730 y = (float) titleArea.getY(); 731 anchor = TextBlockAnchor.TOP_RIGHT; 732 } 733 else if (verticalAlignment == VerticalAlignment.BOTTOM) { 734 y = (float) titleArea.getMaxY(); 735 anchor = TextBlockAnchor.TOP_LEFT; 736 } 737 else if (verticalAlignment == VerticalAlignment.CENTER) { 738 y = (float) titleArea.getCenterY(); 739 anchor = TextBlockAnchor.TOP_CENTER; 740 } 741 float x = 0.0f; 742 RectangleEdge position = getPosition(); 743 if (position == RectangleEdge.LEFT) { 744 x = (float) titleArea.getX(); 745 } 746 else if (position == RectangleEdge.RIGHT) { 747 x = (float) titleArea.getMaxX(); 748 if (verticalAlignment == VerticalAlignment.TOP) { 749 anchor = TextBlockAnchor.BOTTOM_RIGHT; 750 } 751 else if (verticalAlignment == VerticalAlignment.CENTER) { 752 anchor = TextBlockAnchor.BOTTOM_CENTER; 753 } 754 else if (verticalAlignment == VerticalAlignment.BOTTOM) { 755 anchor = TextBlockAnchor.BOTTOM_LEFT; 756 } 757 } 758 this.content.draw(g2, x, y, anchor, x, y, -Math.PI / 2.0); 759 } 760 761 /** 762 * Tests this title for equality with another object. 763 * 764 * @param obj the object ({@code null} permitted). 765 * 766 * @return {@code true} or {@code false}. 767 */ 768 @Override 769 public boolean equals(Object obj) { 770 if (obj == this) { 771 return true; 772 } 773 if (!(obj instanceof TextTitle)) { 774 return false; 775 } 776 TextTitle that = (TextTitle) obj; 777 if (!Objects.equals(this.text, that.text)) { 778 return false; 779 } 780 if (!Objects.equals(this.font, that.font)) { 781 return false; 782 } 783 if (!PaintUtils.equal(this.paint, that.paint)) { 784 return false; 785 } 786 if (this.textAlignment != that.textAlignment) { 787 return false; 788 } 789 if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) { 790 return false; 791 } 792 if (this.maximumLinesToDisplay != that.maximumLinesToDisplay) { 793 return false; 794 } 795 if (this.expandToFitSpace != that.expandToFitSpace) { 796 return false; 797 } 798 if (!Objects.equals(this.toolTipText, that.toolTipText)) { 799 return false; 800 } 801 if (!Objects.equals(this.urlText, that.urlText)) { 802 return false; 803 } 804 return super.equals(obj); 805 } 806 807 /** 808 * Returns a hash code. 809 * 810 * @return A hash code. 811 */ 812 @Override 813 public int hashCode() { 814 int result = super.hashCode(); 815 result = 29 * result + (this.text != null ? this.text.hashCode() : 0); 816 result = 29 * result + (this.font != null ? this.font.hashCode() : 0); 817 result = 29 * result + (this.paint != null ? this.paint.hashCode() : 0); 818 result = 29 * result + (this.backgroundPaint != null 819 ? this.backgroundPaint.hashCode() : 0); 820 return result; 821 } 822 823 /** 824 * Returns a clone of this object. 825 * 826 * @return A clone. 827 * 828 * @throws CloneNotSupportedException never. 829 */ 830 @Override 831 public Object clone() throws CloneNotSupportedException { 832 return super.clone(); 833 } 834 835 /** 836 * Provides serialization support. 837 * 838 * @param stream the output stream. 839 * 840 * @throws IOException if there is an I/O error. 841 */ 842 private void writeObject(ObjectOutputStream stream) throws IOException { 843 stream.defaultWriteObject(); 844 SerialUtils.writePaint(this.paint, stream); 845 SerialUtils.writePaint(this.backgroundPaint, stream); 846 } 847 848 /** 849 * Provides serialization support. 850 * 851 * @param stream the input stream. 852 * 853 * @throws IOException if there is an I/O error. 854 * @throws ClassNotFoundException if there is a classpath problem. 855 */ 856 private void readObject(ObjectInputStream stream) 857 throws IOException, ClassNotFoundException { 858 stream.defaultReadObject(); 859 this.paint = SerialUtils.readPaint(stream); 860 this.backgroundPaint = SerialUtils.readPaint(stream); 861 } 862 863} 864