From 3b4a0887d287c890092fc0845eeb57e9a5b03bcd Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 24 May 2026 16:56:30 +0800
Subject: [PATCH] Fix Split View fullscreen overlay detection
---
Apptag/ApptagApp.swift | 88 +++++++++++++++++++--
Scripts/window_logic_qa.sh | 96 ++++++++++++++++++++++++
Release/AppStore-7.6.0-20260524.1652/README.md | 5
Release/AppStore-7.6.0-20260524.1652/FIRST_TIME_MAC_APP_STORE_TODO.md | 0
Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md | 7 +
Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md | 2
Release/AppStore-7.6.0-20260524.1652/APP_STORE_CONNECT_METADATA.md | 0
7 files changed, 181 insertions(+), 17 deletions(-)
diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index a4b4f7a..ea0f0e3 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -74,6 +74,11 @@
let frame: NSRect
}
+ private struct ForeignWindowFrame {
+ let owner: String
+ let frame: NSRect
+ }
+
private var isOverlayVisible: Bool {
overlayWindow?.isVisible == true
}
@@ -882,8 +887,18 @@
private func hasFullscreenWindowOnScreen(_ screen: NSScreen) -> Bool {
let screenFrame = screen.frame
+ let windowFrames = foreignLayerZeroWindows(on: screenFrame)
+
+ if windowFrames.contains(where: { isSingleFullscreenWindow($0.frame, on: screenFrame) }) {
+ return true
+ }
+
+ return hasSplitViewFullscreenWindows(windowFrames, on: screenFrame)
+ }
+
+ private func foreignLayerZeroWindows(on screenFrame: NSRect) -> [ForeignWindowFrame] {
let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
- return windows.contains { info in
+ return windows.compactMap { info in
guard let owner = info[kCGWindowOwnerName as String] as? String,
owner != AppIdentity.displayName,
owner != "Window Server",
@@ -892,20 +907,71 @@
let layer = info[kCGWindowLayer as String] as? Int,
layer == 0,
let bounds = info[kCGWindowBounds as String] as? NSDictionary
- else { return false }
+ else { return nil }
- 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
+ let frame = NSRect(
+ x: cgWindowDimension(bounds, "X"),
+ y: cgWindowDimension(bounds, "Y"),
+ width: cgWindowDimension(bounds, "Width"),
+ height: cgWindowDimension(bounds, "Height")
+ )
+ guard frame.intersects(screenFrame) else { return nil }
+ return ForeignWindowFrame(owner: owner, frame: frame)
}
}
+ private func isSingleFullscreenWindow(_ windowFrame: NSRect, on screenFrame: NSRect) -> Bool {
+ let widthMatches = abs(windowFrame.width - screenFrame.width) <= 12
+ let heightMatches = windowFrame.height >= screenFrame.height * 0.88
+ let horizontallyAligned = abs(windowFrame.midX - screenFrame.midX) <= 12
+ let verticallyAligned = abs(windowFrame.maxY - screenFrame.maxY) <= 32
+ return widthMatches && heightMatches && horizontallyAligned && verticallyAligned
+ }
+
+ private func hasSplitViewFullscreenWindows(_ windows: [ForeignWindowFrame], on screenFrame: NSRect) -> Bool {
+ let clippedWindows = windows.map {
+ ForeignWindowFrame(owner: $0.owner, frame: $0.frame.intersection(screenFrame))
+ }
+ let tallWindows = clippedWindows
+ .filter { window in
+ let frame = window.frame
+ return frame.height >= screenFrame.height * 0.86
+ && frame.width >= screenFrame.width * 0.20
+ && frame.width <= screenFrame.width * 0.86
+ && abs(frame.maxY - screenFrame.maxY) <= 32
+ }
+ .sorted { $0.frame.minX < $1.frame.minX }
+
+ guard tallWindows.count >= 2 else { return false }
+
+ for startIndex in tallWindows.indices {
+ var union = tallWindows[startIndex].frame
+ var lastMaxX = union.maxX
+ var distinctOwners = Swift.Set<String>()
+ distinctOwners.insert(tallWindows[startIndex].owner)
+
+ for window in tallWindows.dropFirst(startIndex + 1) {
+ let gap = window.frame.minX - lastMaxX
+ if gap < -32 || gap > 48 {
+ break
+ }
+ union = union.union(window.frame)
+ lastMaxX = max(lastMaxX, window.frame.maxX)
+ distinctOwners.insert(window.owner)
+
+ let touchesLeft = abs(union.minX - screenFrame.minX) <= 32
+ let touchesRight = abs(union.maxX - screenFrame.maxX) <= 32
+ let coversWidth = union.width >= screenFrame.width * 0.92
+ let coversHeight = union.height >= screenFrame.height * 0.86
+ if distinctOwners.count >= 2 && touchesLeft && touchesRight && coversWidth && coversHeight {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
private func cgWindowDimension(_ bounds: NSDictionary, _ key: String) -> CGFloat {
if let value = bounds[key] as? CGFloat {
return value
diff --git a/Release/AppStore-7.6.0-20260523.2335/APP_STORE_CONNECT_METADATA.md b/Release/AppStore-7.6.0-20260524.1652/APP_STORE_CONNECT_METADATA.md
similarity index 100%
rename from Release/AppStore-7.6.0-20260523.2335/APP_STORE_CONNECT_METADATA.md
rename to Release/AppStore-7.6.0-20260524.1652/APP_STORE_CONNECT_METADATA.md
diff --git a/Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md b/Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md
similarity index 98%
rename from Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md
rename to Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md
index 7bc5a21..e1e5d2c 100644
--- a/Release/AppStore-7.6.0-20260523.2335/BUILD_SIGN_UPLOAD.md
+++ b/Release/AppStore-7.6.0-20260524.1652/BUILD_SIGN_UPLOAD.md
@@ -50,7 +50,7 @@
APP_STORE=1 \
CODESIGN_IDENTITY="3rd Party Mac Developer Application: <TEAM NAME> (<TEAMID>)" \
-APP_BUILD=20260523.2335 \
+APP_BUILD=20260524.1652 \
bash build.sh
productbuild \
diff --git a/Release/AppStore-7.6.0-20260523.2335/FIRST_TIME_MAC_APP_STORE_TODO.md b/Release/AppStore-7.6.0-20260524.1652/FIRST_TIME_MAC_APP_STORE_TODO.md
similarity index 100%
rename from Release/AppStore-7.6.0-20260523.2335/FIRST_TIME_MAC_APP_STORE_TODO.md
rename to Release/AppStore-7.6.0-20260524.1652/FIRST_TIME_MAC_APP_STORE_TODO.md
diff --git a/Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md b/Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md
similarity index 90%
rename from Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md
rename to Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md
index 0aa6f5b..273d9c5 100644
--- a/Release/AppStore-7.6.0-20260523.2335/QA_RELEASE_EVIDENCE.md
+++ b/Release/AppStore-7.6.0-20260524.1652/QA_RELEASE_EVIDENCE.md
@@ -3,7 +3,7 @@
## Local Build
- Version: `7.6.0`
-- Build: `20260523.2335`
+- Build: `20260524.1652`
- 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
-370adb4ad671ec910cc0edf7e6f1ae3ebdd59cbd063f5d8439f2c7e8b05fd0e1 build/TagLauncher.dmg
-9f808b6fc81b587a1508aa31c261cdbe187a3b7727daf8bf163788ba49eb2333 build/TagLauncher.app/Contents/MacOS/TagLauncher
+1bf3e53410e18778038c310e1ed083cd89c27b781cc87942dd3dd9fb57d73cb2 build/TagLauncher.dmg
+13e74a774662dfec88e69b5f51e5c51c5c46e3e5f8884cbf8cbab5cd44c4442d build/TagLauncher.app/Contents/MacOS/TagLauncher
```
## Commands Run
@@ -42,6 +42,7 @@
- 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.
+- 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.
- File panel floats above Settings.
diff --git a/Release/AppStore-7.6.0-20260523.2335/README.md b/Release/AppStore-7.6.0-20260524.1652/README.md
similarity index 93%
rename from Release/AppStore-7.6.0-20260523.2335/README.md
rename to Release/AppStore-7.6.0-20260524.1652/README.md
index 0d5ddbf..97281f7 100644
--- a/Release/AppStore-7.6.0-20260523.2335/README.md
+++ b/Release/AppStore-7.6.0-20260524.1652/README.md
@@ -8,9 +8,9 @@
- Platform: macOS / Mac App Store
- Bundle ID: `com.taglauncher.app`
- Version: `7.6.0`
-- Local QA build: `20260523.2335`
+- Local QA build: `20260524.1652`
- Source branch at preparation time: `main`
-- Release folder: `Release/AppStore-7.6.0-20260523.2335`
+- Release folder: `Release/AppStore-7.6.0-20260524.1652`
## What Changed For This Release
@@ -22,6 +22,7 @@
- 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.
+- Added Split View fullscreen detection so two apps sharing one fullscreen Space also keep the app grid on the current 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 e898312..4990885 100755
--- a/Scripts/window_logic_qa.sh
+++ b/Scripts/window_logic_qa.sh
@@ -66,6 +66,11 @@
fi
}
+reset_dock_for_qa() {
+ killall Dock >/dev/null 2>&1 || true
+ sleep 1.5
+}
+
send_keycode() {
local keycode="$1"
local modifiers="${2:-}"
@@ -434,6 +439,93 @@
}
print("PASS fullscreen overlay above target: overlayLayer=\(tag[0].layer) targetLayer=\(target.layer)")
+case "split-geometry":
+ func isSingleFullscreenWindow(_ windowFrame: CGRect, on screenFrame: CGRect) -> Bool {
+ let widthMatches = abs(windowFrame.width - screenFrame.width) <= 12
+ let heightMatches = windowFrame.height >= screenFrame.height * 0.88
+ let horizontallyAligned = abs(windowFrame.midX - screenFrame.midX) <= 12
+ let verticallyAligned = abs(windowFrame.maxY - screenFrame.maxY) <= 32
+ return widthMatches && heightMatches && horizontallyAligned && verticallyAligned
+ }
+
+ func hasSplitViewFullscreenWindows(_ windows: [CGRect], on screenFrame: CGRect) -> Bool {
+ let clippedWindows = windows.map { $0.intersection(screenFrame) }
+ let tallWindows = clippedWindows
+ .filter { frame in
+ frame.height >= screenFrame.height * 0.86
+ && frame.width >= screenFrame.width * 0.20
+ && frame.width <= screenFrame.width * 0.86
+ && abs(frame.maxY - screenFrame.maxY) <= 32
+ }
+ .sorted { $0.minX < $1.minX }
+
+ guard tallWindows.count >= 2 else { return false }
+
+ for startIndex in tallWindows.indices {
+ var union = tallWindows[startIndex]
+ var lastMaxX = union.maxX
+
+ for window in tallWindows.dropFirst(startIndex + 1) {
+ let gap = window.minX - lastMaxX
+ if gap < -32 || gap > 48 {
+ break
+ }
+ union = union.union(window)
+ lastMaxX = max(lastMaxX, window.maxX)
+
+ let touchesLeft = abs(union.minX - screenFrame.minX) <= 32
+ let touchesRight = abs(union.maxX - screenFrame.maxX) <= 32
+ let coversWidth = union.width >= screenFrame.width * 0.92
+ let coversHeight = union.height >= screenFrame.height * 0.86
+ if touchesLeft && touchesRight && coversWidth && coversHeight {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
+ let screen = CGRect(x: 0, y: 0, width: 1710, height: 1112)
+ let single = CGRect(x: 0, y: 39, width: 1710, height: 1073)
+ let splitHalf = [
+ CGRect(x: 0, y: 39, width: 853, height: 1073),
+ CGRect(x: 857, y: 39, width: 853, height: 1073)
+ ]
+ let splitThird = [
+ CGRect(x: 0, y: 39, width: 568, height: 1073),
+ CGRect(x: 572, y: 39, width: 1138, height: 1073)
+ ]
+ let desktopTiledWithLargeGap = [
+ CGRect(x: 0, y: 90, width: 700, height: 900),
+ CGRect(x: 900, y: 90, width: 700, height: 900)
+ ]
+ let desktopSideBySideBelowDock = [
+ CGRect(x: 0, y: 39, width: 856, height: 983),
+ CGRect(x: 854, y: 39, width: 856, height: 983)
+ ]
+ let halfOnly = [CGRect(x: 0, y: 39, width: 853, height: 1073)]
+
+ guard isSingleFullscreenWindow(single, on: screen) else {
+ fail("split geometry expected single fullscreen window to match")
+ }
+ guard hasSplitViewFullscreenWindows(splitHalf, on: screen) else {
+ fail("split geometry expected 50/50 Split View to match")
+ }
+ guard hasSplitViewFullscreenWindows(splitThird, on: screen) else {
+ fail("split geometry expected 33/67 Split View to match")
+ }
+ guard !hasSplitViewFullscreenWindows(desktopTiledWithLargeGap, on: screen) else {
+ fail("split geometry should not match ordinary tiled desktop windows")
+ }
+ guard !hasSplitViewFullscreenWindows(desktopSideBySideBelowDock, on: screen) else {
+ fail("split geometry should not match side-by-side desktop windows below the Dock")
+ }
+ guard !hasSplitViewFullscreenWindows(halfOnly, on: screen) else {
+ fail("split geometry should not match a single half-width window")
+ }
+ print("PASS split fullscreen geometry detection")
+
case "screen-count":
print("INFO screens=\(NSScreen.screens.count) frames=\(NSScreen.screens.map { NSStringFromRect($0.frame) })")
@@ -665,6 +757,7 @@
log "==> Preparing QA defaults"
defaults write "$DEFAULTS_DOMAIN" showDockIcon -bool true
+reset_dock_for_qa
log "==> Starting clean app instance"
prepare_isolated_app_instance
@@ -692,6 +785,9 @@
sleep 0.4
wait_swift_assert no-overlay
+log "==> QA split-view fullscreen geometry detection"
+swift_assert split-geometry
+
run_fullscreen_space_case() {
local dock_value="$1"
log "==> QA fullscreen Space: overlay stays above the current fullscreen app (showDockIcon=$dock_value)"
--
Gitblit v1.9.3