Vin
Vin

Reputation: 823

JavaFX - Mask text of a TextArea

I would to know if there is a way to mask the text of a TextArea in JavaFX. For example, masking the text using the 'bullet' password character like PasswordField. For TextField, there is the maskText() method that works well. This method is not useful for TextArea. What can I do?

NB: I want that getText() and setText() method must work with the clear text, not with the masked text. Just like PasswordField works.

EDIT
That is the approach I used to achieve the result, but unfortunately unsuccessfully.

My custom TextArea class:

public class PasswordArea extends TextArea {

    @Override
    protected Skin<?> createDefaultSkin() {
        return new PasswordAreaSkin(this); //my custom skin
    }
}

the custom skin used for the custom TextArea:

public class PasswordAreaSkin extends TextAreaSkin {
    public PasswordAreaSkin(PasswordArea control) {
        super(control);
    }

    //here I override the maskText method to mask the text
    @Override
    protected String maskText(String text) {
        int n = text.length();
        StringBuilder passwordBuilder=new StringBuilder(n);
        for(int i = 0; i < n; i++) {
            passwordBuilder.append('\u2022'); //append 'bullet' char
        }

        return passwordBuilder.toString();
    }
}

Upvotes: 4

Views: 2408

Answers (5)

user1803551
user1803551

Reputation: 13427

The problem with what you want is that TextArea is not built for this functionality, at least in JDK 8 (JDK 9 added public skinning API, e.g., TextAreaSkin). Specifically, its skin, TextAreaSkin does not facilitated a masking mechanism.

TextFieldSkin does masking by binding the visual text node's textProperty to the component's textProperty. Thus, any change to the "real" text of the component manifests in the text of the visual component plus the appropriate masking modification (the maskText method):

textNode.textProperty().bind(new StringBinding() {
    { bind(textField.textProperty()); }
    @Override protected String computeValue() {
        return maskText(textField.textProperty().getValueSafe());
    }
});

TextAreaSkin uses a group of Text nodes for its visuals, though only 1 is used in JDK 8. Changes to the visual text are made by listening to changes in the component's text:

textArea.textProperty().addListener(observable -> {
    invalidateMetrics();
    ((Text)paragraphNodes.getChildren().get(0)).setText(textArea.textProperty().getValueSafe());
    contentView.requestLayout();
});

We can use this to listen to changes in the visual text and update it ourselves. Below is a working example of an implementation. The maskText method is mostly copied from TextFieldSkin. We use reflection to gain access to the visual text representation node and then update it with the current text (e.g., from the text area constructor) and register the update listener.

public class Test extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        String s = "some times there are\nmore strings\n\nin here";
        TextArea ta = new TextArea(s);
        ta.setSkin(new TextAreaMaskSkin(ta));

        TextArea view = new TextArea();
        view.textProperty().bind(ta.textProperty());

        Scene scene = new Scene(new HBox(view, ta));
        stage.setScene(scene);
        stage.show();
    }

    private static class TextAreaMaskSkin extends TextAreaSkin {

        public TextAreaMaskSkin(TextArea textArea) throws Exception {
            super(textArea);
            Field field = TextAreaSkin.class.getDeclaredField("paragraphNodes");
            field.setAccessible(true);
            Group group = (Group) field.get(this);
            Text text = (Text) group.getChildren().get(0);
            text.setText(maskText(textArea.textProperty().getValueSafe()));
            text.textProperty().addListener(o -> text.setText(maskText(textArea.textProperty().getValueSafe())));
        }

        @Override
        protected String maskText(String txt) {
            int n = txt.length();
            StringBuilder passwordBuilder = new StringBuilder(n);
            for (int i = 0; i < n; i++) {
                if (txt.charAt(i) == '\n') {
                    passwordBuilder.append('\n');
                } else {
                    passwordBuilder.append(TextFieldSkin.BULLET);
                }
            }
            return passwordBuilder.toString();
        }
    }

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

enter image description here

Upvotes: 2

Kiraged
Kiraged

Reputation: 519

