MultiLine JLabel

To create a multi-line JLabel, Samuel Sjoberg provided a custom class that extended the BasicLabelUI class in order to paint text and wrap them to multiple lines rather than displaying the text as one line and truncated with a …

To use the class, just do

label.setUI(MultiLineLabelUI.labelUI);

Here is the full MultiLineLabelUI class.

public class MultiLineLabelUI extends BasicLabelUI implements ComponentListener {
    /** Shared instance of the UI delegate. */
    public static LabelUI labelUI = new MultiLineLabelUI();
    /**
     * Client property key used to store the calculated wrapped lines on the
     * JLabel.
     */
    public static final String PROPERTY_KEY = "WrappedText";
    // Static references to avoid heap allocations.
    protected static Rectangle paintIconR = new Rectangle();
    protected static Rectangle paintTextR = new Rectangle();
    protected static Rectangle paintViewR = new Rectangle();
    protected static Insets paintViewInsets = new Insets(0, 0, 0, 0);
    /** Font metrics of the JLabel being rendered. */
    protected FontMetrics metrics;
    /** Default size of the lines list. */
    protected static int defaultSize = 4;
    /**
     * Get the shared UI instance.
     *
     * @param c
     *            the component about to be installed
     * @return the shared UI delegate instance
     */
    public static ComponentUI createUI(JComponent c) {
        return labelUI;
    }
    /** {@inheritDoc} */
    @Override
    protected void uninstallDefaults(JLabel c) {
        super.uninstallDefaults(c);
        clearCache(c);
    }
    /** {@inheritDoc} */
    @Override
    protected void installListeners(JLabel c) {
        super.installListeners(c);
        c.addComponentListener(this);
    }
    /** {@inheritDoc} */
    @Override
    protected void uninstallListeners(JLabel c) {
        super.uninstallListeners(c);
        c.removeComponentListener(this);
    }
    /**
     * Clear the wrapped line cache.
     *
     * @param l
     *            the label containing a cached value
     */
    protected void clearCache(JLabel l) {
        l.putClientProperty(PROPERTY_KEY, null);
    }
    /** {@inheritDoc} */
    @Override
    public void propertyChange(PropertyChangeEvent e) {
        super.propertyChange(e);
        final String name = e.getPropertyName();
        if (name.equals("text") || "font".equals(name)) {
            clearCache((JLabel) e.getSource());
        }
    }
    /**
     * Calculate the paint rectangles for the icon and text for the passed
     * label.
     *
     * @param l
     *            a label
     * @param fm
     *            the font metrics to use, or null to get the font
     *            metrics from the label
     * @param width
     *            label width
     * @param height
     *            label height
     */
    protected void updateLayout(JLabel l, FontMetrics fm, int width, int height) {
        if (fm == null) {
            fm = l.getFontMetrics(l.getFont());
        }
        metrics = fm;
        String text = l.getText();
        Icon icon = l.getIcon();
        Insets insets = l.getInsets(paintViewInsets);
        paintViewR.x = insets.left;
        paintViewR.y = insets.top;
        paintViewR.width = width - (insets.left + insets.right);
        paintViewR.height = height - (insets.top + insets.bottom);
        paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0;
        paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0;
        layoutCL(l, fm, text, icon, paintViewR, paintIconR, paintTextR);
    }
    protected void prepareGraphics(Graphics g) {
    }
    /** {@inheritDoc} */
    @Override
    public void paint(Graphics g, JComponent c) {
        // parent's update method fills the background
        prepareGraphics(g);
        JLabel label = (JLabel) c;
        String text = label.getText();
        Icon icon = (label.isEnabled()) ? label.getIcon() : label
                .getDisabledIcon();
        if ((icon == null) && (text == null)) {
            return;
        }
        FontMetrics fm = g.getFontMetrics();
        updateLayout(label, fm, c.getWidth(), c.getHeight());
        if (icon != null) {
            icon.paintIcon(c, g, paintIconR.x, paintIconR.y);
        }
        if (text != null) {
            View v = (View) c.getClientProperty("html");
            if (v != null) {
                // HTML view disables multi-line painting.
                v.paint(g, paintTextR);
            } else {
                // Paint the multi line text
                paintTextLines(g, label, fm);
            }
        }
    }
    /**
     * Paint the wrapped text lines.
     *
     * @param g
     *            graphics component to paint on
     * @param label
     *            the label being painted
     * @param fm
     *            font metrics for current font
     */
    protected void paintTextLines(Graphics g, JLabel label, FontMetrics fm) {
        List lines = getTextLines(label);
        // Available component height to paint on.
        int height = getAvailableHeight(label);
        int textHeight = lines.size() * fm.getHeight();
        while (textHeight > height) {
            // Remove one line until no. of visible lines is found.
            textHeight -= fm.getHeight();
        }
        paintTextR.height = Math.min(textHeight, height);
        paintTextR.y = alignmentY(label, fm, paintTextR);
        int textX = paintTextR.x;
        int textY = paintTextR.y;
        for (Iterator it = lines.iterator(); it.hasNext()
                && paintTextR.contains(textX, textY + getAscent(fm)); textY += fm
                .getHeight()) {
            String text = it.next().trim();
            if (it.hasNext()
                    && !paintTextR.contains(textX, textY + fm.getHeight()
                            + getAscent(fm))) {
                // The last visible row, add a clip indication.
                text = clip(text, fm, paintTextR);
            }
            int x = alignmentX(label, fm, text, paintTextR);
            if (label.isEnabled()) {
                paintEnabledText(label, g, text, x, textY);
            } else {
                paintDisabledText(label, g, text, x, textY);
            }
        }
    }
    /**
     * Returns the available height to paint text on. This is the height of the
     * passed component with insets subtracted.
     *
     * @param l
     *            a component
     * @return the available height
     */
    protected int getAvailableHeight(JLabel l) {
        l.getInsets(paintViewInsets);
        return l.getHeight() - paintViewInsets.top - paintViewInsets.bottom;
    }
    /**
     * Add a clip indication to the string. It is important that the string
     * length does not exceed the length or the original string.
     *
     * @param text
     *            the to be painted
     * @param fm
     *            font metrics
     * @param bounds
     *            the text bounds
     * @return the clipped string
     */
    protected String clip(String text, FontMetrics fm, Rectangle bounds) {
        // Fast and lazy way to insert a clip indication is to simply replace
        // the last characters in the string with the clip indication.
        // A better way would be to use metrics and calculate how many (if any)
        // characters that need to be replaced.
        if (text.length() < 3) {
            return "...";
        }
        return text.substring(0, text.length() - 3) + "...";
    }
    /**
     * Establish the vertical text alignment. The default alignment is to center
     * the text in the label.
     *
     * @param label
     *            the label to paint
     * @param fm
     *            font metrics
     * @param bounds
     *            the text bounds rectangle
     * @return the vertical text alignment, defaults to CENTER.
     */
    protected int alignmentY(JLabel label, FontMetrics fm, Rectangle bounds) {
        final int height = getAvailableHeight(label);
        int textHeight = bounds.height;
        if (label instanceof MultiLineLabel) {
            int align = ((MultiLineLabel) label).getVerticalTextAlignment();
            switch (align) {
            case JLabel.TOP:
                return getAscent(fm) + paintViewInsets.top;
            case JLabel.BOTTOM:
                return getAscent(fm) + height - paintViewInsets.top
                        + paintViewInsets.bottom - textHeight;
            default:
            }
        }
        // Center alignment
        int textY = paintViewInsets.top + (height - textHeight) / 2
                + getAscent(fm);
        // set top by default
        textY = paintViewInsets.top;
        return Math.max(textY, getAscent(fm) + paintViewInsets.top);
    }
    private static int getAscent(FontMetrics fm) {
        return fm.getAscent() + fm.getLeading();
    }
    /**
     * Establish the horizontal text alignment. The default alignment is left
     * aligned text.
     *
     * @param label
     *            the label to paint
     * @param fm
     *            font metrics
     * @param s
     *            the string to paint
     * @param bounds
     *            the text bounds rectangle
     * @return the x-coordinate to use when painting for proper alignment
     */
    protected int alignmentX(JLabel label, FontMetrics fm, String s,
            Rectangle bounds) {
        if (label instanceof MultiLineLabel) {
            int align = ((MultiLineLabel) label).getHorizontalTextAlignment();
            switch (align) {
            case JLabel.RIGHT:
                return bounds.x + paintViewR.width - fm.stringWidth(s);
            case JLabel.CENTER:
                return bounds.x + paintViewR.width / 2 - fm.stringWidth(s) / 2;
            default:
                return bounds.x;
            }
        }
        return bounds.x;
    }
    /**
     * Check the given string to see if it should be rendered as HTML. Code
     * based on implementation found in
     * BasicHTML.isHTMLString(String) in future JDKs.
     *
     * @param s
     *            the string
     * @return true if string is HTML, otherwise false
     */
    private static boolean isHTMLString(String s) {
        if (s != null) {
            if ((s.length() >= 6) && (s.charAt(0) == '<')
                    && (s.charAt(5) == '>')) {
                String tag = s.substring(1, 5);
                return tag.equalsIgnoreCase("html");
            }
        }
        return false;
    }
    /** {@inheritDoc} */
    @Override
    public Dimension getPreferredSize(JComponent c) {
        Dimension d = super.getPreferredSize(c);
        JLabel label = (JLabel) c;
        if (isHTMLString(label.getText())) {
            return d; // HTML overrides everything and we don't need to process
        }
        // Width calculated by super is OK. The preferred width is the width of
        // the unwrapped content as long as it does not exceed the width of the
        // parent container.
        if (c.getParent() != null) {
            // Ensure that preferred width never exceeds the available width
            // (including its border insets) of the parent container.
            Insets insets = c.getParent().getInsets();
            Dimension size = c.getParent().getSize();
            if (size.width > 0) {
                // If width isn't set component shouldn't adjust.
                d.width = size.width - insets.left - insets.right;
            }
        }
        updateLayout(label, null, d.width, d.height);
        // The preferred height is either the preferred height of the text
        // lines, or the height of the icon.
        d.height = Math.max(d.height, getPreferredHeight(label));
        return d;
    }
    /**
     * The preferred height of the label is the height of the lines with added
     * top and bottom insets.
     *
     * @param label
     *            the label
     * @return the preferred height of the wrapped lines.
     */
    protected int getPreferredHeight(JLabel label) {
        int numOfLines = getTextLines(label).size();
        Insets insets = label.getInsets(paintViewInsets);
        return numOfLines * metrics.getHeight() + insets.top + insets.bottom;
    }
    /**
     * Get the lines of text contained in the text label. The prepared lines is
     * cached as a client property, accessible via {@link #PROPERTY_KEY}.
     *
     * @param l
     *            the label
     * @return the text lines of the label.
     */
    @SuppressWarnings("unchecked")
    protected List getTextLines(JLabel l) {
        List lines = (List) l.getClientProperty(PROPERTY_KEY);
        if (lines == null) {
            lines = prepareLines(l);
            l.putClientProperty(PROPERTY_KEY, lines);
        }
        return lines;
    }
    /** {@inheritDoc} */
    public void componentHidden(ComponentEvent e) {
        // Don't care
    }
    /** {@inheritDoc} */
    public void componentMoved(ComponentEvent e) {
        // Don't care
    }
    /** {@inheritDoc} */
    public void componentResized(ComponentEvent e) {
        clearCache((JLabel) e.getSource());
    }
    /** {@inheritDoc} */
    public void componentShown(ComponentEvent e) {
        // Don't care
    }
    /**
     * Prepare the text lines for rendering. The lines are wrapped to fit in the
     * current available space for text. Explicit line breaks are preserved.
     *
     * @param l
     *            the label to render
     * @return a list of text lines to render
     */
    protected List prepareLines(JLabel l) {
        List lines = new ArrayList(defaultSize);
        String text = l.getText();
        if (text == null) {
            return null; // Null guard
        }
        PlainDocument doc = new PlainDocument();
        try {
            doc.insertString(0, text, null);
        } catch (BadLocationException e) {
            return null;
        }
        Element root = doc.getDefaultRootElement();
        for (int i = 0, j = root.getElementCount(); i < j; i++) {
            wrap(lines, root.getElement(i));
        }
        return lines;
    }
    /**
     * If necessary, wrap the text into multiple lines.
     *
     * @param lines
     *            line array in which to store the wrapped lines
     * @param elem
     *            the document element containing the text content
     */
    protected void wrap(List lines, Element elem) {
        int p1 = elem.getEndOffset();
        Document doc = elem.getDocument();
        for (int p0 = elem.getStartOffset(); p0 < p1;) {
            int p = calculateBreakPosition(doc, p0, p1);
            try {
                lines.add(doc.getText(p0, p - p0));
            } catch (BadLocationException e) {
                throw new Error("Can't get line text. p0=" + p0 + " p=" + p);
            }
            p0 = (p == p0) ? p1 : p;
        }
    }
    /**
     * Calculate the position on which to break (wrap) the line.
     *
     * @param doc
     *            the document
     * @param p0
     *            start position
     * @param p1
     *            end position
     * @return the actual end position, will be p1 if content does
     *         not need to wrap, otherwise it will be less than p1.
     */
    protected int calculateBreakPosition(Document doc, int p0, int p1) {
        Segment segment = SegmentCache.getSegment();
        try {
            doc.getText(p0, p1 - p0, segment);
        } catch (BadLocationException e) {
            throw new Error("Can't get line text");
        }
        int width = paintTextR.width;
        int p = p0
                + Utilities.getBreakLocation(segment, metrics, 0, width, null,
                        p0);
        SegmentCache.releaseSegment(segment);
        return p;
    }
    /**
     * Static singleton {@link Segment} cache.
     *
     * @see javax.swing.text.SegmentCache
     *
     * @author Samuel Sjoberg
     */
    protected static final class SegmentCache {
        /** Reused segments. */
        private ArrayList segments = new ArrayList(2);
        /** Singleton instance. */
        private static SegmentCache cache = new SegmentCache();
        /** Private constructor. */
        private SegmentCache() {
        }
        /**
         * Returns a Segment. When done, the Segment
         * should be recycled by invoking {@link #releaseSegment(Segment)}.
         *
         * @return a Segment.
         */
        public static Segment getSegment() {
            int size = cache.segments.size();
            if (size > 0) {
                return cache.segments.remove(size - 1);
            }
            return new Segment();
        }
        /**
         * Releases a Segment. A segment should not be used after
         * it is released, and a segment should never be released more than
         * once.
         */
        public static void releaseSegment(Segment segment) {
            segment.array = null;
            segment.count = 0;
            cache.segments.add(segment);
        }
    }
}

