Tornadofx: Feature: Tray Icon Support

Created on 20 Feb 2017  路  10Comments  路  Source: edvin/tornadofx

Hi,

it would be awesome if TornadoFX would support a tray icon.

Instead of closing, the app would be minimzed.

Most helpful comment

Closing the issue, as the implementation is complete :)

All 10 comments

Hi! It's actually not so hard to do. You have to leverage AWT, since JavaFX doesn't support a Tray Icon yet. Here is a complete application that adds a Tray Icon with show/exit menu options and click-to-show:

class MyApp: App(MainView::class) {
    private lateinit var trayIcon: TrayIcon

    override fun start(stage: Stage) {
        super.start(stage)

        // Don't exit on window close (Optional)
        Platform.setImplicitExit(false)

        // Create tray icon
        SwingUtilities.invokeLater { createTrayIcon() }
    }

    override fun stop() {
        SwingUtilities.invokeLater { SystemTray.getSystemTray().remove(trayIcon) }
    }

    fun createTrayIcon() {
        // Initialize AWT Toolkit
        Toolkit.getDefaultToolkit()

        // Load icon
        val icon = ImageIO.read(MyApp::class.java.getResource("/icon.png"))

        // Create tray icon, assign icons and actions
        trayIcon = TrayIcon(icon).apply {
            // Show app on tray icon click
            addMouseListener(object : MouseAdapter() {
                override fun mouseClicked(e: MouseEvent) {
                    if (e.button == MouseEvent.BUTTON1 && e.clickCount == 1) {
                        Platform.runLater {
                            FX.primaryStage.show()
                            FX.primaryStage.toFront()
                        }
                    }
                }
            })

            // Add a menu item to show the app and one to hide
            popupMenu = PopupMenu("MyApp").apply {
                add(MenuItem("Show...").apply {
                    addActionListener {
                        Platform.runLater {
                            FX.primaryStage.show()
                            FX.primaryStage.toFront()
                        }
                    }
                })
                add(MenuItem("Exit...").apply {
                    addActionListener {
                        Platform.runLater {
                            Platform.exit()
                        }
                    }
                })
            }

            // Add the tray icon to the system tray
            SystemTray.getSystemTray().add(this)
        }

    }

}

We decided to add formal builder support for TrayIcon, so now the above can be written like this:

class MyApp: App(MainView::class) {
    override fun start(stage: Stage) {
        super.start(stage)

        trayicon(resources.stream("/icon.png")) {
            setOnMouseClicked(fxThread = true) {
                FX.primaryStage.show()
                FX.primaryStage.toFront()
            }

            menu("MyApp") {
                item("Show...") {
                    setOnAction(fxThread = true) {
                        FX.primaryStage.show()
                        FX.primaryStage.toFront()
                    }
                }
                item("Exit") {
                    setOnAction(fxThread = true) {
                        Platform.exit()
                    }
                }
            }
        }
    }
}

Notice that setOnAction and setOnMouseClicked takes an optional fxThread parameter, which when set to true will cause the action to be performed on the JavaFX Application Thread so you don't need to wrap the action in Platform.runLater when you want to execute JavaFX specific code.

You can try it out in 1.6.3-SNAPSHOT if you want, or just use the first example until the 1.6.3 release. The builders has multiple extra options, I will document them in the guide tomorrow.

Let me know if you run into any issues.

@edvin in your first example you explicitly call Platform.setImplicitExit(false) you dont in the second example. Does this mean Platform.setImplicitExit(false) is done by the new functionality?

Yes, the trayicon builder actually has an implicitExcit boolean parameter which defaults to false :)

Tobias, did you get a chance to test this? Are you happy with it?

@edvin
The second example looks fantastic! I will give it a try today or tomorrow and leave feedback here afterwards!

I made some more refinements so you can load the icon like this: trayicon(resources.stream("/icon.png"))

trayicon only seems to work in App related classes not in Workspace related classes?

That's correct. Would it make sense to make it available everywhere?

Closing the issue, as the implementation is complete :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ThraaxSession picture ThraaxSession  路  3Comments

eibens picture eibens  路  4Comments

elliotcm picture elliotcm  路  7Comments

vlipovetskii picture vlipovetskii  路  6Comments

AlbRoehm picture AlbRoehm  路  3Comments