import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class PasswordArea extends TextArea {

    private final static List<String> text = new ArrayList<>();

    private final static List<KeyCode> allowedKeys = Arrays.asList(KeyCode.ENTER, KeyCode.SPACE, KeyCode.BACK_SPACE, KeyCode.A, KeyCode.B, KeyCode.C, KeyCode.D, KeyCode.E, KeyCode.F,
            KeyCode.G, KeyCode.H, KeyCode.I, KeyCode.J, KeyCode.K, KeyCode.L, KeyCode.M, KeyCode.N, KeyCode.O, KeyCode.P, KeyCode.Q, KeyCode.R, KeyCode.S, KeyCode.T, KeyCode.V,
            KeyCode.W, KeyCode.X, KeyCode.Y, KeyCode.Z);

    public PasswordArea() {
        this.setEditable(false);
        this.setOnKeyPressed(event -> {
            if (!allowedKeys.contains(event.getCode())) {
                return;
            }
            KeyCombination ctrlDelete = new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCombination.CONTROL_DOWN);
            if(ctrlDelete.match(event)) {
                setPasswordText(getPasswordText());
                return;
            }
            switch (event.getCode()) {
                case ENTER:
                    this.appendText("\n");
                    text.add("\n");
                    break;
                case SPACE:
                    this.appendText(" ");
                    text.add(" ");
                    break;
                case BACK_SPACE:
                    final int size = this.textProperty().length().get();
                    if (size > 0) {
                        this.deleteText(size - 1, size);
                        text.remove(text.size() - 1);
                    }
                    break;
                default:
                    this.appendText("" + '\u2022');
                    text.add(event.getText());
                    break;
            }
        });
    }

    public String getPasswordText() {
        StringBuilder builder = new StringBuilder();
        text.forEach(builder::append);
        return builder.toString();
    }

    public void setPasswordText(String setText) {
        text.clear();
        this.clear();
        for (int i = 0; i < setText.length(); i++) {
            switch (setText.charAt(i)) {
                case ' ':
                    this.appendText(" ");
                    break;
                case '\n':
                    this.appendText("\n");
                    break;
                default:
                    this.appendText("" + '\u2022');
                    break;
            }
        }
        text.add(setText);
    }
}

Usage:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;


public class Launch extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        PasswordArea area = new PasswordArea();
        Scene scene = new Scene(area, 600, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}

This is what I got, probably the cheapest way to do it.

Upvotes: 0

Vin
Vin

Reputation: 823

I solved. I found this solution. It works but it shoud be tested under certain conditions. However this is the code, it involve only the skin.

public class PasswordAreaSkin extends TextAreaSkin {
    public PasswordAreaSkin(PasswordArea control) {
        Text textNode=getTextNode();
        textNode.textProperty().addListener(obs -> {
            textNode().setText(
                maskText(control.textProperty().getValueSafe()));
        });
    }

    @Override
    protected String maskText(String text) {
        int n = txt.length();
        StringBuilder passwordBuilder=new StringBuilder(n);
        for(int i = 0; i < n; i++) {
            passwordBuilder.append('\u2022'); //append 'bullet' char
        }

        return passwordBuilder.toString();
    }

    private Text getTextNode() {
        //WARNING: call ONLY in the constructor because 
        //children list could change
        Region content=
            ((Region)((ScrollPane)getChildren().get(0)).getContent());
        Group g=(Group)content.getChildrenUnmodifiable().get(1);
        return (Text)g.getChildren().get(0);
    }
}

In this way, the text is masked in the control, but getText() return the clear text and setText() works with the clear text and in the ui mask it (that is what I was looking for)

The only problem is that I'm bound to an implementation detail, the position of the Text node in the children list.

Upvotes: 0

deHaar
deHaar

Reputation: 18568

This is a work around which does what you want (at least as far as I could follow your desires…). It uses a ChangeListener and manipulates the input while storing the original. For further manipulation or use, please extend the code yourself. Ah, and by the way: There is no need for a Skin now, but feel free to apply it, the masking is done in the PasswordArea. This may not be the most efficient solution, but it is working (when used in a Main.java like the one posted at the end of this answer).

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextArea;

public class PasswordArea extends TextArea {

    private StringBuilder original = new StringBuilder();
    private StringBuilder masked = new StringBuilder();

    public PasswordArea() {
        this.textProperty().addListener(new ChangeListener<String>() {

        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
            int oldLength = oldValue.length();
            int newLength = newValue.length();

            if (newLength == oldLength) {
                // obviously an unnecessary case to be checked
            } else if (newLength < oldLength) {
                // last character deleted, so delete the last one of each, original and masked text
                original.delete(newLength, oldLength);
                masked.delete(newLength, oldLength);
            } else {
                // one character added, so just replace that one
                char c = newValue.toCharArray()[newLength - 1];
                if (Character.isSpaceChar(c)) {
                    original.append(c);
                    masked.append(c);
                } else if (c == '\u2022') {

                } else {
                    masked.append('\u2022');
                    original.append(c);
                }
            }
            // this output is just for checking the state of the original
            System.out.println(original.toString() + "\t--->\t" + masked.toString());
            textProperty().set(masked.toString());
        }
    });
}

}

Here the Main.java:

import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            StackPane root = new StackPane();
            Scene scene = new Scene(root,400,400);
            PasswordArea passwordArea = new PasswordArea();
            root.getChildren().addAll(passwordArea);
            primaryStage.show();
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

Upvotes: 0

jpell
jpell

Reputation: 173

You could make every character appear as a "bullet" than have a separate string that would actually be the text.

Upvotes: 0

Related Questions