dynamically add textfields using listeners - java

I'm trying to write a program similar to the contacts app on an android phone using javafx. In the fxml file I have a VBox which contains three textfields, the first two fields are for first name and last name, and the third one is for a number.
Now what I want the program to do is when the textfield for number is filled with even a single character, another textfield to be automatically added to the VBox. (for another number).
and I want the same thing to happen for the next field. and any other field that follows, so it has a recursive form.
Now the only method I know that might accomplish this, is using a listener, but I have no idea how to create such a recursive listener. and The listener to the old field would have to be removed once it has accomplished its job, so it wouldn't continuously create new fields when typing something in the old field. but you can't remove a listener while you're inside it.
Is there a way to do this?

A lambda expression can't refer to itself, but an anonymous inner class can, so if you implement your listener as an anonymous inner class, you can achieve what you're looking to do:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DynamicTextFields extends Application {
private TextField lastTextField ;
#Override
public void start(Stage primaryStage) {
lastTextField = new TextField();
VBox vbox = new VBox(5, lastTextField);
ChangeListener<String> textFieldListener = new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> obs, String oldValue, String newValue) {
lastTextField.textProperty().removeListener(this);
lastTextField = new TextField();
lastTextField.textProperty().addListener(this);
vbox.getChildren().add(lastTextField);
}
};
lastTextField.textProperty().addListener(textFieldListener);
Scene scene = new Scene(new ScrollPane(vbox), 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Register a ChangeListener to the text property of the TextFields that adds/removes the TextField based on the index every time the text changes from empty to non-empty or the other way round.
public void addTextField(Pane parent) {
TextField textField = new TextField();
textField.textProperty().addListener((o, oldValue, newValue) -> {
boolean wasEmpty = oldValue.isEmpty();
boolean isEmpty = newValue.isEmpty();
if (wasEmpty != isEmpty) {
if (wasEmpty) {
// append textfield if last becomes non-empty
if (parent.getChildren().get(parent.getChildren().size() - 1) == textField) {
addTextField(parent);
}
} else {
int tfIndex = parent.getChildren().indexOf(textField);
if (tfIndex < parent.getChildren().size() - 1) {
// remove textfield if this is not the last one
parent.getChildren().remove(tfIndex);
parent.getChildren().get(tfIndex).requestFocus();
}
}
}
});
parent.getChildren().add(textField);
}
VBox root = new VBox();
addTextField(root);

Related

JavaFX editable ComboBox not allowing duplicate items

I want to have an editable ComboBox that contains items of some type (e.g. integers), where I can add and delete items, allow the user to edit existing items, and allow for duplicate items.
The problem is that whenever the user edits an existing item, and changes its value to a value of an item already present in the list, the editor (textfield) causes the selection model to select the item already present in the list instead of modifying the edited item.
I tried circumventing this by creating a wrapper class that contains the item and has an unique index. However, this causes problems in StringConverter.fromString because I have to create a new wrapper every time it converts.
An easy solution I think would be to stop the editor from searching through the items whenever an edit is made, so that the selection model does not change.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.Arrays;
public class ComboBoxTest extends Application {
private final ComboBox<Integer> comboBox = new ComboBox<>();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final Group root = new Group();
root.getChildren().add(comboBox);
final Scene scene = new Scene(root, 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
comboBox.getItems().addAll(Arrays.asList(1, 2, 3, 4));
comboBox.setConverter(
new StringConverter<Integer>() {
#Override
public String toString(Integer integer) {
return integer == null ? "" : String.valueOf(integer);
}
#Override
public Integer fromString(String s) {
return Integer.parseInt(s);
}
});
comboBox.setPromptText("select value");
comboBox.setEditable(true);
}
}

How to create a custom dialog in JavaFx without using any "ButtonType"-controls?

I want to create a custom Dialog, which just displays options (see figure 1). If the user selects one of those options, the dialog should close and return the corresponding result instantly.
So far, I can only accomplish this by adding an arbitrary ButtonType to the Dialog, hiding it by using setVisible(false) and applying fire() in the EventHandler of the clicked option.
This weird workaround actually works fine, but seems to me very unprofessional ...
So, how to do this in a more professional or proper way without using the ButtonType trick?
My workaround-code looks like this (Dialog class):
public class CustomDialog extends Dialog<String> {
private static final String[] OPTIONS
= new String[]{"Option1", "Option2", "Option3", "Option4"};
private String selectedOption = null;
Button applyButton;
public CustomDialog() {
super();
initStyle(StageStyle.DECORATED);
VBox vBox = new VBox();
for (String option : OPTIONS) {
Button optionButton = new Button(option);
optionButton.setOnAction((event) -> {
selectedOption = option;
applyButton.fire();
});
vBox.getChildren().add(optionButton);
}
getDialogPane().setContent(vBox);
getDialogPane().getButtonTypes().add(ButtonType.APPLY);
applyButton = (Button) getDialogPane().lookupButton(ButtonType.APPLY);
applyButton.setVisible(false);
setResultConverter((dialogButton) -> {
return selectedOption;
});
}
}
Using the dialog class:
CustomDialog dialog = new CustomDialog();
Optional<String> result = dialog.showAndWait();
String selected = null;
if (result.isPresent()) {
selected = result.get();
} else if (selected == null) {
System.exit(0);
}
A Dialog is just a window displaying a DialogPane, and, quoting the Javadocs for DialogPane:
DialogPane operates on the concept of ButtonType. A ButtonType is a
descriptor of a single button that should be represented visually in
the DialogPane. Developers who create a DialogPane therefore must
specify the button types that they want to display
(my emphasis). Therefore, while you've shown one possible workaround and in the other answer Slaw has shown another, if you're trying to use a Dialog without using ButtonType and its associated result converter, you're really using the Dialog class for something for which it's not intended.
The functionality you describe is perfectly achievable with a regular modal Stage. For example, the following gives the same basic behavior you describe and involves no ButtonTypes:
package org.jamesd.examples.dialog;
import java.util.Optional;
import java.util.stream.Stream;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
public class CustomDialog {
private static final String[] OPTIONS
= {"Option 1", "Option 2", "Option 3", "Option 4"};
private final Stage stage ;
private String selectedOption = null ;
public CustomDialog() {
this(null);
}
public CustomDialog(Window parent) {
var vbox = new VBox();
// Real app should use an external style sheet:
vbox.setStyle("-fx-padding: 12px; -fx-spacing: 5px;");
Stream.of(OPTIONS)
.map(this::createButton)
.forEach(vbox.getChildren()::add);
var scene = new Scene(vbox);
stage = new Stage();
stage.initOwner(parent);
stage.initModality(Modality.WINDOW_MODAL);
stage.setScene(scene);
}
private Button createButton(String text) {
var button = new Button(text);
button.setOnAction(e -> {
selectedOption = text ;
stage.close();
});
return button ;
}
public Optional<String> showDialog() {
selectedOption = null ;
stage.showAndWait();
return Optional.ofNullable(selectedOption);
}
}
Here's a simple application class which uses this custom dialog:
package org.jamesd.examples.dialog;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) throws Exception {
var root = new VBox();
// Real app should use an external style sheet:
root.setStyle("-fx-padding: 12px; -fx-spacing: 5px;");
var showDialog = new Button("Show dialog");
var label = new Label("No option chosen");
showDialog.setOnAction(e ->
new CustomDialog(stage)
.showDialog()
.ifPresentOrElse(label::setText, Platform::exit));
root.getChildren().addAll(showDialog, label);
stage.setScene(new Scene(root));
stage.show();
}
}
As pointed out by both #Sedrick and #James_D, the Dialog API is built around the concept of "button types". Not using ButtonType goes against the API and, because of this, will always seem hacky/wrong. That said, there is a slight alteration you could make to your current code that satisfies your "without using any 'ButtonType'-controls" goal. It doesn't appear to be documented, but if you set the result property manually it triggers the close-and-return-result process. This means you don't need to add any ButtonType and can bypass the resultConverter completely. Here's a proof-of-concept:
OptonsDialog.java:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Dialog;
import javafx.scene.layout.VBox;
public class OptionsDialog<T extends OptionsDialog.Option> extends Dialog<T> {
public interface Option {
String getDisplayText();
}
#SafeVarargs
public OptionsDialog(T... options) {
if (options.length == 0) {
throw new IllegalArgumentException("must provide at least one option");
}
var content = new VBox(10);
content.setAlignment(Pos.CENTER);
content.setPadding(new Insets(15, 25, 15, 25));
for (var option : options) {
var button = new Button(option.getDisplayText());
button.setOnAction(
event -> {
event.consume();
setResult(option);
});
content.getChildren().add(button);
}
getDialogPane().setContent(content);
}
}
App.java:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.stage.Window;
public class App extends Application {
private enum Foo implements OptionsDialog.Option {
OPTION_1("Option Number 1"),
OPTION_2("Option Number 2"),
OPTION_3("Option Number 3"),
OPTION_4("Option Number 4");
private final String displayText;
Foo(String displayText) {
this.displayText = displayText;
}
#Override
public String getDisplayText() {
return displayText;
}
}
#Override
public void start(Stage primaryStage) {
var button = new Button("Click me!");
button.setOnAction(
event -> {
event.consume();
showChosenOption(primaryStage, promptUserForOption(primaryStage));
});
primaryStage.setScene(new Scene(new StackPane(button), 500, 300));
primaryStage.show();
}
private static Foo promptUserForOption(Window owner) {
var dialog = new OptionsDialog<>(Foo.values());
dialog.initOwner(owner);
dialog.setTitle("Choose Option");
return dialog.showAndWait().orElseThrow();
}
private static void showChosenOption(Window owner, OptionsDialog.Option option) {
var alert = new Alert(AlertType.INFORMATION);
alert.initOwner(owner);
alert.setHeaderText("Chosen Option");
alert.setContentText(String.format("You chose the following: \"%s\"", option.getDisplayText()));
alert.show();
}
}
It's not that different from your current workaround and it's still working against the API. This also relies on undocumented behavior (that setting the result property manually closes the dialog and returns the result). The ButtonBar at the bottom still takes up some space, though less than when you add an invisible button. It's possible, however, to get rid of this empty space by adding the following CSS:
.options-dialog-pane .button-bar {
-fx-min-height: 0;
-fx-pref-height: 0;
-fx-max-height: 0;
}
Note the above assumes you've modified the code to add the "options-dialog-pane" style class to the DialogPane used with the OptionsDialog.
I think you should read the following from the Java Docs:
Dialog Closing Rules:
It is important to understand what happens when a Dialog is closed, and also how a Dialog can be closed, especially in abnormal closing situations (such as when the 'X' button is clicked in a dialogs title bar, or when operating system specific keyboard shortcuts (such as alt-F4 on Windows) are entered). Fortunately, the outcome is well-defined in these situations, and can be best summarised in the following bullet points:
JavaFX dialogs can only be closed 'abnormally' (as defined above) in
two situations:
When the dialog only has one button, or
When the dialog has multiple buttons, as long as one of them meets one of the following requirements:
The button has a ButtonType whose ButtonBar.ButtonData is of type ButtonBar.ButtonData.CANCEL_CLOSE.
The button has a ButtonType whose ButtonBar.ButtonData returns true when ButtonBar.ButtonData.isCancelButton() is called.
In all other situations, the dialog will refuse to respond to all close requests, remaining open until the user clicks on one of the available buttons in the DialogPane area of the dialog.
If a dialog is closed abnormally, and if the dialog contains a button which meets one of the two criteria above, the dialog will attempt to set the result property to whatever value is returned from calling the result converter with the first matching ButtonType.
If for any reason the result converter returns null, or if the dialog is closed when only one non-cancel button is present, the result property will be null, and the showAndWait() method will return Optional.empty(). This later point means that, if you use either of option 2 or option 3 (as presented earlier in this class documentation), the Optional.ifPresent(java.util.function.Consumer) lambda will never be called, and code will continue executing as if the dialog had not returned any value at all.
If you don't mind the Buttons being horizontal, you should use ButtonType and setResultConverter to return a String based on which button is pressed.
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.stage.StageStyle;
import javafx.util.Callback;
/**
*
* #author blj0011
*/
public class CustomDialog extends Dialog<String>
{
String result = "";
public CustomDialog()
{
super();
initStyle(StageStyle.DECORATED);
setContentText(null);
setHeaderText(null);
ButtonType buttonOne = new ButtonType("Option1");
ButtonType buttonTwo = new ButtonType("Option2");
ButtonType buttonThree = new ButtonType("Option3");
ButtonType buttonFour = new ButtonType("Option4");
getDialogPane().getButtonTypes().addAll(buttonOne, buttonTwo, buttonThree, buttonFour);
setResultConverter(new Callback<ButtonType, String>()
{
#Override
public String call(ButtonType param)
{
if (param == buttonOne) {
return buttonOne.getText();
}
else if (param == buttonTwo) {
return buttonTwo.getText();
}
else if (param == buttonThree) {
return buttonThree.getText();
}
else if (param == buttonFour) {
return buttonFour.getText();
}
return "";
}
});
}
}
Update: As #Slaw stated in the comments, you can replace setResultConverter(...) with setResultConverter(ButtonType::getText).

