From ba3471291a3e343f335261d83aa9ab23fe5f699f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 24 May 2026 18:52:37 +0800
Subject: [PATCH] Keep secondary windows in fullscreen Space

---
 Release/AppStore-7.6.0-20260524.1849/QA_RELEASE_EVIDENCE.md           |    8 ++-
 Apptag/ApptagApp.swift                                                |   39 ++++++++++++++++---
 Scripts/window_logic_qa.sh                                            |   48 ++++++++++++++++++++++++
 Release/AppStore-7.6.0-20260524.1849/README.md                        |    5 +-
 Release/AppStore-7.6.0-20260524.1849/APP_STORE_CONNECT_METADATA.md    |    0 
 Release/AppStore-7.6.0-20260524.1849/BUILD_SIGN_UPLOAD.md             |    2 
 Release/AppStore-7.6.0-20260524.1849/FIRST_TIME_MAC_APP_STORE_TODO.md |    0 
 7 files changed, 89 insertions(+), 13 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index ea0f0e3..214816c 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -67,6 +67,7 @@
     private var lastShowDockIcon: Bool?
     private var statusMenuScreenForNextOverlay: NSScreen?
     private var overlayGeneration = 0
+    private var overlayAvoidsSpaceSwitch = false
     private var suppressReopenUntil = Date.distantPast
 
     private struct OverlayPlacementContext {
@@ -269,9 +270,8 @@
         let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
         lastShowDockIcon = showDock
 
-        let shouldStayAccessoryForCurrentFullscreenSpace = avoidSpaceSwitch
-            && isOverlayVisible
-            && !isSettingsVisible
+        let shouldStayAccessoryForCurrentFullscreenSpace = isOverlayVisible
+            && (avoidSpaceSwitch || overlayAvoidsSpaceSwitch)
         let desiredPolicy: NSApplication.ActivationPolicy = shouldStayAccessoryForCurrentFullscreenSpace
             ? .accessory
             : (requiresForegroundOwnership
@@ -784,6 +784,7 @@
         let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen)
         let shouldAvoidSpaceSwitch = placement.map { hasFullscreenWindowOnScreen($0.screen) } ?? false
         if let overlayWindow, overlayWindow.isVisible {
+            overlayAvoidsSpaceSwitch = shouldAvoidSpaceSwitch
             moveOverlayToCurrentPlacement(preferredScreen: preferredScreen)
             overlayWindow.level = currentOverlayLevel
             refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch)
@@ -839,6 +840,7 @@
             existingWindow.orderOut(nil)
             overlayWindow = nil
         }
