Skip to content

Commit b57e9bf

Browse files
committed
feat(windows): add menu-opened callback handling to Windows tray
- Introduced `onMenuOpened` support in `WindowsTrayManager` for detecting menu opening events - Integrated JNI binding for `tray_set_menu_opened_callback` in native Windows implementation - Updated initialization and update methods to include the menu-opened callback - Enhanced tray behavior with improved event handling and callback support
1 parent b5df040 commit b57e9bf

6 files changed

Lines changed: 92 additions & 8 deletions

File tree

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>,

src/jvmMain/kotlin/com/kdroid/composetray/tray/impl/WindowsTrayInitializer.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ object WindowsTrayInitializer {
1717
tooltip: String,
1818
onLeftClick: (() -> Unit)? = null,
1919
menuContent: (TrayMenuBuilder.() -> Unit)? = null,
20-
@Suppress("UNUSED_PARAMETER") onMenuOpened: (() -> Unit)? = null,
20+
onMenuOpened: (() -> Unit)? = null,
2121
) {
2222
val menuItems =
2323
WindowsTrayMenuBuilderImpl(iconPath, tooltip, onLeftClick).apply {
@@ -26,11 +26,11 @@ object WindowsTrayInitializer {
2626

2727
val manager = trayManagers[id]
2828
if (manager == null) {
29-
val windowsTrayManager = WindowsTrayManager(id, iconPath, tooltip, onLeftClick)
29+
val windowsTrayManager = WindowsTrayManager(id, iconPath, tooltip, onLeftClick, onMenuOpened)
3030
trayManagers[id] = windowsTrayManager
3131
windowsTrayManager.initialize(menuItems)
3232
} else {
33-
manager.update(iconPath, tooltip, onLeftClick, menuItems)
33+
manager.update(iconPath, tooltip, onLeftClick, onMenuOpened, menuItems)
3434
}
3535
}
3636

@@ -41,10 +41,10 @@ object WindowsTrayInitializer {
4141
tooltip: String,
4242
onLeftClick: (() -> Unit)? = null,
4343
menuContent: (TrayMenuBuilder.() -> Unit)? = null,
44-
@Suppress("UNUSED_PARAMETER") onMenuOpened: (() -> Unit)? = null,
44+
onMenuOpened: (() -> Unit)? = null,
4545
) {
4646
// Same as initialize - it will handle both cases per ID
47-
initialize(id, iconPath, tooltip, onLeftClick, menuContent)
47+
initialize(id, iconPath, tooltip, onLeftClick, menuContent, onMenuOpened)
4848
}
4949

5050
@Synchronized

src/native/windows/jni_bridge.c

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ typedef struct CallbackEntry {
5353

5454
static CallbackEntry *g_trayCallbacks = NULL;
5555
static CallbackEntry *g_menuCallbacks = NULL;
56+
static CallbackEntry *g_menuOpenedCallbacks = NULL;
5657

5758
static void storeCallback(CallbackEntry **list, uintptr_t key, JNIEnv *env, jobject callback) {
5859
CallbackEntry **pp = list;
@@ -137,6 +138,12 @@ static void menu_item_cb_trampoline(struct tray_menu_item *item) {
137138
if (runnable) invokeRunnable(runnable);
138139
}
139140

141+
static void menu_opened_cb_trampoline(struct tray *t) {
142+
uintptr_t key = (uintptr_t)t;
143+
jobject runnable = findCallback(g_menuOpenedCallbacks, key);
144+
if (runnable) invokeRunnable(runnable);
145+
}
146+
140147
/* ========================================================================== */
141148
/* Helper: duplicate UTF-8 string from JNI */
142149
/* ========================================================================== */
@@ -176,8 +183,9 @@ Java_com_kdroid_composetray_lib_windows_WindowsNativeBridge_nativeFreeTray(
176183
struct tray *t = (struct tray *)(uintptr_t)handle;
177184
if (!t) return;
178185

179-
/* Remove tray callback */
186+
/* Remove tray callbacks */
180187
storeCallback(&g_trayCallbacks, (uintptr_t)t, env, NULL);
188+
storeCallback(&g_menuOpenedCallbacks, (uintptr_t)t, env, NULL);
181189

182190
free((void *)t->icon_filepath);
183191
free((void *)t->tooltip);
@@ -218,6 +226,17 @@ Java_com_kdroid_composetray_lib_windows_WindowsNativeBridge_nativeSetTrayCallbac
218226
t->cb = callback ? tray_cb_trampoline : NULL;
219227
}
220228

229+
JNIEXPORT void JNICALL
230+
Java_com_kdroid_composetray_lib_windows_WindowsNativeBridge_nativeSetMenuOpenedCallback(
231+
JNIEnv *env, jclass clazz, jlong handle, jobject callback)
232+
{
233+
(void)clazz;
234+
struct tray *t = (struct tray *)(uintptr_t)handle;
235+
if (!t) return;
236+
storeCallback(&g_menuOpenedCallbacks, (uintptr_t)t, env, callback);
237+
tray_set_menu_opened_callback(t, callback ? menu_opened_cb_trampoline : NULL);
238+
}
239+
221240
JNIEXPORT void JNICALL
222241
Java_com_kdroid_composetray_lib_windows_WindowsNativeBridge_nativeSetTrayMenu(
223242
JNIEnv *env, jclass clazz, jlong trayHandle, jlong menuHandle)
@@ -271,6 +290,7 @@ Java_com_kdroid_composetray_lib_windows_WindowsNativeBridge_nativeExitTray(
271290
(void)env; (void)clazz;
272291
tray_exit();
273292
clearAllCallbacks(&g_menuCallbacks);
293+
clearAllCallbacks(&g_menuOpenedCallbacks);
274294
}
275295

276296
/* ========================================================================== */

src/native/windows/tray.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ TRAY_EXPORT void tray_exit (void); /* Free all resources
6060
TRAY_EXPORT int tray_get_notification_icons_position(int *x, int *y);
6161
TRAY_EXPORT const char *tray_get_notification_icons_region(void);
6262

63+
/* Menu-opened callback: invoked just before the popup menu is shown */
64+
TRAY_EXPORT void tray_set_menu_opened_callback(struct tray *tray, void (*cb)(struct tray *));
65+
6366
#ifdef __cplusplus
6467
} /* extern "C" */
6568
#endif

src/native/windows/tray_windows.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ typedef struct TrayContext {
9999
UINT uID; /* unique id for Shell_NotifyIcon */
100100
DWORD threadId; /* thread that owns this context */
101101
BOOL exiting; /* exit requested for this context */
102+
void (*menu_opened_cb)(struct tray *); /* called before menu popup */
102103
struct TrayContext *next; /* linked list */
103104
} TrayContext;
104105

@@ -337,6 +338,12 @@ static LRESULT CALLBACK tray_wnd_proc(HWND h, UINT msg, WPARAM w, LPARAM l)
337338
GetCursorPos(&p);
338339
SetForegroundWindow(h);
339340

341+
/* Invoke menu-opened callback outside the critical section
342+
* to avoid deadlocks with JNI calls */
343+
if (ctx && ctx->menu_opened_cb && ctx->tray) {
344+
ctx->menu_opened_cb(ctx->tray);
345+
}
346+
340347
EnterCriticalSection(&tray_cs);
341348
if (ctx && ctx->hmenu) {
342349
WORD cmd = TrackPopupMenu(ctx->hmenu,
@@ -655,6 +662,20 @@ void tray_exit(void)
655662
LeaveCriticalSection(&tray_cs);
656663
}
657664

665+
/* -------------------------------------------------------------------------- */
666+
/* Set/clear the menu-opened callback for a given tray */
667+
/* -------------------------------------------------------------------------- */
668+
void tray_set_menu_opened_callback(struct tray *tray, void (*cb)(struct tray *))
669+
{
670+
if (!tray) return;
671+
ensure_critical_section();
672+
EnterCriticalSection(&tray_cs);
673+
TrayContext *ctx = find_ctx_by_tray(tray);
674+
if (!ctx) ctx = find_ctx_by_thread(GetCurrentThreadId());
675+
if (ctx) ctx->menu_opened_cb = cb;
676+
LeaveCriticalSection(&tray_cs);
677+
}
678+
658679
static BOOL get_tray_icon_rect(RECT *r)
659680
{
660681
/* Use per-thread context to identify the correct tray icon */

0 commit comments

Comments
 (0)