3.6 KiB
Feature 1: System Tray Icon
Context
The app lives primarily in the system tray. The window is secondary — it opens for configuration/status and closes back to tray. This is the shell that all UI features plug into.
Dependencies
- Depends on: Feature 0 (Foundation) — logging, platform detection
- Depended on by: Features 2-5 (all UI features are accessed through the tray)
Approach
Use Compose Desktop's built-in Tray composable (androidx.compose.ui.window.Tray). No extra libraries needed — it supports icons, tooltips, context menus, and notifications natively.
App Lifecycle Model
The app always runs with a tray icon visible. The main window is shown/hidden based on user interaction. Closing the window hides it to tray — it does not exit the app. Exiting is only via the tray menu "Quit" item.
Implementation Steps
Step 1: Create a tray icon asset
Files to create:
composeApp/src/jvmMain/resources/tray-icon.png(32x32, simple recognizable icon)
For macOS dark mode support, add JVM arg: -Dapple.awt.enableTemplateImages=true
Step 2: Restructure main.kt for tray-first lifecycle
Files to modify:
composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt
fun main() = application {
// Initialize logging (from Feature 0)
var isWindowVisible by remember { mutableStateOf(true) }
// Which screen to show: STATUS (default) or CONFIG
var currentScreen by remember { mutableStateOf(Screen.STATUS) }
Tray(
icon = painterResource("tray-icon.png"),
tooltip = "WoW Backup",
onAction = { isWindowVisible = true }, // click tray -> show window
menu = {
Item("Status", onClick = {
currentScreen = Screen.STATUS
isWindowVisible = true
})
Item("Settings", onClick = {
currentScreen = Screen.CONFIG
isWindowVisible = true
})
Separator()
Item("Quit", onClick = ::exitApplication)
}
)
if (isWindowVisible) {
Window(
onCloseRequest = { isWindowVisible = false }, // close -> hide to tray
title = "WoW Backup",
) {
App(currentScreen)
}
}
}
Step 3: Define screen navigation enum
Files to create:
composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/Screen.kt
enum class Screen { STATUS, CONFIG, RESTORE }
Step 4: Update App.kt for screen routing
Files to modify:
composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt
Strip all template code. Route to placeholder screens based on currentScreen:
Screen.STATUS-> "Status screen placeholder"Screen.CONFIG-> "Config screen placeholder"Screen.RESTORE-> "Restore screen placeholder"
Step 5: Auto-show config if not configured
In main.kt, on startup check ConfigManager.isConfigured. If false, set currentScreen = Screen.CONFIG and isWindowVisible = true.
Step 6: macOS JVM args
Files to modify:
composeApp/build.gradle.kts— add to desktop application config:
compose.desktop {
application {
jvmArgs("-Dapple.awt.enableTemplateImages=true")
}
}
Verification
./gradlew composeApp:run— app launches with tray icon visible- Tray icon shows context menu with Status, Settings, Quit
- Clicking "Status" or "Settings" opens the window to the correct placeholder
- Closing the window hides it (app stays running in tray)
- Clicking tray icon again re-shows the window
- "Quit" exits the app fully
- On first run (no config), window auto-opens to config screen