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(&quot;Some Text&quot;);
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    }