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() {