diff --git a/build.gradle.kts b/build.gradle.kts index f9ba356..bd58ef8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -198,5 +198,9 @@ mavenPublishing { } publishToMavenCentral() - signAllPublications() + // Sign only when the CI-provided in-memory key is present, so + // `publishToMavenLocal` works locally without a signatory. + if (providers.gradleProperty("signingInMemoryKey").isPresent) { + signAllPublications() + } } diff --git a/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/mac/MacTrayManager.kt b/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/mac/MacTrayManager.kt index b7a3c9f..2f2e9e3 100644 --- a/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/mac/MacTrayManager.kt +++ b/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/mac/MacTrayManager.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -24,8 +23,6 @@ internal class MacTrayManager( private val menuItems: MutableList = mutableListOf() private val running = AtomicBoolean(false) private val lock = ReentrantLock() - private var trayThread: Thread? = null - private val initLatch = CountDownLatch(1) // Coroutine scopes for callback handling private var mainScope: CoroutineScope? = null @@ -146,7 +143,17 @@ internal class MacTrayManager( } } - // Start the tray + // Start the tray. + // + // Runs the whole native init synchronously on the calling thread — no + // dedicated thread, no latch. `tray_init` hops to the GCD main queue by + // itself when called off the macOS main thread, and runs inline when the + // caller *is* the main thread (the Nucleus Tao backend composes on it). + // The previous design (background thread + `initLatch.await()`) deadlocked + // there: the main thread parked on the latch can't drain the main queue, + // so the tray thread's `DispatchQueue.main.sync` inside `tray_init` never + // ran. No event-pump loop is needed either: status-item events are + // delivered by the application's own AppKit run loop. fun startTray() { lock.withLock { if (running.get()) { @@ -159,54 +166,27 @@ internal class MacTrayManager( mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - // Create and start the tray thread - trayThread = - Thread { - try { - // 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 = MacNativeBridge.nativeInitTray(trayHandle) - if (initResult != 0) { - throw IllegalStateException("Failed to initialize tray: $initResult") - } - - // Set menu-opened callback after init (TrayContext must exist) - initializeOnMenuOpenedCallback() + try { + // Create tray structure via JNI + trayHandle = MacNativeBridge.nativeCreateTray(iconPath, tooltip) + if (trayHandle == 0L) { + throw IllegalStateException("Failed to allocate native tray struct") + } - // Signal that initialization is complete - initLatch.countDown() + initializeOnLeftClickCallback() + initializeTrayMenu() - // Run the event loop - while (running.get()) { - val result = MacNativeBridge.nativeLoopTray(0) - if (result != 0) { - break - } - Thread.sleep(100) - } - } catch (e: Exception) { - e.printStackTrace() - } finally { - cleanupTray() - } - }.apply { - name = "MacTray-Thread" - isDaemon = true - start() + val initResult = MacNativeBridge.nativeInitTray(trayHandle) + if (initResult != 0) { + throw IllegalStateException("Failed to initialize tray: $initResult") } - // Wait for initialization to complete - try { - initLatch.await() - } catch (e: InterruptedException) { + // Set menu-opened callback after init (TrayContext must exist) + initializeOnMenuOpenedCallback() + } catch (e: Exception) { e.printStackTrace() + running.set(false) + cleanupTray() } } } @@ -346,18 +326,7 @@ internal class MacTrayManager( } running.set(false) - } - - // Wait for the tray thread to finish - trayThread?.let { thread -> - try { - thread.join(5000) // Wait up to 5 seconds - if (thread.isAlive) { - thread.interrupt() - } - } catch (e: InterruptedException) { - e.printStackTrace() - } + cleanupTray() } // Cancel coroutines @@ -365,7 +334,6 @@ internal class MacTrayManager( ioScope?.cancel() mainScope = null ioScope = null - trayThread = null } fun getNativeTrayHandle(): Long { diff --git a/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/windows/WindowsNativeBridge.kt b/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/windows/WindowsNativeBridge.kt index 1d38964..b35e9e2 100644 --- a/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/windows/WindowsNativeBridge.kt +++ b/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/windows/WindowsNativeBridge.kt @@ -92,14 +92,4 @@ internal object WindowsNativeBridge { @JvmStatic external fun nativeGetNotificationIconsPosition(outXY: IntArray): Int @JvmStatic external fun nativeGetNotificationIconsRegion(): String? - - // -- Mouse hook (for outside-click detection) -- - - @JvmStatic external fun nativeInstallMouseHook(callback: Runnable): Long - - @JvmStatic external fun nativeRunMouseHookLoop(hookId: Long) - - @JvmStatic external fun nativeStopMouseHook(hookId: Long) - - @JvmStatic external fun nativeGetLastMouseHookClick(outXY: IntArray) } diff --git a/src/native/windows/jni_bridge.c b/src/native/windows/jni_bridge.c index 35a6bf0..c141a6a 100644 --- a/src/native/windows/jni_bridge.c +++ b/src/native/windows/jni_bridge.c @@ -394,128 +394,3 @@ Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nati if (!result) return NULL; return (*env)->NewStringUTF(env, result); } - -/* ========================================================================== */ -/* Mouse hook (outside-click watcher, replaces JNA WH_MOUSE_LL) */ -/* ========================================================================== */ - -typedef struct MouseHookContext { - HHOOK hook; - DWORD threadId; - jobject callback; /* Runnable GlobalRef */ - volatile LONG lastClickX; - volatile LONG lastClickY; - volatile BOOL stopping; -} MouseHookContext; - -/* Single global hook context pointer, protected by the hook thread's own - * identity (each hook runs on its dedicated thread). */ -static MouseHookContext *g_mouseHookCtx = NULL; - -static LRESULT CALLBACK lowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) { - MouseHookContext *ctx = g_mouseHookCtx; - HHOOK hook = ctx ? ctx->hook : NULL; - - if (nCode >= 0 && ctx && !ctx->stopping) { - int msg = (int)wParam; - if (msg == WM_LBUTTONDOWN || msg == 0x00A1 /* WM_NCLBUTTONDOWN */) { - MSLLHOOKSTRUCT *p = (MSLLHOOKSTRUCT *)lParam; - InterlockedExchange(&ctx->lastClickX, p->pt.x); - InterlockedExchange(&ctx->lastClickY, p->pt.y); - if (ctx->callback) { - invokeRunnable(ctx->callback); - } - } - } - - return CallNextHookEx(hook, nCode, wParam, lParam); -} - -JNIEXPORT jlong JNICALL -Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nativeInstallMouseHook( - JNIEnv *env, jclass clazz, jobject callback) -{ - (void)clazz; - - MouseHookContext *ctx = (MouseHookContext *)calloc(1, sizeof(MouseHookContext)); - if (!ctx) return 0; - - ctx->threadId = GetCurrentThreadId(); - ctx->callback = (*env)->NewGlobalRef(env, callback); - ctx->stopping = FALSE; - ctx->lastClickX = 0; - ctx->lastClickY = 0; - - g_mouseHookCtx = ctx; - - HMODULE hMod = GetModuleHandleW(NULL); - ctx->hook = SetWindowsHookExW(WH_MOUSE_LL, lowLevelMouseProc, hMod, 0); - - if (!ctx->hook) { - g_mouseHookCtx = NULL; - (*env)->DeleteGlobalRef(env, ctx->callback); - free(ctx); - return 0; - } - - return (jlong)(uintptr_t)ctx; -} - -JNIEXPORT void JNICALL -Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nativeRunMouseHookLoop( - JNIEnv *env, jclass clazz, jlong hookId) -{ - (void)env; (void)clazz; - MouseHookContext *ctx = (MouseHookContext *)(uintptr_t)hookId; - if (!ctx) return; - - MSG msg; - while (!ctx->stopping) { - BOOL r = GetMessageW(&msg, NULL, 0, 0); - if (r == 0 || r == -1) break; /* WM_QUIT or error */ - } -} - -JNIEXPORT void JNICALL -Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nativeStopMouseHook( - JNIEnv *env, jclass clazz, jlong hookId) -{ - (void)clazz; - MouseHookContext *ctx = (MouseHookContext *)(uintptr_t)hookId; - if (!ctx) return; - - ctx->stopping = TRUE; - - if (ctx->hook) { - UnhookWindowsHookEx(ctx->hook); - ctx->hook = NULL; - } - - /* Post WM_QUIT to break GetMessage loop */ - if (ctx->threadId != 0) { - PostThreadMessageW(ctx->threadId, WM_QUIT, 0, 0); - } - - if (g_mouseHookCtx == ctx) g_mouseHookCtx = NULL; - - if (ctx->callback) { - (*env)->DeleteGlobalRef(env, ctx->callback); - ctx->callback = NULL; - } - - free(ctx); -} - -JNIEXPORT void JNICALL -Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nativeGetLastMouseHookClick( - JNIEnv *env, jclass clazz, jintArray outXY) -{ - (void)clazz; - MouseHookContext *ctx = g_mouseHookCtx; - jint buf[2] = { 0, 0 }; - if (ctx) { - buf[0] = (jint)InterlockedCompareExchange(&ctx->lastClickX, 0, 0); - buf[1] = (jint)InterlockedCompareExchange(&ctx->lastClickY, 0, 0); - } - (*env)->SetIntArrayRegion(env, outXY, 0, 2, buf); -}