JavaFX how to count errors made typing - java

I am trying to make my first app an have hit a roadblock. The app is basically just a simple typing practice app where the user is given a string which they copy. I am trying to implement a count that counts the number of errors made by the user.
Currently I have code that uses a listener to observe the string the user is inputting to a text area. I have an error count that is added to every time the users string does not equal the provided string, the issue with this is I want to only add 1 to the count for every error made. For example, currently if you make a mistake and type 2 wrong characters the error count will go up to 3 as the code runs every time a key is pressed meaning it counts the 2 wrong chars and 1 backspace to remove them and I would like this to only add 1 to the error count.
The code I have currently is below:
public class MainController implements Initializable {
#FXML
private BorderPane errorStatus;
#FXML
private TextArea inputTextArea;
#FXML
public TextArea generatedText;
private String userText;
private String thePassage;
private Integer errorCount = 0;
private Boolean errorCheck = false;
timer time = new timer();
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
thePassage = PassageData.getPassage();
generatedText.setText(thePassage);
inputTextArea.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable,
String oldValue, String newValue) {
if (!inputTextArea.getText().isEmpty()) {
time.start();
}
userText = newValue;
if (userText.equals("") || userText.equals(null)) {
errorStatus.setStyle(
"-fx-background-color: radial-gradient(radius 100%, white, white); -fx-background-radius: 10; -fx-border-radius: 10");
} else if (thePassage.regionMatches(0, userText, 0, userText.length())) {
errorCheck = false;
errorStatus.setStyle(
"-fx-background-color: radial-gradient(radius 100%, green, white); -fx-background-radius: 10; -fx-border-radius: 10");
} else {
errorCheck = true;
errorStatus.setStyle(
"-fx-background-color: radial-gradient(radius 100%, red, white); -fx-background-radius: 10; -fx-border-radius: 10");
}
if (userText.equals(thePassage)) {
thePassage = PassageData.getPassage();
generatedText.setText(thePassage);
Platform.runLater(() -> {
inputTextArea.clear();
});
errorStatus.setStyle(
"-fx-background-color: radial-gradient(radius 100%, white, white); -fx-background-radius: 10; -fx-border-radius: 10");
time.pause();
}
if (errorCheck) {
errorCount++;
System.out.println("Error count = " + errorCount);
}
}
});
}
}

Repeating (across several questions) Don't use low-level listeners if the framework provides higher-level support!
In this context, the high level support for fine-grained changes of text input is a TextFormatter, in particular its filter property. Such a filter will be notified whenever the text changes in any way (including caret navigation) and even allows to modify the change - all before the textProperty is changed.
A raw snippet that will count errors against a given text - for context see the Zephyr's answer
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.isAdded()) {
// tbd: guard against off-range
int pos = c.getRangeStart();
if (!c.getText().equals(target.substring(pos, pos + 1))) {
errorCount.set(errorCount.get() +1);
}
}
return c;
};
textArea.setTextFormatter(new TextFormatter<>(filter));

You will want to setup a change listener for the TextArea, but then compare each character of your input String with each character of the entered text.
The MCVE below demonstrates this:
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
IntegerProperty errorCount = new SimpleIntegerProperty(0);
String target = "This is the sample text to be typed correctly.";
Label instructions = new Label(target);
TextArea textArea = new TextArea();
textArea.setWrapText(true);
HBox hbox = new HBox(5);
hbox.setAlignment(Pos.CENTER);
Label errorsLabel = new Label();
hbox.getChildren().addAll(new Label("Errors:"), errorsLabel);
// Bind the label to errorcount
errorsLabel.textProperty().bind(errorCount.asString());
// Listen for changes to the textArea text and check again target string
textArea.textProperty().addListener((observableValue, s, newValue) -> {
if (newValue != null) {
errorCount.set(getErrorCount(target, newValue));
}
});
root.getChildren().addAll(instructions, textArea, hbox);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private int getErrorCount(String target, String entered) {
int errors = 0;
// Compare each character in the strings
char[] targetChars = target.toCharArray();
char[] enteredChars = entered.toCharArray();
// Starting at the beginning of the entered text, check that each character, in order, matches the target String
for (int i = 0; i < enteredChars.length; i++) {
if (enteredChars[i] != targetChars[i]) {
errors++;
}
}
return errors;
}
}
So what is happening here is each time a character is typed (or deleted) in the TextArea, the getErrorCount() method compares the entered text to the target String. If any characters are incorrect, it will increase the error count.
This is a lot like the "Submit" button idea from the comments above, but is performed each time the text changes in the TextArea, without a need for the extra button.

