javajavafxtaskbarjava-ffm

How to manage taskbar from JavaFX, via native code in Linux


I'm trying get more experience with the new Foreign Function & Memory API in Java 22+. The best way how to learn a new API is by using it in a project.

My project’s goal is to report on the taskbar the progress of some long-running task. As far as I know, there is no "native" support of this in JavaFX. There are some libraries like FXTaskbarProgressBar which serves the purpose, but only for Windows OS. And it is using the "old" Java Native Interface (JNI).

After a short research, I found a simple Go library taskbar. This library inspired me to try porting to Java for JavaFX.

First I used jextract to get java bindings to native library calls:

jextract --output target/generated-sources/jextract -t "taskbar_test.gen" --include-function "XOpenDisplay" --include-function "XChangeProperty" --include-function "XFlush" --include-function "XCloseDisplay" /usr/include/X11/Xlib.h

Then I created a simple application to simulate long running process where I try to update progress on taskbar by calling method "XChangeProperty" which I found in documentation of X11: https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#XChangeProperty

Unfortunately this does not work. The program does not crash, task is running on background, but no update on taskbar is happening.

Here is the code I created:

package taskbar_test;

import com.sun.glass.ui.Window;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import taskbar_test.gen.Xlib_h;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;


public class AppLinuxXlib extends Application {

