From 8c06dfafa2f46212a8f5a52836af9e7add53519a Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Fri, 20 Feb 2026 16:30:31 +0200 Subject: [PATCH] Replace JNA with JNI for macOS native bridge Migrate all macOS JNA (Java Native Access) bindings to JNI (Java Native Interface) to reduce dependency surface on macOS. JNA remains for Windows/Linux. - Create MacTrayBridge.m: Objective-C JNI bridge (~420 lines) with callback trampolines, GlobalRef lifecycle management, and JAWT support - Create MacNativeBridge.kt: Kotlin JNI binding with auto library loading - Add 7 new Swift @_cdecl functions for dock, space, and mouse APIs - Rewrite MacTrayManager to use opaque Long handles instead of JNA Structures - Rewrite MacOsWindowManager to use native bridge instead of ObjC runtime - Simplify MacOutsideClickWatcher (remove ApplicationServices JNA binding) - Update build.sh to compile ObjC bridge with JNI headers - Delete MacTrayLoader.kt (no longer needed) --- maclib/MacTrayBridge.m | 631 ++++++++++++++++++ maclib/build.sh | 80 ++- maclib/tray.h | 16 + maclib/tray.swift | 91 +++ .../composetray/lib/mac/MacNativeBridge.kt | 118 ++++ .../lib/mac/MacOSMenuBarThemeDetector.kt | 17 +- .../composetray/lib/mac/MacOsWindowManager.kt | 295 +------- .../lib/mac/MacOutsideClickWatcher.kt | 31 +- .../composetray/lib/mac/MacTrayLoader.kt | 16 - .../composetray/lib/mac/MacTrayManager.kt | 258 +++---- .../kdroid/composetray/tray/api/TrayApp.kt | 10 +- .../tray/impl/MacTrayInitializer.kt | 4 +- .../kdroid/composetray/utils/TrayPosition.kt | 29 +- 13 files changed, 1044 insertions(+), 552 deletions(-) create mode 100644 maclib/MacTrayBridge.m create mode 100644 src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacNativeBridge.kt delete mode 100644 src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayLoader.kt diff --git a/maclib/MacTrayBridge.m b/maclib/MacTrayBridge.m new file mode 100644 index 00000000..86eefba4 --- /dev/null +++ b/maclib/MacTrayBridge.m @@ -0,0 +1,631 @@ +/* + * MacTrayBridge.m – JNI bridge for ComposeNativeTray macOS native library. + * + * Target Kotlin class: com.kdroid.composetray.lib.mac.MacNativeBridge + * JNI prefix: Java_com_kdroid_composetray_lib_mac_MacNativeBridge_ + */ + +#import +#import +#import +#import +#import +#import +#include "tray.h" + +/* ========================================================================== */ +/* JavaVM cache */ +/* ========================================================================== */ + +static JavaVM *g_jvm = NULL; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + (void)reserved; + g_jvm = vm; + return JNI_VERSION_1_8; +} + +static JNIEnv *getJNIEnv(void) { + JNIEnv *env = NULL; + if (g_jvm == NULL) return NULL; + jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_8); + if (rc == JNI_EDETACHED) { + (*g_jvm)->AttachCurrentThread(g_jvm, (void **)&env, NULL); + } + return env; +} + +/* ========================================================================== */ +/* JNI class/method name prefix */ +/* ========================================================================== */ +#define JNI_PREFIX Java_com_kdroid_composetray_lib_mac_MacNativeBridge_ + +/* Concatenation helper */ +#define JNI_FN(name) JNICALL JNI_PREFIX ## name + +/* ========================================================================== */ +/* Callback GlobalRef storage */ +/* ========================================================================== */ + +/* + * We store per-tray and per-menu-item GlobalRefs in simple linked lists + * keyed by the native pointer. This is adequate for the small number of + * callbacks involved (typically 1 tray + a few dozen menu items). + */ + +typedef struct CallbackEntry { + void *key; /* struct tray* or struct tray_menu_item* */ + jobject globalRef; /* GlobalRef to Java callback object */ + struct CallbackEntry *next; +} CallbackEntry; + +static CallbackEntry *g_trayCallbacks = NULL; /* tray left-click */ +static CallbackEntry *g_menuCallbacks = NULL; /* menu item click */ +static CallbackEntry *g_themeCallback = NULL; /* theme change (single) */ + +static void storeCallback(CallbackEntry **list, void *key, JNIEnv *env, jobject callback) { + /* Remove existing entry for this key if any */ + CallbackEntry **pp = list; + while (*pp) { + if ((*pp)->key == key) { + CallbackEntry *old = *pp; + *pp = old->next; + (*env)->DeleteGlobalRef(env, old->globalRef); + free(old); + break; + } + pp = &(*pp)->next; + } + if (callback == NULL) return; + CallbackEntry *entry = (CallbackEntry *)malloc(sizeof(CallbackEntry)); + entry->key = key; + entry->globalRef = (*env)->NewGlobalRef(env, callback); + entry->next = *list; + *list = entry; +} + +static jobject findCallback(CallbackEntry *list, void *key) { + for (CallbackEntry *e = list; e; e = e->next) { + if (e->key == key) return e->globalRef; + } + return NULL; +} + +static void removeCallback(CallbackEntry **list, void *key) { + JNIEnv *env = getJNIEnv(); + if (!env) return; + CallbackEntry **pp = list; + while (*pp) { + if ((*pp)->key == key) { + CallbackEntry *old = *pp; + *pp = old->next; + (*env)->DeleteGlobalRef(env, old->globalRef); + free(old); + return; + } + pp = &(*pp)->next; + } +} + +static void clearAllCallbacks(CallbackEntry **list) { + JNIEnv *env = getJNIEnv(); + CallbackEntry *e = *list; + while (e) { + CallbackEntry *next = e->next; + if (env) (*env)->DeleteGlobalRef(env, e->globalRef); + free(e); + e = next; + } + *list = NULL; +} + +/* ========================================================================== */ +/* C callback trampolines */ +/* ========================================================================== */ + +/* Called by the Swift tray_callback when the tray icon is left-clicked */ +static void trayCbTrampoline(struct tray *t) { + JNIEnv *env = getJNIEnv(); + if (!env) return; + jobject runnable = findCallback(g_trayCallbacks, t); + if (!runnable) return; + jclass cls = (*env)->GetObjectClass(env, runnable); + jmethodID run = (*env)->GetMethodID(env, cls, "run", "()V"); + (*env)->CallVoidMethod(env, runnable, run); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); +} + +/* Called by the Swift menu delegate when a menu item is clicked */ +static void menuItemCbTrampoline(struct tray_menu_item *item) { + JNIEnv *env = getJNIEnv(); + if (!env) return; + jobject runnable = findCallback(g_menuCallbacks, item); + if (!runnable) return; + jclass cls = (*env)->GetObjectClass(env, runnable); + jmethodID run = (*env)->GetMethodID(env, cls, "run", "()V"); + (*env)->CallVoidMethod(env, runnable, run); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); +} + +/* Called by the Swift appearance observer when the theme changes */ +static void themeCbTrampoline(int isDark) { + JNIEnv *env = getJNIEnv(); + if (!env) return; + if (!g_themeCallback) return; + jobject cb = g_themeCallback->globalRef; + if (!cb) return; + jclass cls = (*env)->GetObjectClass(env, cb); + jmethodID method = (*env)->GetMethodID(env, cls, "onThemeChanged", "(I)V"); + (*env)->CallVoidMethod(env, cb, method, (jint)isDark); + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); +} + +/* ========================================================================== */ +/* Tray lifecycle */ +/* ========================================================================== */ + +JNIEXPORT jlong JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeCreateTray( + JNIEnv *env, jclass clazz, jstring iconPath, jstring tooltip) +{ + (void)clazz; + struct tray *t = (struct tray *)calloc(1, sizeof(struct tray)); + if (!t) return 0; + + const char *iconUtf = (*env)->GetStringUTFChars(env, iconPath, NULL); + t->icon_filepath = strdup(iconUtf); + (*env)->ReleaseStringUTFChars(env, iconPath, iconUtf); + + const char *tooltipUtf = (*env)->GetStringUTFChars(env, tooltip, NULL); + t->tooltip = strdup(tooltipUtf); + (*env)->ReleaseStringUTFChars(env, tooltip, tooltipUtf); + + t->menu = NULL; + t->cb = NULL; + return (jlong)(uintptr_t)t; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeFreeTray( + JNIEnv *env, jclass clazz, jlong handle) +{ + (void)env; (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + removeCallback(&g_trayCallbacks, t); + free((void *)t->icon_filepath); + free((void *)t->tooltip); + free(t); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetTrayIcon( + JNIEnv *env, jclass clazz, jlong handle, jstring iconPath) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + free((void *)t->icon_filepath); + const char *utf = (*env)->GetStringUTFChars(env, iconPath, NULL); + t->icon_filepath = strdup(utf); + (*env)->ReleaseStringUTFChars(env, iconPath, utf); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetTrayTooltip( + JNIEnv *env, jclass clazz, jlong handle, jstring tooltip) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + free((void *)t->tooltip); + const char *utf = (*env)->GetStringUTFChars(env, tooltip, NULL); + t->tooltip = strdup(utf); + (*env)->ReleaseStringUTFChars(env, tooltip, utf); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetTrayCallback( + JNIEnv *env, jclass clazz, jlong handle, jobject callback) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + storeCallback(&g_trayCallbacks, t, env, callback); + t->cb = (callback != NULL) ? trayCbTrampoline : NULL; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetTrayMenu( + JNIEnv *env, jclass clazz, jlong trayHandle, jlong menuHandle) +{ + (void)env; (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)trayHandle; + if (!t) return; + t->menu = (struct tray_menu_item *)(uintptr_t)menuHandle; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeClearTrayMenu( + JNIEnv *env, jclass clazz, jlong trayHandle) +{ + (void)env; (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)trayHandle; + if (!t) return; + t->menu = NULL; +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeInitTray( + JNIEnv *env, jclass clazz, jlong handle) +{ + (void)env; (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return -1; + return (jint)tray_init(t); +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeLoopTray( + JNIEnv *env, jclass clazz, jint blocking) +{ + (void)env; (void)clazz; + return (jint)tray_loop((int)blocking); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeUpdateTray( + JNIEnv *env, jclass clazz, jlong handle) +{ + (void)env; (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + tray_update(t); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeDisposeTray( + JNIEnv *env, jclass clazz, jlong handle) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + tray_dispose(t); + /* Clean up callback refs for this tray */ + removeCallback(&g_trayCallbacks, t); + /* Free the struct and its strings */ + free((void *)t->icon_filepath); + free((void *)t->tooltip); + free(t); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeExitTray( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + tray_exit(); + clearAllCallbacks(&g_trayCallbacks); + clearAllCallbacks(&g_menuCallbacks); +} + +/* ========================================================================== */ +/* Menu items */ +/* ========================================================================== */ + +JNIEXPORT jlong JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeCreateMenuItems( + JNIEnv *env, jclass clazz, jint count) +{ + (void)env; (void)clazz; + /* Allocate count+1 items; the last one is the sentinel (all zeros/NULL) */ + struct tray_menu_item *items = (struct tray_menu_item *)calloc((size_t)count + 1, sizeof(struct tray_menu_item)); + return (jlong)(uintptr_t)items; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetMenuItem( + JNIEnv *env, jclass clazz, jlong menuHandle, jint index, + jstring text, jstring iconPath, jint disabled, jint checked) +{ + (void)clazz; + struct tray_menu_item *items = (struct tray_menu_item *)(uintptr_t)menuHandle; + if (!items) return; + struct tray_menu_item *item = &items[index]; + + /* Free previous strings if overwriting */ + free((void *)item->text); + free((void *)item->icon_filepath); + + const char *textUtf = (*env)->GetStringUTFChars(env, text, NULL); + item->text = strdup(textUtf); + (*env)->ReleaseStringUTFChars(env, text, textUtf); + + if (iconPath != NULL) { + const char *iconUtf = (*env)->GetStringUTFChars(env, iconPath, NULL); + item->icon_filepath = strdup(iconUtf); + (*env)->ReleaseStringUTFChars(env, iconPath, iconUtf); + } else { + item->icon_filepath = NULL; + } + + item->disabled = (int)disabled; + item->checked = (int)checked; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetMenuItemCallback( + JNIEnv *env, jclass clazz, jlong menuHandle, jint index, jobject callback) +{ + (void)clazz; + struct tray_menu_item *items = (struct tray_menu_item *)(uintptr_t)menuHandle; + if (!items) return; + struct tray_menu_item *item = &items[index]; + storeCallback(&g_menuCallbacks, item, env, callback); + item->cb = (callback != NULL) ? menuItemCbTrampoline : NULL; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetMenuItemSubmenu( + JNIEnv *env, jclass clazz, jlong menuHandle, jint index, jlong submenuHandle) +{ + (void)env; (void)clazz; + struct tray_menu_item *items = (struct tray_menu_item *)(uintptr_t)menuHandle; + if (!items) return; + items[index].submenu = (struct tray_menu_item *)(uintptr_t)submenuHandle; +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeFreeMenuItems( + JNIEnv *env, jclass clazz, jlong menuHandle, jint count) +{ + (void)clazz; + struct tray_menu_item *items = (struct tray_menu_item *)(uintptr_t)menuHandle; + if (!items) return; + for (int i = 0; i < count; i++) { + removeCallback(&g_menuCallbacks, &items[i]); + free((void *)items[i].text); + free((void *)items[i].icon_filepath); + /* Note: submenus are freed by their own nativeFreeMenuItems call */ + } + free(items); +} + +/* ========================================================================== */ +/* Theme */ +/* ========================================================================== */ + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetThemeCallback( + JNIEnv *env, jclass clazz, jobject callback) +{ + (void)clazz; + /* Store a single global theme callback */ + if (g_themeCallback) { + (*env)->DeleteGlobalRef(env, g_themeCallback->globalRef); + free(g_themeCallback); + g_themeCallback = NULL; + } + if (callback != NULL) { + g_themeCallback = (CallbackEntry *)malloc(sizeof(CallbackEntry)); + g_themeCallback->key = NULL; + g_themeCallback->globalRef = (*env)->NewGlobalRef(env, callback); + g_themeCallback->next = NULL; + tray_set_theme_callback(themeCbTrampoline); + } +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeIsMenuDark( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + return (jint)tray_is_menu_dark(); +} + +/* ========================================================================== */ +/* Position */ +/* ========================================================================== */ + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeGetStatusItemPosition( + JNIEnv *env, jclass clazz, jintArray outXY) +{ + (void)clazz; + int x = 0, y = 0; + int precise = tray_get_status_item_position(&x, &y); + jint buf[2] = { (jint)x, (jint)y }; + (*env)->SetIntArrayRegion(env, outXY, 0, 2, buf); + return (jint)precise; +} + +JNIEXPORT jstring JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeGetStatusItemRegion( + JNIEnv *env, jclass clazz) +{ + (void)clazz; + const char *region = tray_get_status_item_region(); + if (!region) return (*env)->NewStringUTF(env, "top-right"); + jstring result = (*env)->NewStringUTF(env, region); + free((void *)region); + return result; +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeGetStatusItemPositionFor( + JNIEnv *env, jclass clazz, jlong handle, jintArray outXY) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + int x = 0, y = 0; + int precise = tray_get_status_item_position_for(t, &x, &y); + jint buf[2] = { (jint)x, (jint)y }; + (*env)->SetIntArrayRegion(env, outXY, 0, 2, buf); + return (jint)precise; +} + +JNIEXPORT jstring JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeGetStatusItemRegionFor( + JNIEnv *env, jclass clazz, jlong handle) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + const char *region = tray_get_status_item_region_for(t); + if (!region) return (*env)->NewStringUTF(env, "top-right"); + jstring result = (*env)->NewStringUTF(env, region); + free((void *)region); + return result; +} + +/* ========================================================================== */ +/* Appearance */ +/* ========================================================================== */ + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetIconsForAppearance( + JNIEnv *env, jclass clazz, jlong handle, jstring lightIcon, jstring darkIcon) +{ + (void)clazz; + struct tray *t = (struct tray *)(uintptr_t)handle; + if (!t) return; + const char *lightUtf = (*env)->GetStringUTFChars(env, lightIcon, NULL); + const char *darkUtf = (*env)->GetStringUTFChars(env, darkIcon, NULL); + tray_set_icons_for_appearance(t, lightUtf, darkUtf); + (*env)->ReleaseStringUTFChars(env, lightIcon, lightUtf); + (*env)->ReleaseStringUTFChars(env, darkIcon, darkUtf); +} + +/* ========================================================================== */ +/* Window management */ +/* ========================================================================== */ + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeShowInDock( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + return (jint)tray_show_in_dock(); +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeHideFromDock( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + return (jint)tray_hide_from_dock(); +} + +JNIEXPORT void JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetMoveToActiveSpace( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + tray_set_windows_move_to_active_space(); +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeSetMoveToActiveSpaceForWindow( + JNIEnv *env, jclass clazz, jlong viewPtr) +{ + (void)env; (void)clazz; + return (jint)tray_set_move_to_active_space_for_view((void *)(uintptr_t)viewPtr); +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeIsFloatingWindowOnActiveSpace( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + return (jint)tray_is_floating_window_on_active_space(); +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeBringFloatingWindowToFront( + JNIEnv *env, jclass clazz) +{ + (void)env; (void)clazz; + return (jint)tray_bring_floating_window_to_front(); +} + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeIsOnActiveSpaceForView( + JNIEnv *env, jclass clazz, jlong viewPtr) +{ + (void)env; (void)clazz; + return (jint)tray_is_on_active_space_for_view((void *)(uintptr_t)viewPtr); +} + +/* ========================================================================== */ +/* Mouse */ +/* ========================================================================== */ + +JNIEXPORT jint JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeGetMouseButtonState( + JNIEnv *env, jclass clazz, jint button) +{ + (void)env; (void)clazz; + return (jint)tray_get_mouse_button_state((int)button); +} + +/* ========================================================================== */ +/* JAWT – resolve AWT native view pointer dynamically */ +/* ========================================================================== */ + +/* + * JAWT types and function pointer – resolved at runtime via dlsym so we + * don't need to link against the JAWT framework at build time. + */ + +#ifndef _JAWT_H_ +/* Minimal JAWT declarations if the header isn't available */ +typedef jint JAWT_Version; + +typedef struct { + jint x, y, width, height; +} JAWT_Rectangle; + +typedef struct JAWT_DrawingSurfaceInfo { + void *platformInfo; + void *ds; /* back-pointer */ + JAWT_Rectangle bounds; + jint clipSize; + JAWT_Rectangle *clip; +} JAWT_DrawingSurfaceInfo; + +typedef struct JAWT_DrawingSurface { + JNIEnv *env; + jobject target; + jint (JNICALL *Lock)(struct JAWT_DrawingSurface *ds); + JAWT_DrawingSurfaceInfo *(JNICALL *GetDrawingSurfaceInfo)(struct JAWT_DrawingSurface *ds); + void (JNICALL *FreeDrawingSurfaceInfo)(JAWT_DrawingSurfaceInfo *dsi); + void (JNICALL *Unlock)(struct JAWT_DrawingSurface *ds); +} JAWT_DrawingSurface; + +typedef struct { + JAWT_Version version; + JAWT_DrawingSurface *(JNICALL *GetDrawingSurface)(JNIEnv *env, jobject target); + void (JNICALL *FreeDrawingSurface)(JAWT_DrawingSurface *ds); + void (JNICALL *Lock)(JNIEnv *env); + void (JNICALL *Unlock)(JNIEnv *env); + /* JAWT 9+ */ + jobject (JNICALL *GetComponent)(JNIEnv *env, void *platformInfo); +} JAWT; + +#define JAWT_VERSION_1_4 0x00010004 +#define JAWT_VERSION_9 0x00090000 +#define JAWT_LOCK_ERROR 0x00000001 + +typedef jboolean (JNICALL *JAWT_GetAWT_t)(JNIEnv *env, JAWT *awt); +#endif /* _JAWT_H_ */ + +JNIEXPORT jlong JNICALL Java_com_kdroid_composetray_lib_mac_MacNativeBridge_nativeGetAWTViewPtr( + JNIEnv *env, jclass clazz, jobject awtComponent) +{ + (void)clazz; + if (awtComponent == NULL) return 0; + + /* Dynamically resolve JAWT_GetAWT */ + static JAWT_GetAWT_t jawt_GetAWT = NULL; + if (jawt_GetAWT == NULL) { + jawt_GetAWT = (JAWT_GetAWT_t)dlsym(RTLD_DEFAULT, "JAWT_GetAWT"); + if (jawt_GetAWT == NULL) return 0; + } + + JAWT awt; + awt.version = JAWT_VERSION_1_4; + if (!jawt_GetAWT(env, &awt)) return 0; + + JAWT_DrawingSurface *ds = awt.GetDrawingSurface(env, awtComponent); + if (!ds) return 0; + + jint lockResult = ds->Lock(ds); + if (lockResult & JAWT_LOCK_ERROR) { + awt.FreeDrawingSurface(ds); + return 0; + } + + JAWT_DrawingSurfaceInfo *dsi = ds->GetDrawingSurfaceInfo(ds); + jlong viewPtr = 0; + if (dsi && dsi->platformInfo) { + /* + * On macOS Cocoa, platformInfo points to an id which is + * the NSView (specifically an AWTSurfaceLayers subclass). + * We cast it to a raw pointer and return as jlong. + */ + viewPtr = (jlong)(uintptr_t)(*(void **)dsi->platformInfo); + } + + if (dsi) ds->FreeDrawingSurfaceInfo(dsi); + ds->Unlock(ds); + awt.FreeDrawingSurface(ds); + + return viewPtr; +} diff --git a/maclib/build.sh b/maclib/build.sh index e267df6c..64c4e027 100755 --- a/maclib/build.sh +++ b/maclib/build.sh @@ -3,39 +3,69 @@ # Exit on error set -e -echo "Building MacTray library..." +echo "Building MacTray library (Swift + JNI bridge)..." SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_DIR="${NATIVE_LIBS_OUTPUT_DIR:-$SCRIPT_DIR/../src/commonMain/resources}" echo "Output dir for mac is: $OUTPUT_DIR" -echo "Building for ARM64 (Apple Silicon)..." +# Detect JAVA_HOME +if [ -z "$JAVA_HOME" ]; then + JAVA_HOME=$(/usr/libexec/java_home 2>/dev/null || true) +fi +if [ -z "$JAVA_HOME" ] || [ ! -d "$JAVA_HOME" ]; then + echo "ERROR: JAVA_HOME not found. Install a JDK or set JAVA_HOME." + exit 1 +fi +echo "Using JAVA_HOME: $JAVA_HOME" + +JNI_INCLUDE="$JAVA_HOME/include" +JNI_INCLUDE_DARWIN="$JAVA_HOME/include/darwin" + +if [ ! -f "$JNI_INCLUDE/jni.h" ]; then + echo "ERROR: jni.h not found at $JNI_INCLUDE/jni.h" + exit 1 +fi mkdir -p "$OUTPUT_DIR/darwin-aarch64" mkdir -p "$OUTPUT_DIR/darwin-x86-64" -swiftc -emit-library -o "$OUTPUT_DIR/darwin-aarch64/libMacTray.dylib" \ - -module-name MacTray \ - -swift-version 5 \ - -target arm64-apple-macosx11.0 \ - -O -whole-module-optimization \ - -framework Foundation \ - -framework Cocoa \ - -Xlinker -rpath -Xlinker @executable_path/../Frameworks \ - -Xlinker -rpath -Xlinker @loader_path/Frameworks \ - tray.swift - -echo "Building for x86_64 (Intel)..." - -swiftc -emit-library -o "$OUTPUT_DIR/darwin-x86-64/libMacTray.dylib" \ - -module-name MacTray \ - -swift-version 5 \ - -target x86_64-apple-macosx11.0 \ - -O -whole-module-optimization \ - -framework Foundation \ - -framework Cocoa \ - -Xlinker -rpath -Xlinker @executable_path/../Frameworks \ - -Xlinker -rpath -Xlinker @loader_path/Frameworks \ - tray.swift +build_arch() { + local ARCH=$1 + local TARGET=$2 + local OUT_DIR=$3 + + echo "Building for $ARCH..." + + # 1. Compile the JNI bridge (Objective-C) -> bridge.o + clang -c -o "$SCRIPT_DIR/bridge_${ARCH}.o" \ + -arch "$ARCH" \ + -mmacosx-version-min=11.0 \ + -I "$JNI_INCLUDE" \ + -I "$JNI_INCLUDE_DARWIN" \ + -I "$SCRIPT_DIR" \ + -fobjc-arc \ + "$SCRIPT_DIR/MacTrayBridge.m" + + # 2. Compile Swift + link with the bridge object -> dylib + swiftc -emit-library -o "$OUT_DIR/libMacTray.dylib" \ + -module-name MacTray \ + -swift-version 5 \ + -target "$TARGET" \ + -O -whole-module-optimization \ + -framework Foundation \ + -framework Cocoa \ + -framework ApplicationServices \ + -Xlinker -rpath -Xlinker @executable_path/../Frameworks \ + -Xlinker -rpath -Xlinker @loader_path/Frameworks \ + "$SCRIPT_DIR/tray.swift" \ + "$SCRIPT_DIR/bridge_${ARCH}.o" + + # Clean up intermediate object + rm -f "$SCRIPT_DIR/bridge_${ARCH}.o" +} + +build_arch "arm64" "arm64-apple-macosx11.0" "$OUTPUT_DIR/darwin-aarch64" +build_arch "x86_64" "x86_64-apple-macosx11.0" "$OUTPUT_DIR/darwin-x86-64" echo "Build completed successfully." diff --git a/maclib/tray.h b/maclib/tray.h index 3b2cc90a..cdc1170b 100644 --- a/maclib/tray.h +++ b/maclib/tray.h @@ -82,6 +82,22 @@ TRAY_API const char *tray_get_status_item_region_for(struct tray *tray); /* macOS: pre-rendered appearance icons for instant light/dark switching */ TRAY_API void tray_set_icons_for_appearance(struct tray *tray, const char *light_icon, const char *dark_icon); +/* macOS: space behavior for all windows */ +TRAY_API void tray_set_windows_move_to_active_space(void); + +/* macOS: dock visibility */ +TRAY_API int tray_show_in_dock(void); +TRAY_API int tray_hide_from_dock(void); + +/* macOS: floating window / Space management */ +TRAY_API int tray_is_floating_window_on_active_space(void); +TRAY_API int tray_bring_floating_window_to_front(void); +TRAY_API int tray_set_move_to_active_space_for_view(void *nsViewPtr); +TRAY_API int tray_is_on_active_space_for_view(void *nsViewPtr); + +/* macOS: mouse button state */ +TRAY_API int tray_get_mouse_button_state(int button); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/maclib/tray.swift b/maclib/tray.swift index bcd957ea..df139a75 100644 --- a/maclib/tray.swift +++ b/maclib/tray.swift @@ -563,4 +563,95 @@ public func tray_set_windows_move_to_active_space() { for window in NSApp.windows { window.collectionBehavior.insert(.moveToActiveSpace) } +} + +// MARK: - Dock visibility + +@_cdecl("tray_show_in_dock") +public func tray_show_in_dock() -> Int32 { + let doWork = { () -> Int32 in + NSApp.setActivationPolicy(.regular) + return 0 + } + if Thread.isMainThread { return doWork() } + return DispatchQueue.main.sync { doWork() } +} + +@_cdecl("tray_hide_from_dock") +public func tray_hide_from_dock() -> Int32 { + let doWork = { () -> Int32 in + NSApp.setActivationPolicy(.accessory) + return 0 + } + if Thread.isMainThread { return doWork() } + return DispatchQueue.main.sync { doWork() } +} + +// MARK: - Floating window / Space management + +@_cdecl("tray_is_floating_window_on_active_space") +public func tray_is_floating_window_on_active_space() -> Int32 { + let doWork = { () -> Int32 in + for window in NSApp.windows { + if window.level.rawValue > 0 { + return window.isOnActiveSpace ? 1 : 0 + } + } + return 1 // No floating window found, assume on active Space + } + if Thread.isMainThread { return doWork() } + return DispatchQueue.main.sync { doWork() } +} + +@_cdecl("tray_bring_floating_window_to_front") +public func tray_bring_floating_window_to_front() -> Int32 { + let doWork = { () -> Int32 in + NSApp.activate(ignoringOtherApps: true) + for window in NSApp.windows { + if window.level.rawValue > 0 { + window.makeKeyAndOrderFront(nil) + return 0 + } + } + return -1 // No floating window found + } + if Thread.isMainThread { return doWork() } + return DispatchQueue.main.sync { doWork() } +} + +@_cdecl("tray_set_move_to_active_space_for_view") +public func tray_set_move_to_active_space_for_view(_ nsViewPtr: UnsafeMutableRawPointer?) -> Int32 { + let doWork = { () -> Int32 in + guard let viewPtr = nsViewPtr else { return -1 } + let nsView = Unmanaged.fromOpaque(viewPtr).takeUnretainedValue() + guard let nsWindow = nsView.window else { return -1 } + var behavior = nsWindow.collectionBehavior + behavior.remove(.canJoinAllSpaces) + behavior.insert(.moveToActiveSpace) + nsWindow.collectionBehavior = behavior + return 0 + } + if Thread.isMainThread { return doWork() } + return DispatchQueue.main.sync { doWork() } +} + +@_cdecl("tray_is_on_active_space_for_view") +public func tray_is_on_active_space_for_view(_ nsViewPtr: UnsafeMutableRawPointer?) -> Int32 { + let doWork = { () -> Int32 in + guard let viewPtr = nsViewPtr else { return 1 } // fail-open + let nsView = Unmanaged.fromOpaque(viewPtr).takeUnretainedValue() + guard let nsWindow = nsView.window else { return 1 } + return nsWindow.isOnActiveSpace ? 1 : 0 + } + if Thread.isMainThread { return doWork() } + return DispatchQueue.main.sync { doWork() } +} + +// MARK: - Mouse button state + +@_cdecl("tray_get_mouse_button_state") +public func tray_get_mouse_button_state(_ button: Int32) -> Int32 { + let cgButton = CGMouseButton(rawValue: UInt32(button))! + let state = CGEventSource.buttonState(.combinedSessionState, button: cgButton) + return state ? 1 : 0 } \ No newline at end of file diff --git a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacNativeBridge.kt b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacNativeBridge.kt new file mode 100644 index 00000000..6d30c1ec --- /dev/null +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacNativeBridge.kt @@ -0,0 +1,118 @@ +package com.kdroid.composetray.lib.mac + +import com.kdroid.composetray.utils.extractToTempIfDifferent +import java.io.File + +/** + * JNI bridge to the native macOS tray library (libMacTray.dylib). + * Replaces the previous JNA direct-mapping approach. + * All methods are static JNI calls into MacTrayBridge.m. + */ +internal object MacNativeBridge { + + init { + loadNativeLibrary() + } + + private fun loadNativeLibrary() { + val arch = System.getProperty("os.arch") ?: "aarch64" + val resourceDir = when { + arch.contains("aarch64") || arch.contains("arm64") -> "darwin-aarch64" + else -> "darwin-x86-64" + } + val resourcePath = "$resourceDir/libMacTray.dylib" + + // Try to find the dylib on the classpath (inside a JAR or on disk) + val url = MacNativeBridge::class.java.classLoader?.getResource(resourcePath) + if (url != null) { + val protocol = url.protocol + if (protocol == "jar") { + // Extract from JAR to a temp file + val tempFile = extractToTempIfDifferent(url.toString()) + if (tempFile != null) { + System.load(tempFile.absolutePath) + return + } + } else if (protocol == "file") { + // Direct file on disk (development mode) + val file = File(url.toURI()) + if (file.exists()) { + System.load(file.absolutePath) + return + } + } + } + + // Fallback: let the JVM find it on java.library.path + System.loadLibrary("MacTray") + } + + // ── Tray lifecycle ────────────────────────────────────────────────── + + @JvmStatic external fun nativeCreateTray(iconPath: String, tooltip: String): Long + @JvmStatic external fun nativeFreeTray(handle: Long) + @JvmStatic external fun nativeSetTrayIcon(handle: Long, iconPath: String) + @JvmStatic external fun nativeSetTrayTooltip(handle: Long, tooltip: String) + @JvmStatic external fun nativeSetTrayCallback(handle: Long, callback: Runnable?) + @JvmStatic external fun nativeSetTrayMenu(trayHandle: Long, menuHandle: Long) + @JvmStatic external fun nativeClearTrayMenu(trayHandle: Long) + @JvmStatic external fun nativeInitTray(handle: Long): Int + @JvmStatic external fun nativeLoopTray(blocking: Int): Int + @JvmStatic external fun nativeUpdateTray(handle: Long) + @JvmStatic external fun nativeDisposeTray(handle: Long) + @JvmStatic external fun nativeExitTray() + + // ── Menu items ────────────────────────────────────────────────────── + + @JvmStatic external fun nativeCreateMenuItems(count: Int): Long + @JvmStatic external fun nativeSetMenuItem( + menuHandle: Long, index: Int, + text: String, iconPath: String?, + disabled: Int, checked: Int + ) + @JvmStatic external fun nativeSetMenuItemCallback(menuHandle: Long, index: Int, callback: Runnable?) + @JvmStatic external fun nativeSetMenuItemSubmenu(menuHandle: Long, index: Int, submenuHandle: Long) + @JvmStatic external fun nativeFreeMenuItems(menuHandle: Long, count: Int) + + // ── Theme ─────────────────────────────────────────────────────────── + + @JvmStatic external fun nativeSetThemeCallback(callback: ThemeChangeCallback?) + @JvmStatic external fun nativeIsMenuDark(): Int + + // ── Position ──────────────────────────────────────────────────────── + + /** Writes [x, y] into outXY. Returns 1 if precise, 0 if fallback. */ + @JvmStatic external fun nativeGetStatusItemPosition(outXY: IntArray): Int + @JvmStatic external fun nativeGetStatusItemRegion(): String + @JvmStatic external fun nativeGetStatusItemPositionFor(handle: Long, outXY: IntArray): Int + @JvmStatic external fun nativeGetStatusItemRegionFor(handle: Long): String + + // ── Appearance ────────────────────────────────────────────────────── + + @JvmStatic external fun nativeSetIconsForAppearance(handle: Long, lightIcon: String, darkIcon: String) + + // ── Window management ─────────────────────────────────────────────── + + @JvmStatic external fun nativeShowInDock(): Int + @JvmStatic external fun nativeHideFromDock(): Int + @JvmStatic external fun nativeSetMoveToActiveSpace() + @JvmStatic external fun nativeSetMoveToActiveSpaceForWindow(viewPtr: Long): Int + @JvmStatic external fun nativeIsFloatingWindowOnActiveSpace(): Int + @JvmStatic external fun nativeBringFloatingWindowToFront(): Int + @JvmStatic external fun nativeIsOnActiveSpaceForView(viewPtr: Long): Int + + // ── Mouse ─────────────────────────────────────────────────────────── + + @JvmStatic external fun nativeGetMouseButtonState(button: Int): Int + + // ── JAWT ──────────────────────────────────────────────────────────── + + /** Returns the NSView pointer for an AWT component, or 0 on failure. */ + @JvmStatic external fun nativeGetAWTViewPtr(awtComponent: Any): Long + + // ── Callback interface ────────────────────────────────────────────── + + interface ThemeChangeCallback { + fun onThemeChanged(isDark: Int) + } +} diff --git a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOSMenuBarThemeDetector.kt b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOSMenuBarThemeDetector.kt index 4a0509b7..64ab2784 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOSMenuBarThemeDetector.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOSMenuBarThemeDetector.kt @@ -1,26 +1,19 @@ package com.kdroid.composetray.lib.mac -import com.sun.jna.Native import java.util.function.Consumer import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors -import com.kdroid.composetray.lib.mac.MacTrayManager.MacTrayLibrary - -// Removed kermit Logger import and usage -// private val logger = Logger.withTag("MacOSMenuBarThemeDetector") object MacOSMenuBarThemeDetector { - private val trayLib: MacTrayLibrary = MacTrayLoader.lib - private val listeners: MutableSet> = ConcurrentHashMap.newKeySet() private val callbackExecutor = Executors.newSingleThreadExecutor { r -> Thread(r, "MacOS MenuBar Theme Detector Thread").apply { isDaemon = true } } - private val themeChangedCallback = object : MacTrayManager.ThemeCallback { - override fun invoke(isDark: Int) { + private val themeChangedCallback = object : MacNativeBridge.ThemeChangeCallback { + override fun onThemeChanged(isDark: Int) { callbackExecutor.execute { val dark = isDark != 0 notifyListeners(dark) @@ -29,11 +22,11 @@ object MacOSMenuBarThemeDetector { } init { - trayLib.tray_set_theme_callback(themeChangedCallback) + MacNativeBridge.nativeSetThemeCallback(themeChangedCallback) } fun isDark(): Boolean { - return trayLib.tray_is_menu_dark() != 0 + return MacNativeBridge.nativeIsMenuDark() != 0 } fun registerListener(listener: Consumer) { @@ -49,4 +42,4 @@ object MacOSMenuBarThemeDetector { private fun notifyListeners(isDark: Boolean) { listeners.forEach { it.accept(isDark) } } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOsWindowManager.kt b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOsWindowManager.kt index c411a0ef..bfdb93bc 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOsWindowManager.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOsWindowManager.kt @@ -1,148 +1,26 @@ package com.kdroid.composetray.lib.mac import com.kdroid.composetray.utils.debugln -import com.sun.jna.Library -import com.sun.jna.Native -import com.sun.jna.Pointer import io.github.kdroidfilter.platformtools.OperatingSystem import io.github.kdroidfilter.platformtools.getOperatingSystem -// JNA interface to access the Objective-C runtime -interface ObjectiveC : Library { - fun objc_msgSend(receiver: Pointer, selector: Pointer, vararg args: Any): Pointer - fun objc_msgSend(receiver: Pointer, selector: Pointer): Pointer - fun objc_msgSend(receiver: Pointer, selector: Pointer, arg: Long): Pointer - fun sel_registerName(name: String): Pointer - fun objc_getClass(name: String): Pointer - - companion object { - val INSTANCE: ObjectiveC = Native.load("objc", ObjectiveC::class.java) - } -} - -// Interface for Foundation framework -interface Foundation : Library { - companion object { - val INSTANCE: Foundation = Native.load("Foundation", Foundation::class.java) - } -} - class MacOSWindowManager { // Detect platform once private val isMacOs: Boolean = getOperatingSystem() == OperatingSystem.MACOS - - // Load Objective-C runtime only on macOS and only when needed - private val objc: ObjectiveC? by lazy { - if (!isMacOs) return@lazy null - try { - ObjectiveC.INSTANCE - } catch (t: Throwable) { - debugln { "Failed to load Objective-C runtime: ${t.message}" } - null - } - } - - private var nsApplicationInstance: Pointer? = null - - /** - * Initialize NSApplication if not already done - */ - private fun ensureNSApplicationInitialized(): Boolean { - if (!isMacOs) { - // Not on macOS: pretend not initialized, but avoid side effects - return false - } - - if (nsApplicationInstance != null && nsApplicationInstance != Pointer.NULL) { - return true - } - - val localObjc = objc ?: return false - - return try { - debugln { "Initializing NSApplication..." } - - // Get NSApplication class - val nsApplicationClass = localObjc.objc_getClass("NSApplication") - if (nsApplicationClass == Pointer.NULL) { - debugln { "Unable to get NSApplication class" } - return false - } - - // Get selector for sharedApplication - val sharedApplicationSelector = localObjc.sel_registerName("sharedApplication") - if (sharedApplicationSelector == Pointer.NULL) { - debugln { "Unable to get sharedApplication selector" } - return false - } - - // Call [NSApplication sharedApplication] - nsApplicationInstance = localObjc.objc_msgSend(nsApplicationClass, sharedApplicationSelector) - if (nsApplicationInstance == Pointer.NULL) { - debugln { "Unable to get NSApplication instance" } - nsApplicationInstance = null - return false - } - - debugln { "NSApplication initialized successfully" } - true - - } catch (e: Exception) { - debugln { "Error while initializing NSApplication: ${e.message}" } - e.printStackTrace() - nsApplicationInstance = null - false - } - } - - /** - * Get the shared NSApplication instance via the Objective-C runtime - */ - private fun getNSApplication(): Pointer? { - if (!ensureNSApplicationInitialized()) { - return null - } - return nsApplicationInstance - } - /** * Show the application in the Dock */ fun showInDock(): Boolean { if (!isMacOs) { - // No-op on non-macOS debugln { "showInDock(): non-macOS platform detected, no action performed" } return true } - val localObjc = objc ?: return false return try { - val nsApp = getNSApplication() - if (nsApp == null) { - debugln { "NSApplication not available" } - return false - } - - val setActivationPolicySelector = localObjc.sel_registerName("setActivationPolicy:") - if (setActivationPolicySelector == Pointer.NULL) { - debugln { "Unable to get setActivationPolicy: selector" } - return false - } - - // Restore normal policy - localObjc.objc_msgSend( - nsApp, - setActivationPolicySelector, - NSApplicationActivationPolicyRegular - ) - - debugln { "Application restored in the Dock" } - true - + MacNativeBridge.nativeShowInDock() == 0 } catch (e: Exception) { debugln { "Error while restoring in Dock: ${e.message}" } - e.printStackTrace() false } } @@ -152,36 +30,13 @@ class MacOSWindowManager { */ fun hideFromDock(): Boolean { if (!isMacOs) { - // No-op on non-macOS debugln { "hideFromDock(): non-macOS platform detected, no action performed" } return true } - val localObjc = objc ?: return false return try { - val nsApp = getNSApplication() - if (nsApp == null) { - debugln { "NSApplication not available" } - return false - } - - val setActivationPolicySelector = localObjc.sel_registerName("setActivationPolicy:") - if (setActivationPolicySelector == Pointer.NULL) { - debugln { "Unable to get setActivationPolicy: selector" } - return false - } - - localObjc.objc_msgSend( - nsApp, - setActivationPolicySelector, - NSApplicationActivationPolicyAccessory - ) - - debugln { "Application set as accessory" } - true - + MacNativeBridge.nativeHideFromDock() == 0 } catch (e: Exception) { debugln { "Error while setting as accessory: ${e.message}" } - e.printStackTrace() false } } @@ -190,8 +45,7 @@ class MacOSWindowManager { * Check if the application can be hidden from the Dock */ fun canHideFromDock(): Boolean { - if (!isMacOs) return false - return getNSApplication() != null + return isMacOs } /** @@ -201,100 +55,35 @@ class MacOSWindowManager { */ fun setMoveToActiveSpace(awtWindow: java.awt.Window): Boolean { if (!isMacOs) return false - val localObjc = objc ?: return false return try { - // Try direct approach via Native.getComponentID - val viewPtr = Native.getComponentID(awtWindow) + // Try direct approach via JAWT to get NSView pointer + val viewPtr = MacNativeBridge.nativeGetAWTViewPtr(awtWindow) debugln { "[MacOSWindowManager] setMoveToActiveSpace: viewPtr=$viewPtr" } if (viewPtr != 0L) { - val nsView = Pointer(viewPtr) - val windowSel = localObjc.sel_registerName("window") - val nsWindow = localObjc.objc_msgSend(nsView, windowSel) - if (nsWindow != Pointer.NULL) { - applySpaceBehavior(localObjc, nsWindow) - return true - } + val result = MacNativeBridge.nativeSetMoveToActiveSpaceForWindow(viewPtr) + if (result == 0) return true } - // Fallback: iterate NSApp windows and set on floating-level windows - // (tray popup uses alwaysOnTop=true which sets a floating window level) - debugln { "[MacOSWindowManager] Fallback: searching NSApp windows for floating window..." } - val nsApp = getNSApplication() ?: return false - - val windowsSel = localObjc.sel_registerName("windows") - val windowsArray = localObjc.objc_msgSend(nsApp, windowsSel) - val countSel = localObjc.sel_registerName("count") - val count = Pointer.nativeValue(localObjc.objc_msgSend(windowsArray, countSel)).toInt() - debugln { "[MacOSWindowManager] Found $count NSWindows" } - - val objectAtIndexSel = localObjc.sel_registerName("objectAtIndex:") - val levelSel = localObjc.sel_registerName("level") - - var applied = false - for (i in 0 until count) { - val nsWindow = localObjc.objc_msgSend(windowsArray, objectAtIndexSel, i.toLong()) - val level = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, levelSel)) - debugln { "[MacOSWindowManager] Window[$i]: level=$level" } - // Floating windows have level > 0 (NSFloatingWindowLevel = 3) - if (level > 0) { - applySpaceBehavior(localObjc, nsWindow) - applied = true - } - } - applied + // Fallback: set on all app windows via native helper + debugln { "[MacOSWindowManager] Fallback: setting moveToActiveSpace on all windows..." } + MacNativeBridge.nativeSetMoveToActiveSpace() + true } catch (e: Throwable) { debugln { "Failed to set moveToActiveSpace: ${e.message}" } false } } - private fun applySpaceBehavior(localObjc: ObjectiveC, nsWindow: Pointer) { - val getCollSel = localObjc.sel_registerName("collectionBehavior") - val current = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, getCollSel)) - // Ensure moveToActiveSpace is set (moves window to active Space when ordered front) - val desired = (current and NSWindowCollectionBehaviorCanJoinAllSpaces.inv()) or NSWindowCollectionBehaviorMoveToActiveSpace - if (current != desired) { - debugln { "[MacOSWindowManager] collectionBehavior before=$current, desired=$desired" } - val setCollSel = localObjc.sel_registerName("setCollectionBehavior:") - localObjc.objc_msgSend(nsWindow, setCollSel, desired) - val after = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, getCollSel)) - debugln { "[MacOSWindowManager] collectionBehavior after=$after" } - } - debugln { "Window configured with moveToActiveSpace" } - } - /** * Check if any floating-level NSWindow is on the active Space. - * Uses NSApp.windows iteration (same fallback as setMoveToActiveSpace). * Returns true if on active Space or if check fails (fail-open). */ fun isFloatingWindowOnActiveSpace(): Boolean { if (!isMacOs) return true - val localObjc = objc ?: return true return try { - val nsApp = getNSApplication() ?: return true - - val windowsSel = localObjc.sel_registerName("windows") - val windowsArray = localObjc.objc_msgSend(nsApp, windowsSel) - val countSel = localObjc.sel_registerName("count") - val count = Pointer.nativeValue(localObjc.objc_msgSend(windowsArray, countSel)).toInt() - - val objectAtIndexSel = localObjc.sel_registerName("objectAtIndex:") - val levelSel = localObjc.sel_registerName("level") - val isOnActiveSpaceSel = localObjc.sel_registerName("isOnActiveSpace") - - for (i in 0 until count) { - val nsWindow = localObjc.objc_msgSend(windowsArray, objectAtIndexSel, i.toLong()) - val level = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, levelSel)) - if (level > 0) { - val onActiveSpace = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, isOnActiveSpaceSel)) != 0L - debugln { "[MacOSWindowManager] Floating window level=$level, isOnActiveSpace=$onActiveSpace" } - return onActiveSpace - } - } - true // No floating window found, assume on active Space + MacNativeBridge.nativeIsFloatingWindowOnActiveSpace() != 0 } catch (e: Throwable) { - debugln { "Failed to check isOnActiveSpace: ${e.message}" } + debugln { "Failed to check isFloatingWindowOnActiveSpace: ${e.message}" } true } } @@ -306,19 +95,10 @@ class MacOSWindowManager { */ fun isOnActiveSpace(awtWindow: java.awt.Window): Boolean { if (!isMacOs) return true - val localObjc = objc ?: return true return try { - val viewPtr = Native.getComponentID(awtWindow) + val viewPtr = MacNativeBridge.nativeGetAWTViewPtr(awtWindow) if (viewPtr == 0L) return true - - val nsView = Pointer(viewPtr) - val windowSel = localObjc.sel_registerName("window") - val nsWindow = localObjc.objc_msgSend(nsView, windowSel) - if (nsWindow == Pointer.NULL) return true - - val isOnActiveSpaceSel = localObjc.sel_registerName("isOnActiveSpace") - val result = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, isOnActiveSpaceSel)) - result != 0L + MacNativeBridge.nativeIsOnActiveSpaceForView(viewPtr) != 0 } catch (e: Throwable) { debugln { "Failed to check isOnActiveSpace: ${e.message}" } true // fail-open: assume on active Space @@ -332,54 +112,11 @@ class MacOSWindowManager { */ fun bringFloatingWindowToFront(): Boolean { if (!isMacOs) return false - val localObjc = objc ?: return false return try { - val nsApp = getNSApplication() ?: return false - - // Activate the app so it can receive focus - val activateSel = localObjc.sel_registerName("activateIgnoringOtherApps:") - localObjc.objc_msgSend(nsApp, activateSel, 1L) - - val windowsSel = localObjc.sel_registerName("windows") - val windowsArray = localObjc.objc_msgSend(nsApp, windowsSel) - val countSel = localObjc.sel_registerName("count") - val count = Pointer.nativeValue(localObjc.objc_msgSend(windowsArray, countSel)).toInt() - - val objectAtIndexSel = localObjc.sel_registerName("objectAtIndex:") - val levelSel = localObjc.sel_registerName("level") - val makeKeyAndOrderFrontSel = localObjc.sel_registerName("makeKeyAndOrderFront:") - - for (i in 0 until count) { - val nsWindow = localObjc.objc_msgSend(windowsArray, objectAtIndexSel, i.toLong()) - val level = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, levelSel)) - if (level > 0) { - debugln { "[MacOSWindowManager] bringFloatingWindowToFront: level=$level" } - localObjc.objc_msgSend(nsWindow, makeKeyAndOrderFrontSel, Pointer.NULL) - return true - } - } - false + MacNativeBridge.nativeBringFloatingWindowToFront() == 0 } catch (e: Throwable) { debugln { "Failed to bringFloatingWindowToFront: ${e.message}" } false } } - - companion object { - // Constants for NSApplication activation policies - const val NSApplicationActivationPolicyRegular = 0L - const val NSApplicationActivationPolicyAccessory = 1L - const val NSApplicationActivationPolicyProhibited = 2L - - // Constants for window levels - const val NSNormalWindowLevel = 0L - const val NSFloatingWindowLevel = 3L - const val NSModalPanelWindowLevel = 8L - - // NSWindowCollectionBehavior - const val NSWindowCollectionBehaviorCanJoinAllSpaces = 1L // 1 << 0 - const val NSWindowCollectionBehaviorMoveToActiveSpace = 2L // 1 << 1 - } - } - diff --git a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOutsideClickWatcher.kt b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOutsideClickWatcher.kt index dbfacc8b..757c548e 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOutsideClickWatcher.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOutsideClickWatcher.kt @@ -1,10 +1,6 @@ package com.kdroid.composetray.lib.mac import com.kdroid.composetray.utils.isPointWithinMacStatusItem -import com.sun.jna.Library -import com.sun.jna.Native -import com.sun.jna.Pointer -import com.sun.jna.Structure import io.github.kdroidfilter.platformtools.OperatingSystem import io.github.kdroidfilter.platformtools.getOperatingSystem import java.awt.MouseInfo @@ -13,28 +9,6 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit -interface ApplicationServices : Library { - companion object { - val INSTANCE: ApplicationServices = - Native.load("ApplicationServices", ApplicationServices::class.java) - } - - fun CGEventCreate(source: Pointer?): Pointer // CGEventRef - fun CGEventGetLocation(event: Pointer): CGPoint.ByValue // <-- ByValue is crucial - fun CFRelease(ref: Pointer) - - fun CGEventSourceButtonState(stateID: Int, button: Int): Boolean -} - -// CGPoint as ByValue (two doubles) -open class CGPoint : Structure() { - @JvmField var x: Double = 0.0 - @JvmField var y: Double = 0.0 - override fun getFieldOrder() = listOf("x", "y") - class ByValue : CGPoint(), Structure.ByValue -} - - /** * MacOutsideClickWatcher: encapsulates macOS-specific logic to detect a left-click * outside the provided window and invoke a callback to hide it. It also ignores @@ -61,8 +35,7 @@ class MacOutsideClickWatcher( private fun pollOnce() { try { - val asvc = ApplicationServices.INSTANCE - val left = asvc.CGEventSourceButtonState(0, 0) // kCGEventSourceStateCombinedSessionState, BUTTON_LEFT + val left = MacNativeBridge.nativeGetMouseButtonState(0) != 0 if (left && left != prevLeft) { val win = windowSupplier.invoke() @@ -99,4 +72,4 @@ class MacOutsideClickWatcher( try { scheduler?.shutdownNow() } catch (_: Throwable) {} scheduler = null } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayLoader.kt b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayLoader.kt deleted file mode 100644 index 7c618e37..00000000 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayLoader.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.kdroid.composetray.lib.mac - -import com.kdroid.composetray.lib.mac.MacTrayManager.MacTrayLibrary - -/** - * Centralized, process-wide loader for the macOS native tray library. - * - * With JNA direct mapping, registration happens inside MacTrayLibrary's init block. - * We keep this object to preserve API shape if referenced elsewhere, but simply return - * the singleton object so callers can keep using MacTrayLoader.lib if needed. - */ -internal object MacTrayLoader { - val lib: MacTrayLibrary by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { - MacTrayLibrary - } -} \ No newline at end of file diff --git a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt index 62f9a377..ef2ef94c 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt @@ -1,12 +1,6 @@ -// Modified MacTrayManager.kt (add ThemeCallback and update MacTrayLibrary) package com.kdroid.composetray.lib.mac import androidx.compose.runtime.mutableStateOf -import com.sun.jna.Native -import com.sun.jna.Pointer -import com.sun.jna.Structure -import com.sun.jna.Callback -import com.sun.jna.ptr.IntByReference import kotlinx.coroutines.* import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -20,9 +14,9 @@ internal class MacTrayManager( private var tooltip: String = "", onLeftClick: (() -> Unit)? = null ) { - // Use JNA direct mapping via object MacTrayLibrary - private val trayLib = MacTrayLibrary - private var tray: MacTray? = null + private var trayHandle: Long = 0L + private var menuHandle: Long = 0L + private var menuItemCount: Int = 0 private val menuItems: MutableList = mutableListOf() private val running = AtomicBoolean(false) private val lock = ReentrantLock() @@ -33,9 +27,9 @@ internal class MacTrayManager( private var mainScope: CoroutineScope? = null private var ioScope: CoroutineScope? = null - // Maintain a reference to all callbacks to avoid GC - private val callbackReferences: MutableList = mutableListOf() - private val nativeMenuItemsReferences: MutableList = mutableListOf() + // Keep submenu handles for cleanup + private val submenuHandles: MutableList> = mutableListOf() + private val onLeftClickCallback = mutableStateOf(onLeftClick) // Top level MenuItem class @@ -70,7 +64,7 @@ internal class MacTrayManager( // Update the tray with new properties and menu items fun update(newIconPath: String, newTooltip: String, newOnLeftClick: (() -> Unit)?, newMenuItems: List? = null) { lock.withLock { - if (!running.get() || tray == null) return + if (!running.get() || trayHandle == 0L) return // Update properties val iconChanged = this.iconPath != newIconPath @@ -82,17 +76,14 @@ internal class MacTrayManager( this.tooltip = newTooltip this.onLeftClickCallback.value = newOnLeftClick - // Update the tray object with new values - tray?.let { - if (iconChanged) { - it.icon_filepath = newIconPath - } - if (tooltipChanged) { - it.tooltip = newTooltip - } - if (onLeftClickChanged) { - initializeOnLeftClickCallback() - } + if (iconChanged) { + MacNativeBridge.nativeSetTrayIcon(trayHandle, newIconPath) + } + if (tooltipChanged) { + MacNativeBridge.nativeSetTrayTooltip(trayHandle, newTooltip) + } + if (onLeftClickChanged) { + initializeOnLeftClickCallback() } // Update menu items if provided @@ -101,25 +92,38 @@ internal class MacTrayManager( menuItems.addAll(newMenuItems) recreateMenu() } else if (iconChanged || tooltipChanged || onLeftClickChanged) { - // If any property changed but menu items didn't, still update the tray - trayLib.tray_update(tray!!) + MacNativeBridge.nativeUpdateTray(trayHandle) } } } // Recreate the menu with updated state private fun recreateMenu() { - if (!running.get() || tray == null) return + if (!running.get() || trayHandle == 0L) return - // Clear old references - callbackReferences.clear() - nativeMenuItemsReferences.clear() + // Free old menu + freeCurrentMenu() // Recreate the menu initializeTrayMenu() // Update the tray - trayLib.tray_update(tray!!) + MacNativeBridge.nativeUpdateTray(trayHandle) + } + + private fun freeCurrentMenu() { + // Free submenus first (in reverse order to handle nested) + for ((handle, count) in submenuHandles.reversed()) { + MacNativeBridge.nativeFreeMenuItems(handle, count) + } + submenuHandles.clear() + + // Free top-level menu + if (menuHandle != 0L) { + MacNativeBridge.nativeFreeMenuItems(menuHandle, menuItemCount) + menuHandle = 0L + menuItemCount = 0 + } } // Start the tray @@ -138,16 +142,16 @@ internal class MacTrayManager( // Create and start the tray thread trayThread = Thread { try { - // Create tray structure - tray = MacTray().apply { - icon_filepath = iconPath - tooltip = this@MacTrayManager.tooltip + // Create tray structure via JNI + trayHandle = MacNativeBridge.nativeCreateTray(iconPath, tooltip) + if (trayHandle == 0L) { + throw IllegalStateException("Failed to allocate native tray struct") } initializeOnLeftClickCallback() initializeTrayMenu() - val initResult = trayLib.tray_init(tray!!) + val initResult = MacNativeBridge.nativeInitTray(trayHandle) if (initResult != 0) { throw IllegalStateException("Failed to initialize tray: $initResult") } @@ -157,7 +161,7 @@ internal class MacTrayManager( // Run the event loop while (running.get()) { - val result = trayLib.tray_loop(0) + val result = MacNativeBridge.nativeLoopTray(0) if (result != 0) { break } @@ -184,103 +188,89 @@ internal class MacTrayManager( } private fun initializeOnLeftClickCallback() { - val trayObj = tray ?: return - - if (onLeftClickCallback.value != null) { - trayObj.cb = object : TrayCallback { - override fun invoke(tray: Pointer?) { - mainScope?.launch { - ioScope?.launch { - onLeftClickCallback.value?.invoke() - } + if (trayHandle == 0L) return + + val onClick = onLeftClickCallback.value + if (onClick != null) { + MacNativeBridge.nativeSetTrayCallback(trayHandle, Runnable { + mainScope?.launch { + ioScope?.launch { + onClick() } } - } - callbackReferences.add(trayObj.cb!!) + }) + } else { + MacNativeBridge.nativeSetTrayCallback(trayHandle, null) } } private fun initializeTrayMenu() { - val trayObj = tray ?: return + if (trayHandle == 0L) return if (menuItems.isEmpty()) { - // When there are no menu items, clear the native menu pointer so macOS detaches the menu. - trayObj.menu = null + MacNativeBridge.nativeClearTrayMenu(trayHandle) return } - val menuItemPrototype = MacTrayMenuItem() - val nativeMenuItems = menuItemPrototype.toArray(menuItems.size + 1) as Array + val count = menuItems.size + val handle = MacNativeBridge.nativeCreateMenuItems(count) + menuHandle = handle + menuItemCount = count menuItems.forEachIndexed { index, item -> - val nativeItem = nativeMenuItems[index] - initializeNativeMenuItem(nativeItem, item) - nativeItem.write() - nativeMenuItemsReferences.add(nativeItem) + initializeNativeMenuItem(handle, index, item) } - // Last element to mark the end of the menu - nativeMenuItems[menuItems.size].text = null - nativeMenuItems[menuItems.size].write() - - trayObj.menu = nativeMenuItems[0].pointer + MacNativeBridge.nativeSetTrayMenu(trayHandle, handle) } - private fun initializeNativeMenuItem(nativeItem: MacTrayMenuItem, menuItem: MenuItem) { - nativeItem.text = menuItem.text - nativeItem.icon_filepath = menuItem.icon - nativeItem.disabled = if (menuItem.isEnabled) 0 else 1 - nativeItem.checked = if (menuItem.isChecked) 1 else 0 + private fun initializeNativeMenuItem(parentHandle: Long, index: Int, menuItem: MenuItem) { + MacNativeBridge.nativeSetMenuItem( + parentHandle, index, + menuItem.text, + menuItem.icon, + if (menuItem.isEnabled) 0 else 1, + if (menuItem.isChecked) 1 else 0 + ) menuItem.onClick?.let { onClick -> - val callback = object : MenuItemCallback { - override fun invoke(item: Pointer?) { - if (!running.get()) return - - mainScope?.launch { - ioScope?.launch { - onClick() - // For checkable items, the onClick handler in MacTrayMenuBuilderImpl - // will call updateMenuItemCheckedState which will recreate the menu - } + MacNativeBridge.nativeSetMenuItemCallback(parentHandle, index, Runnable { + if (!running.get()) return@Runnable + mainScope?.launch { + ioScope?.launch { + onClick() } } - } - nativeItem.cb = callback - callbackReferences.add(callback) + }) } if (menuItem.subMenuItems.isNotEmpty()) { - val subMenuPrototype = MacTrayMenuItem() - val subMenuItemsArray = subMenuPrototype.toArray(menuItem.subMenuItems.size + 1) as Array + val subCount = menuItem.subMenuItems.size + val subHandle = MacNativeBridge.nativeCreateMenuItems(subCount) + submenuHandles.add(subHandle to subCount) - menuItem.subMenuItems.forEachIndexed { index, subItem -> - initializeNativeMenuItem(subMenuItemsArray[index], subItem) - subMenuItemsArray[index].write() - nativeMenuItemsReferences.add(subMenuItemsArray[index]) + menuItem.subMenuItems.forEachIndexed { subIndex, subItem -> + initializeNativeMenuItem(subHandle, subIndex, subItem) } - subMenuItemsArray[menuItem.subMenuItems.size].text = null - subMenuItemsArray[menuItem.subMenuItems.size].write() - nativeItem.submenu = subMenuItemsArray[0].pointer + MacNativeBridge.nativeSetMenuItemSubmenu(parentHandle, index, subHandle) } } private fun cleanupTray() { lock.withLock { - tray?.let { + if (trayHandle != 0L) { try { - trayLib.tray_dispose(it) + // Free menu first + freeCurrentMenu() + // Dispose the tray (this frees the struct too) + MacNativeBridge.nativeDisposeTray(trayHandle) } catch (e: Exception) { e.printStackTrace() } + trayHandle = 0L } - - // Clear all references - callbackReferences.clear() - nativeMenuItemsReferences.clear() menuItems.clear() - tray = null } } @@ -313,80 +303,14 @@ internal class MacTrayManager( trayThread = null } - fun getNativeTrayStruct(): MacTray? { - return tray + fun getNativeTrayHandle(): Long { + return trayHandle } fun setAppearanceIcons(lightIconPath: String, darkIconPath: String) { lock.withLock { - val t = tray ?: return - trayLib.tray_set_icons_for_appearance(t, lightIconPath, darkIconPath) - } - } - - // Callback interfaces - interface TrayCallback : Callback { - fun invoke(tray: Pointer?) - } - - interface MenuItemCallback : Callback { - fun invoke(item: Pointer?) - } - - interface ThemeCallback : Callback { - fun invoke(isDark: Int) - } - - // JNA direct-mapped native library - object MacTrayLibrary { - init { - Native.register("MacTray") - } - - @JvmStatic external fun tray_init(tray: MacTray): Int - @JvmStatic external fun tray_loop(blocking: Int): Int - @JvmStatic external fun tray_update(tray: MacTray) - @JvmStatic external fun tray_dispose(tray: MacTray) - @JvmStatic external fun tray_exit() - @JvmStatic external fun tray_set_theme_callback(cb: ThemeCallback) - @JvmStatic external fun tray_is_menu_dark(): Int - - @JvmStatic external fun tray_get_status_item_position(x: IntByReference, y: IntByReference): Int - @JvmStatic external fun tray_get_status_item_position_for(tray: MacTray, x: IntByReference, y: IntByReference): Int - - @JvmStatic external fun tray_get_status_item_region(): String? - @JvmStatic external fun tray_get_status_item_region_for(tray: MacTray): String? - - @JvmStatic external fun tray_set_windows_move_to_active_space() - - @JvmStatic external fun tray_set_icons_for_appearance(tray: MacTray, light_icon: String, dark_icon: String) - } - - // Structure for a menu item - @Structure.FieldOrder("text", "icon_filepath", "disabled", "checked", "cb", "submenu") - class MacTrayMenuItem : Structure() { - @JvmField var text: String? = null - @JvmField var icon_filepath: String? = null - @JvmField var disabled: Int = 0 - @JvmField var checked: Int = 0 - @JvmField var cb: MenuItemCallback? = null - @JvmField var submenu: Pointer? = null - - override fun getFieldOrder(): List { - return listOf("text", "icon_filepath", "disabled", "checked", "cb", "submenu") - } - } - - // Structure for the tray - @Structure.FieldOrder("icon_filepath", "tooltip", "menu", "cb") - class MacTray : Structure() { - @JvmField var icon_filepath: String? = null - @JvmField var tooltip: String? = null - @JvmField var menu: Pointer? = null - @JvmField var cb: TrayCallback? = null - - override fun getFieldOrder(): List { - return listOf("icon_filepath", "tooltip", "menu", "cb") + if (trayHandle == 0L) return + MacNativeBridge.nativeSetIconsForAppearance(trayHandle, lightIconPath, darkIconPath) } } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt b/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt index 478de4c8..563fd468 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt @@ -26,7 +26,7 @@ import com.kdroid.composetray.lib.linux.LinuxOutsideClickWatcher import com.kdroid.composetray.utils.debugln import com.kdroid.composetray.lib.mac.MacOSWindowManager import com.kdroid.composetray.lib.mac.MacOutsideClickWatcher -import com.kdroid.composetray.lib.mac.MacTrayLoader +import com.kdroid.composetray.lib.mac.MacNativeBridge import com.kdroid.composetray.lib.windows.WindowsOutsideClickWatcher import com.kdroid.composetray.tray.impl.WindowsTrayInitializer import com.kdroid.composetray.menu.api.TrayMenuBuilder @@ -719,10 +719,10 @@ private fun ApplicationScope.TrayAppImplOriginal( // Move the popup to the current Space before bringing it to front (macOS) if (getOperatingSystem() == MACOS) { debugln { "[TrayApp] Setting up macOS Space behavior on window..." } - val nativeResult = runCatching { MacTrayLoader.lib.tray_set_windows_move_to_active_space() } - debugln { "[TrayApp] tray_set_windows_move_to_active_space: ${nativeResult.exceptionOrNull()?.message ?: "OK"}" } - val jnaResult = runCatching { MacOSWindowManager().setMoveToActiveSpace(window) } - debugln { "[TrayApp] setMoveToActiveSpace result=${jnaResult.getOrNull()}, error=${jnaResult.exceptionOrNull()?.message}" } + val nativeResult = runCatching { MacNativeBridge.nativeSetMoveToActiveSpace() } + debugln { "[TrayApp] nativeSetMoveToActiveSpace: ${nativeResult.exceptionOrNull()?.message ?: "OK"}" } + val moveResult = runCatching { MacOSWindowManager().setMoveToActiveSpace(window) } + debugln { "[TrayApp] setMoveToActiveSpace result=${moveResult.getOrNull()}, error=${moveResult.exceptionOrNull()?.message}" } } debugln { "[TrayApp] After invokeLater: window at x=${window.x}, y=${window.y}" } runCatching { diff --git a/src/commonMain/kotlin/com/kdroid/composetray/tray/impl/MacTrayInitializer.kt b/src/commonMain/kotlin/com/kdroid/composetray/tray/impl/MacTrayInitializer.kt index cc4a0cb5..5b0dd361 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/tray/impl/MacTrayInitializer.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/tray/impl/MacTrayInitializer.kt @@ -17,7 +17,7 @@ object MacTrayInitializer { internal fun getManager(id: String): MacTrayManager? = trayManagers[id] @Synchronized - internal fun getNativeTrayStruct(id: String): MacTrayManager.MacTray? = trayManagers[id]?.getNativeTrayStruct() + internal fun getNativeTrayHandle(id: String): Long = trayManagers[id]?.getNativeTrayHandle() ?: 0L @Synchronized fun initialize( @@ -110,4 +110,4 @@ object MacTrayInitializer { update(DEFAULT_ID, iconPath, tooltip, onLeftClick, menuContent) fun dispose() = dispose(DEFAULT_ID) -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/com/kdroid/composetray/utils/TrayPosition.kt b/src/commonMain/kotlin/com/kdroid/composetray/utils/TrayPosition.kt index 5e15ea54..343ca8cb 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/utils/TrayPosition.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/utils/TrayPosition.kt @@ -3,9 +3,8 @@ package com.kdroid.composetray.utils import com.kdroid.composetray.lib.windows.WindowsNativeTrayLibrary import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPosition -import com.kdroid.composetray.lib.mac.MacTrayLoader +import com.kdroid.composetray.lib.mac.MacNativeBridge import com.kdroid.composetray.tray.impl.MacTrayInitializer -import com.sun.jna.ptr.IntByReference import io.github.kdroidfilter.platformtools.LinuxDesktopEnvironment import io.github.kdroidfilter.platformtools.OperatingSystem import io.github.kdroidfilter.platformtools.detectLinuxDesktopEnvironment @@ -216,7 +215,7 @@ internal fun getWindowsTrayPosition(nativeResult: String?): TrayPosition = when fun getTrayPosition(): TrayPosition { return when (getOperatingSystem()) { OperatingSystem.WINDOWS -> getWindowsTrayPosition(WindowsNativeTrayLibrary.tray_get_notification_icons_region()) - OperatingSystem.MACOS -> getMacTrayPosition(MacTrayLoader.lib.tray_get_status_item_region()) + OperatingSystem.MACOS -> getMacTrayPosition(MacNativeBridge.nativeGetStatusItemRegion()) OperatingSystem.LINUX -> { TrayClickTracker.getLastClickPosition()?.position ?: loadTrayClickPosition()?.position @@ -341,18 +340,16 @@ fun getTrayWindowPositionForInstance( ) } OperatingSystem.MACOS -> { - val trayStruct = MacTrayInitializer.getNativeTrayStruct(instanceId) - if (trayStruct != null) { - val xRef = IntByReference() - val yRef = IntByReference() - val lib = MacTrayLoader.lib + val trayHandle = MacTrayInitializer.getNativeTrayHandle(instanceId) + if (trayHandle != 0L) { + val outXY = IntArray(2) val precise = try { - lib.tray_get_status_item_position_for(trayStruct, xRef, yRef) != 0 + MacNativeBridge.nativeGetStatusItemPositionFor(trayHandle, outXY) != 0 } catch (_: Throwable) { false } - val x = xRef.value - val y = yRef.value + val x = outXY[0] + val y = outXY[1] if (precise) { - val regionStr = runCatching { lib.tray_get_status_item_region_for(trayStruct) }.getOrNull() + val regionStr = runCatching { MacNativeBridge.nativeGetStatusItemRegionFor(trayHandle) }.getOrNull() val trayPos = if (regionStr != null) getMacTrayPosition(regionStr) else { val bounds = getScreenBoundsAt(x, y) @@ -445,11 +442,9 @@ internal fun getMacTrayPosition(nativeResult: String?): TrayPosition = when (nat } internal fun getStatusItemXYForMac(): Pair { - val xRef = IntByReference() - val yRef = IntByReference() - val lib = MacTrayLoader.lib - lib.tray_get_status_item_position(xRef, yRef) // if not precise, returns (0,0) - return xRef.value to yRef.value + val outXY = IntArray(2) + MacNativeBridge.nativeGetStatusItemPosition(outXY) // if not precise, returns (0,0) + return outXY[0] to outXY[1] } fun debugDeleteTrayPropertiesFiles() {