Related

Dialog ResultConverter does not clear password field

I'm currently working on a password manager. Before making any changes to a certain service, the program will ask the user for a password for authorization and then proceed to show the appropriate dialog, if the password is correct.
The issue that I'm having is that if I go through the cycle of putting in my password to make the change, click "ok", and then proceeding to make changes on the shown dialog, on the next turn if instead of putting the password when prompted I close the prompt, then the program retrieves the password from the previous iteration although it has been explicitly cleared. Resulting in the concurrent dialog showing, which is only supposed to show if you put in the correct password.
private void handleEditButton(MouseEvent event) {
Optional<String> rslt = passwordConfirmDialog.showAndWait();
if (rslt.get().equals(""))
return; //Do not proceed
String userInput = rslt.get().trim();
// Complex expression, but use of && statement is necessary to avoid an
// unecessary call to db and have return statement on this IF
if (!(!userInput.isBlank() && isCorrectPassword(userInput))) {
// show dialog
AlertConfigs.invalidPasswordTransactionFailed.showAndWait();
return;
}
System.out.println("Edit Handler: Correct password. -> " + userInput);
//Proceed to show next dialog...
private void initializePasswordConfirmDialog() {
passwordConfirmDialog.setTitle("User Account Control");
passwordConfirmDialog.setHeaderText("Please enter your password to continue.");
// Set the button types.
ButtonType ok = new ButtonType("Ok", ButtonData.OK_DONE);
passwordConfirmDialog.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL);
final PasswordField psField = new PasswordField();
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10));
grid.add(new Label("Please Enter your password"), 0, 0);
grid.add(psField, 1, 0);
passwordConfirmDialog.getDialogPane().setContent(grid);
passwordConfirmDialog.setResultConverter(buttonType -> {
String rslt = "";
if (buttonType == ok) {
rslt = psField.getText();
}
psField.clear();
return rslt;
});
}
I've posted a video on YouTube to help visualize the problem. https://youtu.be/sgayh7Q7Ne8
The PasswordField in initializePasswordConfirmDialog() is cleared because whenever I run the the prompt the second time, the PasswordField is blank (visually). Nevertheless, for some reason it still grabs the result from the previous iteration.
The initializePasswordConfirmDialog() is called once inside the constructor and is responsible for set the passwordConfirmDialog variable with the adequate properties.
Some additional code:
HomeController.java
#FXML
private GridPane servicesGrid;
private Dialog<String> passwordConfirmDialog;
private Dialog<Service> editServiceDialog;
private final int NUM_COLUMNS = 7;
public HomeController() {
passwordConfirmDialog = new Dialog<>();
initializePasswordConfirmDialog();
editServiceDialog = new Dialog<>();
}
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
loadServicesGridpane();
}
private void loadServicesGridpane() {
ArrayList<Service> currS = acct.getServices();
// int currentRow = 1;
for (Service s : currS)
addRowToServiceGrid(s);
}
private void addRowToServiceGrid(Service s) {
int rowIdx = servicesGrid.getChildren().size() / 4;
Button editButton = new Button("Edit");
editButton.setOnMouseClicked(event -> {
handleEditButton(event);
});
Button deleteButton = new Button("Delete");
deleteButton.setOnMouseClicked(event -> {
handleDeleteButton(event);
});
deleteButton.setId(s.getServiceName());
Label currServiceName = new Label(s.getServiceName());
currServiceName.setId(s.getServiceName());
Label currUsername = new Label(s.getServiceUsername());
Label currPassword = new Label(s.getServicePassword());
Label dateCreated = new Label(s.getDateCreated());
Label lastPssdChange = new Label(s.getLastPasswordChange());
servicesGrid.addRow(rowIdx, currServiceName, currUsername, currPassword, dateCreated, lastPssdChange,
deleteButton, editButton);
}
To study the problem in isolation, I refactored this example to permit reusing the dialog. As shown below, reusing the dialog requires clearing the password field. Replace the parameter dialog with an invocation of createDialog() to see that creating the dialog each time does not require clearing the password field. Comparing the profile of each approach may help you decide which approach is acceptable; in my experiments, reuse added negligible memory overhead (~250 KB), and it protracted garbage collection slightly(~50 ms).
#!/bin/sh
java … DialogTest -reuse &
pid1=$!
java … DialogTest -no-reuse &
pid2=$!
echo $pid1 $pid2
jconsole $pid1 $pid2
Unfortunately, creating the dialog each time may only appear to solve the problem; it may have exposed a latent synchronization problem. In particular, verify that your result converter's callback executes on the JavaFX Application Thread. To illustrate, I've added a call to Platform.isFxApplicationThread() in resultsNotPresent() below.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/q/73328282/230513
* #see https://stackoverflow.com/a/44172143/230513
*/
public class DialogTest extends Application {
private static boolean REUSE_DIALOG = true;
private record Results(String text, String pass) {
private static Results of(String text, String pass) {
return new Results(text, pass);
}
}
#Override
public void start(Stage stage) {
var label = new Label("Reuse: " + REUSE_DIALOG);
var button = new Button("Button");
if (REUSE_DIALOG) {
var dialog = createDialog();
button.setOnAction(e -> showDialog(dialog));
} else {
button.setOnAction(e -> showDialog(createDialog()));
}
stage.setScene(new Scene(new HBox(8, label, button)));
stage.show();
}
private Dialog<Results> createDialog() {
var dialog = new Dialog<Results>();
dialog.setTitle("Dialog Test");
dialog.setHeaderText("Please authenticate…");
var dialogPane = dialog.getDialogPane();
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
var text = new TextField("Name");
var pass = new PasswordField();
dialogPane.setContent(new VBox(8, text, pass));
dialog.showingProperty().addListener((o, wasShowing, isShowing) -> {
if (isShowing) {
Platform.runLater(pass::requestFocus);
}
});
dialog.setResultConverter((ButtonType bt) -> {
if (ButtonType.OK == bt) {
var results = Results.of(text.getText(), pass.getText());
pass.clear();
return results;
}
pass.clear();
return null;
});
return dialog;
}
private void showDialog(Dialog<Results> dialog) {
var optionalResult = dialog.showAndWait();
optionalResult.ifPresentOrElse(
(var results) -> System.out.println(results),
(this::resultsNotPresent));
}
private void resultsNotPresent() {
System.out.println("Canceled on FX application thread: "
+ Platform.isFxApplicationThread());
}
public static void main(String[] args) {
if (args.length > 0) {
REUSE_DIALOG = args[0].startsWith("-r");
}
launch(args);
}
}

