Skip to content

Commit 2aa3263

Browse files
authored
Merge pull request #321 from kdroidFilter/add-more-window-control-to-trayapp-api
Update `TrayApp` with new parameters and enhanced documentation
2 parents 2726544 + b4166da commit 2aa3263

2 files changed

Lines changed: 202 additions & 234 deletions

File tree

README.md

Lines changed: 63 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@
6060
- [Icon Limitations](#icon-limitations)
6161
- [Theme Behavior](#theme-behavior)
6262
- [🧪 TrayApp (Experimental)](#-trayapp-experimental)
63-
- [Overview](#overview)
64-
- [Basic Usage](#basic-usage)
65-
- [TrayAppState API](#trayappstate-api)
66-
- [Advanced Examples](#advanced-examples)
6763
- [📄 License](#-license)
6864
- [🤝 Contribution](#-contribution)
6965
- [👨‍💻 Author](#-author)
@@ -556,249 +552,123 @@ By default, icons are optimized by OS: 32x32px (Windows), 44x44px (macOS), 24x24
556552
- **Windows**: Follows the system theme
557553
- **Linux**: Varies by desktop environment (GNOME/KDE/etc.)
558554

559-
## 🧪 TrayApp (Experimental)
555+
# 🧪 TrayApp (Experimental)
560556

557+
`TrayApp` gives your desktop app a **system‑tray/menu‑bar icon** and a **tiny popup window** for quick actions. It’s perfect for quick toggles, mini dashboards, and “control center” UIs.
561558

562-
<p align="center">
563-
<img src="screenshots/trayappdemo.gif" alt="demo">
564-
</p>
559+
**Works on Windows, macOS, and Linux.** Smooth fade animations, smart positioning near the tray, and a simple API so you stay productive.
560+
561+
---
565562

566-
### Overview
567-
TrayApp is a high-level API that creates a system tray icon and an undecorated popup window that toggles when the tray icon is clicked. The popup auto-hides when it loses focus or when you click outside it (macOS/Linux watchers supported) and can fade in/out.
563+
## Why you’ll like it
568564

569-
Use TrayApp when you want a compact companion window (like a quick settings or mini dashboard) anchored to the system tray, in addition to or instead of your main window – ideal for building apps in the style of JetBrains Toolbox.
565+
* **One‑click popup** anchored to the tray/menu bar
566+
* **Auto‑dismiss** on outside click (or **manual** if you prefer)
567+
* **State preserved**: toggling visibility doesn’t remount your UI
568+
* **Easy sizing** with `setWindowSize(...)`
569+
* **Tray menu builder** for quick actions
570+
* **Theming**: transparent/undecorated styles for a modern look
570571

571-
### Basic Usage
572+
---
573+
574+
## Quick Start (minimal)
572575

573576
```kotlin
574577
@OptIn(ExperimentalTrayAppApi::class)
575578
application {
576-
// Create TrayAppState to control the popup
577579
val trayAppState = rememberTrayAppState(
578-
initialWindowSize = DpSize(300.dp, 500.dp),
579-
initiallyVisible = true // Show on startup
580+
initialWindowSize = DpSize(300.dp, 420.dp),
581+
initiallyVisible = true // default is false
582+
// initialDismissMode defaults to TrayWindowDismissMode.AUTO
580583
)
581-
584+
582585
TrayApp(
583-
state = trayAppState, // Required: pass the state
584-
icon = Icons.Default.Book,
585-
tooltip = "My App",
586-
menu = {
587-
Item("Open") { /* ... */ }
586+
state = trayAppState,
587+
icon = Icons.Default.Dashboard, // required (or Painter / platform-specific overloads)
588+
tooltip = "My Tray App", // required
589+
590+
// Optional visual controls (defaults shown below)
591+
transparent = true, // default = true
592+
undecorated = true, // default = true
593+
resizable = false, // default = false
594+
windowsTitle = "My Tray Popup", // default = "" — recommended (esp. on Linux & when undecorated=false)
595+
windowIcon = null, // default = null — set your app icon; important on Linux & when undecorated=false
596+
597+
menu = { // optional (default = null)
598+
Item("Toggle popup") { trayAppState.toggle() }
588599
Divider()
589600
Item("Quit") { exitApplication() }
590601
}
591602
) {
592-
// Popup content
593-
MaterialTheme {
594-
Text("Quick Settings Panel")
603+
// Your Compose UI (DialogWindowScope receiver)
604+
MaterialTheme {
605+
Text("Quick Settings")
606+
Button(onClick = { trayAppState.hide() }) { Text("Close") }
595607
}
596608
}
597609
}
598610
```
599611

600-
### TrayAppState API
612+
---
601613

602-
TrayAppState provides comprehensive control over the popup window:
614+
## Common recipes
603615

604-
#### Creating State
605-
```kotlin
606-
val trayAppState = rememberTrayAppState(
607-
initialWindowSize = DpSize(300.dp, 400.dp),
608-
initiallyVisible = false // Hidden by default
609-
)
610-
```
616+
### Show / Hide / Toggle
611617

612-
#### Controlling Visibility
613618
```kotlin
614-
// Show the popup
615619
trayAppState.show()
616-
617-
// Hide the popup
618620
trayAppState.hide()
619-
620-
// Toggle visibility
621621
trayAppState.toggle()
622622
```
623623

624-
#### Observing State
625-
```kotlin
626-
// Observe visibility as State
627-
val isVisible by trayAppState.isVisible.collectAsState()
628-
629-
// Observe window size
630-
val windowSize by trayAppState.windowSize.collectAsState()
631-
632-
// Callback for visibility changes
633-
LaunchedEffect(trayAppState) {
634-
trayAppState.onVisibilityChanged { visible ->
635-
println("Popup is now ${if (visible) "visible" else "hidden"}")
636-
}
637-
}
638-
```
624+
### Resize the popup
639625

640-
#### Dynamic Window Resizing
641626
```kotlin
642-
// Change size programmatically
643627
trayAppState.setWindowSize(400.dp, 600.dp)
644-
645-
// Or using DpSize
646-
trayAppState.setWindowSize(DpSize(350.dp, 500.dp))
628+
// or
629+
trayAppState.setWindowSize(DpSize(250.dp, 350.dp))
647630
```
648631

649-
## 🧩 New: Tray Window Dismiss Modes
650-
651-
By default, the `TrayApp` popup window closes automatically when it loses focus or when the user clicks outside of it.
652-
With the new `TrayWindowDismissMode` API, you can choose between:
653-
654-
* **AUTO** (default): The popup closes automatically when focus is lost or when clicking outside.
655-
* **MANUAL**: The popup remains visible until you explicitly call `trayAppState.hide()`.
656-
657-
### Example
632+
### Dismiss mode
658633

659634
```kotlin
660-
@OptIn(ExperimentalTrayAppApi::class)
661-
application {
662-
val trayAppState = rememberTrayAppState(
663-
initialWindowSize = DpSize(300.dp, 400.dp),
664-
initiallyVisible = false,
665-
initialDismissMode = TrayWindowDismissMode.MANUAL // 👈 Manual mode
666-
)
667-
668-
TrayApp(
669-
state = trayAppState,
670-
icon = Icons.Default.Settings,
671-
tooltip = "Quick Settings"
672-
) {
673-
Column {
674-
Text("This popup will NOT auto-close")
675-
Button(onClick = { trayAppState.hide() }) {
676-
Text("Close manually")
677-
}
678-
}
679-
}
680-
}
681-
```
635+
// AUTO (default): closes when user clicks outside or focus is lost
636+
val state = rememberTrayAppState(initialDismissMode = TrayWindowDismissMode.AUTO)
682637

683-
### Switching at runtime
684-
685-
```kotlin
638+
// MANUAL: you decide when to hide
686639
LaunchedEffect(Unit) {
687-
trayAppState.setDismissMode(TrayWindowDismissMode.AUTO)
640+
state.setDismissMode(TrayWindowDismissMode.MANUAL)
688641
}
689642
```
690643

691-
### Advanced Examples
644+
### Tray menu (compact)
692645

693-
#### Example 1: Control from Main Window
694646
```kotlin
695-
@OptIn(ExperimentalTrayAppApi::class)
696-
application {
697-
val trayAppState = rememberTrayAppState()
698-
var isMainWindowVisible by remember { mutableStateOf(true) }
699-
700-
// Tray with popup
701-
TrayApp(
702-
state = trayAppState,
703-
icon = Icons.Default.Settings,
704-
tooltip = "Quick Settings"
705-
) {
706-
// Popup content
707-
Column {
708-
Text("Quick Settings")
709-
Button(onClick = {
710-
isMainWindowVisible = true
711-
trayAppState.hide()
712-
}) {
713-
Text("Open Main Window")
714-
}
715-
}
716-
}
717-
718-
// Main window can control the popup
719-
if (isMainWindowVisible) {
720-
Window(onCloseRequest = { isMainWindowVisible = false }) {
721-
Column {
722-
Button(onClick = { trayAppState.show() }) {
723-
Text("Show Quick Settings")
724-
}
725-
726-
Button(onClick = {
727-
trayAppState.setWindowSize(250.dp, 350.dp)
728-
}) {
729-
Text("Make Popup Smaller")
730-
}
731-
}
732-
}
733-
}
734-
}
735-
```
736-
737-
#### Example 2: Reactive UI Based on State
738-
```kotlin
739-
@OptIn(ExperimentalTrayAppApi::class)
740647
TrayApp(
741648
state = trayAppState,
742-
icon = Icons.Default.Dashboard,
743-
tooltip = "Dashboard",
649+
icon = Icons.Default.Settings,
650+
tooltip = "Quick Settings",
744651
menu = {
745652
val isVisible by trayAppState.isVisible.collectAsState()
746-
747-
Item(
748-
label = if (isVisible) "Hide Dashboard" else "Show Dashboard",
749-
icon = if (isVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility
750-
) {
751-
trayAppState.toggle()
752-
}
753-
754-
SubMenu("Window Size") {
755-
Item("Small (250x350)") {
756-
trayAppState.setWindowSize(250.dp, 350.dp)
757-
}
758-
Item("Medium (350x500)") {
759-
trayAppState.setWindowSize(350.dp, 500.dp)
760-
}
761-
Item("Large (450x600)") {
762-
trayAppState.setWindowSize(450.dp, 600.dp)
763-
}
653+
Item(if (isVisible) "Hide" else "Show") { trayAppState.toggle() }
654+
SubMenu("Size") {
655+
Item("250×350") { trayAppState.setWindowSize(250.dp, 350.dp) }
656+
Item("350×500") { trayAppState.setWindowSize(350.dp, 500.dp) }
657+
Item("450×600") { trayAppState.setWindowSize(450.dp, 600.dp) }
764658
}
765659
}
766660
) {
767-
// Popup content
768-
val windowSize by trayAppState.windowSize.collectAsState()
769-
Text("Window size: ${windowSize.width} x ${windowSize.height}")
661+
// your content
770662
}
771663
```
772664

773-
#### Example 3: Integration with Application State
774-
```kotlin
775-
@OptIn(ExperimentalTrayAppApi::class)
776-
application {
777-
val trayAppState = rememberTrayAppState()
778-
val appViewModel = remember { AppViewModel() }
779-
780-
// React to app events
781-
LaunchedEffect(appViewModel.hasNotification) {
782-
if (appViewModel.hasNotification) {
783-
trayAppState.show() // Show popup when notification arrives
784-
}
785-
}
786-
787-
TrayApp(
788-
state = trayAppState,
789-
icon = Icons.Default.Notifications,
790-
tooltip = "Notifications"
791-
) {
792-
NotificationPanel(
793-
notifications = appViewModel.notifications,
794-
onClear = {
795-
appViewModel.clearNotifications()
796-
trayAppState.hide()
797-
}
798-
)
799-
}
800-
}
801-
```
665+
---
666+
667+
## Tips
668+
669+
* **Title & icon matter:** set `windowsTitle` and `windowIcon`. Even with undecorated UIs, Linux desktop environments often show a dock/taskbar entry; providing a title/icon prevents generic placeholders and improves discoverability.
670+
671+
---
802672
## 📄 License
803673

804674
This library is licensed under the MIT License. The Linux module uses Apache 2.0

0 commit comments

Comments
 (0)