001/* 002 * $Id: JXLabel.java 4158 2012-02-03 18:29:40Z kschaefe $ 003 * 004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 022package org.jdesktop.swingx; 023 024import java.applet.Applet; 025import java.awt.Color; 026import java.awt.Container; 027import java.awt.Dimension; 028import java.awt.Font; 029import java.awt.Graphics; 030import java.awt.Graphics2D; 031import java.awt.Insets; 032import java.awt.Rectangle; 033import java.awt.Shape; 034import java.awt.Window; 035import java.awt.event.HierarchyBoundsAdapter; 036import java.awt.event.HierarchyEvent; 037import java.awt.font.TextAttribute; 038import java.awt.geom.Point2D; 039import java.beans.PropertyChangeEvent; 040import java.beans.PropertyChangeListener; 041import java.io.Reader; 042import java.io.StringReader; 043 044import javax.swing.Icon; 045import javax.swing.JLabel; 046import javax.swing.JPanel; 047import javax.swing.JViewport; 048import javax.swing.SwingConstants; 049import javax.swing.border.Border; 050import javax.swing.event.DocumentEvent; 051import javax.swing.event.DocumentEvent.ElementChange; 052import javax.swing.plaf.basic.BasicHTML; 053import javax.swing.text.AbstractDocument; 054import javax.swing.text.AttributeSet; 055import javax.swing.text.BoxView; 056import javax.swing.text.ComponentView; 057import javax.swing.text.DefaultStyledDocument; 058import javax.swing.text.Document; 059import javax.swing.text.Element; 060import javax.swing.text.IconView; 061import javax.swing.text.LabelView; 062import javax.swing.text.MutableAttributeSet; 063import javax.swing.text.ParagraphView; 064import javax.swing.text.SimpleAttributeSet; 065import javax.swing.text.StyleConstants; 066import javax.swing.text.StyledEditorKit; 067import javax.swing.text.View; 068import javax.swing.text.ViewFactory; 069import javax.swing.text.WrappedPlainView; 070 071import org.jdesktop.beans.JavaBean; 072import org.jdesktop.swingx.painter.AbstractPainter; 073import org.jdesktop.swingx.painter.Painter; 074 075/** 076 * <p> 077 * A {@link javax.swing.JLabel} subclass which supports {@link org.jdesktop.swingx.painter.Painter}s, multi-line text, 078 * and text rotation. 079 * </p> 080 * 081 * <p> 082 * Painter support consists of the <code>foregroundPainter</code> and <code>backgroundPainter</code> properties. The 083 * <code>backgroundPainter</code> refers to a painter responsible for painting <i>beneath</i> the text and icon. This 084 * painter, if set, will paint regardless of the <code>opaque</code> property. If the background painter does not 085 * fully paint each pixel, then you should make sure the <code>opaque</code> property is set to false. 086 * </p> 087 * 088 * <p> 089 * The <code>foregroundPainter</code> is responsible for painting the icon and the text label. If no foregroundPainter 090 * is specified, then the look and feel will paint the label. Note that if opaque is set to true and the look and feel 091 * is rendering the foreground, then the foreground <i>may</i> paint over the background. Most look and feels will 092 * paint a background when <code>opaque</code> is true. To avoid this behavior, set <code>opaque</code> to false. 093 * </p> 094 * 095 * <p> 096 * Since JXLabel is not opaque by default (<code>isOpaque()</code> returns false), neither of these problems 097 * typically present themselves. 098 * </p> 099 * 100 * <p> 101 * Multi-line text is enabled via the <code>lineWrap</code> property. Simply set it to true. By default, line wrapping 102 * occurs on word boundaries. 103 * </p> 104 * 105 * <p> 106 * The text (actually, the entire foreground and background) of the JXLabel may be rotated. Set the 107 * <code>rotation</code> property to specify what the rotation should be. Specify rotation angle in radian units. 108 * </p> 109 * 110 * @author joshua.marinacci@sun.com 111 * @author rbair 112 * @author rah 113 * @author mario_cesar 114 */ 115@JavaBean 116public class JXLabel extends JLabel implements BackgroundPaintable { 117 118 /** 119 * Text alignment enums. Controls alignment of the text when line wrapping is enabled. 120 */ 121 public enum TextAlignment implements IValue { 122 LEFT(StyleConstants.ALIGN_LEFT), CENTER(StyleConstants.ALIGN_CENTER), RIGHT(StyleConstants.ALIGN_RIGHT), JUSTIFY(StyleConstants.ALIGN_JUSTIFIED); 123 124 private int value; 125 private TextAlignment(int val) { 126 value = val; 127 } 128 129 @Override 130 public int getValue() { 131 return value; 132 } 133 134 } 135 136 protected interface IValue { 137 int getValue(); 138 } 139 140 // textOrientation value declarations... 141 public static final double NORMAL = 0; 142 143 public static final double INVERTED = Math.PI; 144 145 public static final double VERTICAL_LEFT = 3 * Math.PI / 2; 146 147 public static final double VERTICAL_RIGHT = Math.PI / 2; 148 149 private double textRotation = NORMAL; 150 151 private boolean painting = false; 152 153 private Painter foregroundPainter; 154 155 private Painter backgroundPainter; 156 157 private boolean multiLine; 158 159 private int pWidth; 160 161 private int pHeight; 162 163 // using reverse logic ... some methods causing re-flow of text are called from super constructor, but private variables are initialized only after call to super so have to rely on default for boolean being false 164 private boolean dontIgnoreRepaint = false; 165 166 private int occupiedWidth; 167 168 private static final String oldRendererKey = "was" + BasicHTML.propertyKey; 169 170// private static final Logger log = Logger.getAnonymousLogger(); 171// static { 172// log.setLevel(Level.FINEST); 173// } 174 175 /** 176 * Create a new JXLabel. This has the same semantics as creating a new JLabel. 177 */ 178 public JXLabel() { 179 super(); 180 initPainterSupport(); 181 initLineWrapSupport(); 182 } 183 184 /** 185 * Creates new JXLabel with given icon. 186 * @param image the icon to set. 187 */ 188 public JXLabel(Icon image) { 189 super(image); 190 initPainterSupport(); 191 initLineWrapSupport(); 192 } 193 194 /** 195 * Creates new JXLabel with given icon and alignment. 196 * @param image the icon to set. 197 * @param horizontalAlignment the text alignment. 198 */ 199 public JXLabel(Icon image, int horizontalAlignment) { 200 super(image, horizontalAlignment); 201 initPainterSupport(); 202 initLineWrapSupport(); 203 } 204 205 /** 206 * Create a new JXLabel with the given text as the text for the label. This is shorthand for: 207 * 208 * <pre><code> 209 * JXLabel label = new JXLabel(); 210 * label.setText("Some Text"); 211 * </code></pre> 212 * 213 * @param text the text to set. 214 */ 215 public JXLabel(String text) { 216 super(text); 217 initPainterSupport(); 218 initLineWrapSupport(); 219 } 220 221 /** 222 * Creates new JXLabel with given text, icon and alignment. 223 * @param text the test to set. 224 * @param image the icon to set. 225 * @param horizontalAlignment the text alignment relative to the icon. 226 */ 227 public JXLabel(String text, Icon image, int horizontalAlignment) { 228 super(text, image, horizontalAlignment); 229 initPainterSupport(); 230 initLineWrapSupport(); 231 } 232 233 /** 234 * Creates new JXLabel with given text and alignment. 235 * @param text the test to set. 236 * @param horizontalAlignment the text alignment. 237 */ 238 public JXLabel(String text, int horizontalAlignment) { 239 super(text, horizontalAlignment); 240 initPainterSupport(); 241 initLineWrapSupport(); 242 } 243 244 private void initPainterSupport() { 245 foregroundPainter = new AbstractPainter<JXLabel>() { 246 @Override 247 protected void doPaint(Graphics2D g, JXLabel label, int width, int height) { 248 Insets i = getInsets(); 249 g = (Graphics2D) g.create(-i.left, -i.top, width, height); 250 251 try { 252 label.paint(g); 253 } finally { 254 g.dispose(); 255 } 256 } 257 //if any of the state of the JButton that affects the foreground has changed, 258 //then I must clear the cache. This is really hard to get right, there are 259 //bound to be bugs. An alternative is to NEVER cache. 260 @Override 261 protected boolean shouldUseCache() { 262 return false; 263 } 264 265 @Override 266 public boolean equals(Object obj) { 267 return obj != null && this.getClass().equals(obj.getClass()); 268 } 269 270 }; 271 ((AbstractPainter<?>) foregroundPainter).setAntialiasing(false); 272 } 273 274 /** 275 * Helper method for initializing multi line support. 276 */ 277 private void initLineWrapSupport() { 278 addPropertyChangeListener(new MultiLineSupport()); 279 // FYI: no more listening for componentResized. Those events are delivered out 280 // of order and without old values are meaningless and forcing us to react when 281 // not necessary. Instead overriding reshape() ensures we have control over old AND new size. 282 addHierarchyBoundsListener(new HierarchyBoundsAdapter() { 283 @Override 284 public void ancestorResized(HierarchyEvent e) { 285 // if one of the parents is viewport, resized events will not be propagated down unless viewport is changing visibility of scrollbars. 286 // To make sure Label is able to re-wrap text when viewport size changes, initiate re-wrapping here by changing size of view 287 if (e.getChanged() instanceof JViewport) { 288 Rectangle viewportBounds = e.getChanged().getBounds(); 289 if (viewportBounds.getWidth() < getWidth()) { 290 View view = getWrappingView(); 291 if (view != null) { 292 view.setSize(viewportBounds.width, viewportBounds.height); 293 } 294 } 295 } 296 }}); 297 } 298 299 /** 300 * Returns the current foregroundPainter. This is a bound property. By default the foregroundPainter will be an 301 * internal painter which executes the standard painting code (paintComponent()). 302 * 303 * @return the current foreground painter. 304 */ 305 public final Painter getForegroundPainter() { 306 return foregroundPainter; 307 } 308 309 @Override 310 @SuppressWarnings("deprecation") 311 public void reshape(int x, int y, int w, int h) { 312 int oldH = getHeight(); 313 super.reshape(x, y, w, h); 314 if (!isLineWrap()) { 315 return; 316 } 317 if (oldH == 0) { 318 return; 319 } 320 if (w > getVisibleRect().width) { 321 w = getVisibleRect().width; 322 } 323 View view = (View) getClientProperty(BasicHTML.propertyKey); 324 if (view != null && view instanceof Renderer) { 325 view.setSize(w - occupiedWidth, h); 326 } 327 } 328 329 /** 330 * {@inheritDoc} 331 */ 332 @Override 333 public void setBackground(Color bg) { 334 super.setBackground(bg); 335 336 SwingXUtilities.installBackground(this, bg); 337 } 338 339 /** 340 * Sets a new foregroundPainter on the label. This will replace the existing foreground painter. Existing painters 341 * can be wrapped by using a CompoundPainter. 342 * 343 * @param painter 344 */ 345 public void setForegroundPainter(Painter painter) { 346 Painter old = this.getForegroundPainter(); 347 if (painter == null) { 348 //restore default painter 349 initPainterSupport(); 350 } else { 351 this.foregroundPainter = painter; 352 } 353 firePropertyChange("foregroundPainter", old, getForegroundPainter()); 354 repaint(); 355 } 356 357 /** 358 * Sets a Painter to use to paint the background of this component By default there is already a single painter 359 * installed which draws the normal background for this component according to the current Look and Feel. Calling 360 * <CODE>setBackgroundPainter</CODE> will replace that existing painter. 361 * 362 * @param p the new painter 363 * @see #getBackgroundPainter() 364 */ 365 @Override 366 public void setBackgroundPainter(Painter p) { 367 Painter old = getBackgroundPainter(); 368 backgroundPainter = p; 369 firePropertyChange("backgroundPainter", old, getBackgroundPainter()); 370 repaint(); 371 } 372 373 /** 374 * Returns the current background painter. The default value of this property is a painter which draws the normal 375 * JPanel background according to the current look and feel. 376 * 377 * @return the current painter 378 * @see #setBackgroundPainter(Painter) 379 */ 380 @Override 381 public final Painter getBackgroundPainter() { 382 return backgroundPainter; 383 } 384 385 /** 386 * Gets current value of text rotation in rads. 387 * 388 * @return a double representing the current rotation of the text 389 * @see #setTextRotation(double) 390 */ 391 public double getTextRotation() { 392 return textRotation; 393 } 394 395 @Override 396 public Dimension getPreferredSize() { 397 Dimension size = super.getPreferredSize(); 398 //if (true) return size; 399 if (isPreferredSizeSet()) { 400 //log.fine("ret 0"); 401 return size; 402 } else if (this.textRotation != NORMAL) { 403 // #swingx-680 change the preferred size when rotation is set ... ideally this would be solved in the LabelUI rather then here 404 double theta = getTextRotation(); 405 size.setSize(rotateWidth(size, theta), rotateHeight(size, 406 theta)); 407 } else { 408 // #swingx-780 preferred size is not set properly when parent container doesn't enforce the width 409 View view = getWrappingView(); 410 if (view == null) { 411 if (isLineWrap() && !MultiLineSupport.isHTML(getText())) { 412 getMultiLineSupport(); 413 // view might get lost on LAF change ... 414 putClientProperty(BasicHTML.propertyKey, 415 MultiLineSupport.createView(this)); 416 view = (View) getClientProperty(BasicHTML.propertyKey); 417 } else { 418 return size; 419 } 420 } 421 Insets insets = getInsets(); 422 int dx = insets.left + insets.right; 423 int dy = insets.top + insets.bottom; 424 //log.fine("INSETS:" + insets); 425 //log.fine("BORDER:" + this.getBorder()); 426 Rectangle textR = new Rectangle(); 427 Rectangle viewR = new Rectangle(); 428 textR.x = textR.y = textR.width = textR.height = 0; 429 viewR.x = dx; 430 viewR.y = dy; 431 viewR.width = viewR.height = Short.MAX_VALUE; 432 // layout label 433 // 1) icon 434 Rectangle iconR = calculateIconRect(); 435 // 2) init textR 436 boolean textIsEmpty = (getText() == null) || getText().equals(""); 437 int lsb = 0; 438 /* Unless both text and icon are non-null, we effectively ignore 439 * the value of textIconGap. 440 */ 441 int gap; 442 if (textIsEmpty) { 443 textR.width = textR.height = 0; 444 gap = 0; 445 } 446 else { 447 int availTextWidth; 448 gap = (iconR.width == 0) ? 0 : getIconTextGap(); 449 450 occupiedWidth = dx + iconR.width + gap; 451 Object parent = getParent(); 452 if (parent != null && (parent instanceof JPanel)) { 453 JPanel panel = ((JPanel) parent); 454 Border b = panel.getBorder(); 455 if (b != null) { 456 Insets in = b.getBorderInsets(panel); 457 occupiedWidth += in.left + in.right; 458 } 459 } 460 if (getHorizontalTextPosition() == CENTER) { 461 availTextWidth = viewR.width; 462 } 463 else { 464 availTextWidth = viewR.width - (iconR.width + gap); 465 } 466 float xPrefSpan = view.getPreferredSpan(View.X_AXIS); 467 //log.fine("atw:" + availTextWidth + ", vps:" + xPrefSpan); 468 textR.width = Math.min(availTextWidth, (int) xPrefSpan); 469 if (maxLineSpan > 0) { 470 textR.width = Math.min(textR.width, maxLineSpan); 471 if (xPrefSpan > maxLineSpan) { 472 view.setSize(maxLineSpan, textR.height); 473 } 474 } 475 textR.height = (int) view.getPreferredSpan(View.Y_AXIS); 476 if (textR.height == 0) { 477 textR.height = getFont().getSize(); 478 } 479 //log.fine("atw:" + availTextWidth + ", vps:" + xPrefSpan + ", h:" + textR.height); 480 481 } 482 // 3) set text xy based on h/v text pos 483 if (getVerticalTextPosition() == TOP) { 484 if (getHorizontalTextPosition() != CENTER) { 485 textR.y = 0; 486 } 487 else { 488 textR.y = -(textR.height + gap); 489 } 490 } 491 else if (getVerticalTextPosition() == CENTER) { 492 textR.y = (iconR.height / 2) - (textR.height / 2); 493 } 494 else { // (verticalTextPosition == BOTTOM) 495 if (getVerticalTextPosition() != CENTER) { 496 textR.y = iconR.height - textR.height; 497 } 498 else { 499 textR.y = (iconR.height + gap); 500 } 501 } 502 503 if (getHorizontalTextPosition() == LEFT) { 504 textR.x = -(textR.width + gap); 505 } 506 else if (getHorizontalTextPosition() == CENTER) { 507 textR.x = (iconR.width / 2) - (textR.width / 2); 508 } 509 else { // (horizontalTextPosition == RIGHT) 510 textR.x = (iconR.width + gap); 511 } 512 513 // 4) shift label around based on its alignment 514 int labelR_x = Math.min(iconR.x, textR.x); 515 int labelR_width = Math.max(iconR.x + iconR.width, 516 textR.x + textR.width) - labelR_x; 517 int labelR_y = Math.min(iconR.y, textR.y); 518 int labelR_height = Math.max(iconR.y + iconR.height, 519 textR.y + textR.height) - labelR_y; 520 521 int dax, day; 522 523 if (getVerticalAlignment() == TOP) { 524 day = viewR.y - labelR_y; 525 } 526 else if (getVerticalAlignment() == CENTER) { 527 day = (viewR.y + (viewR.height / 2)) - (labelR_y + (labelR_height / 2)); 528 } 529 else { // (verticalAlignment == BOTTOM) 530 day = (viewR.y + viewR.height) - (labelR_y + labelR_height); 531 } 532 533 if (getHorizontalAlignment() == LEFT) { 534 dax = viewR.x - labelR_x; 535 } 536 else if (getHorizontalAlignment() == RIGHT) { 537 dax = (viewR.x + viewR.width) - (labelR_x + labelR_width); 538 } 539 else { // (horizontalAlignment == CENTER) 540 dax = (viewR.x + (viewR.width / 2)) - 541 (labelR_x + (labelR_width / 2)); 542 } 543 544 textR.x += dax; 545 textR.y += day; 546 547 iconR.x += dax; 548 iconR.y += day; 549 550 if (lsb < 0) { 551 // lsb is negative. Shift the x location so that the text is 552 // visually drawn at the right location. 553 textR.x -= lsb; 554 } 555 // EO layout label 556 557 int x1 = Math.min(iconR.x, textR.x); 558 int x2 = Math.max(iconR.x + iconR.width, textR.x + textR.width); 559 int y1 = Math.min(iconR.y, textR.y); 560 int y2 = Math.max(iconR.y + iconR.height, textR.y + textR.height); 561 Dimension rv = new Dimension(x2 - x1, y2 - y1); 562 563 rv.width += dx; 564 rv.height += dy; 565 //log.fine("returning: " + rv); 566 return rv; 567 } 568 //log.fine("ret 3"); 569 return size; 570 } 571 572 private View getWrappingView() { 573 if (super.getTopLevelAncestor() == null) { 574 return null; 575 } 576 View view = (View) getClientProperty(BasicHTML.propertyKey); 577 if (!(view instanceof Renderer)) { 578 return null; 579 } 580 return view; 581 } 582 583 private Container getViewport() { 584 for(Container p = this; p != null; p = p.getParent()) { 585 if(p instanceof Window || p instanceof Applet || p instanceof JViewport) { 586 return p; 587 } 588 } 589 return null; 590 } 591 592 private Rectangle calculateIconRect() { 593 Rectangle iconR = new Rectangle(); 594 Icon icon = isEnabled() ? getIcon() : getDisabledIcon(); 595 iconR.x = iconR.y = iconR.width = iconR.height = 0; 596 if (icon != null) { 597 iconR.width = icon.getIconWidth(); 598 iconR.height = icon.getIconHeight(); 599 } 600 else { 601 iconR.width = iconR.height = 0; 602 } 603 return iconR; 604 } 605 606 public int getMaxLineSpan() { 607 return maxLineSpan ; 608 } 609 610 public void setMaxLineSpan(int maxLineSpan) { 611 int old = getMaxLineSpan(); 612 this.maxLineSpan = maxLineSpan; 613 firePropertyChange("maxLineSpan", old, getMaxLineSpan()); 614 } 615 616 private static int rotateWidth(Dimension size, double theta) { 617 return (int)Math.round(size.width*Math.abs(Math.cos(theta)) + 618 size.height*Math.abs(Math.sin(theta))); 619 } 620 621 private static int rotateHeight(Dimension size, double theta) { 622 return (int)Math.round(size.width*Math.abs(Math.sin(theta)) + 623 size.height*Math.abs(Math.cos(theta))); 624 } 625 626 /** 627 * Sets new value for text rotation. The value can be anything in range <0,2PI>. Note that although property name 628 * suggests only text rotation, the whole foreground painter is rotated in fact. Due to various reasons it is 629 * strongly discouraged to access any size related properties of the label from other threads then EDT when this 630 * property is set. 631 * 632 * @param textOrientation Value for text rotation in range <0,2PI> 633 * @see #getTextRotation() 634 */ 635 public void setTextRotation(double textOrientation) { 636 double old = getTextRotation(); 637 this.textRotation = textOrientation; 638 if (old != getTextRotation()) { 639 firePropertyChange("textRotation", old, getTextRotation()); 640 } 641 repaint(); 642 } 643 644 /** 645 * Enables line wrapping support for plain text. By default this support is disabled to mimic default of the JLabel. 646 * Value of this property has no effect on HTML text. 647 * 648 * @param b the new value 649 */ 650 public void setLineWrap(boolean b) { 651 boolean old = isLineWrap(); 652 this.multiLine = b; 653 if (isLineWrap() != old) { 654 firePropertyChange("lineWrap", old, isLineWrap()); 655 if (getForegroundPainter() != null) { 656 // XXX There is a bug here. In order to make painter work with this, caching has to be disabled 657 ((AbstractPainter) getForegroundPainter()).setCacheable(!b); 658 } 659 //repaint(); 660 } 661 } 662 663 /** 664 * Returns the current status of line wrap support. The default value of this property is false to mimic default 665 * JLabel behavior. Value of this property has no effect on HTML text. 666 * 667 * @return the current multiple line splitting status 668 */ 669 public boolean isLineWrap() { 670 return this.multiLine; 671 } 672 673 private boolean paintBorderInsets = true; 674 675 private int maxLineSpan = -1; 676 677 public boolean painted; 678 679 private TextAlignment textAlignment = TextAlignment.LEFT; 680 681 /** 682 * Gets current text wrapping style. 683 * 684 * @return the text alignment for this label 685 */ 686 public TextAlignment getTextAlignment() { 687 return textAlignment; 688 } 689 690 /** 691 * Sets style of wrapping the text. 692 * @see TextAlignment for accepted values. 693 * @param alignment 694 */ 695 public void setTextAlignment(TextAlignment alignment) { 696 TextAlignment old = getTextAlignment(); 697 this.textAlignment = alignment; 698 firePropertyChange("textAlignment", old, getTextAlignment()); 699 } 700 701 /** 702 * Returns true if the background painter should paint where the border is 703 * or false if it should only paint inside the border. This property is 704 * true by default. This property affects the width, height, 705 * and initial transform passed to the background painter. 706 * @return current value of the paintBorderInsets property 707 */ 708 @Override 709 public boolean isPaintBorderInsets() { 710 return paintBorderInsets; 711 } 712 713 @Override 714 public boolean isOpaque() { 715 return painting ? false : super.isOpaque(); 716 } 717 718 /** 719 * Sets the paintBorderInsets property. 720 * Set to true if the background painter should paint where the border is 721 * or false if it should only paint inside the border. This property is true by default. 722 * This property affects the width, height, 723 * and initial transform passed to the background painter. 724 * 725 * This is a bound property. 726 * @param paintBorderInsets new value of the paintBorderInsets property 727 */ 728 @Override 729 public void setPaintBorderInsets(boolean paintBorderInsets) { 730 boolean old = this.isPaintBorderInsets(); 731 this.paintBorderInsets = paintBorderInsets; 732 firePropertyChange("paintBorderInsets", old, isPaintBorderInsets()); 733 } 734 735 /** 736 * @param g graphics to paint on 737 */ 738 @Override 739 protected void paintComponent(Graphics g) { 740 //log.fine("in"); 741 // resizing the text view causes recursive callback to the paint down the road. In order to prevent such 742 // computationally intensive series of repaints every call to paint is skipped while top most call is being 743 // executed. 744// if (!dontIgnoreRepaint) { 745// return; 746// } 747 painted = true; 748 if (painting || backgroundPainter == null && foregroundPainter == null) { 749 super.paintComponent(g); 750 } else { 751 pWidth = getWidth(); 752 pHeight = getHeight(); 753 if (backgroundPainter != null) { 754 Graphics2D tmp = (Graphics2D) g.create(); 755 756 try { 757 SwingXUtilities.paintBackground(this, tmp); 758 } finally { 759 tmp.dispose(); 760 } 761 } 762 if (foregroundPainter != null) { 763 Insets i = getInsets(); 764 pWidth = getWidth() - i.left - i.right; 765 pHeight = getHeight() - i.top - i.bottom; 766 767 Point2D tPoint = calculateT(); 768 double wx = Math.sin(textRotation) * tPoint.getY() + Math.cos(textRotation) * tPoint.getX(); 769 double wy = Math.sin(textRotation) * tPoint.getX() + Math.cos(textRotation) * tPoint.getY(); 770 double x = (getWidth() - wx) / 2 + Math.sin(textRotation) * tPoint.getY(); 771 double y = (getHeight() - wy) / 2; 772 Graphics2D tmp = (Graphics2D) g.create(); 773 if (i != null) { 774 tmp.translate(i.left + x, i.top + y); 775 } else { 776 tmp.translate(x, y); 777 } 778 tmp.rotate(textRotation); 779 780 painting = true; 781 // uncomment to highlight text area 782 // Color c = g2.getColor(); 783 // g2.setColor(Color.RED); 784 // g2.fillRect(0, 0, getWidth(), getHeight()); 785 // g2.setColor(c); 786 //log.fine("PW:" + pWidth + ", PH:" + pHeight); 787 foregroundPainter.paint(tmp, this, pWidth, pHeight); 788 tmp.dispose(); 789 painting = false; 790 pWidth = 0; 791 pHeight = 0; 792 } 793 } 794 } 795 796 private Point2D calculateT() { 797 double tx = (double) getWidth(); 798 double ty = (double) getHeight(); 799 800 // orthogonal cases are most likely the most often used ones, so give them preferential treatment. 801 if ((textRotation > 4.697 && textRotation < 4.727) || (textRotation > 1.555 && textRotation < 1.585)) { 802 // vertical 803 int tmp = pHeight; 804 pHeight = pWidth; 805 pWidth = tmp; 806 tx = pWidth; 807 ty = pHeight; 808 } else if ((textRotation > -0.015 && textRotation < 0.015) 809 || (textRotation > 3.140 && textRotation < 3.1430)) { 810 // normal & inverted 811 pHeight = getHeight(); 812 pWidth = getWidth(); 813 } else { 814 // the rest of it. Calculate best rectangle that fits the bounds. "Best" is considered one that 815 // allows whole text to fit in, spanned on preferred axis (X). If that doesn't work, fit the text 816 // inside square with diagonal equal min(height, width) (Should be the largest rectangular area that 817 // fits in, math proof available upon request) 818 819 dontIgnoreRepaint = false; 820 double square = Math.min(getHeight(), getWidth()) * Math.cos(Math.PI / 4d); 821 822 View v = (View) getClientProperty(BasicHTML.propertyKey); 823 if (v == null) { 824 // no html and no wrapline enabled means no view 825 // ... find another way to figure out the heigh 826 ty = getFontMetrics(getFont()).getHeight(); 827 double cw = (getWidth() - Math.abs(ty * Math.sin(textRotation))) 828 / Math.abs(Math.cos(textRotation)); 829 double ch = (getHeight() - Math.abs(ty * Math.cos(textRotation))) 830 / Math.abs(Math.sin(textRotation)); 831 // min of whichever is above 0 (!!! no min of abs values) 832 tx = cw < 0 ? ch : ch > 0 ? Math.min(cw, ch) : cw; 833 } else { 834 float w = v.getPreferredSpan(View.X_AXIS); 835 float h = v.getPreferredSpan(View.Y_AXIS); 836 double c = w; 837 double alpha = textRotation;// % (Math.PI/2d); 838 boolean ready = false; 839 while (!ready) { 840 // shorten the view len until line break is forced 841 while (h == v.getPreferredSpan(View.Y_AXIS)) { 842 w -= 10; 843 v.setSize(w, h); 844 } 845 if (w < square || h > square) { 846 // text is too long to fit no matter what. Revert shape to square since that is the 847 // best option (1st derivation for area size of rotated rect in rect is equal 0 for 848 // rotated rect with equal w and h i.e. for square) 849 w = h = (float) square; 850 // set view height to something big to prevent recursive resize/repaint requests 851 v.setSize(w, 100000); 852 break; 853 } 854 // calc avail width with new view height 855 h = v.getPreferredSpan(View.Y_AXIS); 856 double cw = (getWidth() - Math.abs(h * Math.sin(alpha))) / Math.abs(Math.cos(alpha)); 857 double ch = (getHeight() - Math.abs(h * Math.cos(alpha))) / Math.abs(Math.sin(alpha)); 858 // min of whichever is above 0 (!!! no min of abs values) 859 c = cw < 0 ? ch : ch > 0 ? Math.min(cw, ch) : cw; 860 // make it one pix smaller to ensure text is not cut on the left 861 c--; 862 if (c > w) { 863 v.setSize((float) c, 10 * h); 864 ready = true; 865 } else { 866 v.setSize((float) c, 10 * h); 867 if (v.getPreferredSpan(View.Y_AXIS) > h) { 868 // set size back to figure out new line break and height after 869 v.setSize(w, 10 * h); 870 } else { 871 w = (float) c; 872 ready = true; 873 } 874 } 875 } 876 877 tx = Math.floor(w);// xxx: watch out for first letter on each line missing some pixs!!! 878 ty = h; 879 } 880 pWidth = (int) tx; 881 pHeight = (int) ty; 882 dontIgnoreRepaint = true; 883 } 884 return new Point2D.Double(tx,ty); 885 } 886 887 @Override 888 public void repaint() { 889 if (!dontIgnoreRepaint) { 890 return; 891 } 892 super.repaint(); 893 } 894 895 @Override 896 public void repaint(int x, int y, int width, int height) { 897 if (!dontIgnoreRepaint) { 898 return; 899 } 900 super.repaint(x, y, width, height); 901 } 902 903 @Override 904 public void repaint(long tm) { 905 if (!dontIgnoreRepaint) { 906 return; 907 } 908 super.repaint(tm); 909 } 910 911 @Override 912 public void repaint(long tm, int x, int y, int width, int height) { 913 if (!dontIgnoreRepaint) { 914 return; 915 } 916 super.repaint(tm, x, y, width, height); 917 } 918 919 // ---------------------------------------------------------- 920 // textOrientation magic 921 @Override 922 public int getHeight() { 923 int retValue = super.getHeight(); 924 if (painting) { 925 retValue = pHeight; 926 } 927 return retValue; 928 } 929 930 @Override 931 public int getWidth() { 932 int retValue = super.getWidth(); 933 if (painting) { 934 retValue = pWidth; 935 } 936 return retValue; 937 } 938 939 protected MultiLineSupport getMultiLineSupport() { 940 return new MultiLineSupport(); 941 } 942 // ---------------------------------------------------------- 943 // WARNING: 944 // Anything below this line is related to lineWrap support and can be safely ignored unless 945 // in need to mess around with the implementation details. 946 // ---------------------------------------------------------- 947 // FYI: This class doesn't reinvent line wrapping. Instead it makes use of existing support 948 // made for JTextComponent/JEditorPane. 949 // All the classes below named Alter* are verbatim copy of swing.text.* classes made to 950 // overcome package visibility of some of the code. All other classes here, when their name 951 // matches corresponding class from swing.text.* package are copy of the class with removed 952 // support for highlighting selection. In case this is ever merged back to JDK all of this 953 // can be safely removed as long as corresponding swing.text.* classes make appropriate checks 954 // before casting JComponent into JTextComponent to find out selected region since 955 // JLabel/JXLabel does not support selection of the text. 956 957 public static class MultiLineSupport implements PropertyChangeListener { 958 959 private static final String HTML = "<html>"; 960 961 private static ViewFactory basicViewFactory; 962 963 private static BasicEditorKit basicFactory; 964 965 @Override 966 public void propertyChange(PropertyChangeEvent evt) { 967 String name = evt.getPropertyName(); 968 JXLabel src = (JXLabel) evt.getSource(); 969 if ("ancestor".equals(name)) { 970 src.dontIgnoreRepaint = true; 971 } 972 if (src.isLineWrap()) { 973 if ("font".equals(name) || "foreground".equals(name) || "maxLineSpan".equals(name) || "textAlignment".equals(name) || "icon".equals(name) || "iconTextGap".equals(name)) { 974 if (evt.getOldValue() != null && !isHTML(src.getText())) { 975 updateRenderer(src); 976 } 977 } else if ("text".equals(name)) { 978 if (isHTML((String) evt.getOldValue()) && evt.getNewValue() != null 979 && !isHTML((String) evt.getNewValue())) { 980 // was html , but is not 981 if (src.getClientProperty(oldRendererKey) == null 982 && src.getClientProperty(BasicHTML.propertyKey) != null) { 983 src.putClientProperty(oldRendererKey, src.getClientProperty(BasicHTML.propertyKey)); 984 } 985 src.putClientProperty(BasicHTML.propertyKey, createView(src)); 986 } else if (!isHTML((String) evt.getOldValue()) && evt.getNewValue() != null 987 && !isHTML((String) evt.getNewValue())) { 988 // wasn't html and isn't 989 updateRenderer(src); 990 } else { 991 // either was html and is html or wasn't html, but is html 992 restoreHtmlRenderer(src); 993 } 994 } else if ("lineWrap".equals(name) && !isHTML(src.getText())) { 995 src.putClientProperty(BasicHTML.propertyKey, createView(src)); 996 } 997 } else if ("lineWrap".equals(name) && !((Boolean)evt.getNewValue())) { 998 restoreHtmlRenderer(src); 999 } 1000 } 1001 1002 private static void restoreHtmlRenderer(JXLabel src) { 1003 Object current = src.getClientProperty(BasicHTML.propertyKey); 1004 if (current == null || current instanceof Renderer) { 1005 src.putClientProperty(BasicHTML.propertyKey, src.getClientProperty(oldRendererKey)); 1006 } 1007 } 1008 1009 private static boolean isHTML(String s) { 1010 return s != null && s.toLowerCase().startsWith(HTML); 1011 } 1012 1013 public static View createView(JXLabel c) { 1014 BasicEditorKit kit = getFactory(); 1015 float rightIndent = 0; 1016 if (c.getIcon() != null && c.getHorizontalTextPosition() != SwingConstants.CENTER) { 1017 rightIndent = c.getIcon().getIconWidth() + c.getIconTextGap(); 1018 } 1019 Document doc = kit.createDefaultDocument(c.getFont(), c.getForeground(), c.getTextAlignment(), rightIndent); 1020 Reader r = new StringReader(c.getText() == null ? "" : c.getText()); 1021 try { 1022 kit.read(r, doc, 0); 1023 } catch (Throwable e) { 1024 } 1025 ViewFactory f = kit.getViewFactory(); 1026 View hview = f.create(doc.getDefaultRootElement()); 1027 View v = new Renderer(c, f, hview, true); 1028 return v; 1029 } 1030 1031 public static void updateRenderer(JXLabel c) { 1032 View value = null; 1033 View oldValue = (View) c.getClientProperty(BasicHTML.propertyKey); 1034 if (oldValue == null || oldValue instanceof Renderer) { 1035 value = createView(c); 1036 } 1037 if (value != oldValue && oldValue != null) { 1038 for (int i = 0; i < oldValue.getViewCount(); i++) { 1039 oldValue.getView(i).setParent(null); 1040 } 1041 } 1042 c.putClientProperty(BasicHTML.propertyKey, value); 1043 } 1044 1045 private static BasicEditorKit getFactory() { 1046 if (basicFactory == null) { 1047 basicViewFactory = new BasicViewFactory(); 1048 basicFactory = new BasicEditorKit(); 1049 } 1050 return basicFactory; 1051 } 1052 1053 private static class BasicEditorKit extends StyledEditorKit { 1054 public Document createDefaultDocument(Font defaultFont, Color foreground, TextAlignment textAlignment, float rightIndent) { 1055 BasicDocument doc = new BasicDocument(defaultFont, foreground, textAlignment, rightIndent); 1056 doc.setAsynchronousLoadPriority(Integer.MAX_VALUE); 1057 return doc; 1058 } 1059 1060 @Override 1061 public ViewFactory getViewFactory() { 1062 return basicViewFactory; 1063 } 1064 } 1065 } 1066 1067 private static class BasicViewFactory implements ViewFactory { 1068 @Override 1069 public View create(Element elem) { 1070 1071 String kind = elem.getName(); 1072 View view = null; 1073 if (kind == null) { 1074 // default to text display 1075 view = new LabelView(elem); 1076 } else if (kind.equals(AbstractDocument.ContentElementName)) { 1077 view = new LabelView(elem); 1078 } else if (kind.equals(AbstractDocument.ParagraphElementName)) { 1079 view = new ParagraphView(elem); 1080 } else if (kind.equals(AbstractDocument.SectionElementName)) { 1081 view = new BoxView(elem, View.Y_AXIS); 1082 } else if (kind.equals(StyleConstants.ComponentElementName)) { 1083 view = new ComponentView(elem); 1084 } else if (kind.equals(StyleConstants.IconElementName)) { 1085 view = new IconView(elem); 1086 } 1087 return view; 1088 } 1089 } 1090 1091 static class BasicDocument extends DefaultStyledDocument { 1092 BasicDocument(Font defaultFont, Color foreground, TextAlignment textAlignment, float rightIndent) { 1093 setFontAndColor(defaultFont, foreground); 1094 1095 MutableAttributeSet attr = new SimpleAttributeSet(); 1096 StyleConstants.setAlignment(attr, textAlignment.getValue()); 1097 getStyle("default").addAttributes(attr); 1098 1099 attr = new SimpleAttributeSet(); 1100 StyleConstants.setRightIndent(attr, rightIndent); 1101 getStyle("default").addAttributes(attr); 1102 } 1103 1104 private void setFontAndColor(Font font, Color fg) { 1105 if (fg != null) { 1106 1107 MutableAttributeSet attr = new SimpleAttributeSet(); 1108 StyleConstants.setForeground(attr, fg); 1109 getStyle("default").addAttributes(attr); 1110 } 1111 1112 if (font != null) { 1113 MutableAttributeSet attr = new SimpleAttributeSet(); 1114 StyleConstants.setFontFamily(attr, font.getFamily()); 1115 getStyle("default").addAttributes(attr); 1116 1117 attr = new SimpleAttributeSet(); 1118 StyleConstants.setFontSize(attr, font.getSize()); 1119 getStyle("default").addAttributes(attr); 1120 1121 attr = new SimpleAttributeSet(); 1122 StyleConstants.setBold(attr, font.isBold()); 1123 getStyle("default").addAttributes(attr); 1124 1125 attr = new SimpleAttributeSet(); 1126 StyleConstants.setItalic(attr, font.isItalic()); 1127 getStyle("default").addAttributes(attr); 1128 1129 attr = new SimpleAttributeSet(); 1130 Object underline = font.getAttributes().get(TextAttribute.UNDERLINE); 1131 boolean canUnderline = underline instanceof Integer && (Integer) underline != -1; 1132 StyleConstants.setUnderline(attr, canUnderline); 1133 getStyle("default").addAttributes(attr); 1134 } 1135 1136 MutableAttributeSet attr = new SimpleAttributeSet(); 1137 StyleConstants.setSpaceAbove(attr, 0f); 1138 getStyle("default").addAttributes(attr); 1139 1140 } 1141 } 1142 1143 /** 1144 * Root text view that acts as an renderer. 1145 */ 1146 static class Renderer extends WrappedPlainView { 1147 1148 JXLabel host; 1149 1150 boolean invalidated = false; 1151 1152 private float width; 1153 1154 private float height; 1155 1156 Renderer(JXLabel c, ViewFactory f, View v, boolean wordWrap) { 1157 super(null, wordWrap); 1158 factory = f; 1159 view = v; 1160 view.setParent(this); 1161 host = c; 1162 //log.fine("vir: " + host.getVisibleRect()); 1163 int w; 1164 if (host.getVisibleRect().width == 0) { 1165 invalidated = true; 1166 return; 1167 } else { 1168 w = host.getVisibleRect().width; 1169 } 1170 //log.fine("w:" + w); 1171 // initially layout to the preferred size 1172 //setSize(c.getMaxLineSpan() > -1 ? c.getMaxLineSpan() : view.getPreferredSpan(X_AXIS), view.getPreferredSpan(Y_AXIS)); 1173 setSize(c.getMaxLineSpan() > -1 ? c.getMaxLineSpan() : w, host.getVisibleRect().height); 1174 } 1175 1176 @Override 1177 protected void updateLayout(ElementChange ec, DocumentEvent e, Shape a) { 1178 if ( (a != null)) { 1179 // should damage more intelligently 1180 preferenceChanged(null, true, true); 1181 Container host = getContainer(); 1182 if (host != null) { 1183 host.repaint(); 1184 } 1185 } 1186 } 1187 1188 @Override 1189 public void preferenceChanged(View child, boolean width, boolean height) { 1190 if (host != null && host.painted) { 1191 host.revalidate(); 1192 host.repaint(); 1193 } 1194 } 1195 1196 1197 /** 1198 * Fetches the attributes to use when rendering. At the root level there are no attributes. If an attribute is 1199 * resolved up the view hierarchy this is the end of the line. 1200 */ 1201 @Override 1202 public AttributeSet getAttributes() { 1203 return null; 1204 } 1205 1206 /** 1207 * Renders the view. 1208 * 1209 * @param g the graphics context 1210 * @param allocation the region to render into 1211 */ 1212 @Override 1213 public void paint(Graphics g, Shape allocation) { 1214 Rectangle alloc = allocation.getBounds(); 1215 //log.fine("aloc:" + alloc + "::" + host.getVisibleRect() + "::" + host.getBounds()); 1216 //view.setSize(alloc.width, alloc.height); 1217 //this.width = alloc.width; 1218 //this.height = alloc.height; 1219 if (g.getClipBounds() == null) { 1220 g.setClip(alloc); 1221 view.paint(g, allocation); 1222 g.setClip(null); 1223 } else { 1224 //g.translate(alloc.x, alloc.y); 1225 view.paint(g, allocation); 1226 //g.translate(-alloc.x, -alloc.y); 1227 } 1228 } 1229 1230 /** 1231 * Sets the view parent. 1232 * 1233 * @param parent the parent view 1234 */ 1235 @Override 1236 public void setParent(View parent) { 1237 throw new Error("Can't set parent on root view"); 1238 } 1239 1240 /** 1241 * Returns the number of views in this view. Since this view simply wraps the root of the view hierarchy it has 1242 * exactly one child. 1243 * 1244 * @return the number of views 1245 * @see #getView 1246 */ 1247 @Override 1248 public int getViewCount() { 1249 return 1; 1250 } 1251 1252 /** 1253 * Gets the n-th view in this container. 1254 * 1255 * @param n the number of the view to get 1256 * @return the view 1257 */ 1258 @Override 1259 public View getView(int n) { 1260 return view; 1261 } 1262 1263 /** 1264 * Returns the document model underlying the view. 1265 * 1266 * @return the model 1267 */ 1268 @Override 1269 public Document getDocument() { 1270 return view == null ? null : view.getDocument(); 1271 } 1272 1273 /** 1274 * Sets the view size. 1275 * 1276 * @param width the width 1277 * @param height the height 1278 */ 1279 @Override 1280 public void setSize(float width, float height) { 1281 if (host.maxLineSpan > 0) { 1282 width = Math.min(width, host.maxLineSpan); 1283 } 1284 if (width == this.width && height == this.height) { 1285 return; 1286 } 1287 this.width = (int) width; 1288 this.height = (int) height; 1289 view.setSize(width, height == 0 ? Short.MAX_VALUE : height); 1290 if (this.height == 0) { 1291 this.height = view.getPreferredSpan(View.Y_AXIS); 1292 } 1293 } 1294 1295 @Override 1296 public float getPreferredSpan(int axis) { 1297 if (axis == X_AXIS) { 1298 //log.fine("inv: " + invalidated + ", w:" + width + ", vw:" + host.getVisibleRect()); 1299 // width currently laid out to 1300 if (invalidated) { 1301 int w = host.getVisibleRect().width; 1302 if (w != 0) { 1303 //log.fine("vrh: " + host.getVisibleRect().height); 1304 invalidated = false; 1305 // JXLabelTest4 works 1306 setSize(w - (host.getOccupiedWidth()), host.getVisibleRect().height); 1307 // JXLabelTest3 works; 20 == width of the parent border!!! ... why should this screw with us? 1308 //setSize(w - (host.getOccupiedWidth()+20), host.getVisibleRect().height); 1309 } 1310 } 1311 return width > 0 ? width : view.getPreferredSpan(axis); 1312 } else { 1313 return view.getPreferredSpan(axis); 1314 } 1315 } 1316 1317 /** 1318 * Fetches the container hosting the view. This is useful for things like scheduling a repaint, finding out the 1319 * host components font, etc. The default implementation of this is to forward the query to the parent view. 1320 * 1321 * @return the container 1322 */ 1323 @Override 1324 public Container getContainer() { 1325 return host; 1326 } 1327 1328 /** 1329 * Fetches the factory to be used for building the various view fragments that make up the view that represents 1330 * the model. This is what determines how the model will be represented. This is implemented to fetch the 1331 * factory provided by the associated EditorKit. 1332 * 1333 * @return the factory 1334 */ 1335 @Override 1336 public ViewFactory getViewFactory() { 1337 return factory; 1338 } 1339 1340 private View view; 1341 1342 private ViewFactory factory; 1343 1344 @Override 1345 public int getWidth() { 1346 return (int) width; 1347 } 1348 1349 @Override 1350 public int getHeight() { 1351 return (int) height; 1352 } 1353 1354 } 1355 1356 protected int getOccupiedWidth() { 1357 return occupiedWidth; 1358 } 1359}