+        overlayAvoidsSpaceSwitch = shouldAvoidSpaceSwitch
 
         let window = makeOverlayWindow(
             on: placement.screen,
@@ -1098,6 +1100,7 @@
         overlayWindow?.orderOut(nil)
         removeOverlayKeyMonitor()
         removeQuickSearchExternalMouseMonitor()
+        overlayAvoidsSpaceSwitch = false
         refreshLauncherChromeState()
         NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
         if discardWindow {
@@ -1203,7 +1206,10 @@
 
     private func showQuickSearchFromGlobalHotkey() {
         if overlayWindow?.isVisible == true {
-            refreshLauncherChromeState(activate: true)
+            refreshLauncherChromeState(
+                activate: !overlayAvoidsSpaceSwitch,
+                avoidSpaceSwitch: overlayAvoidsSpaceSwitch
+            )
             requestQuickSearch(source: QuickSearchOpenSource.globalVisible)
             return
         }
@@ -1299,12 +1305,20 @@
 
         var behavior = window.collectionBehavior
         behavior.remove(.canJoinAllSpaces)
-        behavior.formUnion([.fullScreenAuxiliary, .moveToActiveSpace])
+        if overlayAvoidsSpaceSwitch {
+            behavior.remove(.moveToActiveSpace)
+            behavior.formUnion([.fullScreenAuxiliary, .stationary, .transient, .ignoresCycle])
+        } else {
+            behavior.formUnion([.fullScreenAuxiliary, .moveToActiveSpace])
+        }
         window.collectionBehavior = behavior
         window.makeKeyAndOrderFront(nil)
         window.orderFrontRegardless()
         settingsWindow = window
-        refreshLauncherChromeState(activate: true)
+        refreshLauncherChromeState(
+            activate: !overlayAvoidsSpaceSwitch,
+            avoidSpaceSwitch: overlayAvoidsSpaceSwitch
+        )
     }
 
     private func attachSettingsWindow(_ window: NSWindow, to overlayWindow: NSWindow) {
@@ -1454,6 +1468,13 @@
     }
 
     private func promoteOverlayToForegroundInput() {
+        if overlayAvoidsSpaceSwitch {
+            refreshLauncherChromeState(activate: false, avoidSpaceSwitch: true)
+            guard let overlayWindow else { return }
+            overlayWindow.makeKeyAndOrderFront(nil)
+            overlayWindow.orderFrontRegardless()
+            return
+        }
         beginLauncherForegroundOwnership()
         guard let overlayWindow else { return }
         overlayWindow.makeKeyAndOrderFront(nil)
@@ -1551,7 +1572,11 @@
 
     @objc private func openPreferences(_ sender: Any? = nil) {
         TagDatabase.flushPendingCategorySchemeBackupBatch()
-        beginLauncherForegroundOwnership()
+        if overlayAvoidsSpaceSwitch {
+            refreshLauncherChromeState(activate: false, avoidSpaceSwitch: true)
+        } else {
+            beginLauncherForegroundOwnership()
+        }
         // Don't hide overlay — keep it visible for real-time setting preview.
         if let overlayWindow, overlayWindow.isVisible {
             overlayWindow.makeKeyAndOrderFront(nil)
diff --git a/Release/AppStore-7.6.0-20260524.1652/APP_STORE_CONNECT_METADATA.md b/Release/AppStore-7.6.0-20260524.1849/APP_STORE_CONNECT_METADATA.md
similarity index 100%
rename from Release/AppStore-7.6.0-20260524.1652/APP_STORE_CONNECT_METADATA.md
rename to Release/AppStore-7.6.0-20260524.1849/APP_STORE_CONNECT_METADATA.md
diff --git a/Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md b/Release/AppStore-7.6.0-20260524.1849/BUILD_SIGN_UPLOAD.md
similarity index 98%
rename from Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md
rename to Release/AppStore-7.6.0-20260524.1849/BUILD_SIGN_UPLOAD.md
index e1e5d2c..d2514a5 100644
--- a/Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md
+++ b/Release/AppStore-7.6.0-20260524.1849/BUILD_SIGN_UPLOAD.md
@@ -50,7 +50,7 @@
 
 APP_STORE=1 \
 CODESIGN_IDENTITY="3rd Party Mac Developer Application: <TEAM NAME> (<TEAMID>)" \
-APP_BUILD=20260524.1652 \
+APP_BUILD=20260524.1849 \
 bash build.sh
 
 productbuild \
diff --git a/Release/AppStore-7.6.0-20260524.1652/FIRST_TIME_MAC_APP_STORE_TODO.md b/Release/AppStore-7.6.0-20260524.1849/FIRST_TIME_MAC_APP_STORE_TODO.md
similarity index 100%
rename from Release/AppStore-7.6.0-20260524.1652/FIRST_TIME_MAC_APP_STORE_TODO.md
rename to Release/AppStore-7.6.0-20260524.1849/FIRST_TIME_MAC_APP_STORE_TODO.md
diff --git a/Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md b/Release/AppStore-7.6.0-20260524.1849/QA_RELEASE_EVIDENCE.md
similarity index 88%
rename from Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md
rename to Release/AppStore-7.6.0-20260524.1849/QA_RELEASE_EVIDENCE.md
index 273d9c5..5f28d6e 100644
--- a/Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md
+++ b/Release/AppStore-7.6.0-20260524.1849/QA_RELEASE_EVIDENCE.md
@@ -3,7 +3,7 @@
 ## Local Build
 
 - Version: `7.6.0`
-- Build: `20260524.1652`
+- Build: `20260524.1849`
 - App path: `/Users/ar/Projects/Taglauncher/build/TagLauncher.app`
 - DMG path: `/Users/ar/Projects/Taglauncher/build/TagLauncher.dmg`
 - App executable size: `1.4M`
@@ -12,8 +12,8 @@
 ## Hashes
 
 ```text
-1bf3e53410e18778038c310e1ed083cd89c27b781cc87942dd3dd9fb57d73cb2  build/TagLauncher.dmg
-13e74a774662dfec88e69b5f51e5c51c5c46e3e5f8884cbf8cbab5cd44c4442d  build/TagLauncher.app/Contents/MacOS/TagLauncher
+dc5bc9cf90b9e04a9e4fda155b757e6e0af681ff1b79bf109552ae0c3ce2a193  build/TagLauncher.dmg
+a59f08f214bd4ac667197f6b2f2672aaf4a57c1fdd9872ea385099e84f18fa57  build/TagLauncher.app/Contents/MacOS/TagLauncher
 ```
 
 ## Commands Run
@@ -42,6 +42,8 @@
 - Fullscreen Space overlay appears above the current fullscreen app.
 - Fullscreen Space overlay is checked with `showDockIcon=true` and `showDockIcon=false`.
 - Fullscreen Space overlay bounds must match the fullscreen target display and remain stable across repeated samples.
+- Fullscreen Space Quick Search opens from app grid without switching away from the current fullscreen app.
+- Fullscreen Space Settings opens from app grid without switching away from the current fullscreen app.
 - Split View fullscreen geometry is checked for 50/50 and 33/67 split layouts, while ordinary side-by-side desktop windows are rejected.
 - App grid claims foreground, hides Dock, and keeps menu bar visible.
 - Settings floats above app grid.
diff --git a/Release/AppStore-7.6.0-20260524.1652/README.md b/Release/AppStore-7.6.0-20260524.1849/README.md
similarity index 93%
rename from Release/AppStore-7.6.0-20260524.1652/README.md
rename to Release/AppStore-7.6.0-20260524.1849/README.md
index 97281f7..b886612 100644
--- a/Release/AppStore-7.6.0-20260524.1652/README.md
+++ b/Release/AppStore-7.6.0-20260524.1849/README.md
@@ -8,9 +8,9 @@
 - Platform: macOS / Mac App Store
 - Bundle ID: `com.taglauncher.app`
 - Version: `7.6.0`
-- Local QA build: `20260524.1652`
+- Local QA build: `20260524.1849`
 - Source branch at preparation time: `main`
-- Release folder: `Release/AppStore-7.6.0-20260524.1652`
+- Release folder: `Release/AppStore-7.6.0-20260524.1849`
 
 ## What Changed For This Release
 
@@ -23,6 +23,7 @@
 - Added an About-page Help PDF button that opens the online PDF matching the current app language, plus a copy-link button.
 - Hardened fullscreen Space presentation so the app grid stays above the current fullscreen app instead of moving to another Space.
 - Added Split View fullscreen detection so two apps sharing one fullscreen Space also keep the app grid on the current Space.
+- Kept Quick Search and Settings inside the same fullscreen/Split View Space session instead of activating TagLauncher and sliding to another Space.
 - Expanded window QA to cover fullscreen Space behavior with Dock icon both enabled and disabled, including stability checks.
 - Strengthened the window logic QA script to cover fullscreen Space, duplicate Dock tiles, repeated self-launch, force quit layering, Settings/file panel layering, Quick Search, and two-display pointer-following behavior.
 
diff --git a/Scripts/window_logic_qa.sh b/Scripts/window_logic_qa.sh
index 4990885..48f7b31 100755
--- a/Scripts/window_logic_qa.sh
+++ b/Scripts/window_logic_qa.sh
@@ -93,6 +93,26 @@
   osascript -e 'tell application "System Events" to keystroke "w" using {command down}'
 }
 
+close_settings_window() {
+  osascript <<'OSA' >/dev/null 2>&1 || true
+tell application "System Events"
+  tell process "TagLauncher"
+    repeat 20 times
+      repeat with windowRef in windows
+        try
+          if (name of windowRef as text) is not "" then
+            click button 1 of windowRef
+            return
+          end if
+        end try
+      end repeat
+      delay 0.1
+    end repeat
+  end tell
+end tell
+OSA
+}
+
 dismiss_reopen_dialog() {
   osascript <<'OSA' >/dev/null 2>&1 || true
 tell application "System Events"
@@ -438,6 +458,20 @@
         fail("TagLauncher layer \(tag[0].layer) is not above fullscreen target layer \(target.layer)")
     }
     print("PASS fullscreen overlay above target: overlayLayer=\(tag[0].layer) targetLayer=\(target.layer)")
+
+case "fullscreen-settings":
+    guard tag.count == 2 else { fail("fullscreen settings expected 2 TagLauncher windows, got \(tag.count)") }
+    assertTagLayer(tag)
+    guard let target = windows.first(where: { $0.name == "TagLauncherFullscreenQATargetFullscreen" }) else {
+        fail("fullscreen target disappeared after opening settings; TagLauncher likely switched to another Space")
+    }
+    guard !tag[0].name.isEmpty, tag[1].name.isEmpty else {
+        fail("fullscreen settings order wrong: \(tag.map(\.name))")
+    }
+    guard tag.allSatisfy({ $0.layer > target.layer }) else {
+        fail("TagLauncher settings stack is not above fullscreen target")
+    }
+    print("PASS fullscreen settings above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(target.layer)")
 
 case "split-geometry":
     func isSingleFullscreenWindow(_ windowFrame: CGRect, on screenFrame: CGRect) -> Bool {
@@ -797,6 +831,20 @@
   send_main_hotkey
   wait_swift_assert fullscreen-overlay
   assert_fullscreen_overlay_stable
+  log "==> QA fullscreen Space: quick search from appgrid does not switch Space"
+  send_keycode 49
+  sleep 0.4
+  assert_fullscreen_overlay_stable
+  send_keycode 53
+  sleep 0.3
+  wait_swift_assert fullscreen-overlay
+  log "==> QA fullscreen Space: settings from appgrid does not switch Space"
+  send_cmd_comma
+  sleep 0.7
+  wait_swift_assert fullscreen-settings
+  close_settings_window
+  sleep 0.5
+  wait_swift_assert fullscreen-overlay
   send_keycode 53
   sleep 0.4
   wait_swift_assert no-overlay

--
Gitblit v1.9.3