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