Skip to content

Commit b5df040

Browse files
kdroidFilterclaude
andcommitted
feat(macos): implement onMenuOpened callback for tray menu
- Add menu opened callback storage and dispatch in TrayContext (Swift) - Invoke callback in InstanceButtonClickHandler.handleClick() before menu.popUp() - Expose tray_set_menu_opened_callback() C API with per-instance support - Add JNI bridge and trampoline for menu opened callback - Wire callback through MacNativeBridge, MacTrayManager, and MacTrayInitializer - Initialize callback after tray_init() to ensure TrayContext exists - Clear native library cache in macOS build script (darwin-aarch64/x86-64) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f6df296 commit b5df040

7 files changed

Lines changed: 93 additions & 6 deletions

File tree

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/tray/impl/MacTrayInitializer.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ object MacTrayInitializer {
2525
tooltip: String,
2626
onLeftClick: (() -> Unit)? = null,
2727
menuContent: (TrayMenuBuilder.() -> Unit)? = null,
28-
@Suppress("UNUSED_PARAMETER") onMenuOpened: (() -> Unit)? = null,
28+
onMenuOpened: (() -> Unit)? = null,
2929
) {
3030
var manager = trayManagers[id]
3131
if (manager == null) {
3232
// Create a new manager for this ID
33-
manager = MacTrayManager(iconPath, tooltip, onLeftClick)
33+
manager = MacTrayManager(iconPath, tooltip, onLeftClick, onMenuOpened)
3434
trayManagers[id] = manager
3535

3636
// Build menu for this manager
@@ -55,7 +55,7 @@ object MacTrayInitializer {
5555
manager.startTray()
5656
} else {
5757
// Existing manager: delegate to update with the provided content
58-
update(id, iconPath, tooltip, onLeftClick, menuContent)
58+
update(id, iconPath, tooltip, onLeftClick, menuContent, onMenuOpened)
5959
}
6060
}
6161

@@ -66,12 +66,12 @@ object MacTrayInitializer {
6666
tooltip: String,
6767
onLeftClick: (() -> Unit)? = null,
6868
menuContent: (TrayMenuBuilder.() -> Unit)? = null,
69-
@Suppress("UNUSED_PARAMETER") onMenuOpened: (() -> Unit)? = null,
69+
onMenuOpened: (() -> Unit)? = null,
7070
) {
7171
val manager = trayManagers[id]
7272
if (manager == null) {
7373
// If manager doesn't exist, initialize it
74-
initialize(id, iconPath, tooltip, onLeftClick, menuContent)
74+
initialize(id, iconPath, tooltip, onLeftClick, menuContent, onMenuOpened)
7575
return
7676
}
7777

@@ -94,7 +94,7 @@ object MacTrayInitializer {
9494
null
9595
}
9696

97-
manager.update(iconPath, tooltip, onLeftClick, newMenuItems)
97+
manager.update(iconPath, tooltip, onLeftClick, newMenuItems, onMenuOpened)
9898
}
9999

100100
@Synchronized

src/native/macos/MacTrayBridge.m

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
6161

6262
static CallbackEntry *g_trayCallbacks = NULL; /* tray left-click */
6363
static CallbackEntry *g_menuCallbacks = NULL; /* menu item click */
64+
static CallbackEntry *g_menuOpenedCallbacks = NULL; /* menu opened */
6465
static CallbackEntry *g_themeCallback = NULL; /* theme change (single) */
6566

6667
static void storeCallback(CallbackEntry **list, void *key, JNIEnv *env, jobject callback) {
@@ -177,6 +178,18 @@ static void menuItemCbTrampoline(struct tray_menu_item *item) {
177178
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
178179
}
179180

181+
/* Called by the Swift click handler when the menu is about to open */
182+
static void menuOpenedCbTrampoline(struct tray *t) {
183+
JNIEnv *env = getJNIEnv();
184+
if (!env) return;
185+
jobject runnable = findCallback(g_menuOpenedCallbacks, t);
186+
if (!runnable) return;
187+
jmethodID run = getRunnableRunMethod(env);
188+
if (!run) return;
189+
(*env)->CallVoidMethod(env, runnable, run);
190+
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
191+
}
192+
180193
/* Called by the Swift appearance observer when the theme changes */
181194
static void themeCbTrampoline(int isDark) {
182195
JNIEnv *env = getJNIEnv();
@@ -260,6 +273,16 @@ JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativ
260273
t->cb = (callback != NULL) ? trayCbTrampoline : NULL;
261274
}
262275

276+
JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetMenuOpenedCallback(
277+
JNIEnv *env, jclass clazz, jlong handle, jobject callback)
278+
{
279+
(void)clazz;
280+
struct tray *t = (struct tray *)(uintptr_t)handle;
281+
if (!t) return;
282+
storeCallback(&g_menuOpenedCallbacks, t, env, callback);
283+
tray_set_menu_opened_callback(t, (callback != NULL) ? menuOpenedCbTrampoline : NULL);
284+
}
285+
263286
JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetTrayMenu(
264287
JNIEnv *env, jclass clazz, jlong trayHandle, jlong menuHandle)
265288
{
@@ -312,6 +335,7 @@ JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativ
312335
tray_dispose(t);
313336
/* Clean up callback refs for this tray */
314337
removeCallback(&g_trayCallbacks, t);
338+
removeCallback(&g_menuOpenedCallbacks, t);
315339
/* Free the struct and its strings */
316340
free((void *)t->icon_filepath);
317341
free((void *)t->tooltip);
@@ -325,6 +349,7 @@ JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativ
325349
tray_exit();
326350
clearAllCallbacks(&g_trayCallbacks);
327351
clearAllCallbacks(&g_menuCallbacks);
352+
clearAllCallbacks(&g_menuOpenedCallbacks);
328353
}
329354

330355
/* ========================================================================== */

src/native/macos/build.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,14 @@ build_arch() {
6868
build_arch "arm64" "arm64-apple-macosx11.0" "$OUTPUT_DIR/darwin-aarch64"
6969
build_arch "x86_64" "x86_64-apple-macosx11.0" "$OUTPUT_DIR/darwin-x86-64"
7070

71+
# Invalidate runtime cache (NativeLibraryLoader validates by size only,
72+
# so a same-size rebuild would serve the stale cached copy)
73+
for PLATFORM_DIR in darwin-aarch64 darwin-x86-64; do
74+
CACHE_FILE="$HOME/.cache/composetray/native/$PLATFORM_DIR/libMacTray.dylib"
75+
if [ -f "$CACHE_FILE" ]; then
76+
rm -f "$CACHE_FILE"
77+
echo "Cleared cached library: $CACHE_FILE"
78+
fi
79+
done
80+
7181
echo "Build completed successfully."

src/native/macos/tray.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ TRAY_API void tray_exit (void); /* free everything and exit */
6565
/* Additional options / information */
6666
/* -------------------------------------------------------------------------- */
6767
TRAY_API void tray_set_theme_callback(theme_callback cb);
68+
TRAY_API void tray_set_menu_opened_callback(struct tray *tray, tray_callback cb);
6869
TRAY_API int tray_is_menu_dark(void); /* 1 = dark mode */
6970

7071
/* Windows: corner and coordinates of notification area */

src/native/macos/tray.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ private class TrayContext {
2121
let appearanceObserver: MenuBarAppearanceObserver
2222
var lightImage: NSImage?
2323
var darkImage: NSImage?
24+
var menuOpenedCallback: TrayCallback?
2425
init(statusItem: NSStatusItem, clickHandler: InstanceButtonClickHandler, appearanceObserver: MenuBarAppearanceObserver) {
2526
self.statusItem = statusItem
2627
self.clickHandler = clickHandler
@@ -51,6 +52,7 @@ private class MenuDelegate: NSObject, NSMenuDelegate {
5152

5253
if event.type == .rightMouseUp || event.modifierFlags.contains(.control) {
5354
if let menu = ctx.contextMenu {
55+
ctx.menuOpenedCallback?(trayPtr)
5456
let menuLocation = NSPoint(
5557
x: sender.frame.minX,
5658
y: sender.frame.minY - 5
@@ -384,6 +386,19 @@ public func tray_exit() {
384386
menuDelegate = nil
385387
}
386388

389+
@_cdecl("tray_set_menu_opened_callback")
390+
public func tray_set_menu_opened_callback(
391+
_ tray: UnsafeMutableRawPointer?,
392+
_ cb: TrayCallback?
393+
) {
394+
let doWork = {
395+
guard let tray = tray, let ctx = contexts[tray] else { return }
396+
ctx.menuOpenedCallback = cb
397+
}
398+
if Thread.isMainThread { doWork() }
399+
else { DispatchQueue.main.sync { doWork() } }
400+
}
401+
387402
@_cdecl("tray_set_theme_callback")
388403
public func tray_set_theme_callback(_ cb: @escaping ThemeCallback) {
389404
themeCallback = cb

0 commit comments

Comments
 (0)