Reputation: 823
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
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);
}
}
Upvotes: 2
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
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
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
Reputation: 173
You could make every character appear as a "bullet" than have a separate string that would actually be the text.
Upvotes: 0