From ffa76a42b2ffddb9271065af794281a170a4bd6f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 23 May 2026 23:42:00 +0800
Subject: [PATCH] Fix fullscreen Space overlay presentation

---
 Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md             |    2 
 Apptag/ApptagApp.swift                                                |   71 +++++++++++++++++++++--
 Scripts/window_logic_qa.sh                                            |   54 ++++++++++++++---
 Release/AppStore-7.6.0-20260523.2335/README.md                        |    6 +
 Release/AppStore-7.6.0-20260523.2335/APP_STORE_CONNECT_METADATA.md    |    0 
 Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md           |    8 +-
 Release/AppStore-7.6.0-20260523.2335/FIRST_TIME_MAC_APP_STORE_TODO.md |    0 
 7 files changed, 118 insertions(+), 23 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index e11a479..075c7fc 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -258,13 +258,18 @@
         }
     }
 
-    private func refreshLauncherChromeState(activate: Bool = false) {
+    private func refreshLauncherChromeState(activate: Bool = false, avoidSpaceSwitch: Bool = false) {
         let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
         lastShowDockIcon = showDock
 
-        let desiredPolicy: NSApplication.ActivationPolicy = requiresForegroundOwnership
+        let shouldStayAccessoryForCurrentFullscreenSpace = avoidSpaceSwitch
+            && isOverlayVisible
+            && !isSettingsVisible
+        let desiredPolicy: NSApplication.ActivationPolicy = shouldStayAccessoryForCurrentFullscreenSpace
+            ? .accessory
+            : (requiresForegroundOwnership
             ? .regular
-            : (showDock ? .regular : .accessory)
+            : (showDock ? .regular : .accessory))
         if NSApp.activationPolicy() != desiredPolicy {
             NSApp.setActivationPolicy(desiredPolicy)
         }
@@ -274,7 +279,7 @@
             NSApp.presentationOptions = desiredPresentation
         }
 
-        if activate && requiresForegroundOwnership {
+        if activate && requiresForegroundOwnership && !shouldStayAccessoryForCurrentFullscreenSpace {
             let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil)
             claimLauncherForeground(
                 keyWindow: keyWindow,
@@ -749,10 +754,12 @@
     }
 
     private func showOrFocusOverlay(preferredScreen: NSScreen? = nil) {
+        let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen)
+        let shouldAvoidSpaceSwitch = placement.map { hasFullscreenWindowOnScreen($0.screen) } ?? false
         if let overlayWindow, overlayWindow.isVisible {
             moveOverlayToCurrentPlacement(preferredScreen: preferredScreen)
             overlayWindow.level = currentOverlayLevel
-            refreshLauncherChromeState(activate: true)
+            refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch)
             overlayWindow.makeKeyAndOrderFront(nil)
             overlayWindow.orderFrontRegardless()
             if let settingsWindow, settingsWindow.isVisible {
@@ -778,7 +785,8 @@
     ) {
         guard let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen) else { return }
 
-        let shouldStageAsAccessory = shouldStageOverlayAsAccessory
+        let shouldAvoidSpaceSwitch = hasFullscreenWindowOnScreen(placement.screen)
+        let shouldStageAsAccessory = shouldStageOverlayAsAccessory || shouldAvoidSpaceSwitch
 
         if !stagedForAllSpaces && shouldStageAsAccessory {
             // Let the all-spaces panel attach to the pointer's display before the app reclaims focus.
@@ -831,7 +839,7 @@
         let placementFrame = placement.frame
         let finishForegroundClaim: () -> Void = { [weak self, weak window] in
             guard let self, let window, self.overlayWindow === window else { return }
-            self.refreshLauncherChromeState(activate: true)
+            self.refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch)
             if window.frame != placementFrame {
                 window.setFrame(placementFrame, display: true)
                 window.makeKeyAndOrderFront(nil)
@@ -848,6 +856,42 @@
         } else {
             finishForegroundClaim()
         }
+    }
+
+    private func hasFullscreenWindowOnScreen(_ screen: NSScreen) -> Bool {
+        let screenFrame = screen.frame
+        let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
+        return windows.contains { info in
+            guard let owner = info[kCGWindowOwnerName as String] as? String,
+                  owner != AppIdentity.displayName,
+                  owner != "Window Server",
+                  owner != "Dock",
+                  owner != "loginwindow",
+                  let layer = info[kCGWindowLayer as String] as? Int,
+                  layer == 0,
+                  let bounds = info[kCGWindowBounds as String] as? NSDictionary
+            else { return false }
+
+            let x = cgWindowDimension(bounds, "X")
+            let y = cgWindowDimension(bounds, "Y")
+            let width = cgWindowDimension(bounds, "Width")
+            let height = cgWindowDimension(bounds, "Height")
+            let windowFrame = NSRect(x: x, y: y, width: width, height: height)
+            let widthMatches = abs(windowFrame.width - screenFrame.width) <= 12
+            let heightMatches = windowFrame.height >= screenFrame.height * 0.88
+            let horizontallyAligned = abs(windowFrame.midX - screenFrame.midX) <= 12
+            return widthMatches && heightMatches && horizontallyAligned
+        }
+    }
+
+    private func cgWindowDimension(_ bounds: NSDictionary, _ key: String) -> CGFloat {
+        if let value = bounds[key] as? CGFloat {
+            return value
+        }
+        if let value = bounds[key] as? NSNumber {
+            return CGFloat(truncating: value)
+        }
+        return 0
     }
 
     private func installOverlayKeyMonitor() {
@@ -1119,6 +1163,11 @@
                 return
             }
 
+            if self.isAppOwnedDocumentWindow(keyWindow) {
+                self.prepareSettingsWindow(keyWindow)
+                return
+            }
+
             // Settings/Preferences window -> float it above overlay for real-time preview.
             if self.isSettingsWindowCandidate(keyWindow) {
                 self.prepareSettingsWindow(keyWindow)
@@ -1176,6 +1225,7 @@
     private func isSettingsWindowCandidate(_ window: NSWindow) -> Bool {
         if window == settingsWindow { return true }
         if window.identifier?.rawValue == "TagLauncherPreferencesWindow" { return true }
+        if isAppOwnedDocumentWindow(window) { return true }
         guard NSApp.windows.contains(window),
               window != overlayWindow,
               window.isVisible,
@@ -1184,6 +1234,13 @@
         return settingsWindowTitleCandidates().contains(normalizedWindowTitle(window.title))
     }
 
+    private func isAppOwnedDocumentWindow(_ window: NSWindow) -> Bool {
+        NSApp.windows.contains(window)
+            && window != overlayWindow
+            && window.isVisible
+            && !(window is NSPanel)
+    }
+
     private func settingsWindowTitleCandidates() -> Set<String> {
         let keys = [
             "menu.preferences",
diff --git a/Release/AppStore-7.6.0-20260523.1803/APP_STORE_CONNECT_METADATA.md b/Release/AppStore-7.6.0-20260523.2335/APP_STORE_CONNECT_METADATA.md
similarity index 100%
rename from Release/AppStore-7.6.0-20260523.1803/APP_STORE_CONNECT_METADATA.md
rename to Release/AppStore-7.6.0-20260523.2335/APP_STORE_CONNECT_METADATA.md
diff --git a/Release/AppStore-7.6.0-20260523.1803/BUILD_SIGN_UPLOAD.md b/Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md
similarity index 98%
rename from Release/AppStore-7.6.0-20260523.1803/BUILD_SIGN_UPLOAD.md
rename to Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md
index 5cdb9b3..7bc5a21 100644
--- a/Release/AppStore-7.6.0-20260523.1803/BUILD_SIGN_UPLOAD.md
+++ b/Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md
@@ -50,7 +50,7 @@
 
 APP_STORE=1 \
 CODESIGN_IDENTITY="3rd Party Mac Developer Application: <TEAM NAME> (<TEAMID>)" \
-APP_BUILD=20260523.1803 \
+APP_BUILD=20260523.2335 \
 bash build.sh
 
 productbuild \
diff --git a/Release/AppStore-7.6.0-20260523.1803/FIRST_TIME_MAC_APP_STORE_TODO.md b/Release/AppStore-7.6.0-20260523.2335/FIRST_TIME_MAC_APP_STORE_TODO.md
similarity index 100%
rename from Release/AppStore-7.6.0-20260523.1803/FIRST_TIME_MAC_APP_STORE_TODO.md
rename to Release/AppStore-7.6.0-20260523.2335/FIRST_TIME_MAC_APP_STORE_TODO.md
diff --git a/Release/AppStore-7.6.0-20260523.1803/QA_RELEASE_EVIDENCE.md b/Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md
similarity index 87%
rename from Release/AppStore-7.6.0-20260523.1803/QA_RELEASE_EVIDENCE.md
rename to Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md
index e5745c5..0aa6f5b 100644
--- a/Release/AppStore-7.6.0-20260523.1803/QA_RELEASE_EVIDENCE.md
+++ b/Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md
@@ -3,7 +3,7 @@
 ## Local Build
 
 - Version: `7.6.0`
-- Build: `20260523.1803`
+- Build: `20260523.2335`
 - 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
-c848f902a8219547b59767b172d5b26453748e7f34ee1ce7d10c421fc61005de  build/TagLauncher.dmg
-8a541e00734f0bd5f461b13881e194b29cb9c33b79fc0203cd59864680928d4e  build/TagLauncher.app/Contents/MacOS/TagLauncher
+370adb4ad671ec910cc0edf7e6f1ae3ebdd59cbd063f5d8439f2c7e8b05fd0e1  build/TagLauncher.dmg
+9f808b6fc81b587a1508aa31c261cdbe187a3b7727daf8bf163788ba49eb2333  build/TagLauncher.app/Contents/MacOS/TagLauncher
 ```
 
 ## Commands Run
@@ -40,6 +40,8 @@
 - Repeated self-launch keeps exactly one process.
 - Repeated self-launch keeps exactly one Dock tile.
 - 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.
 - App grid claims foreground, hides Dock, and keeps menu bar visible.
 - Settings floats above app grid.
 - File panel floats above Settings.
diff --git a/Release/AppStore-7.6.0-20260523.1803/README.md b/Release/AppStore-7.6.0-20260523.2335/README.md
similarity index 89%
rename from Release/AppStore-7.6.0-20260523.1803/README.md
rename to Release/AppStore-7.6.0-20260523.2335/README.md
index 52b8f4d..0d5ddbf 100644
--- a/Release/AppStore-7.6.0-20260523.1803/README.md
+++ b/Release/AppStore-7.6.0-20260523.2335/README.md
@@ -8,9 +8,9 @@
 - Platform: macOS / Mac App Store
 - Bundle ID: `com.taglauncher.app`
 - Version: `7.6.0`
-- Local QA build: `20260523.1803`
+- Local QA build: `20260523.2335`
 - Source branch at preparation time: `main`
-- Release folder: `Release/AppStore-7.6.0-20260523.1803`
+- Release folder: `Release/AppStore-7.6.0-20260523.2335`
 
 ## What Changed For This Release
 
@@ -21,6 +21,8 @@
 - Removed the invalid all-spaces + move-to-active-space window behavior combination that caused the 07:23 crash.
 - Removed the broken macOS system Help menu entry.
 - 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.
+- 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.
 
 ## Folder Contents
diff --git a/Scripts/window_logic_qa.sh b/Scripts/window_logic_qa.sh
index 04524c3..bb6877c 100755
--- a/Scripts/window_logic_qa.sh
+++ b/Scripts/window_logic_qa.sh
@@ -417,6 +417,17 @@
     guard let target = windows.first(where: { $0.name == "TagLauncherFullscreenQATargetFullscreen" }) else {
         fail("fullscreen target disappeared; TagLauncher likely switched to another Space")
     }
+    let overlayWidth = dimension(tag[0].bounds, "Width")
+    let overlayHeight = dimension(tag[0].bounds, "Height")
+    let overlayMidX = dimension(tag[0].bounds, "X") + overlayWidth / 2
+    let targetWidth = dimension(target.bounds, "Width")
+    let targetHeight = dimension(target.bounds, "Height")
+    let targetMidX = dimension(target.bounds, "X") + targetWidth / 2
+    guard abs(overlayWidth - targetWidth) <= 12,
+          overlayHeight >= targetHeight * 0.88,
+          abs(overlayMidX - targetMidX) <= 12 else {
+        fail("fullscreen overlay is not on the target fullscreen display: overlay=\(tag[0].bounds) target=\(target.bounds)")
+    }
     guard tag[0].layer > target.layer else {
         fail("TagLauncher layer \(tag[0].layer) is not above fullscreen target layer \(target.layer)")
     }
@@ -562,6 +573,18 @@
   return 1
 }
 
+assert_fullscreen_overlay_stable() {
+  local output=""
+  for _ in {1..20}; do
+    if ! output="$(swift "$assert_swift" fullscreen-overlay 2>&1)"; then
+      printf '%s\n' "$output" >&2
+      return 1
+    fi
+    sleep 0.1
+  done
+  printf '%s\n' "$output"
+}
+
 show_overlay() {
   send_main_hotkey
   if wait_swift_assert overlay >/dev/null 2>&1; then
@@ -650,16 +673,27 @@
 sleep 0.4
 wait_swift_assert no-overlay
 
-log "==> QA fullscreen Space: overlay stays above the current fullscreen app"
-start_fullscreen_qa_target
-send_main_hotkey
-wait_swift_assert fullscreen-overlay
-frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')"
-[[ "$frontmost" == "TagLauncher" ]] || { echo "FAIL: fullscreen overlay frontmost app is $frontmost, expected TagLauncher" >&2; exit 1; }
-send_keycode 53
-sleep 0.4
-wait_swift_assert no-overlay
-kill_fullscreen_qa_target
+run_fullscreen_space_case() {
+  local dock_value="$1"
+  log "==> QA fullscreen Space: overlay stays above the current fullscreen app (showDockIcon=$dock_value)"
+  defaults write "$DEFAULTS_DOMAIN" showDockIcon -bool "$dock_value"
+  prepare_isolated_app_instance
+  start_fullscreen_qa_target
+  send_main_hotkey
+  wait_swift_assert fullscreen-overlay
+  assert_fullscreen_overlay_stable
+  send_keycode 53
+  sleep 0.4
+  wait_swift_assert no-overlay
+  kill_fullscreen_qa_target
+}
+
+run_fullscreen_space_case true
+run_fullscreen_space_case false
+
+log "==> Restoring dock-visible QA app instance for remaining checks"
+defaults write "$DEFAULTS_DOMAIN" showDockIcon -bool true
+prepare_isolated_app_instance
 
 log "==> QA 1/7: overlay claims foreground, hides Dock, keeps menu bar visible"
 show_overlay

--
Gitblit v1.9.3