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 }