@@ -48,6 +48,7 @@ import org.jetbrains.compose.resources.painterResource
4848import java.awt.EventQueue.invokeLater
4949import java.awt.event.WindowEvent
5050import java.awt.event.WindowFocusListener
51+ import java.util.concurrent.atomic.AtomicLong
5152
5253// --------------------- Public API (defaults) ---------------------
5354
@@ -500,12 +501,12 @@ private fun ApplicationScope.TrayAppImplOriginal(
500501 var shouldShowWindow by remember { mutableStateOf(false ) }
501502
502503 var lastPrimaryActionAt by remember { mutableStateOf(0L ) }
503- val toggleDebounceMs = 280L
504+ val toggleDebounceMs = 150L
504505
505506 var lastShownAt by remember { mutableStateOf(0L ) }
506507 var lastHiddenAt by remember { mutableStateOf(0L ) }
507- val minVisibleDurationMs = 350L
508- val minHiddenDurationMs = 250L
508+ val minVisibleDurationMs = 200L
509+ val minHiddenDurationMs = 100L
509510
510511 var lastFocusLostAt by remember { mutableStateOf(0L ) }
511512 var autoHideEnabledAt by remember { mutableStateOf(0L ) }
@@ -522,53 +523,69 @@ private fun ApplicationScope.TrayAppImplOriginal(
522523 // Visibility controller for exit-finish observation; content will NOT be disposed.
523524 val visibleState = remember { MutableTransitionState (false ) }
524525
526+ // Thread-safe timestamps for cross-thread communication (IO thread reads, EDT writes)
527+ val lastFocusLostAtMs = remember { java.util.concurrent.atomic.AtomicLong (0L ) }
528+ val lastHiddenAtMs = remember { java.util.concurrent.atomic.AtomicLong (0L ) }
529+ val lastShownAtMs = remember { java.util.concurrent.atomic.AtomicLong (0L ) }
530+ val lastPrimaryActionAtMs = remember { java.util.concurrent.atomic.AtomicLong (0L ) }
531+
525532 val requestHideExplicit: () -> Unit = {
526533 val now = System .currentTimeMillis()
527- val sinceShow = now - lastShownAt
534+ val sinceShow = now - lastShownAtMs.get()
535+ debugln { " [TrayApp] requestHideExplicit called, sinceShow=${sinceShow} ms, thread=${Thread .currentThread().name} " }
528536 if (sinceShow >= minVisibleDurationMs) {
529537 trayAppState.hide()
530- lastHiddenAt = System .currentTimeMillis()
538+ lastHiddenAtMs.set(System .currentTimeMillis())
539+ lastHiddenAt = lastHiddenAtMs.get()
531540 } else {
532541 val wait = minVisibleDurationMs - sinceShow
533542 CoroutineScope (Dispatchers .IO ).launch {
534543 delay(wait)
535544 trayAppState.hide()
536- lastHiddenAt = System .currentTimeMillis()
545+ lastHiddenAtMs.set(System .currentTimeMillis())
546+ lastHiddenAt = lastHiddenAtMs.get()
537547 }
538548 }
539549 }
540550
541551 val internalPrimaryAction: () -> Unit = {
542552 val now = System .currentTimeMillis()
543- if (now - lastPrimaryActionAt >= toggleDebounceMs) {
553+ // Read directly from StateFlow for thread-safe cross-thread access
554+ val isVisibleNow = trayAppState.isVisible.value
555+ val timeSinceLastAction = now - lastPrimaryActionAtMs.get()
556+ debugln { " [TrayApp] primaryAction: isVisibleNow=$isVisibleNow , isVisible(compose)=$isVisible , timeSinceLastAction=${timeSinceLastAction} ms, thread=${Thread .currentThread().name} " }
557+ if (timeSinceLastAction >= toggleDebounceMs) {
558+ lastPrimaryActionAtMs.set(now)
544559 lastPrimaryActionAt = now
545- if (isVisible) {
546- // On macOS, check if window has focus before hiding
547- // If it doesn't have focus (e.g., on another Space), bring it to front instead
548- if (getOperatingSystem() == MACOS && windowRef != null ) {
549- val hasFocus = runCatching { windowRef!! .isFocused() }.getOrElse { false }
550- if (! hasFocus) {
551- // Window is not focused (likely on another Space), bring it to current Space
560+ if (isVisibleNow) {
561+ // On macOS, check if the window is on another Space
562+ if (getOperatingSystem() == MACOS ) {
563+ val onActiveSpace = runCatching { MacOSWindowManager ().isFloatingWindowOnActiveSpace() }.getOrElse { true }
564+ debugln { " [TrayApp] primaryAction: onActiveSpace=$onActiveSpace " }
565+ if (! onActiveSpace) {
566+ // Window is on another Space → move it to current Space via native API
567+ debugln { " [TrayApp] primaryAction -> MOVE TO CURRENT SPACE" }
552568 invokeLater {
553- runCatching { MacTrayLoader .lib.tray_set_windows_move_to_active_space() }
554- runCatching { MacOSWindowManager ().setMoveToActiveSpace(windowRef!! ) }
555- runCatching {
556- windowRef!! .toFront()
557- windowRef!! .requestFocus()
558- windowRef!! .requestFocusInWindow()
559- }
569+ runCatching { MacOSWindowManager ().bringFloatingWindowToFront() }
560570 }
561571 } else {
572+ debugln { " [TrayApp] primaryAction -> HIDE" }
562573 requestHideExplicit()
563574 }
564575 } else {
576+ debugln { " [TrayApp] primaryAction -> HIDE" }
565577 requestHideExplicit()
566578 }
567579 } else {
568- if (now - lastHiddenAt >= minHiddenDurationMs) {
569- if (getOperatingSystem() == WINDOWS && (now - lastFocusLostAt) < 300 ) {
570- // ignore immediate re-show after focus loss on Windows
580+ val hiddenAgo = now - lastHiddenAtMs.get()
581+ val focusLostAgo = now - lastFocusLostAtMs.get()
582+ debugln { " [TrayApp] primaryAction SHOW path: hiddenAgo=${hiddenAgo} ms, focusLostAgo=${focusLostAgo} ms" }
583+ if (hiddenAgo >= minHiddenDurationMs) {
584+ if ((getOperatingSystem() == WINDOWS || getOperatingSystem() == MACOS ) && focusLostAgo < 150 ) {
585+ // ignore immediate re-show after focus loss on Windows/macOS
586+ debugln { " [TrayApp] primaryAction -> SHOW BLOCKED (recent focus loss)" }
571587 } else {
588+ debugln { " [TrayApp] primaryAction -> SHOW" }
572589 // Pre-compute position at click time: the native status item
573590 // geometry is guaranteed to be available right now.
574591 runCatching {
@@ -580,8 +597,12 @@ private fun ApplicationScope.TrayAppImplOriginal(
580597 }
581598 trayAppState.show()
582599 }
600+ } else {
601+ debugln { " [TrayApp] primaryAction -> SHOW BLOCKED (too soon after hide: ${hiddenAgo} ms < ${minHiddenDurationMs} ms)" }
583602 }
584603 }
604+ } else {
605+ debugln { " [TrayApp] primaryAction -> DEBOUNCED (${timeSinceLastAction} ms < ${toggleDebounceMs} ms)" }
585606 }
586607 }
587608
@@ -629,22 +650,25 @@ private fun ApplicationScope.TrayAppImplOriginal(
629650 dialogState.position = position
630651
631652 // Wait for Compose to apply the position before showing the window
632- // This prevents the window from flashing at the wrong position
633- delay(150 ) // Give Compose time to recompose with new position
653+ delay(30 )
634654
635655 if (getOperatingSystem() == WINDOWS ) {
636656 autoHideEnabledAt = System .currentTimeMillis() + 1000
637657 }
638658 debugln { " [TrayApp] Now showing window" }
639659 shouldShowWindow = true
640- lastShownAt = System .currentTimeMillis()
660+ val showTime = System .currentTimeMillis()
661+ lastShownAt = showTime
662+ lastShownAtMs.set(showTime)
641663 }
642664 } else {
643665 // Wait for exit animation to finish, then actually hide the window
644666 if (shouldShowWindow) {
645667 snapshotFlow { visibleState.isIdle && ! visibleState.currentState }.first { it }
646668 shouldShowWindow = false
647- lastHiddenAt = System .currentTimeMillis()
669+ val hideTime = System .currentTimeMillis()
670+ lastHiddenAt = hideTime
671+ lastHiddenAtMs.set(hideTime)
648672 }
649673 }
650674 }
@@ -694,8 +718,11 @@ private fun ApplicationScope.TrayAppImplOriginal(
694718 invokeLater {
695719 // Move the popup to the current Space before bringing it to front (macOS)
696720 if (getOperatingSystem() == MACOS ) {
697- runCatching { MacTrayLoader .lib.tray_set_windows_move_to_active_space() }
698- runCatching { MacOSWindowManager ().setMoveToActiveSpace(window) }
721+ debugln { " [TrayApp] Setting up macOS Space behavior on window..." }
722+ val nativeResult = runCatching { MacTrayLoader .lib.tray_set_windows_move_to_active_space() }
723+ debugln { " [TrayApp] tray_set_windows_move_to_active_space: ${nativeResult.exceptionOrNull()?.message ? : " OK" } " }
724+ val jnaResult = runCatching { MacOSWindowManager ().setMoveToActiveSpace(window) }
725+ debugln { " [TrayApp] setMoveToActiveSpace result=${jnaResult.getOrNull()} , error=${jnaResult.exceptionOrNull()?.message} " }
699726 }
700727 debugln { " [TrayApp] After invokeLater: window at x=${window.x} , y=${window.y} " }
701728 runCatching {
@@ -708,8 +735,18 @@ private fun ApplicationScope.TrayAppImplOriginal(
708735 val focusListener = object : WindowFocusListener {
709736 override fun windowGainedFocus (e : WindowEvent ? ) = Unit
710737 override fun windowLostFocus (e : WindowEvent ? ) {
711- lastFocusLostAt = System .currentTimeMillis()
712- if (getOperatingSystem() == WINDOWS && lastFocusLostAt < autoHideEnabledAt) return
738+ val now = System .currentTimeMillis()
739+ lastFocusLostAtMs.set(now)
740+ lastFocusLostAt = now
741+ debugln { " [TrayApp] windowLostFocus at $now , dismissMode=$dismissMode , thread=${Thread .currentThread().name} " }
742+ if (getOperatingSystem() == WINDOWS && now < autoHideEnabledAt) return
743+ // On macOS, don't auto-hide if the window is not on the active Space.
744+ // A Space switch caused the focus loss — let the primary action handle it.
745+ if (getOperatingSystem() == MACOS ) {
746+ val onActiveSpace = runCatching { MacOSWindowManager ().isFloatingWindowOnActiveSpace() }.getOrElse { true }
747+ debugln { " [TrayApp] windowLostFocus: onActiveSpace=$onActiveSpace " }
748+ if (! onActiveSpace) return
749+ }
713750 if (dismissMode == TrayWindowDismissMode .AUTO ) requestHideExplicit()
714751 }
715752 }
0 commit comments