Ariver
2026-06-04 be83948fab7b643634ede2403abdca27e6b45f29
Fix quick search early mouse dismiss
7 files modified
134 ■■■■ changed files
Apptag/ApptagApp.swift 36 ●●●● patch | view | raw | blame | history
Apptag/ContentView.swift 50 ●●●●● patch | view | raw | blame | history
Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
Apptag/QuickSearch.swift 7 ●●●●● patch | view | raw | blame | history
CHANGELOG.md 7 ●●●●● patch | view | raw | blame | history
Scripts/window_logic_qa.sh 24 ●●●● patch | view | raw | blame | history
TODO.md 6 ●●●●● patch | view | raw | blame | history
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 {
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)
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>
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 {
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 结果行右侧标签显示:短标签完整显示,右侧标签列右对齐并减少不必要留白
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
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`。