I'm trying to make a simple Java transaction app with JavaFX as UI.
What I want to do now is to detect user idle state from my application which has 1 primary stage and many scenes.
Example : if user idle for 3 minutes then go back to main menu.
I already try some examples on the web about how to detect JavaFX idle state, but what I found is always -one function idle state detection which is occuring all scenes- method which is (I think) dangerous for transaction app (ex : apps detect idle state in the middle of transaction process).
It's possible to detect user idle state on every single scene? how?
Thanks.
EDIT :
Examples that I already try :
http://tomasmikula.github.io/blog/2014/06/04/timers-in-javafx-and-reactfx.html
and
http://ochafik.com/blog/?p=98
I don't really understand the point you are making about transactional behavior. Transactions concern guarantees about the data, and your transactional behavior should be defined at the data level and should not be impacted by what is happening in the UI. In other words, your atomic behavior should complete or rollback even if the UI resets due to the user being idle.
Maybe this will help, though. (Note I used Java 8 code in these examples, but you can fairly easily make it JavaF 2.2 compliant if you need.) This follows Tomas Mikula's general approach in that it uses a Timeline to implement the idle check. I didn't use Tomas' FX Timer wrapper but you could certainly do so if you like. This class encapsulates a monitor for whether the user is idle. You can register any node (or scene) and type of event: if an event of that type occurs on that node (or scene), the user is determined not to be idle. If the specified time elapses without any registered events occurring, the provided runnable is executed (on the FX Application Thread). This gives you the flexibility to create multiple monitors, if needed, and to register one or more nodes with each.
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.util.Duration;
public class IdleMonitor {
private final Timeline idleTimeline ;
private final EventHandler<Event> userEventHandler ;
public IdleMonitor(Duration idleTime, Runnable notifier, boolean startMonitoring) {
idleTimeline = new Timeline(new KeyFrame(idleTime, e -> notifier.run()));
idleTimeline.setCycleCount(Animation.INDEFINITE);
userEventHandler = e -> notIdle() ;
if (startMonitoring) {
startMonitoring();
}
}
public IdleMonitor(Duration idleTime, Runnable notifier) {
this(idleTime, notifier, false);
}
public void register(Scene scene, EventType<? extends Event> eventType) {
scene.addEventFilter(eventType, userEventHandler);
}
public void register(Node node, EventType<? extends Event> eventType) {
node.addEventFilter(eventType, userEventHandler);
}
public void unregister(Scene scene, EventType<? extends Event> eventType) {
scene.removeEventFilter(eventType, userEventHandler);
}
public void unregister(Node node, EventType<? extends Event> eventType) {
node.removeEventFilter(eventType, userEventHandler);
}
public void notIdle() {
if (idleTimeline.getStatus() == Animation.Status.RUNNING) {
idleTimeline.playFromStart();
}
}
public void startMonitoring() {
idleTimeline.playFromStart();
}
public void stopMonitoring() {
idleTimeline.stop();
}
}
Here's a test. The "Start" buttons are perhaps stand-ins for logging in. The main UI has a tab pane with two tabs: each individual tab starts with its own "Start" button and then the main content has a label, text field, and button.
The tab contents each have a (short, for testing) idle monitor associated with them. Any event on the content of the tab will reset the idle monitor, but events outside of the tab content will not reset it. There's also a "global" idle monitor for the entire window which resets the whole UI after 30 seconds.
Note that the data is preserved: i.e. if you timeout due to the idle, any text you type in the text field is preserved properly. This is why I think the issue with "transactions" should not matter at all.
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class IdleTest extends Application {
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Parent mainUI = buildMainUI();
Scene scene = new Scene(root, 350, 150);
Parent startUI = buildStartUI(() -> root.getChildren().setAll(mainUI));
root.getChildren().add(startUI);
IdleMonitor idleMonitor = new IdleMonitor(Duration.seconds(30),
() -> root.getChildren().setAll(startUI), true);
idleMonitor.register(scene, Event.ANY);
primaryStage.setScene(scene);
primaryStage.show();
}
private Parent buildStartUI(Runnable start) {
Button button = new Button("Start");
button.setOnAction(e -> start.run());
StackPane root = new StackPane(button);
return root ;
}
private Parent buildMainUI() {
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("One");
Parent tab1Content = buildTabUI("Tab 1");
Parent tab1StartContent = buildStartUI(() -> tab1.setContent(tab1Content));
tab1.setContent(tab1StartContent);
IdleMonitor tab1IdleMonitor = new IdleMonitor(Duration.seconds(5),
() -> tab1.setContent(tab1StartContent), true);
tab1IdleMonitor.register(tab1Content, Event.ANY);
Tab tab2 = new Tab("Two");
Parent tab2Content = buildTabUI("Tab 2") ;
Parent tab2StartContent = buildStartUI(() -> tab2.setContent(tab2Content));
tab2.setContent(tab2StartContent);
IdleMonitor tab2IdleMonitor = new IdleMonitor(Duration.seconds(10),
() -> tab2.setContent(tab2StartContent), true);
tab2IdleMonitor.register(tab2Content, Event.ANY);
tabPane.getTabs().addAll(tab1, tab2);
return tabPane ;
}
private Parent buildTabUI(String text) {
Button button = new Button("Click here");
button.setOnAction(e -> System.out.println("Click in "+text));
VBox content = new VBox(10, new Label(text), new TextField(), button);
content.setAlignment(Pos.CENTER);
return content ;
}
public static void main(String[] args) {
launch(args);
}
}
Related
How can I make a custom Event that triggers on Stage.setScene()?
In my code, the button switches the Scenes and that works fine. However, I would like to extend the Stage to have an additional Event that is triggered when a button or possibly any other Element triggers a setScene.
Example:
package sample;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
Group g1 = new Group();
Button b1 = new Button("2");
g1.getChildren().setAll(b1);
Scene scene1 = new Scene(g1, 50, 50);
Group g2 = new Group();
Button b2 = new Button("1");
g2.getChildren().setAll(b2);
Scene scene2 = new Scene(g2, 50, 50);
stage.setScene(scene1);
stage.setTitle("JavaFX Application Life Cycle");
b1.setOnAction(actionEvent -> {
System.out.println("1");
stage.setScene(scene2);
});
b2.setOnAction(actionEvent -> {
System.out.println("2");
stage.setScene(scene1);
});
stage.show();
}
}
You can add a ChangeListener<Scene> to your Stage like this:
stage.sceneProperty().addListener((observable, oldScene, newScene) -> {
System.out.println("New scene: " + newScene);
System.out.println("Old scene: " + oldScene);
});
I believe using a listener, as shown in the answer by #M.S., is probably the best and simplest way to react to scene changes. However, you ask about how to make a "custom event" that you can fire when the scene changes; by "event" I assume you mean a subclass of javafx.event.Event. So while I recommend sticking with a simple listener, here's an example of a custom event.
First, you need a custom event class:
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Scene;
import javafx.stage.Window;
public class SceneChangedEvent extends Event {
public static final EventType<SceneChangedEvent> SCENE_CHANGED =
new EventType<>(Event.ANY, "SCENE_CHANGED");
public static final EventType<SceneChangedEvent> ANY = SCENE_CHANGED;
private transient Window window;
private transient Scene oldScene;
private transient Scene newScene;
public SceneChangedEvent(Window window, Scene oldScene, Scene newScene) {
super(window, window, SCENE_CHANGED);
this.window = window;
this.oldScene = oldScene;
this.newScene = newScene;
}
public Window getWindow() {
return window;
}
public Scene getOldScene() {
return oldScene;
}
public Scene getNewScene() {
return newScene;
}
}
I'm not sure what information you want to carry with the event so I just added the source Window as well as the old and new Scenes. If you're wondering about the ANY = SCENE_CHANGED, I'm just following the pattern used by javafx.event.ActionEvent (which also only has a single event-type).
Then you simply need to fire the event when the scene changes. To implement this you're still going to need a change listener. As you mention wanting to extend Stage here's an example of that:
import javafx.beans.NamedArg;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class CustomStage extends Stage {
private final ObjectProperty<EventHandler<? super SceneChangedEvent>> onSceneChanged =
new SimpleObjectProperty<>(this, "onSceneChanged") {
#Override
protected void invalidated() {
setEventHandler(SceneChangedEvent.SCENE_CHANGED, get());
}
};
public final void setOnSceneChanged(EventHandler<? super SceneChangedEvent> handler) {
onSceneChanged.set(handler);
}
public final EventHandler<? super SceneChangedEvent> getOnSceneChanged() {
return onSceneChanged.get();
}
public final ObjectProperty<EventHandler<? super SceneChangedEvent>> onSceneChangedProperty() {
return onSceneChanged;
}
public CustomStage() {
this(StageStyle.DECORATED);
}
public CustomStage(#NamedArg(value = "style", defaultValue = "DECORATED") StageStyle style) {
super(style);
sceneProperty().addListener((obs, ov, nv) -> fireEvent(new SceneChangedEvent(this, ov, nv)));
}
}
This would let you react to the scene changing using any of the following:
CustomStage stage = new CustomStage();
// addEventFilter/addEventHandler
stage.addEventFilter(SceneChangedEvent.SCENE_CHANGED, e -> { ... });
stage.addEventHandler(SceneChangedEvent.SCENE_CHANGED, e -> { ... });
// setOnSceneChanged
stage.setOnSceneChanged(e -> { ... });
Keep in mind that the event will only target the CustomStage instance. In other words, only event handlers added to the CustomStage instance will be notified of the event. And as you can see, this is much more complicated than simply adding a change listener to the scene property of the Stage.
I'm struggling with making an animation showing the searchin in Binary Search Tree in JavaFX.
The goal is to make a visualization of comparing the value of tree node with possibility to:
pause and play it any time
being able to play the animation backwards (to go at least one step back),
give a user an ability to play the animation step-by-step or whole at once.
The preview of visualization
My vision was to make a series of TranslateTransitions(TT) added in one SequentialTransition(ST). If the animation is marked as "step-by-step" the each TT pauses the whole ST in their OnFinished handler. However this kinda works only for going one-way.
My question is. What is the best approach to maintain going fluent and step-by-step animation in reverse direction ?
I was thinking about:
maybe making another sequence of inverse transitions (but how to tell
it from which step to continue ?)
somehow work with rate property ? is it possible to change it while the ST is running ?
Thank you very much for your answers.
In general, you can change the rate property of an Animation while it is in progress. The idea of using a SequentialTransition is appealing, but it doesn't work as easily as you might think. The problem arises when the sequential transition is paused at the boundary between two individual transitions: you don't have any way to tell which of the individual transitions is considered the current one (i.e. the next one or the previous one). So when you try to reverse the rate and play, the sequential transition can get confused and immediately think it's at the end of the one it's trying to play.
You might be able to hack this a little by using Animation.getCurrentTime() and Animation.jumpTo(...) to "nudge" the sequential transition a tiny amount in the correct direction before starting to play any step, but I think it's probably easier just to manage the individual transitions on their own instead of using a SequentialTransition.
Here's a simple example of using this technique to move a rectangle around:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ReverseSequentialTransitionTest extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Rectangle rect = new Rectangle(50, 50, 250, 150);
rect.setFill(Color.color(.5, .5, .1));
pane.getChildren().add(rect);
TranslateTransition ttForward = new TranslateTransition(Duration.seconds(1), rect);
ttForward.setFromX(0);
ttForward.setToX(400);
TranslateTransition ttDown = new TranslateTransition(Duration.seconds(1), rect);
ttDown.setFromY(0);
ttDown.setToY(100);
TranslateTransition ttBackward = new TranslateTransition(Duration.seconds(1), rect);
ttBackward.setFromX(400);
ttBackward.setToX(0);
TranslateTransition ttUp = new TranslateTransition(Duration.seconds(1), rect);
ttUp.setFromY(100);
ttUp.setToY(0);
List<Animation> transitions = Arrays.asList(ttForward, ttDown, ttBackward, ttUp);
IntegerProperty nextTransitionIndex = new SimpleIntegerProperty();
Button playButton = new Button("Play Forward");
playButton.setOnAction(event -> {
int index = nextTransitionIndex.get();
Animation anim = transitions.get(index);
anim.setOnFinished(evt -> nextTransitionIndex.set(index+1));
anim.setRate(1);
anim.play();
});
Button reverseButton = new Button("Play backward");
reverseButton.setOnAction(event -> {
int index = nextTransitionIndex.get()-1;
Animation anim = transitions.get(index);
anim.setOnFinished(evt -> nextTransitionIndex.set(index));
anim.setRate(-1);
anim.play();
});
// This is not really part of the answer to the current question, but the
// next three statements just disable the buttons when appropriate.
// This is a binding which is true if and only if any of the transitions are
// currently running:
BooleanBinding anyPlaying = createAnyPlayingBinding(transitions);
// Disable playButton if we are at the end of the last transition, or if
// any transitions are playing:
playButton.disableProperty().bind(
nextTransitionIndex.greaterThanOrEqualTo(transitions.size())
.or(anyPlaying)
);
// Disable reverseButton if we are at the beginning of the first transition,
// or if any transitions are currently playing:
reverseButton.disableProperty().bind(
nextTransitionIndex.lessThanOrEqualTo(0)
.or(anyPlaying));
HBox controls = new HBox(5);
controls.setAlignment(Pos.CENTER);
controls.getChildren().addAll(playButton, reverseButton);
BorderPane root = new BorderPane();
root.setCenter(pane);
root.setBottom(controls);
primaryStage.setScene(new Scene(root, 800, 400));
primaryStage.show();
}
private BooleanBinding createAnyPlayingBinding(List<Animation> transitions) {
return new BooleanBinding() {
{ // Anonymous constructor
// bind to the status properties of all the transitions
// (i.e. mark this binding as invalid if any of the status properties change)
transitions.stream()
.map(Animation::statusProperty)
.forEach(this::bind);
}
#Override
protected boolean computeValue() {
// return true if any of the transitions statuses are equal to RUNNING:
return transitions.stream()
.anyMatch(anim -> anim.getStatus()==Status.RUNNING);
}
};
}
public static void main(String[] args) {
launch(args);
}
}
In JDK 7, the event handler for the playButton looks like this:
playButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
final int index = nextTransitionIndex.get();
Animation anim = transitions.get(index);
anim.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent evt) {
nextTransitionIndex.set(index + 1) ;
}
});
anim.setRate(1);
anim.play();
}
});
and similarly for reverseButton. You will need to declare a couple of things as final as well. The createAnyPlayingBinding method is something like
private BooleanBinding createAnyPlayingBinding(final List<Animation> transitions) {
return new BooleanBinding() {
{
for (Animation transition : transitions) {
this.bind(transition.statusProperty();
}
}
#Override
protected boolean computeValue() {
// return true if any of the transitions statuses are equal to RUNNING:
for (Animation anim : transitions) {
if (anim.getStatus() == Status.RUNNING) {
return true ;
}
}
return false ;
}
};
}
I am developing an application with several TextField objects that need to be updated to reflect changes in associated back-end properties. The TextFields are not editable, only the back-end may change their content.
As I understand, the correct way about this is to run the heavy computation on a separate thread so as not to block the UI. I did this using javafx.concurrent.Task and communicated a single value back to the JavaFX thread using updateMessage(), which worked well. However, I need more than one value to be updated as the back-end does its crunching.
Since the back-end values are stored as JavaFX properties, I tried simply binding them to the textProperty of each GUI element and let the bindings do the work. This doesn't work, however; after running for a few moments, the TextFields stop updating even though the back-end task is still running. No exceptions are raised.
I also tried using Platform.runLater() to actively update the TextFields rather than binding. The issue here is that the runLater() tasks are scheduled faster than the platform can run them, and so the GUI becomes sluggish and needs to time to "catch up" even after the back-end task is finished.
I found a few questions on here:
Logger entries translated to the UI stops being updated with time
Multithreading in JavaFX hangs the UI
but my issue persists.
In summary: I have a back-end making changes to properties, and I want those changes to appear on the GUI. The back-end is a genetic algorithm, so its operation is broken down into discrete generations. What I would like is for the TextFields to refresh at least once in between generations, even if this delays the next generation. It is more important that the GUI responds well than that the GA runs fast.
I can post a few code examples if I haven't made the issue clear.
UPDATE
I managed to do it following James_D's suggestion. To solve the issue of the back-end having to wait for the console to print, I implemented a buffered console of sorts. It stores the strings to print in a StringBuffer and actually appends them to the TextArea when a flush() method is called. I used an AtomicBoolean to prevent the next generation from happening until the flush is complete, as it is done by a Platform.runLater() runnable. Also note that this solution is incredibly slow.
Not sure if I completely understand, but I think this may help.
Using Platform.runLater(...) is an appropriate approach for this.
The trick to avoiding flooding the FX Application Thread is to use an Atomic variable to store the value you're interested in. In the Platform.runLater method, retrieve it and set it to a sentinel value. From your background thread, update the Atomic variable, but only issue a new Platform.runLater if it's been set back to its sentinel value.
I figured this out by looking at the source code for Task. Have a look at how the updateMessage method (line 1131 at the time of writing) is implemented.
Here's an example which uses the same technique. This just has a (busy) background thread which counts as fast as it can, updating an IntegerProperty. An observer watches that property and updates an AtomicInteger with the new value. If the current value of the AtomicInteger is -1, it schedules a Platform.runLater.
In the Platform.runLater, I retrieve the value of the AtomicInteger and use it to update a Label, setting the value back to -1 in the process. This signals that I am ready for another UI update.
import java.text.NumberFormat;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class ConcurrentModel extends Application {
#Override
public void start(Stage primaryStage) {
final AtomicInteger count = new AtomicInteger(-1);
final AnchorPane root = new AnchorPane();
final Label label = new Label();
final Model model = new Model();
final NumberFormat formatter = NumberFormat.getIntegerInstance();
formatter.setGroupingUsed(true);
model.intProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(final ObservableValue<? extends Number> observable,
final Number oldValue, final Number newValue) {
if (count.getAndSet(newValue.intValue()) == -1) {
Platform.runLater(new Runnable() {
#Override
public void run() {
long value = count.getAndSet(-1);
label.setText(formatter.format(value));
}
});
}
}
});
final Button startButton = new Button("Start");
startButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
model.start();
}
});
AnchorPane.setTopAnchor(label, 10.0);
AnchorPane.setLeftAnchor(label, 10.0);
AnchorPane.setBottomAnchor(startButton, 10.0);
AnchorPane.setLeftAnchor(startButton, 10.0);
root.getChildren().addAll(label, startButton);
Scene scene = new Scene(root, 100, 100);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
public class Model extends Thread {
private IntegerProperty intProperty;
public Model() {
intProperty = new SimpleIntegerProperty(this, "int", 0);
setDaemon(true);
}
public int getInt() {
return intProperty.get();
}
public IntegerProperty intProperty() {
return intProperty;
}
#Override
public void run() {
while (true) {
intProperty.set(intProperty.get() + 1);
}
}
}
}
If you really want to "drive" the back end from the UI: that is throttle the speed of the backend implementation so you see all updates, consider using an AnimationTimer. An AnimationTimer has a handle(...) which is called once per frame render. So you could block the back-end implementation (for example by using a blocking queue) and release it once per invocation of the handle method. The handle(...) method is invoked on the FX Application Thread.
The handle(...) method takes a parameter which is a timestamp (in nanoseconds), so you can use that to slow the updates further, if once per frame is too fast.
For example:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
final BlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(1);
TextArea console = new TextArea();
Button startButton = new Button("Start");
startButton.setOnAction(event -> {
MessageProducer producer = new MessageProducer(messageQueue);
Thread t = new Thread(producer);
t.setDaemon(true);
t.start();
});
final LongProperty lastUpdate = new SimpleLongProperty();
final long minUpdateInterval = 0 ; // nanoseconds. Set to higher number to slow output.
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
if (now - lastUpdate.get() > minUpdateInterval) {
final String message = messageQueue.poll();
if (message != null) {
console.appendText("\n" + message);
}
lastUpdate.set(now);
}
}
};
timer.start();
HBox controls = new HBox(5, startButton);
controls.setPadding(new Insets(10));
controls.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(console, null, null, controls, null);
Scene scene = new Scene(root,600,400);
primaryStage.setScene(scene);
primaryStage.show();
}
private static class MessageProducer implements Runnable {
private final BlockingQueue<String> messageQueue ;
public MessageProducer(BlockingQueue<String> messageQueue) {
this.messageQueue = messageQueue ;
}
#Override
public void run() {
long messageCount = 0 ;
try {
while (true) {
final String message = "Message " + (++messageCount);
messageQueue.put(message);
}
} catch (InterruptedException exc) {
System.out.println("Message producer interrupted: exiting.");
}
}
}
public static void main(String[] args) {
launch(args);
}
}
The best way to performing this is by usage of Task in JavaFx. This is be by far the best technique I've come across to update UI Controls in JavaFx.
Task task = new Task<Void>() {
#Override public Void call() {
static final int max = 1000000;
for (int i=1; i<=max; i++) {
updateProgress(i, max);
}
return null;
}
};
ProgressBar bar = new ProgressBar();
bar.progressProperty().bind(task.progressProperty());
new Thread(task).start();
I want to listen to some KeyEvent in my scene, say KeyCode.ESCAPE(close the scene when pressed).
scene.addEventHandler(KeyEvent.ANY, event -> {
if (event.isConsumed())
return;
switch (event.getCode()) {
case ESCAPE:
stage.hide();
event.consume();
break;
default:
break;
}
});
Now, the nodes inside the scene could have listened to ESCAPE too.
// ....
someOtherNode.addEventHandler(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
// do stuff
e.consume();
}
});
// ....
How do I make sure that the KeyEvent will be consumed from the node and not the scene?
Based on the diagram from Oracle, A workaround would be adding a dummy Node at the end of the Node hierarchy that listens to KeyCodes
But is there a better solution, like inverting the propagation route?
EDIT:
The use case:
A popup-like node that blocks other nodes would need to listens to the ESC key or focusProperty() so that it can close itself.
There's two ways you can affect events:
Use the Node.addEventFilter(...) method to register a filter. A filter will execute on the capturing phase of the event (as the window is getting more specific, determining which Nodes should get the event).
Use the Node.addEventHandler(...) method to register a handler. The handler will execute starting at the most specific node found in the capturing phase, heading down until it is consumed.
So in the capturing phase, a stack is created. Starting with the window (topmost parent), each node that this event could potentially execute on is added to the stack (ending with the bottom most child). A filter can interrupt this process, or just execute an event during this process.
In the bubbling phase, the event handlers will start firing from the top of the stack (created in the capturing phase) until the stack is empty or the event is consumed.
In your case, you really shouldn't have anything to worry about. If any node cares about processing the "ESC" event, they will do so in the bubbling phase (and they should consume the event to prevent further processing). You can see this behavior in the ComboBox. If they don't care, it will bubble up to your Scene and that handler will execute. Just make sure any custom code you create that processes an "ESC" press also consumes that event.
For more information, there is a explanation and tutorial here: http://docs.oracle.com/javafx/2/events/jfxpub-events.htm
And here is some sample code demonstrating the Escape functionality. Pressing ESC while focused on the ComboBox will not cause the application to close, while it will close with the other controls.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.converter.DefaultStringConverter;
public class FXEventFiltering extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(final Stage stage) throws Exception {
//All the controls are added here
VBox box = new VBox();
ComboBox<String> dropdown = new ComboBox<>();
TextField field = new TextField();
CheckBox check = new CheckBox("Check");
RadioButton radio = new RadioButton("Radio!");
TextArea area = new TextArea();
TableView<String> table = new TableView<String>(FXCollections.observableArrayList(new String[]{"one","two"}));
TableColumn<String, String> tc = new TableColumn<String, String>("Column1");
tc.setEditable(true);
tc.setCellFactory(TextFieldTableCell.<String,String>forTableColumn(new DefaultStringConverter()));
tc.setCellValueFactory(new Callback<CellDataFeatures<String,String>, ObservableValue<String>>(){
#Override
public ObservableValue<String> call(CellDataFeatures<String, String> arg0) {
return new SimpleStringProperty(arg0.getValue());
}});
table.getColumns().add(tc);
box.getChildren().addAll(dropdown, field, check, radio, area, table);
//Setting up your scene
Scene scene = new Scene(box);
stage.setScene(scene);
scene.addEventHandler(KeyEvent.ANY, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
System.out.println("KEYS!" + event.getEventType().getName());
switch (event.getCode()) {
case ESCAPE:
System.out.println("Escape!");
stage.hide();
event.consume();
break;
default:
break;
}
}
});
box.requestFocus(); // Removing default focus
stage.show();
}
}
Maybe you could loop over all nodes after catching the event in the scene to find out which node has actual focus? Then you could call node method to close?
I'm trying to stop the arrow keys from navigating through the controls i have in my example. I'm not sure how to do this. Here is the example i have created:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class KeyEventTest extends Application{
private EventHandler<KeyEvent> keyEventHandler;
#Override
public void start(Stage stage) throws Exception {
// TODO Auto-generated method stub
Group root = new Group();
FlowPane f = new FlowPane();
Button r = new Button("button1");
Button r2 = new Button("button1");
Button r3 = new Button("button1");
f.getChildren().addAll(r,r2,r3);
root.getChildren().add(f);
Scene scene = new Scene(root,600,600);
keyEventHandler = new EventHandler<KeyEvent>() {
public void handle(final KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.LEFT || keyEvent.getCode() == KeyCode.UP) {
System.out.println("arrow keys");
}else{
System.out.println(keyEvent);
}
}
};
stage.addEventHandler(KeyEvent.KEY_PRESSED, keyEventHandler);
stage.setScene(scene);
stage.show();
}
public static void main(String args[]){
Application.launch(args);
}
}
Any help would be appreciated
Thanks
I had a similar Problem and found an easy workaround. Since the controls navigating Part triggers after the self-implemented EventHandler you can simply stop any further propagation of the KeyEvent by calling
keyevent.consume()
at the end of your handle() method.
First, the answer on this question is similar to answer on this question : JavaFX: How to change the focus traversal policy?
You can change traversal policy, so that traverse will not be done, for some situations.
The other decision - is to add listener of focused property, and drop focus back, when it is not wished (but it will work for all navigation keys):
node.focusedProperty().addListener(new ChangeListener<Boolean>(){
public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
Platform.runLater(new Runnable(){
public void run() {
node.requestFocus();
}
});
}
});
You should use runLater, because of such issue : JavaFx: After dialog, two textfields gains focus instead one
The way how you do this - handler on key press may not work always, because actions may happen on key release too.
In common case, behavior on key presses is determined in behavior class, which is accessible by skin class. If you want to change behavior on keys pressing, you can change behavior class.