Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,8 +23,6 @@ internal class MacTrayManager(
private val menuItems: MutableList<MenuItem> = 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
Expand Down Expand Up @@ -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()) {
Expand All @@ -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()
}
}
}
Expand Down Expand Up @@ -346,26 +326,14 @@ 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
mainScope?.cancel()
ioScope?.cancel()
mainScope = null
ioScope = null
trayThread = null
}

fun getNativeTrayHandle(): Long {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
125 changes: 0 additions & 125 deletions src/native/windows/jni_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading