|
60 | 60 | - [Icon Limitations](#icon-limitations) |
61 | 61 | - [Theme Behavior](#theme-behavior) |
62 | 62 | - [🧪 TrayApp (Experimental)](#-trayapp-experimental) |
63 | | - - [Overview](#overview) |
64 | | - - [Basic Usage](#basic-usage) |
65 | | - - [TrayAppState API](#trayappstate-api) |
66 | | - - [Advanced Examples](#advanced-examples) |
67 | 63 | - [📄 License](#-license) |
68 | 64 | - [🤝 Contribution](#-contribution) |
69 | 65 | - [👨💻 Author](#-author) |
@@ -556,249 +552,123 @@ By default, icons are optimized by OS: 32x32px (Windows), 44x44px (macOS), 24x24 |
556 | 552 | - **Windows**: Follows the system theme |
557 | 553 | - **Linux**: Varies by desktop environment (GNOME/KDE/etc.) |
558 | 554 |
|
559 | | -## 🧪 TrayApp (Experimental) |
| 555 | +# 🧪 TrayApp (Experimental) |
560 | 556 |
|
| 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. |
561 | 558 |
|
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 | +--- |
565 | 562 |
|
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 |
568 | 564 |
|
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 |
570 | 571 |
|
571 | | -### Basic Usage |
| 572 | +--- |
| 573 | + |
| 574 | +## Quick Start (minimal) |
572 | 575 |
|
573 | 576 | ```kotlin |
574 | 577 | @OptIn(ExperimentalTrayAppApi::class) |
575 | 578 | application { |
576 | | - // Create TrayAppState to control the popup |
577 | 579 | 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 |
580 | 583 | ) |
581 | | - |
| 584 | + |
582 | 585 | 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() } |
588 | 599 | Divider() |
589 | 600 | Item("Quit") { exitApplication() } |
590 | 601 | } |
591 | 602 | ) { |
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") } |
595 | 607 | } |
596 | 608 | } |
597 | 609 | } |
598 | 610 | ``` |
599 | 611 |
|
600 | | -### TrayAppState API |
| 612 | +--- |
601 | 613 |
|
602 | | -TrayAppState provides comprehensive control over the popup window: |
| 614 | +## Common recipes |
603 | 615 |
|
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 |
611 | 617 |
|
612 | | -#### Controlling Visibility |
613 | 618 | ```kotlin |
614 | | -// Show the popup |
615 | 619 | trayAppState.show() |
616 | | - |
617 | | -// Hide the popup |
618 | 620 | trayAppState.hide() |
619 | | - |
620 | | -// Toggle visibility |
621 | 621 | trayAppState.toggle() |
622 | 622 | ``` |
623 | 623 |
|
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 |
639 | 625 |
|
640 | | -#### Dynamic Window Resizing |
641 | 626 | ```kotlin |
642 | | -// Change size programmatically |
643 | 627 | 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)) |
647 | 630 | ``` |
648 | 631 |
|
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 |
658 | 633 |
|
659 | 634 | ```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) |
682 | 637 |
|
683 | | -### Switching at runtime |
684 | | - |
685 | | -```kotlin |
| 638 | +// MANUAL: you decide when to hide |
686 | 639 | LaunchedEffect(Unit) { |
687 | | - trayAppState.setDismissMode(TrayWindowDismissMode.AUTO) |
| 640 | + state.setDismissMode(TrayWindowDismissMode.MANUAL) |
688 | 641 | } |
689 | 642 | ``` |
690 | 643 |
|
691 | | -### Advanced Examples |
| 644 | +### Tray menu (compact) |
692 | 645 |
|
693 | | -#### Example 1: Control from Main Window |
694 | 646 | ```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) |
740 | 647 | TrayApp( |
741 | 648 | state = trayAppState, |
742 | | - icon = Icons.Default.Dashboard, |
743 | | - tooltip = "Dashboard", |
| 649 | + icon = Icons.Default.Settings, |
| 650 | + tooltip = "Quick Settings", |
744 | 651 | menu = { |
745 | 652 | 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) } |
764 | 658 | } |
765 | 659 | } |
766 | 660 | ) { |
767 | | - // Popup content |
768 | | - val windowSize by trayAppState.windowSize.collectAsState() |
769 | | - Text("Window size: ${windowSize.width} x ${windowSize.height}") |
| 661 | + // your content |
770 | 662 | } |
771 | 663 | ``` |
772 | 664 |
|
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 | +--- |
802 | 672 | ## 📄 License |
803 | 673 |
|
804 | 674 | This library is licensed under the MIT License. The Linux module uses Apache 2.0 |
|
0 commit comments