How do I set a new scene from within FXML Controller - java

I am brand-new to programming and only started learning this past 2 weeks, so I'm sorry for any redundant or sloppy code...
I have 2 scenes, which are in my Main class. But I'm using FXML to develop each scene, and all code has been placed in the first scene's FXML Controller. I'm ready to start building my second scene, but don't know how to properly launch it.
My question is, how can I set the stage to show the second scene (mainCallWindow), specifically from within the first FXML file's controller class. If there is a bettery way, please let me know.
Main Class:
package supportTool;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
public String versionNumber = "v2.1";
#Override
public void start(Stage primaryStage) throws Exception {
// SETTING UP THE STAGE
Stage window;
window = primaryStage;
window.setTitle("Support Tool " + versionNumber);
// SETTING UP THE SCENES
Parent newCallDetailsFXML = FXMLLoader.load(getClass().getResource("newCallDetails.fxml"));
Parent mainCallWindowFXML = FXMLLoader.load(getClass().getResource("mainCallWindow.fxml"));
Scene newCallDetails = new Scene (newCallDetailsFXML, 800, 600);
Scene mainCallWindow = new Scene (mainCallWindowFXML, 800, 600);
// CHOOSING THE SCENE AND SHOWING THE STAGE
window.setScene(newCallDetails);
window.show();
}
}
Scene 1 FXML Controller:
package supportTool;
import javafx.scene.control.*;
import javafx.scene.image.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class newCallController {
private int maxChar;
public ChoiceBox choiceAccount;
public ImageView btnCall;
public TextField tfCallbackNumber;
public TextField tfCallerName;
public TextField tfStoreNumber;
// ACTION COMPLETED WHEN CALL BUTTON IS PRESSED
public void btnCall() {
Caller newCaller = new Caller();
newCaller.setCallerName(tfCallerName.getText());
newCaller.setCallbackNumber(tfCallbackNumber.getText());
newCaller.setAccount(String.valueOf(choiceAccount.getValue()));
newCaller.setStoreNumber(tfStoreNumber.getText());
try {
FileOutputStream fos = new FileOutputStream("caller.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(newCaller);
oos.close();
fos.close();
} catch(IOException e){
e.printStackTrace();
}
// RIGHT HERE IS WHERE I WANT TO SET THE SCENE TO "mainCallWindow"
}
// CHECKS TO SEE IF THE TEXT CONTAINS ONLY LETTERS
private boolean isNumberCheckEvent(String message) {
if (message.matches("[0-9]+")) {
return true;
} else {
return false;
}
}
// SETS THE MAX CHARACTERS FOR ALL TEXTFIELDS
public void maxCharEvent() {
// CALLER NAME MAX CHARACTERS
tfCallerName.setOnKeyTyped(maxCharEvent -> {
maxChar = 20;
if(tfCallerName.getText().length() >= maxChar) {
maxCharEvent.consume();
}
});
// CALLBACK NUMBER MAX CHARACTERS
tfCallbackNumber.setOnKeyTyped(maxCharEvent -> {
maxChar = 10;
if(tfCallbackNumber.getText().length() >= maxChar) {
maxCharEvent.consume();
}
});
// STORE NUMBER MAX CHARACTERS
tfStoreNumber.setOnKeyTyped(maxCharEvent -> {
maxChar = 5;
if (String.valueOf(choiceAccount.getValue()).equals("6 Digit Account")) {
maxChar = 6;
}
if (tfStoreNumber.getText().length() >= maxChar) {
maxCharEvent.consume();
}
});
}
// CHANGES TEXT TO ONLY LETTERS BASED ON isNumberCheckEvent
public void numberValidationEvent() {
tfCallbackNumber.setOnKeyReleased(numberValidationEvent -> {
maxCharEvent();
if(tfCallbackNumber.getText().length() > 0) {
if (!isNumberCheckEvent(tfCallbackNumber.getText())) {
tfCallbackNumber.setText(tfCallbackNumber.getText().substring(0, tfCallbackNumber.getText().length() - 1));
tfCallbackNumber.positionCaret(10);
numberValidationEvent.consume();
}
}
});
tfStoreNumber.setOnKeyReleased(numberValidationEvent -> {
maxCharEvent();
if(tfStoreNumber.getText().length() > 0) {
if (!isNumberCheckEvent(tfStoreNumber.getText())) {
tfStoreNumber.setText(tfStoreNumber.getText().substring(0, tfStoreNumber.getText().length() - 1));
tfStoreNumber.positionCaret(10);
numberValidationEvent.consume();
}
}
});
}
}

You can change scene in various ways. Within your current situation you can try something like below. First, you need reference to your FXMLLoader, scene and stage to change your scene from your controller. Instead of loading in main class, do loading in your controller class.
FXMLLoader loader = new FXMLLoader(getClass().getResource("mainCallWindow.fxml"));
Parent mainCallWindowFXML = loader.load();
//use one of components on your scene to get a reference to your scene object.
Stage stage = (Stage)tfCallerName.getScene.getWindow();//or use any other component in your controller
Scene mainCallWindow = new Scene (mainCallWindowFXML, 800, 600);
stage.setScene(newCallDetails);
stage.show(); //this line may be unnecessary since you are using the same stage.
}
This is not the only way to achieve this. You can use same scene to load different FXML files. I would suggest changing the root node of a scene instead of changing scene completely.