How To Unbold A JLabel

This code unbolds a JLabel on the fly without having to create a new instance of a Font object not having the bold attribute.

Font f = jlabel.getFont();
jlabel.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));

Rotate JLabel Vertically

To rotate a JLabel‘s text vertically, you only need to use this class and pass it as a parameter to the JLabel‘s setUI() method. Kudos to the developer who created this class to make it easy for other developers to plug this class in and create a vertical JLabel in an instant.

To use this class, check the code below:

JLabel jl = new JLabel("TEST");
jl.setUI(new VerticalLabelUI(true));

This is the code for the VerticalLabelUI class.

public class VerticalLabelUI extends BasicLabelUI {
 
    static {
        labelUI = new VerticalLabelUI(false);
    }
 
    protected boolean clockwise;
 
    public VerticalLabelUI(boolean clockwise) {
        super();
        this.clockwise = clockwise;
    }
 
    @Override
    public Dimension getPreferredSize(JComponent c) {
    	Dimension dim = super.getPreferredSize(c);
    	return new Dimension( dim.height, dim.width );
    }
 
    private static Rectangle paintIconR = new Rectangle();
    private static Rectangle paintTextR = new Rectangle();
    private static Rectangle paintViewR = new Rectangle();
    private static Insets paintViewInsets = new Insets(0, 0, 0, 0);
 