Synchronize scrollbars of two JavaFx WebViews

I'm using two WebViews to display two versions of HTML formatted text for comparison. The two display the same amount of text (same number of lines and corresponding lines have always the same length).
When the displayed text exceeds the size of the node, the WebView gets scroll bars. Of course I want these scroll bars to scroll synchronously so that always the corresponding text is displayed.
In order to supply a minimal, complete and verifiable example, I trimmed the code down to this:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.GridPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class SynchronizedWebViewsTest extends Application {
protected class DifferencePanel extends GridPane {
private WebView actualPane;
private WebView expectedPane;
public DifferencePanel() {
setPadding(new Insets(20, 20, 20, 20));
actualPane = new WebView();
expectedPane = new WebView();
setResultPanes();
addRow(0, actualPane, expectedPane);
}
public void setHtml(WebView webView) {
Platform.runLater(() -> {
webView.getEngine().loadContent(createHtml());
});
}
public void synchronizeScrolls() {
final ScrollBar actualScrollBarV = (ScrollBar)actualPane.lookup(".scroll-bar:vertical");
final ScrollBar expectedScrollBarV = (ScrollBar)expectedPane.lookup(".scroll-bar:vertical");
actualScrollBarV.valueProperty().bindBidirectional(expectedScrollBarV.valueProperty());
final ScrollBar actualScrollBarH = (ScrollBar)actualPane.lookup(".scroll-bar:horizontal");
final ScrollBar expectedScrollBarH = (ScrollBar)expectedPane.lookup(".scroll-bar:horizontal");
actualScrollBarH.valueProperty().bindBidirectional(expectedScrollBarH.valueProperty());
}
private String createHtml() {
StringBuilder sb = new StringBuilder(1000000);
for (int i = 0; i < 100; i++) {
sb.append(String.format("<nobr>%03d %2$s%2$s%2$s%2$s%2$s%2$s%2$s%2$s</nobr><br/>\n",
Integer.valueOf(i), "Lorem ipsum dolor sit amet "));
}
return sb.toString();
}
private void setResultPanes() {
setHtml(actualPane);
setHtml(expectedPane);
}
} // ---------------------------- end of DifferencePanel ----------------------------
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage dummy) throws Exception {
Stage stage = new Stage();
stage.setTitle(this.getClass().getSimpleName());
DifferencePanel differencePanel = new DifferencePanel();
Scene scene = new Scene(differencePanel);
stage.setScene(scene);
differencePanel.synchronizeScrolls();
stage.showAndWait();
}
}
I tried using adding a listener:
actualScrollBarV.onScrollFinishedProperty().addListener(event -> {
System.out.println(event);
});
But the listener is never invoked.
I'm using Java version 1.8.0_92, but with version 9.0.4 I get the same result.
Can anybody tell me, what I'm missing here?
I would post a comment, but sadly I did not have enough reputation.
Did you tried the following solution? Create listeners on value changed event, instead of binding. Synchronizing two scroll bars JavaFX
I could not get the ScrollBar approach working. It turned out that the listeners were actually invoked (breakpoints in lambdas are not always working?). Setting the scroll bar value of the other WebView did not get it inclined change the scroll bar or the view port. :-(
There is something strange going on with events in WebView; that might be because there is a native library involved...
However, the approach using the event handler of WebView works. The event handler of each WebView simply mirrors all events to the other WebView, using a synchronizing field Boolean scrolling to avoid recursion.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class SynchronizedWebViewsTest extends Application {
protected class DifferencePanel extends GridPane {
private Boolean scrolling = Boolean.FALSE;
private WebView actualPane;
private WebView expectedPane;
public DifferencePanel() {
setPadding(new Insets(20, 20, 20, 20));
actualPane = new WebView();
expectedPane = new WebView();
setResultPanes();
addRow(0, actualPane, expectedPane);
}
public void setHtml(WebView webView) {
Platform.runLater(() -> {
webView.getEngine().loadContent(createHtml());
});
}
public void synchronizeScrolls() {
wireViews(actualPane, expectedPane);
wireViews(expectedPane, actualPane);
}
private void wireViews(WebView webView, WebView otherWebView) {
webView.addEventHandler(Event.ANY, event -> {
if (!scrolling.booleanValue()) {
synchronized (scrolling) {
scrolling = Boolean.TRUE;
if (event instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) event;
Point2D origin = webView.localToScreen(0, 0);
Point2D otherOrigin = otherWebView.localToScreen(0, 0);
double offsetX = otherOrigin.getX() - origin.getX();
double offsetY = otherOrigin.getY() - origin.getY();
double x = mouseEvent.getX();
double y = mouseEvent.getY();
double screenX = mouseEvent.getScreenX() + offsetX;
double screenY = mouseEvent.getScreenY() + offsetY;
MouseButton button = mouseEvent.getButton();
int clickCount = mouseEvent.getClickCount();
boolean shiftDown = mouseEvent.isShiftDown();
boolean controlDown = mouseEvent.isControlDown();
boolean altDown = mouseEvent.isAltDown();
boolean metaDown = mouseEvent.isMetaDown();
boolean primaryButtonDown = mouseEvent.isPrimaryButtonDown();
boolean middleButtonDown = mouseEvent.isMiddleButtonDown();
boolean secondaryButtonDown = mouseEvent.isSecondaryButtonDown();
boolean synthesized = mouseEvent.isSynthesized();
boolean popupTrigger = mouseEvent.isPopupTrigger();
boolean stillSincePress = mouseEvent.isStillSincePress();
MouseEvent otherMouseEvent =
new MouseEvent(otherWebView, otherWebView, mouseEvent.getEventType(), x, y, screenX,
screenY, button, clickCount, shiftDown, controlDown, altDown, metaDown,
primaryButtonDown, middleButtonDown, secondaryButtonDown, synthesized,
popupTrigger, stillSincePress, null);
otherWebView.fireEvent(otherMouseEvent);
}
else {
otherWebView.fireEvent(event.copyFor(otherWebView, otherWebView));
}
scrolling = Boolean.FALSE;
}
}
});
}
private String createHtml() {
StringBuilder sb = new StringBuilder(1000000);
for (int i = 0; i < 100; i++) {
sb.append(String.format("<nobr>%03d %2$s%2$s%2$s%2$s%2$s%2$s%2$s%2$s</nobr><br/>\n",
Integer.valueOf(i), "Lorem ipsum dolor sit amet "));
}
return sb.toString();
}
private void setResultPanes() {
setHtml(actualPane);
setHtml(expectedPane);
}
}
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage dummy) throws Exception {
Stage stage = new Stage();
stage.setTitle(this.getClass().getSimpleName());
DifferencePanel differencePanel = new DifferencePanel();
Scene scene = new Scene(differencePanel);
stage.setScene(scene);
differencePanel.synchronizeScrolls();
stage.showAndWait();
}
}
This works for all input methods I'm interested in:
Keyboard: PageUp, PageDown, all 4 arrow keys, "space bar" (same as PageDown) and shift-"space bar" (same as PageUp), Home and End
Mouse wheel: RollDown and RollUp as well as shift-RollUp (scroll left) and shift-RollDown (scroll right)
Using the mouse to click or to drag the scroll bar.
Using the mouse to select text outside of the current view port.
Mirroring the mouse events has the added benefit that text gets selected in both WebViews.

