Diamond Block
Diamond Block

Reputation: 159

Auto-expand TextField to fit text

I want to create a text field that expands horizontally as more text is entered.

After binding the prefColumnCount property to the length of the text in the TextField, its size increases as text gets longer. However, the caret's position is not updated and it gets stuck at the same place, no matter the text length. The user has to manually move the caret to the front in order for it to be displayed properly.

Images:

What it looks like after inputting some text, the text doesn't occupy the whole text field:

After moving the caret at the beginning of the text:

Here is my current code:

TextField textField = new TextField();
textField.prefColumnCountProperty().bind(textField.textProperty().length());
setHgrow(textField, Priority.ALWAYS);

How do I fix this problem? Any help is appreciated!

Upvotes: 7

Views: 941

Answers (2)

0009laH
0009laH

Reputation: 1990

The problem comes from the caret, which graphically updates its position as if he is in a static area; once you define TextField's prefWidth property, the caret cannot disappear from the text field.

When I write down content until it reaches the right side, the caret should go outside the field, which is impossible; it seems to be in front of a virtual wall. As a result, all the text content is "translated" to the left to let the caret on the same position. What's puzzling is that your binding works well, so TextField's width continues to grow as you add text...

In your case, I suggest you to refresh the caret position each time you type something into your text field. Add this EventHandler to your TextField instantiation:

textField.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
    if (ke.getCode() != KeyCode.DELETE) {
        textField.positionCaret(0);
        textField.positionCaret(textField.getText().length());
    }
});

ADDITIONAL NOTE

Your binding only works for monospaced fonts like Consolas or Courier New; prefWidth won't be calculated properly if you use fonts like Arial or Calibri. You would have to import com.sun.javafx.tk and use FontMetrics and ToolKit classes.

Instead of your binding, you would have something like:

FontMetrics fm = Toolkit.getToolkit().getFontLoader().getFontMetrics(textField.getFont());
textField.textProperty().addListener((bean_p, old_p, new_p) -> {
    Insets in = textField.getInsets();
    textField.setPrefWidth(fm.computeStringWidth(new_p) + in.getLeft() + in.getRight());
});

Hope it will be useful for you :)

Upvotes: 2

jewelsea
jewelsea

Reputation: 159291

This example uses a separate measuring text variable in its own css styled scene to calculate updates to the preferred size of the TextField so that it's preferred size expands and shrinks as required.

short text

long text

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class MagicExpander extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        TextField textField = new ExpandingTextField();

        Pane layout = new VBox(textField);
        layout.setPadding(new Insets(10));

        stage.setScene(new Scene(layout, 600, 40));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    static class ExpandingTextField extends TextField {
        private static int MAGIC_PADDING = 20;

        private Text measuringText = new Text();
        // To allow potential css formatting to work for the text,
        // it is required that text be placed in a Scene,
        // even though we never explicitly use the scene.
        private Scene measuringScene = new Scene(new Group(measuringText));

        ExpandingTextField() {
            super();
            setPrefWidth(MAGIC_PADDING);
            setMinWidth(MAGIC_PADDING);
            setMaxWidth(USE_PREF_SIZE);

            // note if the text in your text field is styled, then you should also apply the
            // a similar style to the measuring text here if you want to get an accurate measurement.

            textProperty().addListener(observable -> {
                measuringText.setText(getText());
                setPrefWidth(measureTextWidth(measuringText) + MAGIC_PADDING);
            });
        }

        private double measureTextWidth(Text text) {
            text.applyCss();
            return text.getLayoutBounds().getWidth();
        }
    }
}

Upvotes: 2

Related Questions