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