Show non-deletable text before input text in JavaFX text field

I am currently trying to build an application which behaves similar to a command shell. I want to display a path that I give it (or a '>' character at the very least) before the user's input text in a javaFX text field. like this:
I have it so that the text field will clear when the user submits the text. After a submission it sets the text of the field to be my path to achieve a similar effect, but the user can still delete this path while inputting text.
How can I make it so that my path text appears in the field but the user cannot delete it?
I've tried this but it only updates the caret position after submission:
textField.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
textField.positionCaret(textField.getLength());
}
});
You can use a TextFormatter to filter out invalid operations on the text field. A TextFormatter has a filter which filters changes to the text field; you can veto any changes by having the filter return null. The simplest implementation for what you describe would just filter out any changes where the caret position or the anchor for the text field were before the end of the fixed text:
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.getCaretPosition() < prefix.length() || c.getAnchor() < prefix.length()) {
return null ;
} else {
return c ;
}
};
textField.setTextFormatter(new TextFormatter<String>(filter));
You can experiment with other logic here (for example if you want the user to be able to select the fixed text).
Here is a SSCCE:
import java.util.function.UnaryOperator;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TextFieldFixedPrefix extends Application {
private TextField createFixedPrefixTextField(String prefix) {
TextField textField = new TextField(prefix);
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.getCaretPosition() < prefix.length() || c.getAnchor() < prefix.length()) {
return null ;
} else {
return c ;
}
};
textField.setTextFormatter(new TextFormatter<String>(filter));
textField.positionCaret(prefix.length());
return textField ;
}
#Override
public void start(Stage primaryStage) {
TextField textField = createFixedPrefixTextField("/home/currentUser $ ");
StackPane root = new StackPane(textField);
Scene scene = new Scene(root, 300,40);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Disable all MouseEvents on the Children of a Pane

I have a Pane in which i add and remove nodes during a computation. Therefor i save a boolean which is set to true if the computation is running. of course i do some handling on starting and terminating a computation.
What i want to do now is: disable all MouseEvents on the children of the Pane if the computation starts and reenable them if the computation is terminated.
My tries until now where limited to completly remove the EventHandlers, but then i can't add them again later.
unfortunately i couldn't find a way to do this, so i hope for help here :)
Thanks in advance
Assuming you have implemented the long-running computation as a Task or Service (and if you haven't, you should probably consider doing so), you can just do something along the following lines:
Pane pane ;
// ...
Task<ResultType> computation = ... ;
pane.disableProperty().bind(computation.runningProperty());
new Thread(computation).start();
Calling setDisable(true) on a node will disable all its child nodes, so this will disable all the children of the pane, and re-enable them when the task is no longer running.
Here's an SSCCE:
import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class ComputationSimulation extends Application {
#Override
public void start(Stage primaryStage) {
// text fields for input:
TextField xInput = new TextField();
TextField yInput = new TextField();
// Service for performing the computation.
// (For demo here, the computation just computes the sum of
// the two input values. Obviously this doesn't take long, so
// a random pause is inserted.)
Service<Integer> service = new Service<Integer>() {
#Override
protected Task<Integer> createTask() {
final int x = readTextField(xInput);
final int y = readTextField(yInput);
return new Task<Integer>() {
#Override
public Integer call() throws Exception {
// simulate long-running computation...
Thread.sleep((int)(Math.random() * 2000) + 1000);
// this doesn't really take much time(!):
return x + y ;
}
};
}
};
// Label to show result. Just use binding to bind to value of computation:
Label result = new Label();
result.textProperty().bind(service.valueProperty().asString());
// Button starts computation by restarting service:
Button compute = new Button("Compute");
compute.setOnAction(e -> service.restart());
// Pane to hold controls:
GridPane pane = new GridPane();
// Disable pane (and consequently all its children) when computation is running:
pane.disableProperty().bind(service.runningProperty());
// layout etc:
pane.setHgap(5);
pane.setVgap(10);
pane.addRow(0, new Label("x:"), xInput);
pane.addRow(1, new Label("y:"), yInput);
pane.addRow(2, new Label("Total:"), result);
pane.add(compute, 1, 3);
ColumnConstraints left = new ColumnConstraints();
left.setHalignment(HPos.RIGHT);
left.setHgrow(Priority.NEVER);
pane.getColumnConstraints().addAll(left, new ColumnConstraints());
pane.setPadding(new Insets(10));
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
// converts text in text field to an int if possible
// returns 0 if not valid text, and sets text accordingly
private int readTextField(TextField text) {
try {
return Integer.parseInt(text.getText());
} catch (NumberFormatException e) {
text.setText("0");
return 0 ;
}
}
public static void main(String[] args) {
launch(args);
}
}

Wait for and then receive textfield input without freezing GUI

I hope I'm not duplicating a question, but I couldn't find one specifically for my issue.
I'm developing a small math flash card application, using JavaFX to create the GUI. The program should runs as follow:
user selects settings, then presses start button.
gui displays question and textfield for user input.
user inputs answer within X amount of seconds or gui automatically move onto the next question - alternatively, user can move onto next question immediately by pressing next button.
GUI displays score and average.
The problems is getText() from user textfield is processed as soon as start button is pressed, without giving the user a chance to enter an answer. How do I make the program wait for X amount of seconds or for the next button to be clicked before processing the user's answer? Here's my code:
//start button changes view and then runs startTest()
start.setOnAction(e -> {
setLeft(null);
setRight(null);
setCenter(test_container);
running_program_title.setText(getDifficulty().name() + " Test");
buttons_container.getChildren().clear();
buttons_container.getChildren().addAll(next, quit, submit);
startTest();
});
Here is the problem code... at least how I see it.
//startTest method calls askAdd() to ask an addition question
void startTest() {
int asked = 0;
int correct = 0;
while (asked < numberOfQuestions) {
if(askAdd()){
correct++;
asked++;
}
}
boolean askAdd() {
int a = (int) (Math.random() * getMultiplier());
int b = (int) (Math.random() * getMultiplier());
//ask question
question.setText("What is " + a + " + " + b + "?");
//code needed to pause method and wait for user input for X seconds
//retrieve user answer and return if its correct
return answer.getText().equalsIgnoreCase(String.valueOf(a+b));
}
I've tried using Thread.sleep(X) but that freezes the gui for however long I specify and then goes through the addAsk() method and the loop before going to the test screen. (I know because I had the program set up to print the questions and answer input to the console). It shows the last question and that's all.
I didn't include the next button code because I can't get the gui to go to the test page anyway.
Any help on any of the code is appreciated.
This can be achieved by various methods.
PauseTransition is one of the many apt solution present. It waits for X time interval and then performs a Task. It can start, restart, stop at any moment.
Here is an example of how it can used to achieve a similar result.
Complete Code
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.stream.IntStream;
public class Main extends Application {
int questionIndex = 0;
int noOfQuestions = 10;
#Override
public void start(Stage stage) {
VBox box = new VBox(10);
box.setPadding(new Insets(10));
Scene scene = new Scene(new ScrollPane(box), 500, 200);
ObservableList<String> questions =
FXCollections.observableArrayList("1) Whats your (full) name?",
"2) How old are you?",
"3) Whats your Birthday?",
"4) What starsign does that make it?",
"5) Whats your favourite colour?",
"6) Whats your lucky number?",
"7) Do you have any pets?",
"8) Where are you from?",
"9) How tall are you?",
"10) What shoe size are you?");
ObservableList<String> answers = FXCollections.observableArrayList();
final PauseTransition pt = new PauseTransition(Duration.millis(5000));
Label questionLabel = new Label(questions.get(questionIndex));
Label timerLabel = new Label("Time Remaining : ");
Label time = new Label();
time.setStyle("-fx-text-fill: RED");
TextField answerField = new TextField();
Button nextQuestion = new Button("Next");
pt.currentTimeProperty().addListener(new ChangeListener<Duration>() {
#Override
public void changed(ObservableValue<? extends Duration> observable, Duration oldValue, Duration newValue) {
time.setText(String.valueOf(5 - (int)newValue.toSeconds()));
}
});
box.getChildren().addAll(questionLabel, answerField, new HBox(timerLabel, time), nextQuestion);
nextQuestion.setOnAction( (ActionEvent event) -> {
answers.add(questionIndex, answerField.getText());
//Check if it is the last question
if(questionIndex == noOfQuestions-1) {
pt.stop();
box.getChildren().clear();
IntStream.range(0, noOfQuestions).forEach(i -> {
Label question = new Label("Question : " + questions.get(i));
question.setStyle("-fx-text-fill: RED");
Label answer = new Label("Answer : " + answers.get(i));
answer.setStyle("-fx-text-fill: GREEN");
box.getChildren().addAll(question, answer);
});
}
// All other time
else {
//Set new question
questionLabel.setText(questions.get(++questionIndex));
answerField.clear();
pt.playFromStart();
}
});
pt.setOnFinished( ( ActionEvent event ) -> {
nextQuestion.fire();
});
pt.play();
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
For the timer you should (IMO) use a Timeline. Here is an example:
public class MultiGame extends Application {
ProgressBar progressBar;
final int allowedTime = 5; //seconds
final DoubleProperty percentOfTimeUsed = new SimpleDoubleProperty(0);
final Timeline timer =
new Timeline(
new KeyFrame(
Duration.ZERO, new KeyValue(percentOfTimeUsed, 0)),
new KeyFrame(
Duration.seconds(allowedTime), new KeyValue(percentOfTimeUsed, 1))
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
progressBar = new ProgressBar();
progressBar.progressProperty().bindBidirectional(percentOfTimeUsed);
root.setTop(progressBar);
Button answer = new Button("Answer");
answer.setOnAction(ae -> restart());// the on answer handler
Button skip = new Button("Skip");
skip.setOnAction(ae -> restart());// the skip question handler
HBox mainContent = new HBox(15,
new Label("Your Question"), new TextField("The answer"), answer, skip);
root.setCenter(mainContent);
timer.setOnFinished(ae -> restart());// the end of timer handler
primaryStage.setScene(new Scene(root));
primaryStage.show();
restart();
}
void restart() { timer.stop(); timer.playFromStart(); }
void pause() { timer.pause(); }
void resume() { timer.play(); }
}
You just need to capture the text from the input in between the starting of the timeline and the restart method.

Categories

Resources