I'm trying to make it so that when a button is clicked, the user presses any key and this is written to tog1. But I get that ok = null. I don't understand why "ok" which is a string only works inside "nativeKeyReleased". Already tried to use tog1.setText inside "nativeKeyReleased" but it recognizes it. I am using JavaFx and jnativehook.
Controller:
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ToggleButton;
import com.github.kwhat.jnativehook.GlobalScreen;
import com.github.kwhat.jnativehook.NativeHookException;
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener;
public class HelloController implements NativeKeyListener {
public String ok;
public void nativeKeyReleased(NativeKeyEvent e) {
System.out.println("Key Released: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
ok = (NativeKeyEvent.getKeyText(e.getKeyCode()));
}
@FXML
void Togg(ActionEvent e) {
if (tog1.isSelected()) {
tog1.setStyle("-fx-background-color: #333; -fx-border-color: green; -fx-border-radius: 5px; -fx-border-width: 2;");
tog1.setText(ok);
System.out.println(ok);
}
else {
tog1.setStyle("-fx-background-color: #333; -fx-border-color: white; -fx-border-radius: 5px; -fx-border-width: 2;");
tog1.setText("Key");
}
}
@FXML
public ToggleButton tog1;
@FXML
void initialize() {
try {
GlobalScreen.registerNativeHook();
}
catch (NativeHookException ex) {
System.err.println("There was a problem registering the native hook.");
System.err.println(ex.getMessage());
System.exit(1);
}
GlobalScreen.addNativeKeyListener(new HelloController());
}
}
HelloApplication:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.awt.*;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 695, 356);
stage.setTitle("Instapeek");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) throws AWTException {
launch(args);
}
}
hello-view.fxml:
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="657.0" style="-fx-background-color: #333; -fx-border-color: #686868; -fx-border-width: 4;" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.HelloController">
<children>
<ToggleButton fx:id="tog1" layoutX="56.0" layoutY="29.0" mnemonicParsing="false" onAction="#Togg" prefHeight="31.0" prefWidth="164.0" style="-fx-background-color: #333; -fx-border-color: white; -fx-border-radius: 5px; -fx-border-width: 2;" text="Key" textFill="WHITE">
<font>
<Font name="Panton Black Caps" size="11.0" />
</font>
</ToggleButton>
</children>
</AnchorPane>
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>demo</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>18-ea+6</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>18-ea+6</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.9.0</version>
<configuration>
<source>18</source>
<target>18</target>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<!-- Default configuration for running with: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<mainClass>com.example.demo/com.example.demo.HelloApplication</mainClass>
<launcher>app</launcher>
<jlinkZipName>app</jlinkZipName>
<jlinkImageName>app</jlinkImageName>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Your code doesn't work, because you are passing a new instance of HelloController
to GlobalScreen.addNativeKeyListener(...);
. This means nativeKeyReleased()
is being called on one instance of HelloController
(the one you create), but togg()
(I have changed the name of the method to conform to proper naming conventions) is being called on a different instance (the one created by the FXMLLoader
when you load the FXML). So they are both referring different copies of the ok
variable.
The following works for me (at least, to the extent that I can get jnativehook
working on my system), but there are threading and design issues discussed further down to which you should pay attention:
public class HelloController implements NativeKeyListener {
public String ok;
public void nativeKeyReleased(NativeKeyEvent e) {
System.out.println("Key Released: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
ok = (NativeKeyEvent.getKeyText(e.getKeyCode()));
}
@FXML
void togg(ActionEvent e) {
if (tog1.isSelected()) {
tog1.setStyle("-fx-background-color: #333; -fx-border-color: green; -fx-border-radius: 5px; -fx-border-width: 2;");
tog1.setText(ok);
System.out.println(ok);
}
else {
tog1.setStyle("-fx-background-color: #333; -fx-border-color: white; -fx-border-radius: 5px; -fx-border-width: 2;");
tog1.setText("Key");
}
}
@FXML
public ToggleButton tog1;
@FXML
void initialize() {
try {
GlobalScreen.registerNativeHook();
}
catch (NativeHookException ex) {
System.err.println("There was a problem registering the native hook.");
System.err.println(ex.getMessage());
System.exit(1);
}
// GlobalScreen.addNativeKeyListener(new HelloController());
GlobalScreen.addNativeKeyListener(this);
}
}
Note that this is not guaranteed to work. The jnativehook
callbacks are executed on a different thread to the FX Application Thread (on which the event handler for the toggle button is called). So you are setting ok
in one thread and accessing it in another. In this circumstance, the second thread is not guaranteed to ever see the updates to the variable made in the first thread (though, as stated, it does work on some systems/JVMs).
You should modify the code to either make the variable volatile
, or use an atomic wrapper, or (probably the best approach) only access it from a single thread, as shown here:
public void nativeKeyReleased(NativeKeyEvent e) {
System.out.println("Key Released: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
Platform.runLater(() -> {
ok = NativeKeyEvent.getKeyText(e.getKeyCode());
});
}
It's also probably not a good idea to register the controller as a native listener. The reason is that it forces the controller to persist for the entire lifecycle of the application. The controller has references to UI elements, so you also force those to persist. If you were to stop displaying the UI, those elements would not be available for garbage collection, causing a memory leak. You should use a different class for the native listener and arrange for it and the controller to access shared data (i.e. through some kind of shared data model).
The preferred solution looks something like this:
The data model class; an instance of this will be shared between any objects that need to access the shared data (e.g. the key that was typed, in this example):
public class DataModel {
private String typedKey ;
public String getTypedKey() {
return typedKey ;
}
public void setTypedKey(String typedKey) {
this.typedKey = typedKey ;
}
// other properties as needed...
}
Here is the key listener implementation:
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener;
import javafx.application.Platform;
public class GlobalKeyListener implements NativeKeyListener {
private final DataModel dataModel ;
public GlobalKeyListener(DataModel dataModel) {
this.dataModel = dataModel ;
}
@Override
public void nativeKeyReleased(NativeKeyEvent e) {
System.out.println(e);
System.out.println("Key Released: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
Platform.runLater(() -> dataModel.setTypedKey(NativeKeyEvent.getKeyText(e.getKeyCode())));
}
}
Here is the revised controller class. Notice how this class only takes responsibility for control of the UI defined in the FXML file, which is its job. Handling events outside of that part of the UI is delegated elsewhere:
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ToggleButton;
public class HelloController {
@FXML
public ToggleButton tog1;
private DataModel dataModel ;
public DataModel getDataModel() {
return dataModel;
}
public void setDataModel(DataModel dataModel) {
this.dataModel = dataModel;
}
@FXML
void togg(ActionEvent e) {
if (tog1.isSelected()) {
tog1.setStyle("-fx-background-color: #333; -fx-border-color: green; -fx-border-radius: 5px; -fx-border-width: 2;");
tog1.setText(dataModel.getTypedKey());
System.out.println(dataModel.getTypedKey());
}
else {
tog1.setStyle("-fx-background-color: #333; -fx-border-color: white; -fx-border-radius: 5px; -fx-border-width: 2;");
tog1.setText("Key");
}
}
}
Here's the FXML, for completeness (the only change was to modify the method name to conform to naming conventions):
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="657.0" style="-fx-background-color: #333; -fx-border-color: #686868; -fx-border-width: 4;" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jamesd.examples.nativehook.HelloController">
<children>
<ToggleButton fx:id="tog1" layoutX="56.0" layoutY="29.0" mnemonicParsing="false" onAction="#togg" prefHeight="31.0" prefWidth="164.0" style="-fx-background-color: #333; -fx-border-color: white; -fx-border-radius: 5px; -fx-border-width: 2;" text="Key" textFill="WHITE">
<font>
<Font name="Panton Black Caps" size="11.0" />
</font>
</ToggleButton>
</children>
</AnchorPane>
And finally the application class. This class manages application lifecycle, so it is the appropriate place to manage registering and deregistering the native listeners (this allows the application to shut down correctly), creation of persistent objects (e.g. the data model), and dependency management (giving the controller and global key listener access to the data model).
import com.github.kwhat.jnativehook.GlobalScreen;
import com.github.kwhat.jnativehook.NativeHookException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
private GlobalKeyListener keyListener;
@Override
public void start(Stage stage) throws IOException, NativeHookException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 695, 356);
DataModel model = new DataModel();
HelloController controller = fxmlLoader.getController();
controller.setDataModel(model);
keyListener = new GlobalKeyListener(model);
GlobalScreen.registerNativeHook();
GlobalScreen.addNativeKeyListener(keyListener);
stage.setTitle("Instapeek");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
@Override
public void stop() throws NativeHookException {
GlobalScreen.removeNativeKeyListener(keyListener);
GlobalScreen.unregisterNativeHook();
}
public static void main(String[] args) {
launch(args);
}
}