From be83948fab7b643634ede2403abdca27e6b45f29 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 04 Jun 2026 01:09:14 +0800
Subject: [PATCH] Fix quick search early mouse dismiss

---
 Apptag/ApptagApp.swift     |   36 ++++++++++--
 TODO.md                    |    6 ++
 Scripts/window_logic_qa.sh |   24 +++++--
 Apptag/ContentView.swift   |   50 +++++++++++++++-
 Apptag/Info.plist          |    4 
 CHANGELOG.md               |    7 ++
 Apptag/QuickSearch.swift   |    7 ++
 7 files changed, 115 insertions(+), 19 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index c35007f..a70b9e2 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -430,7 +430,11 @@
         }
 
         if isQuickSearchOpen {
-            NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+            NotificationCenter.default.post(
+                name: .tagLauncherQuickSearchDismissRequested,
+                object: nil,
+                userInfo: ["source": QuickSearchDismissSource.programmatic]
+            )
         }
 
         if !requiresForegroundOwnership {
@@ -987,7 +991,11 @@
         isQuickSearchOpen = false
         removeQuickSearchExternalMouseMonitor()
         updateOverlayLevelForTextInput()
-        NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+        NotificationCenter.default.post(
+            name: .tagLauncherQuickSearchDismissRequested,
+            object: nil,
+            userInfo: ["source": QuickSearchDismissSource.keyboard]
+        )
         if shouldHideOverlayAfterDismiss {
             quickSearchShouldHideOverlayOnClose = false
             quickSearchOnlyOverlaySession = false
@@ -1526,7 +1534,11 @@
         ) { [weak self] _ in
             DispatchQueue.main.async {
                 guard self?.isQuickSearchOpen == true else { return }
-                NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+                NotificationCenter.default.post(
+                    name: .tagLauncherQuickSearchDismissRequested,
+                    object: nil,
+                    userInfo: ["source": QuickSearchDismissSource.mouse]
+                )
             }
         }
     }