    @Override
    public void start(Stage primaryStage) {
        Button startButton = new Button("Start Long Running Task");

        startButton.setOnAction(event -> {
            final long rawHandle = Window.getWindows().getFirst().getRawHandle();
            System.out.println(rawHandle);
            // Create a long-running task
            Task<Void> longTask = new Task<>() {
                @Override
                protected Void call() throws Exception {
                    System.out.println("Started");

                    try (var arena = Arena.ofConfined()) {
                        var NET_WM_XAPP_PROGRESS = arena.allocateFrom("NET_WM_XAPP_PROGRESS");
//                        var NET_WM_XAPP_PROGRESS_PULSE = arena.allocateFrom("NET_WM_XAPP_PROGRESS_PULSE");

                        MemorySegment x11Session = Xlib_h.XOpenDisplay(MemorySegment.NULL);
                        System.out.println(x11Session);

                        // Prepare the progress data
                        MemorySegment initData = arena.allocateFrom(ValueLayout.JAVA_INT, 0);
                        Xlib_h.XChangeProperty(x11Session,                    // display
                                MemorySegment.ofAddress(rawHandle).address(), // window
                                NET_WM_XAPP_PROGRESS.address(),               // property
                                6,                                            // type
                                32,                                           // format
                                0,                                            // mode PropModeReplace=0
                                initData,                                     // data
                                1);                                           // nelements
                        Xlib_h.XFlush(x11Session);

                        System.out.println("Countdown started");

                        // Set the taskbar progress
                        for (int i = 0; i <= 100; i+=20) {
                            // Simulate work
                            Thread.sleep(500);
                            System.out.println(i);
                            MemorySegment progressData = arena.allocateFrom(ValueLayout.JAVA_INT, i);
                            // Update taskbar progress
                            // https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#XChangeProperty
                            Xlib_h.XChangeProperty(x11Session,                    // display
                                    MemorySegment.ofAddress(rawHandle).address(), // window
                                    NET_WM_XAPP_PROGRESS.address(),               // property
                                    6,                                            // type
                                    32,                                           // format
                                    0,                                            // mode PropModeReplace=0
                                    progressData,                                 // data
                                    1);                                           // nelements
                            Xlib_h.XFlush(x11Session);
                        }
                        System.out.println("Finished");
                        Xlib_h.XCloseDisplay(x11Session);

                    } catch (Throwable ex) {
                        ex.printStackTrace();
                    }
                    return null;
                }
            };

            // Start the task in a new thread
            new Thread(longTask).start();
        });

        VBox vbox = new VBox(10, startButton);
        Scene scene = new Scene(vbox, 300, 200);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Taskbar Progress Example Linux");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Can someone show me what am I doing wrong and maybe point me to the correct direction with how to make the implementation work?

Thank you very much. Petr


Solution

  • I wasn’t able to solve this, but I suspect it’s because I am running Gnome 43.9 Classic, and I’m not sure it supports showing progress in taskbar buttons. But I can provide some advice, at least:

    With those things in mind, I changed your code to this. (I replaced the taskbar_test.gen.Xlib_h package with explicit calls to java.lang.foreign.)

    import com.sun.glass.ui.Window;
    
    import javafx.application.Application;
    import javafx.concurrent.Task;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    import java.lang.invoke.MethodHandle;
    
    import java.lang.foreign.Arena;
    import java.lang.foreign.MemorySegment;
    import java.lang.foreign.ValueLayout;
    import java.lang.foreign.Linker;
    import java.lang.foreign.SymbolLookup;
    import java.lang.foreign.FunctionDescriptor;
    
    public class AppLinuxXlib extends Application {
        private static final int None = 0;              // from X.h
        private static final int False = 0;             // from Xlib.h
        private static final int PropModeReplace = 0;   // from X.h
    
        private static final int XA_CARDINAL = 6;       // from Xatom.h
    
        private MethodHandle XOpenDisplay;
        private MethodHandle XCloseDisplay;
        private MethodHandle XInternAtom;
        private MethodHandle XChangeProperty;
        private MethodHandle XFlush;
    
        @Override
        public void init()
        throws Exception {
            Linker linker = Linker.nativeLinker();
            SymbolLookup lookup = SymbolLookup.loaderLookup();
    
            XOpenDisplay = linker.downcallHandle(
                lookup.findOrThrow("XOpenDisplay"),
                FunctionDescriptor.of(
                    ValueLayout.ADDRESS,                            // returns Display *
                    ValueLayout.ADDRESS.withName("display_name")    // char *display_name
                ));
            XCloseDisplay = linker.downcallHandle(
                lookup.findOrThrow("XCloseDisplay"),
                FunctionDescriptor.of(
                    ValueLayout.JAVA_INT,                           // returns int
                    ValueLayout.ADDRESS.withName("display")         // Display *display
                ));
            XInternAtom = linker.downcallHandle(
                lookup.findOrThrow("XInternAtom"),
                FunctionDescriptor.of(
                    ValueLayout.JAVA_INT,                            // returns Atom
                    ValueLayout.ADDRESS.withName("display"),         // Display *display
                    ValueLayout.ADDRESS.withName("atom_name"),       // char *atom_name
                    ValueLayout.JAVA_INT.withName("only_if_exists")  // Bool only_if_exists
                ));
            XChangeProperty = linker.downcallHandle(
                lookup.findOrThrow("XChangeProperty"),
                FunctionDescriptor.of(
                    ValueLayout.JAVA_INT,                       // returns int
                    ValueLayout.ADDRESS.withName("display"),    // Display *display
                    ValueLayout.JAVA_INT.withName("w"),         // Window w
                    ValueLayout.JAVA_INT.withName("property"),  // Atom property
                    ValueLayout.JAVA_INT.withName("type"),      // Atom type
                    ValueLayout.JAVA_INT.withName("format"),    // int format
                    ValueLayout.JAVA_INT.withName("mode"),      // int mode
                    ValueLayout.ADDRESS.withName("data"),       // char *data
                    ValueLayout.JAVA_INT.withName("nelements")  // int nelements
                ));
            XFlush = linker.downcallHandle(
                lookup.findOrThrow("XFlush"),
                FunctionDescriptor.of(
                    ValueLayout.JAVA_INT,                   // returns int
                    ValueLayout.ADDRESS.withName("display") // Display *display
                ));
        }
    
        private MemorySegment XOpenDisplay(MemorySegment display)
        throws Throwable {
            return (MemorySegment) XOpenDisplay.invokeExact(display);
        }
    
        private int XCloseDisplay(MemorySegment display)
        throws Throwable {
            return (int) XCloseDisplay.invokeExact(display);
        }
    
        private int XInternAtom(MemorySegment display,
                                MemorySegment atomName,
                                int onlyIfExists)
        throws Throwable {
            return (int) XInternAtom.invokeExact(display, atomName, onlyIfExists);
        }
    
        private int XFlush(MemorySegment display)
        throws Throwable {
            return (int) XFlush.invokeExact(display);
        }
    
        private int XChangeProperty(MemorySegment display,
                                    int window,
                                    int property,
                                    int type,
                                    int format,
                                    int mode,
                                    MemorySegment data,
                                    int dataLen)
        throws Throwable {
            return (int) XChangeProperty.invokeExact(display,
                window, property, type, format, mode, data, dataLen);
        }
    
        @Override
        public void start(Stage primaryStage) {
            Button startButton = new Button("Start Long Running Task");
    
            startButton.setOnAction(event -> {
                final int window = (int) Window.getWindows().getFirst().getNativeWindow();
                System.out.printf("Window=%#x%n", window);
                // Create a long-running task
                Task<Void> longTask = new Task<>() {
                    @Override
                    protected Void call() throws Exception {
                        System.out.println("Started");
    
                        try (var arena = Arena.ofConfined()) {
                            MemorySegment x11Session = XOpenDisplay(MemorySegment.NULL);
                            System.out.println("display=" + x11Session);
    
                            var name = arena.allocateFrom("_NET_WM_XAPP_PROGRESS");
                            var NET_WM_XAPP_PROGRESS = XInternAtom(x11Session, name, 0);
                            if (NET_WM_XAPP_PROGRESS == None) {
                                throw new RuntimeException("XInternAtom failed.");
                            }
    //                        var NET_WM_XAPP_PROGRESS_PULSE = arena.allocateFrom("NET_WM_XAPP_PROGRESS_PULSE");
    
                            // Prepare the progress data
                            MemorySegment progressData = arena.allocateFrom(ValueLayout.JAVA_INT, 0);
                            int status = XChangeProperty(
                                    x11Session,                                   // display
                                    window,                                       // window
                                    NET_WM_XAPP_PROGRESS,                         // property
                                    XA_CARDINAL,                                  // type
                                    32,                                           // format
                                    PropModeReplace,                              // mode
                                    progressData,                                 // data
                                    1);                                           // nelements
                            if (status == False) {
                                throw new RuntimeException("XChangeProperty returned " + status);
                                        
                            }
    
                            status = XFlush(x11Session);
                            if (status == False) {
                                throw new RuntimeException("XFlush returned " + status);
                            }
    
                            System.out.println("Countdown started");
    
                            // Set the taskbar progress
                            for (int i = 0; i <= 100; i+=20) {
                                // Simulate work
                                Thread.sleep(500);
                                System.out.println(i);
                                progressData.set(ValueLayout.JAVA_INT, 0, i);
                                // Update taskbar progress
                                // https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#XChangeProperty
                                status = XChangeProperty(
                                        x11Session,                                   // display
                                        window,                                       // window
                                        NET_WM_XAPP_PROGRESS,                         // property
                                        XA_CARDINAL,                                  // type
                                        32,                                           // format
                                        PropModeReplace,                              // mode
                                        progressData,                                 // data
                                        1);                                           // nelements
                                if (status == False) {
                                    throw new RuntimeException("XChangeProperty returned " + status);
                                }
    
                                status = XFlush(x11Session);
                                if (status == False) {
                                    throw new RuntimeException("XFlush returned " + status);
                                }
                            }
                            System.out.println("Finished");
                            XCloseDisplay(x11Session);
    
                        } catch (Throwable ex) {
                            ex.printStackTrace();
                        }
                        return null;
                    }
                };
    
                // Start the task in a new thread
                new Thread(longTask).start();
            });
    
            VBox vbox = new VBox(10, startButton);
            Scene scene = new Scene(vbox, 300, 200);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Taskbar Progress Example Linux");
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    This runs, but has no visual effect on my system. I don’t know if that’s because my version of Gnome doesn’t support it. I also tried using the xprop program to set the same window property, and that too had no effect.