Search code examples
javajavafxjnativehook

Can't use variable "ok" in "Togg"


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>

Solution

  • 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);
        }
    }