Skip to content

Commit 2df539e

Browse files
authored
Merge pull request #289 from kdroidFilter/dev
Add DrawableResource icon support + fixes for issues #206 and #286
2 parents 5f720b3 + ffea44a commit 2df539e

19 files changed

Lines changed: 762 additions & 34 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
- [🖱️ Primary Action](#️-primary-action)
5050
- [📋 Building the Menu](#-building-the-menu)
5151
- [Icons with painterResource](#icons-with-painterresource)
52+
- [New: Icons with DrawableResource](#new-icons-with-drawableresource-in-menu-items)
5253
- [🔧 Advanced Features](#-advanced-features)
5354
- [🔄 Fully Reactive System Menu](#-fully-reactive-system-menu)
5455
- [🔒 Single Instance Management](#-single-instance-management)
@@ -123,11 +124,27 @@ application {
123124
```
124125

125126
> **💡 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+
> 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
126132
127133
## 📚 Usage Guide
128134

129135
### 🎨 Creating the System Tray Icon
130136

137+
#### New: Using a DrawableResource directly
138+
```kotlin
139+
Tray(
140+
icon = Res.drawable.myIcon, // org.jetbrains.compose.resources.DrawableResource
141+
tooltip = "My Application"
142+
) { /* menu */ }
143+
```
144+
145+
> Requires compose.components.resources in your project. In this library it's already included; in your app add:
146+
> implementation(compose.components.resources)
147+
131148
#### Option 1: Using an ImageVector
132149
```kotlin
133150
Tray(
@@ -265,6 +282,29 @@ Tray(/* configuration */) {
265282
```
266283

267284
### Icons with painterResource
285+
286+
### New: Icons with DrawableResource in menu items
287+
You can now pass DrawableResource directly to menu builders:
288+
289+
```kotlin
290+
Tray(icon = Res.drawable.app_icon, tooltip = "App") {
291+
SubMenu(label = "With icons", icon = Res.drawable.gear) {
292+
Item(label = "Action 1", icon = Res.drawable.star) { /* ... */ }
293+
Item(label = "Action 2", icon = Res.drawable.star) { /* ... */ }
294+
}
295+
296+
Divider()
297+
298+
CheckableItem(
299+
label = "Enabled",
300+
icon = Res.drawable.check,
301+
checked = true,
302+
onCheckedChange = { /* ... */ }
303+
)
304+
}
305+
```
306+
307+
See demo/DemoWithDrawableResources.kt for a complete example.
268308
When using `painterResource` with menu items, declare it in the composable context:
269309

270310
```kotlin

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ kotlin {
3939
implementation(compose.runtime)
4040
implementation(compose.foundation)
4141
implementation(compose.ui)
42+
implementation(compose.components.resources)
4243
implementation(libs.jna)
4344
implementation(libs.jna.platform)
4445
implementation(libs.kotlinx.coroutines.core)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.kdroid.composetray.demo
2+
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.runtime.setValue
7+
import androidx.compose.ui.window.Window
8+
import androidx.compose.ui.window.application
9+
import com.kdroid.composetray.tray.api.Tray
10+
import com.kdroid.composetray.utils.ComposeNativeTrayLoggingLevel
11+
import com.kdroid.composetray.utils.SingleInstanceManager
12+
import com.kdroid.composetray.utils.allowComposeNativeTrayLogging
13+
import com.kdroid.composetray.utils.composeNativeTrayLoggingLevel
14+
import composenativetray.demo.generated.resources.Res
15+
import composenativetray.demo.generated.resources.icon
16+
import composenativetray.demo.generated.resources.icon2
17+
18+
/**
19+
* Demo application that showcases using DrawableResource directly for the Tray icon
20+
* and for context menu icons, e.g.:
21+
* Tray(icon = Res.drawable.icon) { Item(label = "Settings", icon = Res.drawable.icon2) }
22+
*/
23+
fun main() = application {
24+
allowComposeNativeTrayLogging = true
25+
composeNativeTrayLoggingLevel = ComposeNativeTrayLoggingLevel.DEBUG
26+
27+
val logTag = "DemoWithDrawableResources"
28+
29+
var isWindowVisible by remember { mutableStateOf(true) }
30+
var alwaysShowTray by remember { mutableStateOf(true) }
31+
var hideOnClose by remember { mutableStateOf(true) }
32+
33+
val isSingleInstance = SingleInstanceManager.isSingleInstance(onRestoreRequest = {
34+
isWindowVisible = true
35+
})
36+
37+
if (!isSingleInstance) {
38+
exitApplication()
39+
return@application
40+
}
41+
42+
val showTray = alwaysShowTray || !isWindowVisible
43+
44+
if (showTray) {
45+
// Using the DrawableResource overload directly
46+
Tray(
47+
icon = Res.drawable.icon,
48+
tooltip = "Demo: DrawableResource icons",
49+
primaryAction = {
50+
isWindowVisible = true
51+
println("$logTag: Primary action clicked")
52+
}
53+
) {
54+
SubMenu(
55+
label = "Menu with icons",
56+
icon = Res.drawable.icon2
57+
) {
58+
Item(label = "Action 1", icon = Res.drawable.icon) {
59+
println("$logTag: Action 1 selected")
60+
}
61+
Item(label = "Action 2", icon = Res.drawable.icon2) {
62+
println("$logTag: Action 2 selected")
63+
}
64+
}
65+
66+
Divider()
67+
68+
CheckableItem(
69+
label = "Always show tray",
70+
icon = Res.drawable.icon,
71+
checked = alwaysShowTray,
72+
onCheckedChange = { checked ->
73+
alwaysShowTray = checked
74+
println("$logTag: Always show tray ${if (checked) "enabled" else "disabled"}")
75+
}
76+
)
77+
78+
CheckableItem(
79+
label = "Hide on close",
80+
icon = Res.drawable.icon2,
81+
checked = hideOnClose,
82+
onCheckedChange = { checked ->
83+
hideOnClose = checked
84+
println("$logTag: Hide on close ${if (checked) "enabled" else "disabled"}")
85+
}
86+
)
87+
88+
Divider()
89+
90+
Item(label = "Exit", icon = Res.drawable.icon2) {
91+
println("$logTag: Exiting application")
92+
dispose()
93+
exitApplication()
94+
}
95+
}
96+
}
97+
98+
Window(
99+
onCloseRequest = {
100+
if (hideOnClose) {
101+
isWindowVisible = false
102+
} else {
103+
exitApplication()
104+
}
105+
},
106+
title = "Demo with DrawableResource icons",
107+
visible = isWindowVisible,
108+
// Note: Window icon can still use painterResource if desired; omitted here for simplicity
109+
) {
110+
window.toFront()
111+
App(
112+
textVisible = true,
113+
alwaysShowTray = alwaysShowTray,
114+
hideOnClose = hideOnClose
115+
) { alwaysShow, hideOnCloseState ->
116+
alwaysShowTray = alwaysShow
117+
hideOnClose = hideOnCloseState
118+
}
119+
}
120+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.kdroid.composetray.demo
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.window.Window
11+
import androidx.compose.ui.window.application
12+
import com.kdroid.composetray.tray.api.Tray
13+
import composenativetray.demo.generated.resources.Res
14+
import composenativetray.demo.generated.resources.icon
15+
import org.jetbrains.compose.resources.painterResource
16+
17+
/**
18+
* UnicodeTrayDemo
19+
*
20+
* Demonstrates a tray menu with French and Chinese items, including special
21+
* characters like accented letters (é, à, ç) and punctuation.
22+
* This validates end-to-end UTF-8 → UTF-16 handling on Windows and generic
23+
* Unicode rendering across platforms.
24+
*/
25+
fun main() = application {
26+
val painter = painterResource(Res.drawable.icon)
27+
28+
var lastClicked by remember { mutableStateOf("-") }
29+
30+
Tray(
31+
iconContent = {
32+
Image(painter = painter, contentDescription = "Unicode Tray", modifier = Modifier.fillMaxSize())
33+
},
34+
primaryAction = {
35+
// no-op: right-click to open menu
36+
},
37+
tooltip = "Démo: Menu Unicode (Français/中文)"
38+
) {
39+
// French items
40+
Item(label = "Bonjour – Édition") {
41+
lastClicked = "Bonjour – Édition"
42+
println("Clicked: $lastClicked")
43+
}
44+
Item(label = "Préférences…") {
45+
lastClicked = "Préférences…"
46+
println("Clicked: $lastClicked")
47+
}
48+
Item(label = "À propos") {
49+
lastClicked = "À propos"
50+
println("Clicked: $lastClicked")
51+
}
52+
Item(label = "Quitter") {
53+
lastClicked = "Quitter"
54+
println("Clicked: $lastClicked")
55+
exitApplication()
56+
}
57+
58+
Divider()
59+
60+
// Chinese submenu with mixed content
61+
SubMenu(label = "设置 / 設置") {
62+
Item(label = "语言:中文(简体)") {
63+
lastClicked = "语言:中文(简体)"
64+
println("Clicked: $lastClicked")
65+
}
66+
Item(label = "語言:中文(繁體)") {
67+
lastClicked = "語言:中文(繁體)"
68+
println("Clicked: $lastClicked")
69+
}
70+
CheckableItem(label = "启用高级选项 ✓", checked = true) { checked ->
71+
lastClicked = "启用高级选项: ${if (checked) "" else ""}"
72+
println("Clicked: $lastClicked")
73+
}
74+
}
75+
76+
SubMenu(label = "信息 ℹ / 信息") {
77+
Item(label = "关于 / 關於") {
78+
lastClicked = "关于 / 關於"
79+
println("Clicked: $lastClicked")
80+
}
81+
Item(label = "退出 / 退出") {
82+
lastClicked = "退出"
83+
println("Clicked: $lastClicked")
84+
exitApplication()
85+
}
86+
}
87+
88+
Divider()
89+
90+
// Mixed accents and symbols
91+
Item(label = "Café crème – façade – piñata – coöperate") {
92+
lastClicked = "Café crème – façade – piñata – coöperate"
93+
println("Clicked: $lastClicked")
94+
}
95+
Item(label = "Symbols: € £ ¥ • — … ✓ ✗ © ® ™") {
96+
lastClicked = "Symbols"
97+
println("Clicked: $lastClicked")
98+
}
99+
100+
Divider()
101+
102+
Item(label = "Dernier: $lastClicked", isEnabled = false)
103+
}
104+
105+
Window(onCloseRequest = ::exitApplication, title = "Unicode Tray Demo") { }
106+
}

src/commonMain/kotlin/com/kdroid/composetray/lib/windows/WindowsNativeTray.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import com.sun.jna.win32.StdCallLibrary
66

77
@Structure.FieldOrder("icon_filepath", "tooltip", "cb", "menu")
88
internal class WindowsNativeTray : Structure() {
9+
companion object {
10+
init {
11+
// Ensure UTF-8 encoding for JNA string marshaling before any structure write
12+
val key = "jna.encoding"
13+
if (System.getProperty(key).isNullOrBlank()) {
14+
System.setProperty(key, "UTF-8")
15+
}
16+
}
17+
}
18+
919
@JvmField
1020
var icon_filepath: String? = null
1121

src/commonMain/kotlin/com/kdroid/composetray/lib/windows/WindowsNativeTrayLibrary.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import com.sun.jna.win32.StdCallLibrary
99
// This object registers the native library and exposes external (native) methods.
1010
internal object WindowsNativeTrayLibrary : StdCallLibrary {
1111
init {
12+
// Ensure JNA marshals Java/Kotlin strings as UTF-8. This must be set
13+
// before the first interaction with JNA/native code, otherwise JNA will
14+
// default to the system ANSI code page on Windows and corrupt Unicode.
15+
val key = "jna.encoding"
16+
if (System.getProperty(key).isNullOrBlank()) {
17+
System.setProperty(key, "UTF-8")
18+
}
1219
// Register the native library "tray" for direct calls
1320
Native.register("tray")
1421
}

src/commonMain/kotlin/com/kdroid/composetray/lib/windows/WindowsNativeTrayMenuItem.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import com.sun.jna.Structure
55

66
@Structure.FieldOrder("text", "icon_path", "disabled", "checked", "cb", "submenu")
77
internal open class WindowsNativeTrayMenuItem : Structure() {
8+
companion object {
9+
init {
10+
// Ensure UTF-8 encoding for JNA string marshaling as early as possible
11+
val key = "jna.encoding"
12+
if (System.getProperty(key).isNullOrBlank()) {
13+
System.setProperty(key, "UTF-8")
14+
}
15+
}
16+
}
17+
818
@JvmField
919
var text: String? = null
1020

0 commit comments

Comments
 (0)