All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.jna.linux.GtkEventDispatch Maven / Gradle / Ivy

/*
 * Copyright 2015 dorkbox, llc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dorkbox.jna.linux;

import static dorkbox.jna.linux.Gtk2.FALSE;
import static dorkbox.jna.linux.Gtk2.Gtk2;

import java.awt.event.ActionListener;
import java.util.LinkedList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.slf4j.LoggerFactory;

import com.sun.jna.Pointer;

import dorkbox.util.javaFx.JavaFX;
import dorkbox.util.swt.Swt;

public
class GtkEventDispatch {
    static boolean FORCE_GTK2 = false;
    static boolean PREFER_GTK3 = false;
    static boolean DEBUG = false;

    // have to save these in a field to prevent GC on the objects (since they go out-of-scope from java)
    private static final LinkedList gtkCallbacks = new LinkedList();

    // This is required because the EDT needs to have it's own value for this boolean, that is a different value than the main thread
    private static ThreadLocal isDispatch = new ThreadLocal() {
        @Override
        protected
        Boolean initialValue() {
            return false;
        }
    };

    private static boolean started = false;

    @SuppressWarnings("FieldCanBeLocal")
    private static Thread gtkUpdateThread = null;

    private static GMainLoop mainloop;
    private static GMainContext context;

    // when debugging the EDT, we need a longer timeout.
    private static final boolean debugEDT = false;

    // timeout is in seconds
    private static final int TIMEOUT = debugEDT ? 10000000 : 2;


    /**
     * This will load and start GTK
     */
    public static synchronized
    void startGui(final boolean forceGtk2, final boolean preferGkt3, final boolean debug) {
        // only permit one startup per JVM instance
        if (!started) {
            started = true;

            GtkEventDispatch.FORCE_GTK2 = forceGtk2;
            GtkEventDispatch.PREFER_GTK3 = preferGkt3;
            GtkEventDispatch.DEBUG = debug;

            // startup the GTK GUI event loop. There can be multiple/nested loops.
            if (!GtkLoader.alreadyRunningGTK) {
                // If JavaFX/SWT is used, this is UNNECESSARY (we can detect if the GTK main_loop is running)

                gtkUpdateThread = new Thread() {
                    @Override
                    public
                    void run() {
                        Glib.GLogFunc orig = null;
                        if (debug) {
                            // don't suppress GTK warnings in debug mode
                            LoggerFactory.getLogger(GtkEventDispatch.class).debug("Running GTK Native Event Loop");
                        } else {
                            // NOTE: This can output warnings, so we suppress them. Additionally, setting System.err to null, or trying
                            // to filter it, will not suppress these errors/warnings
                            orig = Glib.g_log_set_default_handler(Glib.nullLogFunc, null);
                        }


                        if (!Gtk2.gtk_init_check(0)) {
                            throw new RuntimeException("Error starting GTK");
                        }


                        // create the main-loop
                        mainloop = Gtk2.g_main_loop_new(null, false);
                        context = Gtk2.g_main_loop_get_context(mainloop);

                        // blocks until we quit the main loop
                        Gtk2.g_main_loop_run(mainloop);


                        if (orig != null) {
                            Glib.g_log_set_default_handler(orig, null);
                        }
                    }
                };
                gtkUpdateThread.setDaemon(false); // explicitly NOT daemon so that this will hold the JVM open as necessary
                gtkUpdateThread.setName("GTK Native Event Loop");
                gtkUpdateThread.start();
            }
        }
    }

    /**
     * Waits for the all posted events to GTK to finish loading
     */
    @SuppressWarnings("Duplicates")
    public static synchronized
    void waitForEventsToComplete() {
        final CountDownLatch blockUntilStarted = new CountDownLatch(1);

        dispatch(new Runnable() {
            @Override
            public
            void run() {
                blockUntilStarted.countDown();
            }
        });

        if (JavaFX.isLoaded) {
            if (!JavaFX.isEventThread()) {
                try {
                    if (!blockUntilStarted.await(10, TimeUnit.SECONDS)) {
                        if (DEBUG) {
                            LoggerFactory.getLogger(GtkEventDispatch.class)
                                         .error("Something is very wrong. The waitForEventsToComplete took longer than expected.",
                                                new Exception(""));
                        }
                    }

                    // we have to WAIT until all events are done processing, OTHERWISE we have initialization issues
                    while (true) {
                        Thread.sleep(100);

                        synchronized (gtkCallbacks) {
                            if (gtkCallbacks.isEmpty()) {
                                break;
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        else if (Swt.isLoaded) {
            if (!Swt.isEventThread()) {
                // we have to WAIT until all events are done processing, OTHERWISE we have initialization issues
                try {
                    if (!blockUntilStarted.await(10, TimeUnit.SECONDS)) {
                        if (DEBUG) {
                            LoggerFactory.getLogger(GtkEventDispatch.class)
                                         .error("Something is very wrong. The waitForEventsToComplete took longer than expected.",
                                                new Exception(""));
                        }
                    }

                    while (true) {
                        Thread.sleep(100);

                        synchronized (gtkCallbacks) {
                            if (gtkCallbacks.isEmpty()) {
                                break;
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        else {
            try {
                if (!blockUntilStarted.await(10, TimeUnit.SECONDS)) {
                    if (DEBUG) {
                        LoggerFactory.getLogger(GtkEventDispatch.class)
                                     .error("Something is very wrong. The waitForEventsToComplete took longer than expected.",
                                            new Exception(""));
                    }
                }

                // we have to WAIT until all events are done processing, OTHERWISE we have initialization issues
                while (true) {
                    Thread.sleep(100);

                    synchronized (gtkCallbacks) {
                        if (gtkCallbacks.isEmpty()) {
                            break;
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Dispatch the runnable to GTK and wait until it has finished executing. If this is called while on the GTK dispatch thread, it will
     * immediately execute the task, otherwise it will submit the task to run on the FIFO queue.
     */
    public static
    void dispatchAndWait(final Runnable runnable) {
        // if we are on the dispatch queue, do not block
        Boolean isDispatch = GtkEventDispatch.isDispatch.get();
        if (isDispatch) {
            // don't block. The ORIGINAL call (before items were queued) will still be blocking. If the original call was a "normal"
            // dispatch, then subsequent dispatchAndWait calls are irrelevant (as they happen in the GTK thread, and not the main thread).
            runnable.run();
            return;
        }


        final CountDownLatch countDownLatch = new CountDownLatch(1);
        dispatch(new Runnable() {
            @Override
            public
            void run() {
                try {
                    runnable.run();
                } catch (Exception e) {
                    LoggerFactory.getLogger(GtkEventDispatch.class).error("Error during GTK run loop: ", e);
                } finally {
                    countDownLatch.countDown();
                }
            }
        });

        // this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI
        // thread occur in REASONABLE time-frames, and alert the user if not.
        try {
            if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) {
                if (DEBUG) {
                    LoggerFactory.getLogger(GtkEventDispatch.class).error(
                                    "Something is very wrong. The Event Dispatch Queue took longer than " + TIMEOUT + " seconds " +
                                    "to complete.", new Exception(""));
                }
                else {
                    throw new RuntimeException("Something is very wrong. The Event Dispatch Queue took longer than " + TIMEOUT +
                                               " seconds " + "to complete.");
                }
            }
        } catch (InterruptedException e) {
            LoggerFactory.getLogger(GtkEventDispatch.class).error("Error waiting for dispatch to complete.", new Exception(""));
        }
    }

    /**
     * Best practices for GTK, is to call everything for it on the GTK THREAD. If we are currently on the dispatch thread, then this
     * task will execute immediately.
     */
    public static
    void dispatch(final Runnable runnable) {
        if (GtkLoader.alreadyRunningGTK) {
            if (JavaFX.isLoaded) {
                // JavaFX only
                if (JavaFX.isEventThread()) {
                    // Run directly on the JavaFX event thread
                    runnable.run();
                }
                else {
                    JavaFX.dispatch(runnable);
                }
                return;
            }

            if (Swt.isLoaded && Swt.isEventThread()) {
                // Run directly on the SWT event thread. If it's not on the dispatch thread, we will use GTK to put it there
                runnable.run();
                return;
            }
        }

        // not javafx
        // gtk/swt are **mostly** the same in how events are dispatched, so we can use "raw" gtk methods for SWT
        if (isDispatch.get()) {
            // Run directly on the dispatch thread. This will be false unless we are running the dispatch queue.
            runnable.run();
            return;
        }

        final FuncCallback callback = new FuncCallback() {
            @Override
            public
            int callback(final Pointer data) {
                isDispatch.set(true);

                try {
                    runnable.run();
                } finally {
                    isDispatch.set(false);
                }

                synchronized (gtkCallbacks) {
                    gtkCallbacks.removeFirst(); // now that we've 'handled' it, we can remove it from our callback list
                }

                return FALSE; // don't want to call this again
            }
        };

        synchronized (gtkCallbacks) {
            gtkCallbacks.offer(callback); // prevent GC from collecting this object before it can be called
        }

        // explicitly invoke on our new GTK main loop context
        Gtk2.g_main_context_invoke(context, callback, null);
    }

    /**
     * required to properly setup the dispatch flag when using native menus
     *
     * @param callback will never be null.
     */
    public static
    void proxyClick(final ActionListener callback) {
        isDispatch.set(true);

        try {
            callback.actionPerformed(null);
        } catch (Throwable t) {
            LoggerFactory.getLogger(GtkEventDispatch.class)
                         .error("Error during GTK click callback: ", t);
        }

        isDispatch.set(false);
    }
    public static synchronized
    void shutdownGui() {
        dispatchAndWait(new Runnable() {
            @Override
            public
            void run() {
                // If JavaFX/SWT is used, this is UNNECESSARY (and will break SWT/JavaFX shutdown)
                if (!GtkLoader.alreadyRunningGTK) {
                    Gtk2.g_main_loop_quit(mainloop);
                    // Gtk2.gtk_main_quit();
                }

                started = false;
            }
        });
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy