From eb33e26d8dbe91688826417871cee100014e1a11 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 3 Feb 2026 11:23:17 +0200 Subject: [PATCH 1/2] Fix TrayApp opening on wrong Virtual Desktop / Space (macOS) Add setMoveToActiveSpace() to MacOSWindowManager that sets NSWindowCollectionBehaviorMoveToActiveSpace on the tray popup's NSWindow via JNA + Objective-C runtime. This tells macOS to move the window to the current Space when ordered front, instead of switching back to the Space where it was originally created. Called in TrayApp's DisposableEffect right before toFront(). Fixes #348 --- .../composetray/lib/mac/MacOsWindowManager.kt | 58 +++++++++++++++---- .../kdroid/composetray/tray/api/TrayApp.kt | 3 + 2 files changed, 49 insertions(+), 12 deletions(-) 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 bd79d053..95cf5222 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOsWindowManager.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacOsWindowManager.kt @@ -29,18 +29,6 @@ interface Foundation : Library { class MacOSWindowManager { - 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 - } - // Detect platform once private val isMacOs: Boolean = getOperatingSystem() == OperatingSystem.MACOS @@ -206,5 +194,51 @@ class MacOSWindowManager { return getNSApplication() != null } + /** + * Configure an AWT window so that macOS moves it to the active Space + * when it is ordered front, instead of switching back to the Space + * where the window was originally created. + */ + fun setMoveToActiveSpace(awtWindow: java.awt.Window): Boolean { + if (!isMacOs) return false + val localObjc = objc ?: return false + return try { + val viewPtr = Native.getComponentID(awtWindow) + if (viewPtr == 0L) return false + + val nsView = Pointer(viewPtr) + val windowSel = localObjc.sel_registerName("window") + val nsWindow = localObjc.objc_msgSend(nsView, windowSel) + if (nsWindow == Pointer.NULL) return false + + // Read current collectionBehavior and add moveToActiveSpace (1 << 1) + val getCollSel = localObjc.sel_registerName("collectionBehavior") + val current = Pointer.nativeValue(localObjc.objc_msgSend(nsWindow, getCollSel)) + val setCollSel = localObjc.sel_registerName("setCollectionBehavior:") + localObjc.objc_msgSend(nsWindow, setCollSel, current or NSWindowCollectionBehaviorMoveToActiveSpace) + + debugln { "Window configured to move to active Space" } + true + } catch (e: Throwable) { + debugln { "Failed to set moveToActiveSpace: ${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 NSWindowCollectionBehaviorMoveToActiveSpace = 2L // 1 << 1 + } + } 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 fa3293ff..2989f3ac 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt @@ -621,6 +621,9 @@ private fun ApplicationScope.TrayAppImplOriginal( invokeLater { runCatching { + if (getOperatingSystem() == MACOS) { + MacOSWindowManager().setMoveToActiveSpace(window) + } window.toFront() window.requestFocus() window.requestFocusInWindow() From e94a5d6d45b7b8610c617abefa0d2d66a2ee5f37 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 3 Feb 2026 14:47:26 +0200 Subject: [PATCH 2/2] Add native tray_set_windows_move_to_active_space() for reliable Spaces support The JNA-based setMoveToActiveSpace() via Native.getComponentID() was not reliably reaching the NSWindow. Add a native Swift function that directly iterates NSApp.windows and inserts .moveToActiveSpace into each window's collectionBehavior. Called from TrayApp before toFront(). Also separate runCatching blocks so a failure in Spaces configuration does not prevent toFront()/requestFocus() from running. --- maclib/tray.swift | 12 ++++++++++++ .../com/kdroid/composetray/lib/mac/MacTrayManager.kt | 2 ++ .../com/kdroid/composetray/tray/api/TrayApp.kt | 9 ++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/maclib/tray.swift b/maclib/tray.swift index 7666c17f..fc28f139 100644 --- a/maclib/tray.swift +++ b/maclib/tray.swift @@ -412,4 +412,16 @@ public func tray_get_status_item_region_for( let midX = screen.frame.midX let region = rect.minX < midX ? "top-left" : "top-right" return strdup(region) +} + +// MARK: - Spaces / Virtual Desktop support + +/// Sets NSWindowCollectionBehavior.moveToActiveSpace on all app windows +/// so that showing a window moves it to the current Space instead of +/// switching back to the Space where it was originally created. +@_cdecl("tray_set_windows_move_to_active_space") +public func tray_set_windows_move_to_active_space() { + for window in NSApp.windows { + window.collectionBehavior.insert(.moveToActiveSpace) + } } \ 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 96f05422..f35f8c98 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/lib/mac/MacTrayManager.kt @@ -349,6 +349,8 @@ internal class MacTrayManager( @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() } // Structure for a menu item 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 2989f3ac..a9f9bf04 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.window.* import com.kdroid.composetray.lib.linux.LinuxOutsideClickWatcher 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.windows.WindowsOutsideClickWatcher import com.kdroid.composetray.menu.api.TrayMenuBuilder import com.kdroid.composetray.utils.* @@ -620,10 +621,12 @@ private fun ApplicationScope.TrayAppImplOriginal( runCatching { WindowVisibilityMonitor.recompute() } invokeLater { + // Move the popup to the current Space before bringing it to front (macOS) + if (getOperatingSystem() == MACOS) { + runCatching { MacTrayLoader.lib.tray_set_windows_move_to_active_space() } + runCatching { MacOSWindowManager().setMoveToActiveSpace(window) } + } runCatching { - if (getOperatingSystem() == MACOS) { - MacOSWindowManager().setMoveToActiveSpace(window) - } window.toFront() window.requestFocus() window.requestFocusInWindow()