JavaFX Application Remains in macOS Dock After Closure and Does Not Reopen Properly or Close

55 views Asked by At

I'm developing a JavaFX application that needs to run in the background and send notifications to the user. To achieve this, I'm using FXTrayIcon for system tray integration and handling the application lifecycle to ensure it continues running in the background even when the main window is closed. However, I've encountered a couple of issues specific to macOS:

Persistent Dock Icon After Closure: Even after closing the application window, the application icon remains in the Dock. While the background process continues as expected (which is intended), I cannot quit the application from the Dock; it requires a force quit.

Reopening Issues: If the application is closed to the Dock (icon still visible) and I try to reopen it by clicking the Dock icon, no window appears, making it seem like the application is unresponsive.

Here's a simplified version of my code to illustrate how I'm setting up the application:

HelloApplication.java: (main)

@Override
public void start(Stage stage) throws Exception {
    this.primaryStage = stage;
    setupPrimaryStage();

    FXTrayIcon trayIcon = new FXTrayIcon(stage, getClass().getResource("/path/to/icon.png"));
    trayIcon.show();

    Platform.setImplicitExit(false);
    javax.swing.SwingUtilities.invokeLater(this::addAppToTray);
}

private void addAppToTray() {
       if (!Platform.isFxApplicationThread()) {
            Platform.runLater(this::addAppToTray);
            return;
        }

        if (primaryStage == null) {
            return;
        }

        if (primaryStage.isShowing()) {
            return;
        }
        primaryStage.show();
}

1

There are 1 answers

4
James_D On

I'm developing a JavaFX application that needs to run in the background and send notifications to the user. To achieve this, I'm using FXTrayIcon ...

The FXTrayIcon library isn't needed for this, and it's not what it really does other than as a side effect of managing a JavaFX application from a tray icon.

... I'm using FXTrayIcon for system tray integration ...

This is what FXTrayIcon does, but nothing else in your question suggests you need system tray integration.

... and handling the application lifecycle

This is done from the standard JavaFX API, mostly by overriding the init(), start(), and stop() methods in the Application class and by calling some utility methods in Platform (such as setImplicitExit(...)).

Even after closing the application window, the application icon remains in the Dock.

This is the expected behavior, since your application is still running (you specifically say

it continues running in the background even when the main window is closed.

The dock item represents a running application. Closing all the windows opened by an application doesn't necessarily mean that an application stops running. On my Mac, I have (for example) Microsoft Word installed. If I start Word and open a couple of windows, then close all the windows associated with the application, the icon remains in the dock and I can right click on it. I can still switch to the application (e.g. by using Command-Tab) and can access the menu, and open new windows. So again, because your application (the JVM) is still running, this is entirely expected.

The default behavior of JavaFX is for the JavaFX platform to exit when all windows are closed. This means the JavaFX Application Thread (and some other threads) will terminate and you can no longer display any JavaFX UI. This behavior can be turned off (as you do, and in fact as is also done for you by the FXTrayIcon library) by calling Platform.setImplicitExit(false), in which case you must call Platform.exit() to exit the JavaFX platform.

When the JavaFX platform exits (either implicitly, or by calling Platform.exit), if no non-daemon threads are running, then the JVM will also exit. This is how the dock icon is usually removed from the dock when you close a JavaFX window (or all windows, if multiple are open).

The dock icon fails to be removed for two reasons in your example: first because you call Platform.setImplicitExit(false), so the JavaFX Platform remains running, but also because the FXTrayIcon library relies on the AWT toolkit and starts the AWT event dispatch thread to manage the AWT tray icon behind the scenes. So even if you call Platform.exit(), the JVM will not exit because the AWT platform is still running.


I am not really convinced you need FXTrayIcon: it has basically nothing to do with the functionality you say you want. However, this is a quick overview of how it works.

AWT provides tray icon integration, but JavaFX does not. To use AWT tray icons in a JavaFX application would require managing multiple threads (the FX Application Thread and the AWT event dispatch thread), which is complex at best, as well as working with, for example, AWT menu items instead of JavaFX menu items. FXTrayIcon thus provides a JavaFX programmer's interface to the AWT tray icon, allowing the JavaFX programmer to work in a single thread and use familiar API such as JavaFX MenuItems, etc.

The typical use case for this is to place an icon in the system tray (which, of course, is completely different from the dock on Mac OSX). The user can pull up a menu from the tray icon, which typically does things like open or close the application window. Since a major use case involves having a non-JavaFX mechanism to manage the windows, FXTrayIcon turns off implicit exit for you, as long as one or more tray icons are displayed. There is also showMessage(...) type functionality, which shows notifications "close to" the tray icon. See the Javadocs for more details.

So the threading in the code in your post is completely unnecessary, since FXTrayIcon manages this for you, and completely wrong anyway. You use SwingUtilities.invokeLater(...) to call addAppToTray() on the AWT event dispatch thread. The first clause in addAppToTray() checks to see if you are on the JavaFX Application Thread: you are not, since you explicitly invoked the method on the AWT Event Dispatch Thread. So it then uses Platform.runLater(...) so send another invocation of addAppToTray() to the FX Application Thread.

When the FX Application Thread receives this second invocation, it first checks if primaryStage is null: it's not, because you necessarily initialized it earlier in start(), and then checks if the stage is not showing, and calls show() if it is not showing. Note this last check is redundant: show() is a no-op if the stage is already showing. Since start() is invoked on the FX Application Thread (read the docs), the entire method can be reduced to a single line (primaryStage.show()) if you omit the pointless SwingUtilities.invokeLater(...) call.

Also note that Platform.setImplicitExit(false) is redundant, because FXTrayIcon is going to do that for you.

To exit the application entirely, you need to arrange for Platform.exit() to be called, and you need to remove any tray icons. If you hide all the tray icons, FXTrayIcon will restore the implicit exit status to its default, so closing all JavaFX windows after that will exit the platform.

Here is an example of using FXTrayIcon and exiting the entire application either via a button or via a menu item in the tray icon.

import com.dustinredmond.fxtrayicon.FXTrayIcon;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class App extends Application {

    private Stage primaryStage;
    private FXTrayIcon trayIcon;

    @Override
    public void start(Stage stage) throws Exception {
        this.primaryStage = stage;
        setupPrimaryStage();


        trayIcon = new FXTrayIcon(stage);

        MenuItem exitMenuItem = new MenuItem("Exit application");
        exitMenuItem.setOnAction(e -> exitCompletely());
        trayIcon.addMenuItem(exitMenuItem);

        trayIcon.addTitleItem(true);

        trayIcon.show();

        primaryStage.show();
    }

    private void exitCompletely() {
        trayIcon.hide();
        primaryStage.hide();
    }
    
    private void setupPrimaryStage() {
        Button exitButton = new Button("Exit Application");
        exitButton.setOnAction(e -> exitCompletely());
        BorderPane root = new BorderPane(exitButton);
        root.setPadding(new Insets(40));
        primaryStage.setScene(new Scene(root));
    }

}

Closing the window will not exit the application, but simply close the window. Using the Exit button, or the "Exit Application" menu item from the tray icon will exit the application completely. If you close the window, you can reopen it from the tray icon.


However, nothing in your question indicates you need any of this functionality. If all you want is a background thread running, which keeps running when all windows are closed, just call Platform.setImplicitExit(false);. Then just show and hide windows as needed, ensuring you do that on the FX Application Thread. A nice example of doing this is shown here.