Skip to content

Commit ac5c0c9

Browse files
authored
Merge pull request #385 from kdroidFilter/feat/on-menu-opened-callback
feat: add onMenuOpened callback to Tray composable
2 parents 3a3a3fb + 4c0d4e6 commit ac5c0c9

22 files changed

Lines changed: 364 additions & 31 deletions

File tree

demo/src/jvmMain/kotlin/com/kdroid/composetray/demo/DynamicIconsDemo.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,19 @@ fun main() = application {
5656

5757
// Basic state for tray icon
5858
var currentTrayIcon by remember { mutableStateOf(Icons.Default.Notifications) }
59-
59+
6060
// States for menu item icons
6161
var weatherIcon by remember { mutableStateOf(Icons.Default.WbSunny) }
6262
var musicIcon by remember { mutableStateOf(Icons.Default.MusicNote) }
6363
var settingsIcon by remember { mutableStateOf(Icons.Default.Settings) }
64-
64+
6565
// States for theme and features
6666
var isDarkTheme by remember { mutableStateOf(false) }
6767
var isWeatherEnabled by remember { mutableStateOf(true) }
6868
var isMusicEnabled by remember { mutableStateOf(true) }
69+
70+
// Counter to demonstrate onMenuOpened callback
71+
var menuOpenCount by remember { mutableIntStateOf(0) }
6972

7073
// Always create the Tray composable, but make it conditional on visibility
7174
val showTray = alwaysShowTray || !isWindowVisible
@@ -77,7 +80,11 @@ fun main() = application {
7780
primaryAction = {
7881
isWindowVisible = true
7982
println("$logTag: Primary action clicked")
80-
}
83+
},
84+
onMenuOpened = {
85+
menuOpenCount++
86+
println("$logTag: Menu opened (count: $menuOpenCount)")
87+
},
8188
) {
8289
// Weather submenu with dynamic icon
8390
SubMenu(label = "Weather", icon = weatherIcon) {
@@ -249,6 +256,12 @@ fun main() = application {
249256

250257
Text(
251258
"This demo showcases dynamic icon changes in the system tray menu.",
259+
modifier = Modifier.padding(bottom = 8.dp)
260+
)
261+
262+
Text(
263+
"Tray menu opened $menuOpenCount times",
264+
style = MaterialTheme.typography.bodyMedium,
252265
modifier = Modifier.padding(bottom = 24.dp)
253266
)
254267

src/jvmMain/kotlin/com/kdroid/composetray/lib/linux/LinuxNativeBridge.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ internal object LinuxNativeBridge {
6565
callback: Runnable?,
6666
)
6767

68+
/** Register a callback invoked when the menu is about to be shown. */
69+
@JvmStatic external fun nativeSetMenuOpenedCallback(
70+
handle: Long,
71+
callback: Runnable?,
72+
)
73+
6874
// -- Click position ----------------------------------------------------------
6975

7076
/** Writes [x, y] into outXY. */

src/jvmMain/kotlin/com/kdroid/composetray/lib/linux/LinuxTrayManager.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal class LinuxTrayManager(
2323
private var iconPath: String,
2424
private var tooltip: String = "",
2525
private var onLeftClick: (() -> Unit)? = null,
26+
private var onMenuOpened: (() -> Unit)? = null,
2627
) {
2728
companion object {
2829
// Ensures only one systray runtime is active at a time
@@ -99,6 +100,7 @@ internal class LinuxTrayManager(
99100
newTooltip: String,
100101
newOnLeftClick: (() -> Unit)?,
101102
newMenuItems: List<MenuItem>?,
103+
newOnMenuOpened: (() -> Unit)? = null,
102104
) {
103105
val iconChanged: Boolean
104106
val tooltipChanged: Boolean
@@ -109,6 +111,7 @@ internal class LinuxTrayManager(
109111
iconPath = newIconPath
110112
tooltip = newTooltip
111113
onLeftClick = newOnLeftClick
114+
onMenuOpened = newOnMenuOpened
112115
if (newMenuItems != null) {
113116
menuItems.clear()
114117
menuItems.addAll(newMenuItems)
@@ -174,6 +177,12 @@ internal class LinuxTrayManager(
174177
},
175178
)
176179

180+
// Set menu-opened callback
181+
native.nativeSetMenuOpenedCallback(
182+
trayHandle,
183+
JniRunnable { onMenuOpened?.invoke() },
184+
)
185+
177186
// Build menu before starting the loop
178187
rebuildMenu()
179188

src/jvmMain/kotlin/com/kdroid/composetray/lib/mac/MacNativeBridge.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ internal object MacNativeBridge {
3535
callback: Runnable?,
3636
)
3737

38+
@JvmStatic external fun nativeSetMenuOpenedCallback(
39+
handle: Long,
40+
callback: Runnable?,
41+
)
42+
3843
@JvmStatic external fun nativeSetTrayMenu(
3944
trayHandle: Long,
4045
menuHandle: Long,

src/jvmMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal class MacTrayManager(
1515
private var iconPath: String,
1616
private var tooltip: String = "",
1717
onLeftClick: (() -> Unit)? = null,
18+
onMenuOpened: (() -> Unit)? = null,
1819
) {
1920
private var trayHandle: Long = 0L
2021
private var menuHandle: Long = 0L
@@ -33,6 +34,7 @@ internal class MacTrayManager(
3334
private val submenuHandles: MutableList<Pair<Long, Int>> = mutableListOf()
3435

3536
private val onLeftClickCallback = mutableStateOf(onLeftClick)
37+
private var onMenuOpenedCallback: (() -> Unit)? = onMenuOpened
3638

3739
// Top level MenuItem class
3840
data class MenuItem(
@@ -72,6 +74,7 @@ internal class MacTrayManager(
7274
newTooltip: String,
7375
newOnLeftClick: (() -> Unit)?,
7476
newMenuItems: List<MenuItem>? = null,
77+
newOnMenuOpened: (() -> Unit)? = null,
7578
) {
7679
lock.withLock {
7780
if (!running.get() || trayHandle == 0L) return
@@ -80,11 +83,13 @@ internal class MacTrayManager(
8083
val iconChanged = this.iconPath != newIconPath
8184
val tooltipChanged = this.tooltip != newTooltip
8285
val onLeftClickChanged = this.onLeftClickCallback.value != newOnLeftClick
86+
val onMenuOpenedChanged = this.onMenuOpenedCallback != newOnMenuOpened
8387

8488
// Update icon path and tooltip
8589
this.iconPath = newIconPath
8690
this.tooltip = newTooltip
8791
this.onLeftClickCallback.value = newOnLeftClick
92+
this.onMenuOpenedCallback = newOnMenuOpened
8893

8994
if (iconChanged) {
9095
MacNativeBridge.nativeSetTrayIcon(trayHandle, newIconPath)
@@ -95,6 +100,9 @@ internal class MacTrayManager(
95100
if (onLeftClickChanged) {
96101
initializeOnLeftClickCallback()
97102
}
103+
if (onMenuOpenedChanged) {
104+
initializeOnMenuOpenedCallback()
105+
}
98106

99107
// Update menu items if provided
100108
if (newMenuItems != null) {
@@ -167,6 +175,9 @@ internal class MacTrayManager(
167175
throw IllegalStateException("Failed to initialize tray: $initResult")
168176
}
169177

178+
// Set menu-opened callback after init (TrayContext must exist)
179+
initializeOnMenuOpenedCallback()
180+
170181
// Signal that initialization is complete
171182
initLatch.countDown()
172183

@@ -218,6 +229,26 @@ internal class MacTrayManager(
218229
}
219230
}
220231

232+
private fun initializeOnMenuOpenedCallback() {
233+
if (trayHandle == 0L) return
234+
235+
val callback = onMenuOpenedCallback
236+
if (callback != null) {
237+
MacNativeBridge.nativeSetMenuOpenedCallback(
238+
trayHandle,
239+
Runnable {
240+
mainScope?.launch {
241+
ioScope?.launch {
242+
callback()
243+
}
244+
}
245+
},
246+
)
247+
} else {
248+
MacNativeBridge.nativeSetMenuOpenedCallback(trayHandle, null)
249+
}
250+
}
251+
221252
private fun initializeTrayMenu() {
222253
if (trayHandle == 0L) return
223254

src/jvmMain/kotlin/com/kdroid/composetray/lib/windows/WindowsNativeBridge.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ internal object WindowsNativeBridge {
3636
callback: Runnable?,
3737
)
3838

39+
@JvmStatic external fun nativeSetMenuOpenedCallback(
40+
handle: Long,
41+
callback: Runnable?,
42+
)
43+
3944
@JvmStatic external fun nativeSetTrayMenu(
4045
trayHandle: Long,
4146
menuHandle: Long,

src/jvmMain/kotlin/com/kdroid/composetray/lib/windows/WindowsTrayManager.kt

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal class WindowsTrayManager(
1818
private var iconPath: String,
1919
private var tooltip: String = "",
2020
private var onLeftClick: (() -> Unit)? = null,
21+
private var onMenuOpened: (() -> Unit)? = null,
2122
) {
2223
private var trayHandle: Long = 0L
2324
private val running = AtomicBoolean(false)
@@ -50,6 +51,7 @@ internal class WindowsTrayManager(
5051
val iconPath: String,
5152
val tooltip: String,
5253
val onLeftClick: (() -> Unit)?,
54+
val onMenuOpened: (() -> Unit)?,
5355
val menuItems: List<MenuItem>,
5456
)
5557

@@ -69,7 +71,7 @@ internal class WindowsTrayManager(
6971
updateLock.withLock {
7072
if (initialized.get()) {
7173
log("Already initialized, delegating to update()")
72-
update(iconPath, tooltip, onLeftClick, menuItems)
74+
update(iconPath, tooltip, onLeftClick, onMenuOpened, menuItems)
7375
return
7476
}
7577

@@ -103,6 +105,9 @@ internal class WindowsTrayManager(
103105
throw RuntimeException("Failed to initialize tray: $initResult")
104106
}
105107

108+
// Set menu-opened callback after init (TrayContext must exist)
109+
setupMenuOpenedCallback(handle)
110+
106111
initialized.set(true)
107112

108113
// Signal that initialization is complete before entering the loop
@@ -141,6 +146,7 @@ internal class WindowsTrayManager(
141146
newIconPath: String,
142147
newTooltip: String,
143148
newOnLeftClick: (() -> Unit)?,
149+
newOnMenuOpened: (() -> Unit)?,
144150
newMenuItems: List<MenuItem>,
145151
) {
146152
log("update() called - icon: $newIconPath, tooltip: $newTooltip, menuItems: ${newMenuItems.size}")
@@ -150,13 +156,14 @@ internal class WindowsTrayManager(
150156
iconPath = newIconPath
151157
tooltip = newTooltip
152158
onLeftClick = newOnLeftClick
159+
onMenuOpened = newOnMenuOpened
153160
initialize(newMenuItems)
154161
return
155162
}
156163

157164
// Queue the update to be processed on the tray thread
158165
synchronized(updateQueueLock) {
159-
updateQueue.add(UpdateRequest(newIconPath, newTooltip, newOnLeftClick, newMenuItems))
166+
updateQueue.add(UpdateRequest(newIconPath, newTooltip, newOnLeftClick, newOnMenuOpened, newMenuItems))
160167
updateQueueLock.notify()
161168
}
162169
}
@@ -357,6 +364,7 @@ internal class WindowsTrayManager(
357364
iconPath = update.iconPath
358365
tooltip = update.tooltip
359366
onLeftClick = update.onLeftClick
367+
onMenuOpened = update.onMenuOpened
360368

361369
val handle = trayHandle
362370
if (handle == 0L) return
@@ -371,6 +379,7 @@ internal class WindowsTrayManager(
371379
// Set up new callbacks and menu
372380
setupLeftClickCallback(handle)
373381
setupMenu(handle, update.menuItems)
382+
setupMenuOpenedCallback(handle)
374383

375384
// Update the native tray
376385
log("Calling nativeUpdateTray()")
@@ -416,6 +425,32 @@ internal class WindowsTrayManager(
416425
}
417426
}
418427

428+
private fun setupMenuOpenedCallback(handle: Long) {
429+
val callback = onMenuOpened
430+
if (callback != null) {
431+
log("Setting up menu opened callback")
432+
WindowsNativeBridge.nativeSetMenuOpenedCallback(
433+
handle,
434+
Runnable {
435+
log("Menu opened callback invoked")
436+
try {
437+
mainScope?.launch {
438+
ioScope?.launch {
439+
callback()
440+
}
441+
}
442+
} catch (e: Exception) {
443+
log("Error in menu opened callback: ${e.message}")
444+
e.printStackTrace()
445+
}
446+
},
447+
)
448+
} else {
449+
log("No menu opened callback set")
450+
WindowsNativeBridge.nativeSetMenuOpenedCallback(handle, null)
451+
}
452+
}
453+
419454
private fun setupMenu(
420455
handle: Long,
421456
menuItems: List<MenuItem>,

0 commit comments

Comments
 (0)