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