@@ -1538,7 +1550,11 @@
         ) { [weak self] event in
             guard let self, self.isQuickSearchOpen else { return event }
             if event.window === self.overlayWindow {
-                NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+                NotificationCenter.default.post(
+                    name: .tagLauncherQuickSearchDismissRequested,
+                    object: nil,
+                    userInfo: ["source": QuickSearchDismissSource.mouse]
+                )
                 return nil
             }
             return event
@@ -1633,7 +1649,11 @@
         updateOverlayLevelForTextInput()
         quickSearchShouldHideOverlayOnClose = false
         quickSearchOnlyOverlaySession = false
-        NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+        NotificationCenter.default.post(
+            name: .tagLauncherQuickSearchDismissRequested,
+            object: nil,
+            userInfo: ["source": QuickSearchDismissSource.programmatic]
+        )
     }
 
     @objc private func switchLanguage(_ sender: NSMenuItem) {
@@ -1695,7 +1715,11 @@
         }
         if hit == self {
             if quickSearchSuppressesBackdropDismiss {
-                NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+                NotificationCenter.default.post(
+                    name: .tagLauncherQuickSearchDismissRequested,
+                    object: nil,
+                    userInfo: ["source": QuickSearchDismissSource.backdrop]
+                )
                 return
             }
             if suppressBackdropDismiss {
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 0fbfc7f..86e511f 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -389,6 +389,7 @@
     @State private var quickSearchSelectionScrollToken = 0
     @State private var quickSearchCloseHidesOverlay = false
     @State private var quickSearchOnlySession = false
+    @State private var quickSearchOpenedAt: Date? = nil
     @State private var quickSearchErrorMessage: String? = nil
     @State private var initialQuickSearchConsumed = false
 
@@ -542,6 +543,7 @@
                     quickSearchVisible = true
                     quickSearchFocusToken &+= 1
                 }
+                quickSearchOpenedAt = Date()
                 refreshQuickSearchResults()
                 NotificationCenter.default.post(
                     name: .tagLauncherQuickSearchVisibilityChanged,
@@ -568,8 +570,9 @@
             let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
             openQuickSearch(source: source)
         }
-        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { _ in
-            closeQuickSearch()
+        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { notification in
+            let source = notification.userInfo?["source"] as? String
+            closeQuickSearch(dismissalSource: source)
         }
         .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
             guard allApps.isEmpty, !refreshInProgress else { return }
@@ -663,7 +666,7 @@
                     .ignoresSafeArea()
                     .contentShape(Rectangle())
                     .onTapGesture {
-                        closeQuickSearch()
+                        closeQuickSearch(dismissalSource: QuickSearchDismissSource.backdrop)
                     }
                     .zIndex(899)
 
@@ -726,6 +729,7 @@
         quickSearchCloseHidesOverlay = quickSearchCloseHidesOverlay
             || source == QuickSearchOpenSource.globalHidden
         quickSearchVisible = true
+        quickSearchOpenedAt = Date()
         quickSearchQuery = ""
         quickSearchManualSelection = false
         quickSearchErrorMessage = nil
@@ -750,10 +754,26 @@
             && !quickSearchVisible
     }
 
-    private func closeQuickSearch(notify: Bool = true, hideOverlayIfNeeded: Bool = true) {
+    private func closeQuickSearch(
+        notify: Bool = true,
+        hideOverlayIfNeeded: Bool = true,
+        dismissalSource: String? = nil
+    ) {
+        Diagnostics.log("content.quickSearch.closeRequest", [
+            "source": dismissalSource,
+            "notify": notify,
+            "hideOverlayIfNeeded": hideOverlayIfNeeded,
+            "quickSearchVisible": quickSearchVisible,
+            "quickSearchCloseHidesOverlay": quickSearchCloseHidesOverlay,
+            "quickSearchOnlySession": quickSearchOnlySession
+        ])
+        if shouldIgnoreEarlyQuickSearchDismiss(from: dismissalSource) {
+            return
+        }
         let shouldHideOverlay = quickSearchVisible && quickSearchCloseHidesOverlay && hideOverlayIfNeeded
         guard quickSearchVisible else { return }
         quickSearchVisible = false
+        quickSearchOpenedAt = nil
         quickSearchQuery = ""
         quickSearchResults = []
         quickSearchSelectedID = nil
@@ -774,6 +794,23 @@
         } else {
             quickSearchCloseHidesOverlay = false
         }
+    }
+
+    private func shouldIgnoreEarlyQuickSearchDismiss(from source: String?) -> Bool {
+        guard quickSearchVisible,
+              let quickSearchOpenedAt,
+              source == QuickSearchDismissSource.mouse || source == QuickSearchDismissSource.backdrop
+        else { return false }
+
+        let age = Date().timeIntervalSince(quickSearchOpenedAt)
+        let gracePeriod: TimeInterval = quickSearchOnlySession ? 1.25 : 0.30
+        guard age < gracePeriod else { return false }
+        Diagnostics.log("content.quickSearch.ignoredEarlyDismiss", [
+            "source": source,
+            "age": String(format: "%.3f", age),
+            "gracePeriod": String(format: "%.2f", gracePeriod)
+        ])
+        return true
     }
 
     private func refreshQuickSearchResults() {
@@ -800,6 +837,11 @@
     }
 
     private func handleQuickSearchCommand(_ command: QuickSearchCommand) {
+        Diagnostics.log("content.quickSearch.command", [
+            "command": String(describing: command),
+            "quickSearchCloseHidesOverlay": quickSearchCloseHidesOverlay,
+            "quickSearchOnlySession": quickSearchOnlySession
+        ])
         switch command {
         case .moveUp:
             moveQuickSearchSelection(by: -1)
diff --git a/Apptag/Info.plist b/Apptag/Info.plist
index e276278..78ff12b 100644
--- a/Apptag/Info.plist
+++ b/Apptag/Info.plist
@@ -19,9 +19,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>7.8.11</string>
+	<string>7.8.12</string>
 	<key>CFBundleVersion</key>
-	<string>20260603.2149</string>
+	<string>20260604.0105</string>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.utilities</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/Apptag/QuickSearch.swift b/Apptag/QuickSearch.swift
index 4df8dc6..b933fd5 100644
--- a/Apptag/QuickSearch.swift
+++ b/Apptag/QuickSearch.swift
@@ -15,6 +15,13 @@
     static let globalVisible = "globalVisible"
 }
 
+enum QuickSearchDismissSource {
+    static let keyboard = "keyboard"
+    static let mouse = "mouse"
+    static let backdrop = "backdrop"
+    static let programmatic = "programmatic"
+}
+
 // MARK: - Hotkeys
 
 struct LauncherHotkey: Equatable {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b50892..7b4f277 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # TagLauncher Changelog
 
+## [7.8.12] — 2026-06-04
+
+- 修复 Quick Search-only 模式刚打开后可能被过期鼠标关闭事件立即关掉的问题;现在 `Fn + Space` 打开后候选面板会保持稳定,键盘取消和再次按 `Fn + Space` 仍可立即关闭
+- 窗口 QA 增加对合成 `Fn + Space` 投递的重试,并把 Quick Search 结果点击坐标修正到首条结果行中心,避免测试误点面板背景
+- 本次只收敛 Quick Search 打开/关闭时序和 QA 覆盖,不改搜索匹配、App Grid 布局、Settings、文件面板或窗口层级策略
+- 版本号更新为 `7.8.12`,Build 更新为 `20260604.0105`
+
 ## [7.8.11] — 2026-06-03
 
 - 优化 Quick Search 结果行右侧标签显示:短标签完整显示,右侧标签列右对齐并减少不必要留白
diff --git a/Scripts/window_logic_qa.sh b/Scripts/window_logic_qa.sh
index 5935181..9e0c433 100755
--- a/Scripts/window_logic_qa.sh
+++ b/Scripts/window_logic_qa.sh
@@ -801,7 +801,7 @@
     let x = dimension(bounds, "X")
     let y = dimension(bounds, "Y")
     let width = dimension(bounds, "Width")
-    print("\(Int(round(x + width * 0.35))) \(Int(round(y + 125)))")
+    print("\(Int(round(x + width * 0.35))) \(Int(round(y + 190)))")
 case "fullscreen-target-center":
     guard let target = raw.first(where: { ($0[kCGWindowName as String] as? String) == "TagLauncherFullscreenQATargetFullscreen" }),
           let bounds = target[kCGWindowBounds as String] as? NSDictionary else {
@@ -1015,6 +1015,20 @@
   return 1
 }
 
+open_quick_search_with_retry() {
+  local output=""
+  for _ in {1..3}; do
+    send_quick_search_hotkey
+    sleep 0.8
+    if output="$(wait_swift_assert quick-search 2>&1)"; then
+      printf '%s\n' "$output"
+      return 0
+    fi
+  done
+  printf '%s\n' "$output" >&2
+  return 1
+}
+
 assert_fullscreen_overlay_stable() {
   local output=""
   for _ in {1..20}; do
@@ -1202,9 +1216,7 @@
 prepare_isolated_app_instance
 assert_no_dock_tile
 log "==> QA hidden Dock: Fn+Space Quick Search hover does not resurrect App Grid"
-send_quick_search_hotkey
-sleep 0.8
-wait_swift_assert quick-search
+open_quick_search_with_retry
 hover_quick_search_results
 sleep 0.8
 wait_swift_assert quick-search
@@ -1219,9 +1231,7 @@
 prepare_isolated_app_instance
 assert_no_dock_tile
 log "==> QA hidden Dock: clicking Quick Search result closes without showing App Grid"
-send_quick_search_hotkey
-sleep 0.8
-wait_swift_assert quick-search
+open_quick_search_with_retry
 click_quick_search_result
 sleep 1.4
 wait_swift_assert no-overlay
diff --git a/TODO.md b/TODO.md
index 151204a..4e14362 100644
--- a/TODO.md
+++ b/TODO.md
@@ -23,6 +23,12 @@
 
 ## Done
 
+- [2026-06-04] 修复 Quick Search 刚打开即被过期 dismiss 事件关闭的 QA 失败。
+  - 提交: 本次提交
+  - 结果: quick-search-only 模式会忽略打开初期的过期鼠标/背板关闭事件;键盘 Esc 和再次按 `Fn + Space` 仍立即关闭;QA 脚本对合成 `Fn + Space` 投递增加重试,并把结果点击坐标修正到首条结果行中心。
+  - 范围: 不改搜索匹配、App Grid 布局、Settings、文件面板或窗口层级策略。
+  - 验证: targeted 两段式 `Fn + Space` 打开/关闭通过;targeted 点击 Quick Search 结果后窗口数为 0;`Scripts/window_logic_qa.sh` 全部通过。
+
 - [2026-05-29] 版本拆分与 7.8.1 默认图标大小调整。
   - 提交: 本次提交
   - 结果: `CHANGELOG.md` 已补充 `7.7.0`、`7.8.0`、`7.8.1` 三段版本记录;源码版本号更新为 `7.8.1`;新用户默认 `iconSize` 从 `80` 调整为 `64`。

--
Gitblit v1.9.3