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