javaswing

"Stretchable" JLabel with dynamic text


I don't like scrollbars for their clumsiness. I wrote a custom JLabel extension that dynamically trims the label's text on each paint() call and appends with an ellipsis

package StretchableLabel;

import lombok.SneakyThrows;

import javax.swing.*;
import java.awt.*;

public class StretchableLabel extends JLabel {
    private String originalText;
    public StretchableLabel(String text) {
        super(text);
        this.originalText = text;
    }

    @Override
    public void setText(String text) {
        this.originalText = text;
        String displayedText = trimTextIfNecessary(text);
        super.setText(displayedText);
    }

    private String trimTextIfNecessary(String text) {
        if (getFont() == null) return text;
        FontMetrics fontMetrics = getFontMetrics(getFont());
        int textWidth = fontMetrics.stringWidth(text);
        int parentWidth = getParent().getWidth();
        if (textWidth > parentWidth) {
            return trimText(text, parentWidth, fontMetrics);
        } else {
            return text;
        }
    }

    private String trimText(String text, int parentWidth, FontMetrics fontMetrics) {
        int textLimit = parentWidth / fontMetrics.charWidth('m');
        return text.substring(0, textLimit) + "...";
    }

    @SneakyThrows
    @Override
    protected void paintComponent(Graphics g) {
        String displayedText = trimTextIfNecessary(originalText);
        super.setText(displayedText);
        super.paintComponent(g);
    }
}
package StretchableLabel;

import org.apache.commons.lang3.StringUtils;

import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        JFrame frame = new JFrame("StretchableLabel Demo");
        JPanel panel = new JPanel();
        StretchableLabel label = new StretchableLabel((StringUtils.repeat("long label ", 20)));
        panel.add(label);
        frame.add(panel);
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

enter image description here

enter image description here

Drawbacks:

  1. I believe getText() should return the same value that was set with setText() (an original string). However, getText() returns an actual, displayed text while setText() sets a trimmed text. I can't override getText() since it's invoked by paintComponent() -- unless I check the caller which is tricky. It's important to intercept any setText() calls, as I do, so that direct text setting is not possible by the class's clients
  2. Checking the width of m is too conservative. It would be more precise to check the right substring length by applying some binary search algorithm on the string (though it would be worse from the performance standpoint)
  3. I'm not sure comparing the length of the text with the parent's width is a good idea as the parent can contain other children

How do I properly implement my idea?


Solution

  • You are implementing a feature that does already exist. Apparently, there is a bug in the Basic/Metal Look&Feel that makes it calculating with wrong sizes when no font has been set.

    There are two simple solutions to this

    1. Set a font explicitly

      public static void main(String[] args) {
          JFrame frame = new JFrame("StretchableLabel Demo");
          JLabel label = new JLabel("long label ".repeat(20));
          label.setFont(new Font(Font.DIALOG, 0, 16));
          frame.add(label, BorderLayout.CENTER);
          frame.pack();
          frame.setSize(frame.getWidth() >> 1, frame.getHeight());
          frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
          frame.setVisible(true);
      }
      
    2. Use the system (i.e. Windows) Look&Feel

      public static void main(String[] args) {
          try {
              UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
          }
          catch(ReflectiveOperationException | UnsupportedLookAndFeelException e) {
              e.printStackTrace();
          }
          JFrame frame = new JFrame("StretchableLabel Demo");
          JLabel label = new JLabel("long label ".repeat(20));
          frame.add(label, BorderLayout.CENTER);
          frame.pack();
          frame.setSize(frame.getWidth() >> 1, frame.getHeight());
          frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
          frame.setVisible(true);
      }
      

    In both cases you get the intended feature, clipping the text and appending an ellipsis, for free. Keep in mind that you have to set up the layout manager such that they will shrink the JLabel when there’s not enough space.