    @Override
    public void paint(Graphics g, JComponent c) {
        JLabel label = (JLabel)c;
        String text = label.getText();
        Icon icon = (label.isEnabled()) ? label.getIcon() : label.getDisabledIcon();
 
        if ((icon == null) &amp;&amp; (text == null)) {
            return;
        }
 
        FontMetrics fm = g.getFontMetrics();
        paintViewInsets = c.getInsets(paintViewInsets);
 
        paintViewR.x = paintViewInsets.left;
        paintViewR.y = paintViewInsets.top;
 
    	// Use inverted height & width
        paintViewR.height = c.getWidth() - (paintViewInsets.left + paintViewInsets.right);
        paintViewR.width = c.getHeight() - (paintViewInsets.top + paintViewInsets.bottom);
 
        paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0;
        paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0;
 
        String clippedText = layoutCL(label, fm, text, icon, paintViewR, paintIconR, paintTextR);
 
    	Graphics2D g2 = (Graphics2D) g;
    	AffineTransform tr = g2.getTransform();
    	if (clockwise) {
            g2.rotate( Math.PI / 2 );
            g2.translate( 0, - c.getWidth() );
    	} else {
            g2.rotate( - Math.PI / 2 );
            g2.translate( - c.getHeight(), 0 );
    	}
 
    	if (icon != null) {
            icon.paintIcon(c, g, paintIconR.x, paintIconR.y);
        }
 
        if (text != null) {
            int textX = paintTextR.x;
            int textY = paintTextR.y + fm.getAscent();
 
            if (label.isEnabled()) {
                paintEnabledText(label, g, clippedText, textX, textY);
            } else {
                paintDisabledText(label, g, clippedText, textX, textY);
            }
        }
	g2.setTransform( tr );
    }
 
}
Related Posts Plugin for WordPress, Blogger...