javaswing

Multi-line tooltip, dynamically


Imagine a component has a dynamic tooltip.

It may be quite long. In that case, the user wants it to be split into multiple lines. The end goal is to never cut a tooltip with a screen edge. For example, on screens with low resolution long tooltips may exceed the total screen width.

tooltip's too long for screen resolution, hence cut

How can I achieve it?

Remember, the text is dynamic, I can't wrap it in <html></html> and scatter <br> tags here and there.

package demos.text.field;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import java.awt.Container;

public class TextFieldDemo {
    public static void main(String[] args) {
        Container mainPanel = createMainPanel();
        JFrame frame = new JFrame("Text field demo");
        frame.setContentPane(mainPanel);
        frame.setLocationRelativeTo(null);
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    private static JPanel createMainPanel() {
        JPanel panel = new JPanel();
        panel.add(createTextField());
        return panel;
    }

    private static JTextField createTextField() {
        JTextField textField = new JTextField("Text field with a tooltip");
        textField.setToolTipText("A " + "very ".repeat(10) + "long tooltip that must be split");
        return textField;
    }
}

Java 8.


Solution

  • How about adding a JTextArea with JTextArea#setLineWrap(true) to the JToolTip? This JTextArea will automatically wrap the text if it exceeds the width set by JTextArea.html#setColumns(...).

    import java.awt.*;
    import java.awt.event.MouseEvent;
    import java.util.Collections;
    import java.util.Objects;
    import java.util.Optional;
    import javax.swing.*;
    
    public class LineWrapToolTipTest {
      private Component makeUI() {
        String very = String.join(" ", Collections.nCopies(10, "very"));
        String txt = "A " + very + " long tooltip that must be line wrap";
        JButton b1 = new JButton("JToolTip(Default)");
        b1.setToolTipText(txt + ": 1");
        JButton b2 = makeButton("LineWrapToolTip short");
        b2.setToolTipText("short: 2");
        JButton b3 = makeButton("LineWrapToolTip long");
        b3.setToolTipText(txt + ": 3");
        JPanel p = new JPanel();
        p.add(b1);
        p.add(b2);
        p.add(b3);
        return p;
      }
    
      private static JButton makeButton(String title) {
        return new JButton(title) {
          private transient JToolTip tip;
    
          @Override public JToolTip createToolTip() {
            if (tip == null) {
              tip = new LineWrapToolTip();
              tip.setComponent(this);
            }
            return tip;
          }
    
          @Override public String getToolTipText(MouseEvent e) {
            String txt = super.getToolTipText(e);
            EventQueue.invokeLater(() ->
                Optional.ofNullable(SwingUtilities.getWindowAncestor(tip))
                    .filter(w -> w.getType() == Window.Type.POPUP)
                    .ifPresent(Window::pack));
            return txt;
          }
        };
      }
    
      public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
          JFrame frame = new JFrame();
          frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
          frame.getContentPane().add(new LineWrapToolTipTest().makeUI());
          frame.setSize(320, 240);
          frame.setLocationRelativeTo(null);
          frame.setVisible(true);
        });
      }
    }
    
    class LineWrapToolTip extends JToolTip {
      private final JTextArea textArea = new JTextArea(0, 20);
      private final JLabel label = new JLabel(" ");
    
      protected LineWrapToolTip() {
        super();
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        // textArea.setColumns(20);
        textArea.setOpaque(true);
        LookAndFeel.installColorsAndFont(
            textArea, "ToolTip.background", "ToolTip.foreground", "ToolTip.font");
        setLayout(new BorderLayout());
        add(textArea);
      }
    
      @Override public final void setLayout(LayoutManager mgr) {
        super.setLayout(mgr);
      }
    
      @Override public final Component add(Component comp) {
        return super.add(comp);
      }
    
      @Override public Dimension getPreferredSize() {
        // return getLayout().preferredLayoutSize(this);
        Dimension d = getLayout().preferredLayoutSize(this);
        label.setText(textArea.getText());
        Insets i = getInsets();
        Insets ti = textArea.getInsets();
        Insets tm = textArea.getMargin();
        int pad = i.left + i.right + ti.left + ti.right + tm.left + tm.right;
        d.width = Math.min(d.width, label.getPreferredSize().width + pad);
        return d;
      }
    
      @Override public void setTipText(String tipText) {
        String oldValue = textArea.getText();
        if (!Objects.equals(oldValue, tipText)) {
          textArea.setText(tipText);
          firePropertyChange("tiptext", oldValue, tipText);
          revalidate();
          repaint();
        }
      }
    
      @Override public String getTipText() {
        return Optional.ofNullable(textArea).map(JTextArea::getText).orElse(null);
      }
    }