0

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>
LINS
  • 3
  • 2
  • That said, shouldn't `GlobalScreen.addNativeKeyListener(new HelloController());` be `GlobalScreen.addNativeKeyListener(this);`? I don't understand why you'd create another controller instance there. – James_D Jun 29 '22 at 20:09
  • "nativeKeyReleased" won't work without it – LINS Jun 29 '22 at 20:18
  • Won’t work without what? What does “doesn’t work” actually mean? – James_D Jun 29 '22 at 20:18
  • I mean it won't start. There is also a jnativehook in the keyboard in the documentation. https://github.com/kwhat/jnativehook/blob/2.2/doc/Keyboard.md – LINS Jun 29 '22 at 20:23
  • “Won’t start”??? So nothing happens? There’s no error message? – James_D Jun 29 '22 at 20:24
  • Yep, it's just being ignored. – LINS Jun 29 '22 at 20:27
  • So it’s pretty obvious why your current code doesn’t work: `nativeKeyReleased()` is called on one instance of `HelloController` (the instance you explicitly create), but `togg()` is called on a different instance (the one created by the `FXMLLoader`). I don’t understand why passing the current instance to `GlobalScreen.addNativeKeyListener()` doesn’t work. – James_D Jun 29 '22 at 20:39
  • Do you mean that I can't merge 2 instances? – LINS Jun 29 '22 at 20:46
  • I don’t know what “merge two instances” means. I mean just what I said. `nativeKeyReleased()` is called on one instance. `togg()` is called on another. So they are referring to different `ok` variables. The one in the instance on which `togg()` is called is never set to anything, so it is always null. – James_D Jun 29 '22 at 20:48
  • I think I understand you, but then it's not possible to solve if I want to use these libraries. – LINS Jun 29 '22 at 20:56
  • Well the solution (or one solution) would be to have them use the same instance. I can’t understand why passing a new instance works but passing the current instance doesn’t. – James_D Jun 29 '22 at 20:58
  • I'll think about it, thanks for the tip. – LINS Jun 29 '22 at 21:00
  • Using the current instance works for me. (Caveat: it only actually detects modifier keys, such as Shift, Ctrl, etc. I suspect that's because I'm on a MacBook Pro and haven't configured something correctly.) You should probably deal with multithreading for this too, as documented in the library. – James_D Jun 29 '22 at 21:19

1 Answers1

2

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);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322