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