|
27 | 27 | - Improves the appearance of the tray on Linux, which previously resembled Windows 95. |
28 | 28 | - Adds support for checkable items, dividers, and submenus, including nested submenus. |
29 | 29 | - Supports primary action for Windows, macOS, and Linux. |
30 | | - - On Windows and macOS, the primary action is triggered by a left-click on the tray icon. |
31 | | - - On Linux, on GNOME the primary action is triggered by a double left-click on the tray icon, while on the majority of other environments, primarily KDE Plasma, it is triggered by a single left-click, similar to Windows and macOS. |
| 30 | + - On Windows and macOS, the primary action is triggered by a left-click on the tray icon. |
| 31 | + - On Linux, on GNOME the primary action is triggered by a double left-click on the tray icon, while on the majority of other environments, primarily KDE Plasma, it is triggered by a single left-click, similar to Windows and macOS. |
32 | 32 | - **Single Instance Management**: Ensures that only one instance of the application can run at a time and allows restoring focus to the running instance when another instance is attempted. |
33 | 33 | - **Tray Position Detection**: Allows determining the position of the system tray, which helps in positioning related windows appropriately. |
34 | 34 | - **Compose Recomposition Support**: The tray supports Compose recomposition, making it possible to dynamically show or hide the tray icon, for example: |
|
45 | 45 | - [⚡ Installation](#-installation) |
46 | 46 | - [🚀 Quick Start](#-quick-start) |
47 | 47 | - [📚 Usage Guide](#-usage-guide) |
48 | | - - [🎨 Creating the System Tray Icon](#-creating-the-system-tray-icon) |
49 | | - - [🖱️ Primary Action](#️-primary-action) |
50 | | - - [📋 Building the Menu](#-building-the-menu) |
51 | | - - [Icons with painterResource](#icons-with-painterresource) |
52 | | - - [New: Icons with DrawableResource](#new-icons-with-drawableresource-in-menu-items) |
| 48 | + - [🎨 Creating the System Tray Icon](#-creating-the-system-tray-icon) |
| 49 | + - [🖱️ Primary Action](#️-primary-action) |
| 50 | + - [📋 Building the Menu](#-building-the-menu) |
| 51 | + - [Icons with painterResource](#icons-with-painterresource) |
| 52 | + - [New: Icons with DrawableResource](#new-icons-with-drawableresource-in-menu-items) |
53 | 53 | - [🔧 Advanced Features](#-advanced-features) |
54 | | - - [🔄 Fully Reactive System Menu](#-fully-reactive-system-menu) |
55 | | - - [🔒 Single Instance Management](#-single-instance-management) |
56 | | - - [📍 Position Detection](#-position-detection) |
57 | | - - [🌓 Dark Mode Detection](#-dark-mode-detection) |
58 | | - - [🎨 Icon Rendering Customization](#-icon-rendering-customization) |
| 54 | + - [🔄 Fully Reactive System Menu](#-fully-reactive-system-menu) |
| 55 | + - [🔑 Single Instance Management](#-single-instance-management) |
| 56 | + - [📍 Position Detection](#-position-detection) |
| 57 | + - [🌓 Dark Mode Detection](#-dark-mode-detection) |
| 58 | + - [🎨 Icon Rendering Customization](#-icon-rendering-customization) |
59 | 59 | - [⚠️ Platform-Specific Notes](#️-platform-specific-notes) |
60 | | - - [Icon Limitations](#icon-limitations) |
61 | | - - [Theme Behavior](#theme-behavior) |
| 60 | + - [Icon Limitations](#icon-limitations) |
| 61 | + - [Theme Behavior](#theme-behavior) |
62 | 62 | - [🧪 TrayApp (Experimental)](#-trayapp-experimental) |
63 | 63 | - [Overview](#overview) |
64 | | - - [Parameters](#parameters) |
65 | | - - [Examples](#examples) |
| 64 | + - [Basic Usage](#basic-usage) |
| 65 | + - [TrayAppState API](#trayappstate-api) |
| 66 | + - [Advanced Examples](#advanced-examples) |
66 | 67 | - [📄 License](#-license) |
67 | 68 | - [🤝 Contribution](#-contribution) |
68 | 69 | - [👨💻 Author](#-author) |
@@ -126,9 +127,9 @@ application { |
126 | 127 | > **💡 Recommendation**: It is highly recommended to check out the demo examples in the project's `demo` directory. These examples showcase various implementation patterns and features that will help you better understand how to use the library effectively. |
127 | 128 | > |
128 | 129 | > Notable demos: |
129 | | -> - DemoWithDrawableResources.kt — shows using DrawableResource directly for Tray and menu icons |
130 | | -> - PainterResourceWorkaroundDemo.kt — demonstrates the painterResource variable workaround |
131 | | -> - DemoWithoutContextMenu.kt — minimalist tray with primary action only |
| 130 | +> - DemoWithDrawableResources.kt – shows using DrawableResource directly for Tray and menu icons |
| 131 | +> - PainterResourceWorkaroundDemo.kt – demonstrates the painterResource variable workaround |
| 132 | +> - DemoWithoutContextMenu.kt – minimalist tray with primary action only |
132 | 133 |
|
133 | 134 | ## 📚 Usage Guide |
134 | 135 |
|
@@ -403,7 +404,7 @@ application { |
403 | 404 |
|
404 | 405 | All menu properties (icon, labels, states, item visibility) are reactive and update automatically when application states change, without requiring manual recreation of the menu. |
405 | 406 |
|
406 | | -### 🔒 Single Instance Management |
| 407 | +### 🔑 Single Instance Management |
407 | 408 |
|
408 | 409 | Prevent multiple instances of your application: |
409 | 410 |
|
@@ -565,41 +566,197 @@ By default, icons are optimized by OS: 32x32px (Windows), 44x44px (macOS), 24x24 |
565 | 566 | ### Overview |
566 | 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. |
567 | 568 |
|
568 | | -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. |
| 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. |
569 | 570 |
|
570 | | -### Parameters |
571 | | -- icon / windowsIcon / macLinuxIcon / iconContent: the tray icon source. |
572 | | -- tint: optional tint (macOS/Linux ImageVector convenience). |
573 | | -- tooltip: text shown on hover. |
574 | | -- windowSize: popup size (default 300x200dp). |
575 | | -- visibleOnStart: if true, shows the popup shortly after startup with OS-specific handling. On Linux, this is not recommended because there is no system API to retrieve the tray position; the library records the position from the first user click, so on the first launch the popup position will be approximate. After that, the position is saved and persists even after a cold boot. |
576 | | -- menu: optional tray context menu (see Tray menu DSL). |
577 | | -- content: the composable content of the popup window. |
578 | | - |
579 | | -### Example |
| 571 | +### Basic Usage |
580 | 572 |
|
581 | 573 | ```kotlin |
582 | 574 | @OptIn(ExperimentalTrayAppApi::class) |
583 | 575 | application { |
| 576 | + // Create TrayAppState to control the popup |
| 577 | + val trayAppState = rememberTrayAppState( |
| 578 | + initialWindowSize = DpSize(300.dp, 500.dp), |
| 579 | + initiallyVisible = true // Show on startup |
| 580 | + ) |
| 581 | + |
584 | 582 | TrayApp( |
| 583 | + state = trayAppState, // Required: pass the state |
585 | 584 | icon = Icons.Default.Book, |
586 | 585 | tooltip = "My App", |
587 | | - windowSize = DpSize(300.dp, 500.dp), |
588 | | - visibleOnStart = true, |
589 | 586 | menu = { |
590 | 587 | Item("Open") { /* ... */ } |
591 | 588 | Divider() |
592 | 589 | Item("Quit") { exitApplication() } |
593 | 590 | } |
594 | 591 | ) { |
595 | 592 | // Popup content |
596 | | - MaterialTheme { /* ... */ } |
| 593 | + MaterialTheme { |
| 594 | + Text("Quick Settings Panel") |
| 595 | + } |
| 596 | + } |
| 597 | +} |
| 598 | +``` |
| 599 | + |
| 600 | +### TrayAppState API |
| 601 | + |
| 602 | +TrayAppState provides comprehensive control over the popup window: |
| 603 | + |
| 604 | +#### Creating State |
| 605 | +```kotlin |
| 606 | +val trayAppState = rememberTrayAppState( |
| 607 | + initialWindowSize = DpSize(300.dp, 400.dp), |
| 608 | + initiallyVisible = false // Hidden by default |
| 609 | +) |
| 610 | +``` |
| 611 | + |
| 612 | +#### Controlling Visibility |
| 613 | +```kotlin |
| 614 | +// Show the popup |
| 615 | +trayAppState.show() |
| 616 | + |
| 617 | +// Hide the popup |
| 618 | +trayAppState.hide() |
| 619 | + |
| 620 | +// Toggle visibility |
| 621 | +trayAppState.toggle() |
| 622 | +``` |
| 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 | +``` |
| 639 | + |
| 640 | +#### Dynamic Window Resizing |
| 641 | +```kotlin |
| 642 | +// Change size programmatically |
| 643 | +trayAppState.setWindowSize(400.dp, 600.dp) |
| 644 | + |
| 645 | +// Or using DpSize |
| 646 | +trayAppState.setWindowSize(DpSize(350.dp, 500.dp)) |
| 647 | +``` |
| 648 | + |
| 649 | +### Advanced Examples |
| 650 | + |
| 651 | +#### Example 1: Control from Main Window |
| 652 | +```kotlin |
| 653 | +@OptIn(ExperimentalTrayAppApi::class) |
| 654 | +application { |
| 655 | + val trayAppState = rememberTrayAppState() |
| 656 | + var isMainWindowVisible by remember { mutableStateOf(true) } |
| 657 | + |
| 658 | + // Tray with popup |
| 659 | + TrayApp( |
| 660 | + state = trayAppState, |
| 661 | + icon = Icons.Default.Settings, |
| 662 | + tooltip = "Quick Settings" |
| 663 | + ) { |
| 664 | + // Popup content |
| 665 | + Column { |
| 666 | + Text("Quick Settings") |
| 667 | + Button(onClick = { |
| 668 | + isMainWindowVisible = true |
| 669 | + trayAppState.hide() |
| 670 | + }) { |
| 671 | + Text("Open Main Window") |
| 672 | + } |
| 673 | + } |
| 674 | + } |
| 675 | + |
| 676 | + // Main window can control the popup |
| 677 | + if (isMainWindowVisible) { |
| 678 | + Window(onCloseRequest = { isMainWindowVisible = false }) { |
| 679 | + Column { |
| 680 | + Button(onClick = { trayAppState.show() }) { |
| 681 | + Text("Show Quick Settings") |
| 682 | + } |
| 683 | + |
| 684 | + Button(onClick = { |
| 685 | + trayAppState.setWindowSize(250.dp, 350.dp) |
| 686 | + }) { |
| 687 | + Text("Make Popup Smaller") |
| 688 | + } |
| 689 | + } |
| 690 | + } |
597 | 691 | } |
598 | 692 | } |
599 | 693 | ``` |
600 | 694 |
|
601 | | -See full demo: demo/src/jvmMain/kotlin/com/kdroid/composetray/demo/TrayAppDemo.kt |
| 695 | +#### Example 2: Reactive UI Based on State |
| 696 | +```kotlin |
| 697 | +@OptIn(ExperimentalTrayAppApi::class) |
| 698 | +TrayApp( |
| 699 | + state = trayAppState, |
| 700 | + icon = Icons.Default.Dashboard, |
| 701 | + tooltip = "Dashboard", |
| 702 | + menu = { |
| 703 | + val isVisible by trayAppState.isVisible.collectAsState() |
| 704 | + |
| 705 | + Item( |
| 706 | + label = if (isVisible) "Hide Dashboard" else "Show Dashboard", |
| 707 | + icon = if (isVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility |
| 708 | + ) { |
| 709 | + trayAppState.toggle() |
| 710 | + } |
| 711 | + |
| 712 | + SubMenu("Window Size") { |
| 713 | + Item("Small (250x350)") { |
| 714 | + trayAppState.setWindowSize(250.dp, 350.dp) |
| 715 | + } |
| 716 | + Item("Medium (350x500)") { |
| 717 | + trayAppState.setWindowSize(350.dp, 500.dp) |
| 718 | + } |
| 719 | + Item("Large (450x600)") { |
| 720 | + trayAppState.setWindowSize(450.dp, 600.dp) |
| 721 | + } |
| 722 | + } |
| 723 | + } |
| 724 | +) { |
| 725 | + // Popup content |
| 726 | + val windowSize by trayAppState.windowSize.collectAsState() |
| 727 | + Text("Window size: ${windowSize.width} x ${windowSize.height}") |
| 728 | +} |
| 729 | +``` |
602 | 730 |
|
| 731 | +#### Example 3: Integration with Application State |
| 732 | +```kotlin |
| 733 | +@OptIn(ExperimentalTrayAppApi::class) |
| 734 | +application { |
| 735 | + val trayAppState = rememberTrayAppState() |
| 736 | + val appViewModel = remember { AppViewModel() } |
| 737 | + |
| 738 | + // React to app events |
| 739 | + LaunchedEffect(appViewModel.hasNotification) { |
| 740 | + if (appViewModel.hasNotification) { |
| 741 | + trayAppState.show() // Show popup when notification arrives |
| 742 | + } |
| 743 | + } |
| 744 | + |
| 745 | + TrayApp( |
| 746 | + state = trayAppState, |
| 747 | + icon = Icons.Default.Notifications, |
| 748 | + tooltip = "Notifications" |
| 749 | + ) { |
| 750 | + NotificationPanel( |
| 751 | + notifications = appViewModel.notifications, |
| 752 | + onClear = { |
| 753 | + appViewModel.clearNotifications() |
| 754 | + trayAppState.hide() |
| 755 | + } |
| 756 | + ) |
| 757 | + } |
| 758 | +} |
| 759 | +``` |
603 | 760 | ## 📄 License |
604 | 761 |
|
605 | 762 | This library is licensed under the MIT License. The Linux module uses Apache 2.0 |
|
0 commit comments