From 1797e722aa5453f646187ab61483f70f7e19fd1f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 29 May 2026 04:58:31 +0800
Subject: [PATCH] Extract quick search panel

---
 Apptag/ApptagApp.swift |   79 +++++++++++++++++++++++++++++++++------
 1 files changed, 67 insertions(+), 12 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index ea0f0e3..3a5f1d7 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -54,6 +54,7 @@
     private var statusItem: NSStatusItem?
     private var overlayWindow: NSWindow?
     private var overlayKeyMonitor: Any?
+    private var quickSearchLocalMouseMonitor: Any?
     private var quickSearchExternalMouseMonitor: Any?
     private var settingsWindow: NSWindow?    // Track Settings window to keep it above overlay
     private var mainHotkeyRef: EventHotKeyRef?
@@ -67,6 +68,7 @@
     private var lastShowDockIcon: Bool?
     private var statusMenuScreenForNextOverlay: NSScreen?
     private var overlayGeneration = 0
+    private var overlayAvoidsSpaceSwitch = false
     private var suppressReopenUntil = Date.distantPast
 
     private struct OverlayPlacementContext {
@@ -269,9 +271,8 @@
         let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
         lastShowDockIcon = showDock
 
-        let shouldStayAccessoryForCurrentFullscreenSpace = avoidSpaceSwitch
-            && isOverlayVisible
-            && !isSettingsVisible
+        let shouldStayAccessoryForCurrentFullscreenSpace = isOverlayVisible
+            && (avoidSpaceSwitch || overlayAvoidsSpaceSwitch)
         let desiredPolicy: NSApplication.ActivationPolicy = shouldStayAccessoryForCurrentFullscreenSpace
             ? .accessory
             : (requiresForegroundOwnership
@@ -784,6 +785,7 @@
         let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen)
         let shouldAvoidSpaceSwitch = placement.map { hasFullscreenWindowOnScreen($0.screen) } ?? false
         if let overlayWindow, overlayWindow.isVisible {
+            overlayAvoidsSpaceSwitch = shouldAvoidSpaceSwitch
             moveOverlayToCurrentPlacement(preferredScreen: preferredScreen)
             overlayWindow.level = currentOverlayLevel
             refreshLauncherChromeState(activate: !shouldAvoidSpaceSwitch, avoidSpaceSwitch: shouldAvoidSpaceSwitch)
@@ -839,6 +841,7 @@
             existingWindow.orderOut(nil)
             overlayWindow = nil
         }
+        overlayAvoidsSpaceSwitch = shouldAvoidSpaceSwitch
 
         let window = makeOverlayWindow(
             on: placement.screen,
@@ -947,8 +950,6 @@
         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
@@ -957,13 +958,12 @@
                 }
                 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 {
+                if touchesLeft && touchesRight && coversWidth && coversHeight {
                     return true
                 }
             }
@@ -1098,6 +1098,7 @@
         overlayWindow?.orderOut(nil)
         removeOverlayKeyMonitor()
         removeQuickSearchExternalMouseMonitor()
+        overlayAvoidsSpaceSwitch = false
         refreshLauncherChromeState()
         NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
         if discardWindow {
@@ -1203,7 +1204,10 @@
 
     private func showQuickSearchFromGlobalHotkey() {
         if overlayWindow?.isVisible == true {
-            refreshLauncherChromeState(activate: true)
+            refreshLauncherChromeState(
+                activate: !overlayAvoidsSpaceSwitch,
+                avoidSpaceSwitch: overlayAvoidsSpaceSwitch
+            )
             requestQuickSearch(source: QuickSearchOpenSource.globalVisible)
             return
         }
@@ -1276,6 +1280,7 @@
 
     /// Settings must always appear centered over the current overlay view and float above it.
     private func prepareSettingsWindow(_ window: NSWindow) {
+        dismissQuickSearchIfNeeded()
         let settingsSize = NSSize(width: 880, height: 460)
         window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
         window.minSize = settingsSize
@@ -1299,12 +1304,20 @@
 
         var behavior = window.collectionBehavior
         behavior.remove(.canJoinAllSpaces)
-        behavior.formUnion([.fullScreenAuxiliary, .moveToActiveSpace])
+        if overlayAvoidsSpaceSwitch {
+            behavior.remove(.moveToActiveSpace)
+            behavior.formUnion([.fullScreenAuxiliary, .stationary, .transient, .ignoresCycle])
+        } else {
+            behavior.formUnion([.fullScreenAuxiliary, .moveToActiveSpace])
+        }
         window.collectionBehavior = behavior
         window.makeKeyAndOrderFront(nil)
         window.orderFrontRegardless()
         settingsWindow = window
-        refreshLauncherChromeState(activate: true)
+        refreshLauncherChromeState(
+            activate: !overlayAvoidsSpaceSwitch,
+            avoidSpaceSwitch: overlayAvoidsSpaceSwitch
+        )
     }
 
     private func attachSettingsWindow(_ window: NSWindow, to overlayWindow: NSWindow) {
@@ -1418,7 +1431,10 @@
             let preferredScreen = self.overlayWindow?.screen
             self.detachSettingsWindow(closingWindow)
             self.settingsWindow = nil
-            self.refreshLauncherChromeState(activate: shouldRefocusOverlay)
+            self.refreshLauncherChromeState(
+                activate: shouldRefocusOverlay && !self.overlayAvoidsSpaceSwitch,
+                avoidSpaceSwitch: self.overlayAvoidsSpaceSwitch
+            )
             guard shouldRefocusOverlay else { return }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in
                 self?.showOrFocusOverlay(preferredScreen: preferredScreen)
@@ -1454,6 +1470,13 @@
     }
 
     private func promoteOverlayToForegroundInput() {
+        if overlayAvoidsSpaceSwitch {
+            refreshLauncherChromeState(activate: false, avoidSpaceSwitch: true)
+            guard let overlayWindow else { return }
+            overlayWindow.makeKeyAndOrderFront(nil)
+            overlayWindow.orderFrontRegardless()
+            return
+        }
         beginLauncherForegroundOwnership()
         guard let overlayWindow else { return }
         overlayWindow.makeKeyAndOrderFront(nil)
@@ -1497,6 +1520,7 @@
     }
 
     private func installQuickSearchExternalMouseMonitor() {
+        installQuickSearchLocalMouseMonitor()
         guard quickSearchExternalMouseMonitor == nil else { return }
         quickSearchExternalMouseMonitor = NSEvent.addGlobalMonitorForEvents(
             matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]
@@ -1508,7 +1532,25 @@
         }
     }
 
+    private func installQuickSearchLocalMouseMonitor() {
+        guard quickSearchLocalMouseMonitor == nil else { return }
+        quickSearchLocalMouseMonitor = NSEvent.addLocalMonitorForEvents(
+            matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]
+        ) { [weak self] event in
+            guard let self, self.isQuickSearchOpen else { return event }
+            if event.window === self.overlayWindow {
+                NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+                return nil
+            }
+            return event
+        }
+    }
+
     private func removeQuickSearchExternalMouseMonitor() {
+        if let quickSearchLocalMouseMonitor {
+            NSEvent.removeMonitor(quickSearchLocalMouseMonitor)
+            self.quickSearchLocalMouseMonitor = nil
+        }
         if let quickSearchExternalMouseMonitor {
             NSEvent.removeMonitor(quickSearchExternalMouseMonitor)
             self.quickSearchExternalMouseMonitor = nil
@@ -1550,8 +1592,13 @@
     }
 
     @objc private func openPreferences(_ sender: Any? = nil) {
+        dismissQuickSearchIfNeeded()
         TagDatabase.flushPendingCategorySchemeBackupBatch()
-        beginLauncherForegroundOwnership()
+        if overlayAvoidsSpaceSwitch {
+            refreshLauncherChromeState(activate: false, avoidSpaceSwitch: true)
+        } else {
+            beginLauncherForegroundOwnership()
+        }
         // Don't hide overlay — keep it visible for real-time setting preview.
         if let overlayWindow, overlayWindow.isVisible {
             overlayWindow.makeKeyAndOrderFront(nil)
@@ -1577,6 +1624,14 @@
         prepareSettingsWindow(window)
     }
 
+    private func dismissQuickSearchIfNeeded() {
+        guard isQuickSearchOpen else { return }
+        isQuickSearchOpen = false
+        removeQuickSearchExternalMouseMonitor()
+        updateOverlayLevelForTextInput()
+        NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+    }
+
     @objc private func switchLanguage(_ sender: NSMenuItem) {
         guard let code = sender.representedObject as? String else { return }
         L10n.switchTo(code)

--
Gitblit v1.9.3