Search code examples
javaswing

Listening to text trims/shows of JTextField


Here's what the user wants: if text is too long for a text field to hold, set it as a tooltip. Otherwise, there should be no tooltip.

The problem: Swing doesn't tell us if it trims a field's text. There's no such property, no events are fired.

For simplicity, forget the tooltip. Just make the prints go off (see the MRE). Basically, I need to "listen" to trims/shows.

Here's an MRE. Change the size of the window to affect the width of the field.

package demos.text.formattedTextField;

import javax.swing.JFormattedTextField;
import javax.swing.JPanel;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.LayoutManager;
import java.text.ParseException;

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

    private static JPanel createMainPanel() {
        LayoutManager layout = new GridBagLayout();
        JPanel panel = new JPanel(layout);
        panel.add(createFormattedTextField(), constraints());
        return panel;
    }

    private static GridBagConstraints constraints() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.weightx = 1;
        constraints.fill = GridBagConstraints.BOTH;
        return constraints;
    }

    private static JFormattedTextField createFormattedTextField() {
        JFormattedTextField textField = new JFormattedTextField();
        DefaultFormatterFactory formatterFactory = new DefaultFormatterFactory();
        formatterFactory.setDisplayFormatter(new DefaultFormatter() {
            @Override
            public String valueToString(Object value) throws ParseException {
                return value instanceof Cat ? ((Cat) value).getName() : super.valueToString(value);
            }
        });
        textField.setFormatterFactory(formatterFactory);
        textField.setValue(createFormattedValue());
        return textField;
    }

    private static Cat createFormattedValue() {
        Cat cat = new Cat("mew".repeat(10));
        return cat;
    }

    private static void onTextTrim() {
        System.out.println("Text has been trimmed...");
    }

    private static void onTextShow() {
        System.out.println("Text is now fully displayed...");
    }

    static class Cat {
        private final String name;

        public Cat(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return super.toString();
        }

        public String getName() {
            return name;
        }
    }
}

enter image description here

enter image description here

Here's GPT's suggestion which works but is, in my opinion, ugly

textField.addComponentListener(new ComponentAdapter() {
    private boolean isTrimmed = false;

    @Override
    public void componentResized(ComponentEvent e) {
        checkTextTrim(textField);
    }

    @Override
    public void componentShown(ComponentEvent e) {
        checkTextTrim(textField);
    }

    private void checkTextTrim(JFormattedTextField field) {
        FontMetrics metrics = field.getFontMetrics(field.getFont());
        String text = field.getText();
        int textWidth = metrics.stringWidth(text);
        int fieldWidth = field.getWidth();

        boolean currentlyTrimmed = textWidth > fieldWidth;

        if (currentlyTrimmed && !isTrimmed) {
            isTrimmed = true;
            onTextTrim();
        } else if (!currentlyTrimmed && isTrimmed) {
            isTrimmed = false;
            onTextShow();
        }
    }
});        

Solution

  • I don’t think that printing a message when the condition changes makes the task easier. There is less work to do when we only check the condition when the preconditions for displaying a tooltip (i.e. when the mouse cursor is hoovering over the component or the user explictly presses control + f1) are met.

    For example you can simply replace the line

    JFormattedTextField textField = new JFormattedTextField();
    

    with

    JFormattedTextField textField = new JFormattedTextField() {
        // needed because we have no static ToolTipText
        {   ToolTipManager.sharedInstance().registerComponent(this);  }
    
        @Override
        public String getToolTipText() {
            return getPreferredSize().width > getWidth()? getText(): null;
        }
    };
    

    To mimic the behavior of some look & feels which display the tooltip of a cut off text directly over the text, you can add

    @Override
    public Point getToolTipLocation(MouseEvent event) {
        return new Point(0, 0);
    }
    

    to this component.