Related

JavaFX - Why does changing the header text of a dialog make the button bar invisible?

Here is a small sample from my custom dialog, which is meant to display the progress of a running javafx.concurrent.Task.
DialogPane pane = this.getDialogPane()
pane.getButtonTypes().addAll(ButtonType.CANCEL);
pane.headerTextProperty().bind(task.titleProperty());
pane.contentTextProperty().bind(task.messageProperty());
For some reason, the buttons in the button bar disappeared, but only after some text updated. After further investigation, I found that binding the header text of a dialog seems to remove all the buttons in the button bar. Why would this happen, and what would I do to stop the buttons from being hidden?
EDIT: Here's an MCVE demonstrating the problem.
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
public class MCVE extends Application {
public static class CustomDialog extends Dialog<ButtonType> {
public CustomDialog(Task<?> task) {
this.initModality(Modality.APPLICATION_MODAL);
DialogPane pane = this.getDialogPane();
{
pane.getButtonTypes().addAll(ButtonType.CANCEL);
pane.headerTextProperty().bind(task.titleProperty());
}
setOnCloseRequest(event -> {
if (task.isRunning()) event.consume();
});
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
StackPane root = new StackPane();
Button starter = new Button("Showcase");
starter.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
root.getChildren().add(starter);
Scene scene = new Scene(root, 500, 500);
stage.setScene(scene);
stage.show();
starter.setOnAction(event -> {
Task<Void> task = new Task<>() {
#Override
protected Void call() throws Exception {
updateTitle("Before loop");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
if (isCancelled()) return null;
}
for (int i = 1; i <= 20; i++) {
updateTitle("loop " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
if (isCancelled()) return null;
}
}
return null;
}
};
Thread worker = new Thread(task);
worker.start();
new CustomDialog(task).showAndWait();
});
}
}
It appears that the issue needs fixing on JavaFX's end. In fact, the button bar is simply pushed out of view. There is a relatively decent workaround though. Dialog internally uses a styled GridPane to display header text and graphics, so I simply replicated that using an external GridPane and instead bound to the textProperty() of a Label.
public static class CustomDialog extends Dialog<ButtonType> {
public CustomDialog(Task<?> task) {
DialogPane pane = this.getDialogPane(); {
pane.getButtonTypes().addAll(ButtonType.CANCEL);
//construct custom header
GridPane headerRoot = new GridPane(); {
Label headerText = new Label();
//headerText is the label containing the header text
headerText.textProperty().bind(task.titleProperty());
headerRoot.add(headerText, 0, 0);
}
headerRoot.getStyleClass().addAll("header-panel");
pane.setHeader(headerRoot);
pane.setContentText("Placeholder content");
pane.getScene().getWindow().setOnCloseRequest(event -> {
if (!task.isDone()) {
event.consume();
}
});
}
}
It looks exactly the same as a default dialog, without the issue mentioned in the question above.

Java Processing 3 PAplet in JavaFX scene as FXNode

I am trying to make a program for visual analyzing Fractal sets. I choose Processing 3 as drawing library and JavaFX for the user interface. There are some screenshots of the current state:
My GUI:
there is Launcher code:
import Graphics.Canvas2D;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import processing.core.PGraphics;
import java.io.IOException;
public class Launcher extends Application {
private static Stage primaryStage;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Parent root = loadFXML("MainUI.fxml");
Scene scene = new Scene(root, 500, 400);
primaryStage.setTitle("Fractal Analyzer");
primaryStage.setScene(scene);
primaryStage.show();
primaryStage.setMaximized(true);
Launcher.primaryStage = primaryStage;
}
#Override
public void init() {
}
#Override
public void stop() {
System.exit(0);
}
public static Stage getPrimaryStage() {
return primaryStage;
}
public void setCanvas(Canvas2D canvas){
}
private Parent loadFXML(String path) {
try {
return FXMLLoader.load(getClass().getResource(path));
} catch (IOException e) {
e.printStackTrace();
}
System.exit(1);
return null;
}
}
Testing fractal PAplet:
There is a code of this PAplet:
package Fractal;
import processing.core.PApplet;
public class SirpenskiTriangle extends PApplet {
public static void main(String[] args) {
PApplet.main("Fractal.SirpenskiTriangle");
}
public void settings() {
size(640, 640);
smooth();
if (frame != null) {
frame.setResizable(true);
}
}
public void draw() {
drawTriangle(new Position(300, 20), new Position(620, 620), new Position(20, 620), 0);
noLoop();
scale(10f);
}
public void setup(){}
public void drawTriangle(Position top, Position right, Position left, int depth) {
if (depth > 10) return;
line(top.x, top.y, right.x, right.y);
line(right.x, right.y, left.x, left.y);
line(left.x, left.y, top.x, top.y);
drawTriangle(top, top.middleWith(right), top.middleWith(left), depth + 1);
drawTriangle(top.middleWith(left), left.middleWith(right), left, depth + 1);
drawTriangle(top.middleWith(right), right, left.middleWith(right), depth + 1);
}
class Position {
final float x;
final float y;
Position(float x, float y) {
this.x = x;
this.y = y;
}
Position middleWith(Position other) {
return new Position((x + other.x) / 2, (y + other.y) / 2);
}
}
}
Is there any way to put processing PAplet into JavaFX scene like canvas or something similar?
I hope it can work like this, but this code is invalid:
I have devised two approaches: in the first, we bypass Processing's JavaFX stage creation and point Processing to draw into a JavaFX stage loaded from an FXML file; in the second, we replace Processing's default JavaFX scene with one loaded from an FXML file during runtime.
1. Launching from an FXML
With the first approach we launch the application like we would a JavaFX app (using Application.launch(Launcher.class);), completely bypassing Processing's JavaFX stage creation code.
You'll have to download a slightly modified core.jar for this approach to work, where I've changed the visibility of a few members of the PSurfaceFX and PGraphicsFX2D classes from Protected to Public. The changes allow us to launch JavaFX from our own ... extends Application class, while maintaining access to the members that Processing needs to set during the launch to function.
Processing 3 crashes in FX2D mode when the JDK in use is above Java 8, so I've also made a working version for 8+, since the FXML files usually need at least Java 9 to work.
Download core.jar (Java 8 & below)
Download core.jar (Above Java 8)
This is the FXML file I am working with in this example:
With the modified core.jar added to your project's classpath, override initSurface() in your PApplet class with the following snippet. With this code, we bypass the PApplet's call to initFrame() - this is where processing creates its own JavaFX stage, which we do not want it to do.
#Override
protected PSurface initSurface() {
g = createPrimaryGraphics();
PSurface genericSurface = g.createSurface();
PSurfaceFX fxSurface = (PSurfaceFX) genericSurface;
fxSurface.sketch = this;
Launcher.surface = fxSurface;
new Thread(new Runnable() {
public void run() {
Application.launch(Launcher.class);
}
}).start();
while (fxSurface.stage == null) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
}
}
this.surface = fxSurface;
return fxSurface;
}
Set the PApplet's renderering mode to FX2D like so:
#Override
public void settings() {
size(0, 0, FX2D);
}
Put the following, or similar, in your Launcher class. In this example, I have manually found the Node that I want to add the canvas object into. There are better, more programmatic, ways of doing this (such as .lookup() using the fx:id of the desired node -- this can be defined in the FXML file). I have also bound the dimensions of the canvas to those of its parent, so when the divisor separating the Master and View panes is dragged, the Processing canvas resizes accordingly.
public class Launcher extends Application {
public static PSurfaceFX surface;
#Override
public void start(Stage primaryStage) throws Exception {
Canvas canvas = (Canvas) surface.getNative(); // boilerplate
GraphicsContext graphicsContext = canvas.getGraphicsContext2D(); // boilerplate
surface.fx.context = graphicsContext; // boilerplate
primaryStage.setTitle("FXML/Processing");
VBox root = FXMLLoader.load(new File("c:/Users/Mike/desktop/test.fxml").toURI().toURL());
SplitPane pane = (SplitPane) root.getChildren().get(1); // Manually get the item I want to add canvas to
AnchorPane pane2 = (AnchorPane) pane.getItems().get(0); // Manually get the item I want to add canvas to
pane2.getChildren().add(canvas); // Manually get the item I want to add canvas to
canvas.widthProperty().bind(pane2.widthProperty());
canvas.heightProperty().bind(pane2.heightProperty());
Scene scene = new Scene(root, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
surface.stage = primaryStage; // boilerplate
}
}
This is the result:
Also see this Github project -- a basic project showing how a Processing sketch and a FXML JavaFX stage may be integrated using this first approach, but includes a JavaFX Controller to populate #FXMLannotated fields (providing an easy way to first get, and then reference, JavaFX objects in code).
2. Launching, then loading a FXML
This approach works with vanilla Processing. Here, we launch Processing like normal and then replace the default scene with new scene loaded from an FXML file during runtime. This is a simpler approach (and doesn't require using a modified .jar!) but will make JavaFX/Processing interoperability more difficult because we can't use a JavaFX Controller to get fields via FXML injection.
Example PDE code:
import java.util.Map;
import java.nio.file.Paths;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.canvas.Canvas;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import processing.javafx.PSurfaceFX;
public void setup() {
size(800, 800, FX2D);
strokeWeight(3);
}
protected PSurface initSurface() {
surface = (PSurfaceFX) super.initSurface();
final Canvas canvas = (Canvas) surface.getNative();
final Scene oldScene = canvas.getScene();
final Stage stage = (Stage) oldScene.getWindow();
try {
FXMLLoader loader = new FXMLLoader(Paths.get("C:\\path--to--fxml\\stage.fxml").toUri().toURL()); // abs path to fxml file
final Parent sceneFromFXML = loader.load();
final Map<String, Object> namespace = loader.getNamespace();
final Scene newScene = new Scene(sceneFromFXML, stage.getWidth(), stage.getHeight(), false,
SceneAntialiasing.BALANCED);
final AnchorPane pane = (AnchorPane) namespace.get("anchorPane"); // get element by fx:id
pane.getChildren().add(canvas); // processing to stackPane
canvas.widthProperty().bind(pane.widthProperty()); // bind canvas dimensions to pane
canvas.heightProperty().bind(pane.heightProperty()); // bind canvas dimensions to pane
Platform.runLater(new Runnable() {
#Override
public void run() {
stage.setScene(newScene);
}
}
);
}
catch (IOException e) {
e.printStackTrace();
}
return surface;
}
public void draw() {
background(125, 125, 98);
ellipse(200, 200, 200, 200);
line(0, 0, width, height);
line(width, 0, 0, height);
}
Result:
…using this FXML file:
To make it work, you have to launch your Processing sketch, not the JavaFX Application.
Simply do
PApplet.main(Launcher.class.getName());
Also thanks so much for your help! I had NO idea how I should use the JavaFX stuff that comes with Processing!
Okay, this is my code, that works! I copied everything and changed the names.
!!!I have not tested this modified code, so don't copy paste everything!!!
The raw principle should definetly work though.
If you still have issues or questions, just comment.
Main
public class Main {
public static void main(String[] args) {
// Your code starts here, and runs Processing.
// This is also, how you normally start Processing sketches.
PApplet.main(Sketch.class.getName());
}
}
Sketch
public class Sketch extends PApplet{
#Override
public void settings() {
size(200, 200, FX2D); // Size doesn't really matter
}
#Override
public void setup() {
}
#Override
public void draw() {
}
// Processing uses this function to determine,
// how to display everything, how to open the canvas...
// We override the code, that would normally open a window with the normal Processing stuff,
// to open start new JavaFX application in a new Thread.
// micycle's code
#Override
protected PSurface initSurface() {
g = createPrimaryGraphics();
PSurface genericSurface = g.createSurface();
PSurfaceFX fxSurface = (PSurfaceFX) genericSurface;
fxSurface.sketch = this;
// Because the JavaFX App is being launched by reflection,
// we can't pass variables to it via constructor, so
// we have to access it in static context.
// Here, we give JavaFX the surface.
ExampleApp.surface = fxSurface;
// New thread started, so JavaFX and Processing don't interrupt each other.
new Thread(new Runnable() {
public void run() {
// JavaFX way of launching a new Application
Application.launch(ExampleApp.class);
}
}).start();
while (fxSurface.stage == null) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
}
}
this.surface = fxSurface;
return fxSurface;
}
}
ExampleApp
public class ExampleApp extends Application {
public Canvas canvas; // The Canvas you will be drawing to
public static PSurfaceFX surface; // The Processing surface
// JavaFX started, this method is being run to set everything up.
#Override
public void start(Stage primaryStage) {
// This sets up the canvas, and the drawing region.
canvas = (Canvas) surface.getNative();
surface.fx.context = canvas.getGraphicsContext2D();
surface.stage = primaryStage;
// I'm just loading my FXML file. You can do all JavaFX stuff via code, if you want
try {
// !!My root Container is a BorderPane!!
BorderPane root = FXMLLoader.load(getClass().getClassLoader().getResource("application.fxml"));
} catch(IOException e) {
e.printStackTrace();
}
// Getting the Anchor pane, that is in the center of my BorderPane
AnchorPane pane = (AnchorPane) root.getCenter();
// The Anchor pane is being used, so the canvas can fill the parent (Center)
// Canvases don't have a property to fill it's parent, like most Containers do (Because it isn't a container)
canvas.widthProperty().bind(pane.widthProperty());
canvas.heightProperty().bind(pane.heightProperty());
// Adding the canvas to your App
root.getChildren().add(canvas);
// Launching the Stage
primaryStage.setTitle("Example App");
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
Okay, since last time , I changed some elements.
The canvas's parent is now just a Pane instead of an AnchorPane.
The FXML won't help you much... It is just a BorderPane with another Pane in it, but alright...
<center>
<VBox prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
<children>
<Pane maxHeight="1.7976931348623157E308" VBox.vgrow="ALWAYS" />
</children>
</VBox>
So, what I'm doing is taking the Canvas element, Processing creates and just adding it to the Pane.

Thread communication: How to signal that a key was clicked? java.lang.IllegalMonitorStateException

i have a simple JavaFX stage with a TextField. What i want to do is: when user inserts letters into the TextField, i want to print "now" (just to look if it works). Im using a Thread because later i want to scan a dictonary to see, if the letters the user entered are part of words from the dictionary.
But i get: java.lang.IllegalMonitorStateException
Any ideas? I don't seem to understand the whole concept of Condition.await and Multithreading..
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DictionaryThreading extends Application {
private static Lock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
public static void main(String[] args) {
launch();
}
private static class ScanWords implements Runnable{
#Override
public void run() {
lock.lock();
try{
while(true){
this.wait();
System.out.println("clicked");
}
} catch (Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
#Override
public void start(Stage primaryStage) throws Exception {
StackPane pane = new StackPane();
new ScanWords().run();
TextField tf = new TextField("Please enter a word");
tf.setOnKeyPressed(e -> {});
pane.getChildren().add(tf);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
}
There is no need to create a thread that does nothing other than wait for user events. The JavaFX framework already provides this for you (it is one of the fundamental pieces of functionality of any UI toolkit). All you need to do to respond to changes in the text in a text field is register a change listener with the text field's text property:
public void start(Stage primaryStage) throws Exception {
StackPane pane = new StackPane();
TextField tf = new TextField("Please enter a word");
tf.textProperty().addListener((obs, oldText, newText) -> {
System.out.println("text changed");
});
pane.getChildren().add(tf);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
If the thing you need to do in response to the text changing takes a long time, then you should launch that process in a background thread in the listener on the text field. If you are searching something large, you probably want to cancel any existing search, so that you don't end up with a large number of searches all running concurrently. The JavaFX Service class provides the functionality you need for this:
public class SearchService extends Service<List<String>> {
// modify and access only on FX Application Thread:
private String searchString ;
#Override
protected Task<List<String>> createTask() {
final String s = searchString ;
return new Task<List<String>>() {
#Override
protected List<String> call() throws Exception {
List<String> matches = new ArrayList<>();
// do search for strings matching s
// be sure to check isCancelled() regularly
return matches ;
}
};
}
public String getSearchString() {
checkThread();
return searchString ;
}
public void setSearchString(String searchString) {
checkThread();
this.searchString = searchString ;
}
private void checkThread() {
if (! Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX Application Thread");
}
}
}
Then you can do
public void start(Stage primaryStage) throws Exception {
StackPane pane = new StackPane();
SearchService searchService = new SearchService();
searchService.setOnSucceeded(e -> {
List<String> matches = searchService.getValue();
// do whatever you need with search results...
// this is called on FX application thread
});
TextField tf = new TextField("Please enter a word");
tf.textProperty().addListener((obs, oldText, newText) -> {
searchService.cancel();
searchService.setSearchText(newText);
searchService.restart();
});
pane.getChildren().add(tf);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
I don't use JavaFX, but I think you need to use EventListener. Try to use TextListener or InputMethodListener. For example:
StackPane pane = new StackPane();
TextField tf = new TextField("Please enter a word");
tf.addTextListener(e -> System.out.println("Pushed"));
pane.getChildren().add(tf);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
wait method should be executed within synchronized block:
try{
while(true){
synchronized(this){
this.wait();
System.out.println("clicked");
}
}
} catch (Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}

Accessible screenreading of JavaFX "console" output

I'm trying to create a simple UI that is accessible for screenreaders. I've been mostly successful, but I can't manage to design the UI in a way that has the screenreader read new text output.
Currently, I have a TextArea displaying the output of an anonymous PrintStream created and set by System.setOut. Sometimes I open up a TextField for string inputs, but I've been working with just the TextArea to test the reading of text (for now it just listens for keystrokes to display more text for testing purposes).
The issue is this: when new text is added via System.out to the TextArea, the screenreader does not read it. I am still able to navigate upward with the arrow keys to read what was added but it is not read when first added. Is there any way to get the screenreader to treat my TextArea more like a standard console (in which it reads all new text automatically)? I'm using NVDA.
Things I have tried:
- Using TextArea.notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT)
- Using TextArea.requestFocus() and TextArea.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE)
- Disabling autoflush on the PrintStream while using TextArea.setAccessibleText(theNewText) during a flush
- Using a hidden Label set to the new text and focusing on it (I'm still fiddling with this one; Screenreaders can't read actual "hidden" text so I'm trying to find a way to draw it but also be "invisible", perhaps behind the TextArea somehow)
- Changing focus to another Node and back, which doesn't work as I like because it reads the other Nodes accessible stuff and then reads the entire body of the TextArea
- Various combinations of these
I just can't seem to get it to work. I feel like I'm missing something simple and obvious here, but the JavaFX Accessibility API is still relatively new and I can't find solutions to specific problems like this one.
Here's the relevant code of my Application, if it helps any:
#Override
public void start(Stage primaryStage) {
try {
primaryStage.setTitle("Test");
root = new BorderPane();
root.setFocusTraversable(false);
Scene scene = new Scene(root,800,600);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
//Create middle
output = new TextArea();
output.setEditable(false);
output.setFocusTraversable(false); //I've tried true also, just to test
output.setAccessibleRole(AccessibleRole.TEXT_AREA);
root.setCenter(output);
...
//Begin
primaryStage.show();
Thread th = new Thread(new AppMain());
th.start();
} catch(Exception e) {
e.printStackTrace();
}
}
#Override
public void init() {
//Set output to TextArea
System.setOut(new PrintStream(new OutputStream() {
#Override
public void write(int b) throws IOException {
appendTextArea(String.valueOf((char) b));
}
}, true)); //I've also overriden flush while this is false, see above
}
public void appendTextArea(String str) {
Platform.runLater(() -> {
output.appendText(str);
});
}
I seriously appreciate any help or suggestions you can provide. I've been messing with this small issue for way too long, and I'm still new to JavaFX. Thank you!
Here is a full working example based off of your code.
Disclosure: For the screen reader I'm using the "Voice Over Utility" on my Mac, but hopefully that doesn't differ too much from your environment.
The key was to utilize the Control#executeAccessibleAction method.
Example:
TextArea.executeAccessibleAction(AccessibleAction.SET_TEXT_SELECTION, start, finish);
The Application Class
package application;
import java.io.PrintStream;
import java.io.IOException;
import java.io.OutputStream;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleRole;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
public class Main extends Application
{
private TextArea output;
private BorderPane root;
private final StringBuilder STR_BUFFER = new StringBuilder();
private static final String NEW_LINE = System.lineSeparator();
#Override
public void start(Stage primaryStage)
{
try
{
primaryStage.setTitle("Test");
root = new BorderPane();
root.setFocusTraversable(false);
Scene scene = new Scene(root, 800, 600);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
// Create middle
output = new TextArea();
output.setEditable(false);
output.setFocusTraversable(true); // I've tried true also, just to test
// ----------------------------------------------
// Tell the Screen Reader what it needs to access
// ----------------------------------------------
output.setAccessibleRole(AccessibleRole.TEXT_AREA);
root.setCenter(output);
// ...
// Begin
primaryStage.show();
// start the thread
Thread th = new Thread(new AppMain());
th.start();
}
catch (Exception e)
{
e.printStackTrace();
}
}
#Override
public void init()
{
// Set output to TextArea when we have a full string
System.setOut(new PrintStream(new OutputStream()
{
#Override
public void write(int b) throws IOException
{
if (b == '\r')
{
return;
}
if (b == '\n')
{
final String text = STR_BUFFER.toString() + NEW_LINE;
appendTextArea(text);
STR_BUFFER.setLength(0);
}
else
{
STR_BUFFER.append((char) b);
}
}
}, true));
}
public void appendTextArea(String str)
{
Platform.runLater(() ->
{
int anchor = output.getText().length();
output.appendText(str);
// just to clear it
output.positionCaret(0);
// ----------------------------------------------
// Tell the Screen Reader what it needs to do
// ----------------------------------------------
output.executeAccessibleAction(AccessibleAction.SET_TEXT_SELECTION, anchor, anchor + str.length());
});
}
public static void main(String[] args)
{
launch(args);
}
}
The Thread Class
/*
* Just to simulate a feed to the console (textArea).
* This will die after 1 minute.
*/
package application;
public class AppMain implements Runnable
{
#Override
public void run()
{
int i = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 60000)
{
try
{
Thread.sleep(3000);
}
catch (InterruptedException e)
{}
System.out.println("This is line number " + ++i);
}
}
}

How to get the current opened stage in JavaFX?

Is there a way to get the current opened Stage in JavaFX, if there is one open?
Something like this:
Stage newStage = new Stage();
newStage.initOwner(JavaFx.getCurrentOpenedStage()); //Like this
Java 9 makes this possible by the addition of the javafx.stage.Window.getWindows() method. Therefore you can just get list of Windows and see which are showing
List<Window> open = Stage.getWindows().stream().filter(Window::isShowing);
If you need the current stage reference inside an event handler method, you can get it from the ActionEvent param. For example:
#FXML
public void OnButtonClick(ActionEvent event) {
Stage stage = (Stage)((Node) event.getSource()).getScene().getWindow();
(...)
}
You can also get it from any control declared in your controller:
#FXML
private Button buttonSave;
(...)
Stage stage = (Stage) buttonSave.getScene().getWindow();
There's no built-in functionality for this. In most use cases, you open a new Stage as a result of user action, so you can call getScene().getWindow() on the node on which the action occurred to get the "current" window.
In other use cases, you will have to write code to track current windows yourself. Of course, multiple windows might be open, so you need to track them in some kind of collection. I'd recommend creating a factory class to manage the stages and registering event handlers for the stages opening and closing, so you can update a property and/or list. You'd probably want this to be a singleton. Here's a sample implementation: here getOpenStages() gives an observable list of open stages - the last one is the most recently opened - and currentStageProperty() gives the focused stage (if any). Your exact implementation might be different, depending on your exact needs.
public enum StageFactory {
INSTANCE ;
private final ObservableList<Stage> openStages = FXCollections.observableArrayList();
public ObservableList<Stage> getOpenStages() {
return openStages ;
}
private final ObjectProperty<Stage> currentStage = new SimpleObjectProperty<>(null);
public final ObjectProperty<Stage> currentStageProperty() {
return this.currentStage;
}
public final javafx.stage.Stage getCurrentStage() {
return this.currentStageProperty().get();
}
public final void setCurrentStage(final javafx.stage.Stage currentStage) {
this.currentStageProperty().set(currentStage);
}
public void registerStage(Stage stage) {
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
openStages.add(stage));
stage.addEventHandler(WindowEvent.WINDOW_HIDDEN, e ->
openStages.remove(stage));
stage.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (isNowFocused) {
currentStage.set(stage);
} else {
currentStage.set(null);
}
});
}
public Stage createStage() {
Stage stage = new Stage();
registerStage(stage);
return stage ;
}
}
Note this only allows you to track stages obtained from StageFactory.INSTANCE.createStage() or created elsewhere and passed to the StageFactory.INSTANCE.registerStage(...) method, so your code has to collaborate with that requirement. On the other hand, it gives you the chance to centralize code that initializes your stages, which may be otherwise beneficial.
Here's a simple example using this:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
public class SceneTrackingExample extends Application {
int count = 0 ;
#Override
public void start(Stage primaryStage) {
StageFactory factory = StageFactory.INSTANCE ;
factory.registerStage(primaryStage);
configureStage(primaryStage);
primaryStage.show();
}
private void configureStage(Stage stage) {
StageFactory stageFactory = StageFactory.INSTANCE;
Stage owner = stageFactory.getCurrentStage() ;
Label ownerLabel = new Label();
if (owner == null) {
ownerLabel.setText("No owner");
} else {
ownerLabel.setText("Owner: "+owner.getTitle());
stage.initOwner(owner);
}
stage.setTitle("Stage "+(++count));
Button newStage = new Button("New Stage");
newStage.setOnAction(e -> {
Stage s = stageFactory.createStage();
Stage current = stageFactory.getCurrentStage() ;
if (current != null) {
s.setX(current.getX() + 20);
s.setY(current.getY() + 20);
}
configureStage(s);
s.show();
});
VBox root = new VBox(10, ownerLabel, newStage);
root.setAlignment(Pos.CENTER);
stage.setScene(new Scene(root, 360, 150));
}
public enum StageFactory {
INSTANCE ;
private final ObservableList<Stage> openStages = FXCollections.observableArrayList();
public ObservableList<Stage> getOpenStages() {
return openStages ;
}
private final ObjectProperty<Stage> currentStage = new SimpleObjectProperty<>(null);
public final ObjectProperty<Stage> currentStageProperty() {
return this.currentStage;
}
public final javafx.stage.Stage getCurrentStage() {
return this.currentStageProperty().get();
}
public final void setCurrentStage(final javafx.stage.Stage currentStage) {
this.currentStageProperty().set(currentStage);
}
public void registerStage(Stage stage) {
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
openStages.add(stage));
stage.addEventHandler(WindowEvent.WINDOW_HIDDEN, e ->
openStages.remove(stage));
stage.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (isNowFocused) {
currentStage.set(stage);
} else {
currentStage.set(null);
}
});
}
public Stage createStage() {
Stage stage = new Stage();
registerStage(stage);
return stage ;
}
}
public static void main(String[] args) {
launch(args);
}
}
You can create a label in your java fxml.
Then in your controller class refer your label like this :
#FXML
private Label label;
Then in any function of the controller class you can access the current stage by this block of code :
private void any_function(){
Stage stage;
stage=(Stage) label.getScene().getWindow();
}

Categories

Resources