How to get user input using KeyCombination in JavaFX?

I am working on a simple in-fix to post-fix converter. I want to get the user's input from the keyboard. To enter some symbols, e.g."+", the user must press shift. I am using a KeyCombination object to capture whether the user is using shift or not.
My code keeps give me this error: Key code must not match modifier key!
However, when I look at the keycode, it is not Shift, rather it is whatever number row key is pressed. E.g., if the user presses Shift + =, the keycode is EQUALS, not the Shift_DOWN modifier. The code works as expected, but I can't figure out how to get rid of this exception.
tfInput.setOnKeyPressed(e -> {
if (e.isShiftDown()) {
KeyCombination kc = new KeyCodeCombination(e.getCode(),
KeyCombination.SHIFT_DOWN);
userInput = kc.toString();
}
The reason you are getting the error is because the first parameter in key combination is keycode and shift is a key modifier you can stop getting this error by checking if the key is SHIFT before you continue
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Stage;
public class Main extends Application {
private String userInput;
#Override
public void start(Stage stage) {
TextField textField = new TextField();
textField.setOnKeyPressed(e -> {
System.out.println(e.getCode());
if (e.isShiftDown()&&!e.getCode().toString().equals("SHIFT")) {
KeyCombination kc = new KeyCodeCombination(e.getCode(), KeyCombination.CONTROL_ANY);
userInput = kc.toString();
System.out.println(userInput);
}
});
Scene scene = new Scene(textField);
stage = new Stage();
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) { launch(args); }
}

How to set OnClick or OnSelection for ControlsFX CustomTextField autocomplete dropdown?

I am using ControlsFX's CustomTextField. When I click on one of the autocomplete options, I need to clear the TextField and create a Tag so I can add it to a FlowPane. How do I set up an OnClick or OnSelectionChange listener or override the current OnClick?
I took a look at the CustomTextField documentation and I can't find a clear way of doing what you want. So I will guess you have to implement it yourself or to find a workaround. In case you decide to choose the second choice here is something that I believe works very well :
import java.util.ArrayList;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestApplication extends Application {
private boolean addedBySelection = false;
private ArrayList<String> tagList = new ArrayList<>();
private FlowPane tagPane;
#Override
public void start(Stage primaryStage) throws Exception {
VBox mainPane = new VBox(10);
mainPane.setStyle("-fx-background-color : white");
mainPane.setPadding(new Insets(15));
mainPane.setAlignment(Pos.CENTER);
tagPane = new FlowPane(15, 10);
tagPane.setPrefHeight(50);
CustomTextField field = new CustomTextField() {
#Override
public void paste() {
super.paste();
addedBySelection = false;
}
};
field.setOnKeyPressed(e -> {
addedBySelection = false;
});
field.setOnKeyReleased(e -> {
addedBySelection = true;
});
field.textProperty().addListener(e -> {
if (addedBySelection) {
System.out.println("Text Changed from the suggession list ");
addTag(field.getText());
addedBySelection = false;
field.clear();
addedBySelection = true;
} else {
System.out.println("User Input (Mouse paste, or typing) ");
}
});
TextFields.bindAutoCompletion(field, new String[] { "Java", "C++", "C#", "Python", "Haskell" });
mainPane.getChildren().addAll(field, tagPane);
Scene scene = new Scene(mainPane, 200, 100);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addTag(String tag) {
if (!tagList.contains(tag)) {
tagList.add(tag);
Label tagLabel = new Label(tag);
tagLabel.setStyle("-fx-background-color : #E1ECF4; -fx-text-fill : #6A739D;");
tagPane.getChildren().add(tagLabel);
}
}
public static void main(String[] args) {
launch(args);
}
}
I tried to keep it a simple as it could. The code above is doing exactly what you are after. The logic is to set a listener on the textProperty ( cause we can't set one on selection with the mouse from the autocomplete list ) and somehow find out if the user actually triggers the event using the autocomplete list or not. Thus I have a flag looking for user inputs (ex. key press) and 'releasing' the flag each time the keys are released. We have to catch the paste action as well in order to avoid mistakes if the user pastes a text on the field. One more last thing is the way we clear the field. We have to set our flag to false because the field.clear() will trigger an event as well and we don't want to fall into an event loop.
Note : With the current workaround, you will see that you are able to make a selection from the autocomplete list by pressing the enter key as well.

JavaFX How do I return the cursor to the first line in TextArea? [duplicate]

I want to use a JavaFX TextArea as though it were exactly like a multi-line TextField. In other words, when I press [Tab] I want to cycle to the next control on the form and when I press [Enter] I want the Key.Event to go to the defaultButton control (rather than be consumed by the TextArea).
The default behavior for TextArea is that [Tab] gets inserted into the TextArea and [Enter] inserts a new-line character.
I know that I need to use EventFilters to get the behavior that I want, but I'm getting it all wrong. I don't want the TextArea to consume these events ... I just want it to let them "go right on by".
The solution here displays two text areas and a default button.
When the user presses the tab key, the focus moves to the next control down.
When the user presses the enter key, the default button is fired.
To achieve this behavior:
The enter key press for each text area is caught in an event filter, copied and targeted to the text area's parent node (which contains the default OK button). This causes the default OK button to be fired when enter is pressed anywhere on the form. The original enter key press is consumed so that it does not cause a new line to be added to the text area's text.
The tab key press for each text area is caught in a filter and the parent's focus traversable list is processed to find the next focusable control and focus is requested for that control. The original tab key press is consumed so that it does not cause new tab spacing to be added to the text area's text.
The code makes use of features implemented in Java 8, so Java 8 is required to execute it.
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.*;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.*;
public class TextAreaTabAndEnterHandler extends Application {
final Label status = new Label();
public static void main(String[] args) { launch(args); }
#Override public void start(final Stage stage) {
final TextArea textArea1 = new TabAndEnterIgnoringTextArea();
final TextArea textArea2 = new TabAndEnterIgnoringTextArea();
final Button defaultButton = new Button("OK");
defaultButton.setDefaultButton(true);
defaultButton.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
status.setText("Default Button Pressed");
}
});
textArea1.textProperty().addListener(new ClearStatusListener());
textArea2.textProperty().addListener(new ClearStatusListener());
VBox layout = new VBox(10);
layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10px;");
layout.getChildren().setAll(
textArea1,
textArea2,
defaultButton,
status
);
stage.setScene(
new Scene(layout)
);
stage.show();
}
class ClearStatusListener implements ChangeListener<String> {
#Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
status.setText("");
}
}
class TabAndEnterIgnoringTextArea extends TextArea {
final TextArea myTextArea = this;
TabAndEnterIgnoringTextArea() {
addEventFilter(KeyEvent.KEY_PRESSED, new TabAndEnterHandler());
}
class TabAndEnterHandler implements EventHandler<KeyEvent> {
private KeyEvent recodedEvent;
#Override public void handle(KeyEvent event) {
if (recodedEvent != null) {
recodedEvent = null;
return;
}
Parent parent = myTextArea.getParent();
if (parent != null) {
switch (event.getCode()) {
case ENTER:
if (event.isControlDown()) {
recodedEvent = recodeWithoutControlDown(event);
myTextArea.fireEvent(recodedEvent);
} else {
Event parentEvent = event.copyFor(parent, parent);
myTextArea.getParent().fireEvent(parentEvent);
}
event.consume();
break;
case TAB:
if (event.isControlDown()) {
recodedEvent = recodeWithoutControlDown(event);
myTextArea.fireEvent(recodedEvent);
} else {
ObservableList<Node> children = parent.getChildrenUnmodifiable();
int idx = children.indexOf(myTextArea);
if (idx >= 0) {
for (int i = idx + 1; i < children.size(); i++) {
if (children.get(i).isFocusTraversable()) {
children.get(i).requestFocus();
break;
}
}
for (int i = 0; i < idx; i++) {
if (children.get(i).isFocusTraversable()) {
children.get(i).requestFocus();
break;
}
}
}
}
event.consume();
break;
}
}
}
private KeyEvent recodeWithoutControlDown(KeyEvent event) {
return new KeyEvent(
event.getEventType(),
event.getCharacter(),
event.getText(),
event.getCode(),
event.isShiftDown(),
false,
event.isAltDown(),
event.isMetaDown()
);
}
}
}
}
An alternate solution would be to implement your own customized skin for TextArea which includes new key handling behavior. I believe that such a process would be more complicated than the solution presented here.
Update
One thing I didn't really like about my original solution to this problem was that once the Tab or Enter key was consumed, there was no way to trigger their default processing. So I updated the solution such that if the user holds the control key down when pressing Tab or Enter, the default Tab or Enter operation will be performed. This updated logic allows the user to insert a new line or tab space into the text area by pressing CTRL+Enter or CTRL+Tab.

Categories