From 01bab5422712299b49f656e6b4c42a3bf314f519 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 11:02:42 +0800
Subject: [PATCH] Release 8.3.5 free tag order persistence

---
 src/Apptag/QuickSearch.swift |  227 +++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 175 insertions(+), 52 deletions(-)

diff --git a/src/Apptag/QuickSearch.swift b/src/Apptag/QuickSearch.swift
index b933fd5..f4365c4 100644
--- a/src/Apptag/QuickSearch.swift
+++ b/src/Apptag/QuickSearch.swift
@@ -6,7 +6,9 @@
     static let tagLauncherQuickSearchRequested = Notification.Name("TagLauncherQuickSearchRequested")
     static let tagLauncherQuickSearchDismissRequested = Notification.Name("TagLauncherQuickSearchDismissRequested")
     static let tagLauncherQuickSearchVisibilityChanged = Notification.Name("TagLauncherQuickSearchVisibilityChanged")
+    static let tagLauncherQuickSearchCompositionChanged = Notification.Name("TagLauncherQuickSearchCompositionChanged")
     static let tagLauncherHotkeyRegistrationChanged = Notification.Name("TagLauncherHotkeyRegistrationChanged")
+    static let tagLauncherProEntitlementChanged = Notification.Name("TagLauncherProEntitlementChanged")
 }
 
 enum QuickSearchOpenSource {
@@ -80,6 +82,10 @@
             }
             return "Key \(keyCode)"
         }
+    }
+
+    static var printableKeyCodes: Set<Int> {
+        Set(keyCodeToPrintableScalar.keys)
     }
 
     private static let keyCodeToPrintableScalar: [Int: Character] = [
@@ -294,31 +300,33 @@
 
 enum QuickSearchEngine {
     static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
-        apps.map { app in
-            let localizedNames = uniqueOrdered(app.localizedNames)
-            let internalBundleNames = internalBundleNames(for: app)
-            let note = store.appNotes[app.path.path] ?? app.note ?? ""
-            let bundleIdentifier = app.bundleIdentifier ?? ""
-            let searchableFields = makeSearchableFields(
-                appName: app.name,
-                localizedNames: localizedNames,
-                internalBundleNames: internalBundleNames,
-                tagNames: app.tags,
-                note: note,
-                bundleIdentifier: bundleIdentifier
-            )
-            return QuickSearchDocument(
-                app: app,
-                localizedNames: localizedNames,
-                internalBundleNames: internalBundleNames,
-                tagNames: app.tags,
-                note: note,
-                bundleIdentifier: bundleIdentifier,
-                lastOpenedAt: store.appLastOpenedAt[app.path.path],
-                openCount: store.appOpenCounts[app.path.path] ?? 0,
-                searchableFields: searchableFields
-            )
-        }
+        apps
+            .filter { !AppIndexer.isInternalHelperAppPath($0.path) }
+            .map { app in
+                let localizedNames = uniqueOrdered(AppDisplayNameResolver.searchAliases(for: app))
+                let internalBundleNames = internalBundleNames(for: app)
+                let note = store.appNotes[app.path.path] ?? app.note ?? ""
+                let bundleIdentifier = app.bundleIdentifier ?? ""
+                let searchableFields = makeSearchableFields(
+                    appName: app.name,
+                    localizedNames: localizedNames,
+                    internalBundleNames: internalBundleNames,
+                    tagNames: app.tags,
+                    note: note,
+                    bundleIdentifier: bundleIdentifier
+                )
+                return QuickSearchDocument(
+                    app: app,
+                    localizedNames: localizedNames,
+                    internalBundleNames: internalBundleNames,
+                    tagNames: app.tags,
+                    note: note,
+                    bundleIdentifier: bundleIdentifier,
+                    lastOpenedAt: store.appLastOpenedAt[app.path.path],
+                    openCount: store.appOpenCounts[app.path.path] ?? 0,
+                    searchableFields: searchableFields
+                )
+            }
     }
 
     static func search(_ query: String, documents: [QuickSearchDocument], limit: Int = 50) -> [QuickSearchResult] {
@@ -690,19 +698,6 @@
         return normalizeField(String(parts))
     }
 
-    private static func localizedNames(for app: AppInfo) -> [String] {
-        guard let bundle = Bundle(url: app.path) else { return [] }
-        let values = [
-            bundle.localizedInfoDictionary?["CFBundleDisplayName"] as? String,
-            bundle.infoDictionary?["CFBundleDisplayName"] as? String,
-            FileManager.default.displayName(atPath: app.path.path).replacingOccurrences(of: ".app", with: "")
-        ]
-        return uniqueOrdered(values.compactMap { $0 }.compactMap { value in
-            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
-            return trimmed.isEmpty || trimmed == app.name ? nil : trimmed
-        })
-    }
-
     private static func internalBundleNames(for app: AppInfo) -> [String] {
         guard let bundle = Bundle(url: app.path) else { return [] }
         let values = [
@@ -855,11 +850,15 @@
                 self.parentWindow = parentWindow
             }
 
+            let shouldOrderForInput = shouldOrderPanelForInput(panel)
             if panel.frame != frame {
                 panel.setFrame(frame, display: true)
             }
-            panel.makeKeyAndOrderFront(nil)
-            panel.orderFrontRegardless()
+            if shouldOrderForInput {
+                NSApp.activate(ignoringOtherApps: true)
+                panel.makeKeyAndOrderFront(nil)
+                panel.orderFrontRegardless()
+            }
         }
 
         func closePanel() {
@@ -882,7 +881,7 @@
 
             let panel = QuickSearchPanel(
                 contentRect: .zero,
-                styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
+                styleMask: [.borderless, .fullSizeContentView],
                 backing: .buffered,
                 defer: false
             )
@@ -904,6 +903,10 @@
             self.panel = panel
             self.hostingView = hostingView
             return panel
+        }
+
+        private func shouldOrderPanelForInput(_ panel: QuickSearchPanel) -> Bool {
+            !panel.isVisible || !panel.isKeyWindow || NSApp.keyWindow !== panel
         }
 
         private func scheduleDeferredApply(
@@ -953,8 +956,52 @@
     override var canBecomeKey: Bool { true }
     override var canBecomeMain: Bool { false }
 
+    override func sendEvent(_ event: NSEvent) {
+        if event.type == .leftMouseDown,
+           let row = quickSearchResultRow(at: event.locationInWindow) {
+            row.launchFromMouseClick()
+            return
+        }
+        super.sendEvent(event)
+    }
+
     override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
         frameRect
+    }
+
+    private func quickSearchResultRow(at windowPoint: NSPoint) -> QuickSearchResultRowView? {
+        guard let contentView else { return nil }
+        if let row = contentView.quickSearchResultRowFromHitTest(atWindowPoint: windowPoint) {
+            return row
+        }
+        return contentView.quickSearchResultRowDescendant(containingWindowPoint: windowPoint)
+    }
+}
+
+private extension NSView {
+    func quickSearchResultRowFromHitTest(atWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
+        let localPoint = convert(windowPoint, from: nil)
+        var hitView = hitTest(localPoint)
+        while let candidate = hitView {
+            if let row = candidate as? QuickSearchResultRowView {
+                return row
+            }
+            hitView = candidate.superview
+        }
+        return nil
+    }
+
+    func quickSearchResultRowDescendant(containingWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
+        for subview in subviews.reversed() {
+            if let row = subview as? QuickSearchResultRowView,
+               row.containsWindowPointForLaunch(windowPoint) {
+                return row
+            }
+            if let row = subview.quickSearchResultRowDescendant(containingWindowPoint: windowPoint) {
+                return row
+            }
+        }
+        return nil
     }
 }
 
@@ -1443,6 +1490,15 @@
         trackingAreaRef = area
     }
 
+    override func hitTest(_ point: NSPoint) -> NSView? {
+        guard !isHidden, alphaValue > 0, bounds.contains(point) else { return nil }
+        return self
+    }
+
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+        true
+    }
+
     override func mouseEntered(with event: NSEvent) {
         super.mouseEntered(with: event)
         if let result {
@@ -1451,13 +1507,31 @@
     }
 
     override func mouseDown(with event: NSEvent) {
-        if let result {
-            DispatchQueue.main.async { [onLaunch] in
-                onLaunch(result)
-            }
-        } else {
+        if !launchFromMouseClick() {
             super.mouseDown(with: event)
         }
+    }
+
+    @discardableResult
+    func launchFromMouseClick() -> Bool {
+        guard let result else { return false }
+        DispatchQueue.main.async { [onLaunch, result] in
+            onLaunch(result)
+        }
+        return true
+    }
+
+    func containsWindowPointForLaunch(_ windowPoint: NSPoint) -> Bool {
+        guard window != nil else { return false }
+        var view: NSView? = self
+        while let candidate = view {
+            if candidate.isHidden || candidate.alphaValue <= 0 {
+                return false
+            }
+            view = candidate.superview
+        }
+        let localPoint = convert(windowPoint, from: nil)
+        return bounds.contains(localPoint) && visibleRect.contains(localPoint)
     }
 
     private func setup() {
@@ -1575,6 +1649,8 @@
         field.placeholderString = placeholder
         field.delegate = context.coordinator
         field.onCommand = onCommand
+        field.target = context.coordinator
+        field.action = #selector(Coordinator.submitAction(_:))
         context.coordinator.onCommand = onCommand
         field.setAccessibilityLabel(tr("quickSearch.inputAccessibility"))
         context.coordinator.field = field
@@ -1583,11 +1659,14 @@
     }
 
     func updateNSView(_ field: QuickSearchNativeTextField, context: Context) {
-        if field.stringValue != text {
+        if !field.isComposingText, field.stringValue != text {
             field.stringValue = text
         }
+        field.publishCompositionStateIfNeeded()
         field.placeholderString = placeholder
         field.onCommand = onCommand
+        field.target = context.coordinator
+        field.action = #selector(Coordinator.submitAction(_:))
         context.coordinator.onCommand = onCommand
         if context.coordinator.lastFocusToken != focusToken {
             context.coordinator.lastFocusToken = focusToken
@@ -1601,10 +1680,13 @@
                 guard let field,
                       let window = field.window,
                       window.isVisible,
-                      window.firstResponder !== field
+                      field.currentEditor() == nil
                 else { return }
-                window.makeKeyAndOrderFront(nil)
-                window.orderFrontRegardless()
+                if !window.isKeyWindow || NSApp.keyWindow !== window {
+                    NSApp.activate(ignoringOtherApps: true)
+                    window.makeKeyAndOrderFront(nil)
+                    window.orderFrontRegardless()
+                }
                 window.makeFirstResponder(field)
             }
         }
@@ -1618,18 +1700,36 @@
         var text: Binding<String>
         var lastFocusToken = 0
         var onCommand: ((QuickSearchCommand) -> Void)?
-        weak var field: NSTextField?
+        weak var field: QuickSearchNativeTextField?
 
         init(text: Binding<String>) {
             self.text = text
         }
 
         func controlTextDidChange(_ obj: Notification) {
-            guard let field = obj.object as? NSTextField else { return }
+            guard let field = obj.object as? QuickSearchNativeTextField else { return }
+            field.publishCompositionStateIfNeeded()
+            if !field.isComposingText {
+                text.wrappedValue = field.stringValue
+            }
+        }
+
+        func controlTextDidEndEditing(_ obj: Notification) {
+            guard let field = obj.object as? QuickSearchNativeTextField else { return }
+            field.publishCompositionState(active: false, force: true)
             text.wrappedValue = field.stringValue
         }
 
+        @objc func submitAction(_ sender: Any?) {
+            guard field?.isComposingText != true else { return }
+            onCommand?(.submit)
+        }
+
         func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+            if textView.hasMarkedText() {
+                (control as? QuickSearchNativeTextField)?.publishCompositionState(active: true)
+                return false
+            }
             switch commandSelector {
             case #selector(NSResponder.moveUp(_:)):
                 onCommand?(.moveUp)
@@ -1655,10 +1755,33 @@
 
 private final class QuickSearchNativeTextField: NSTextField {
     var onCommand: ((QuickSearchCommand) -> Void)?
+    private var lastPublishedCompositionActive = false
 
     override var acceptsFirstResponder: Bool { true }
 
+    var isComposingText: Bool {
+        (currentEditor() as? NSTextView)?.hasMarkedText() == true
+    }
+
+    func publishCompositionStateIfNeeded() {
+        publishCompositionState(active: isComposingText)
+    }
+
+    func publishCompositionState(active: Bool, force: Bool = false) {
+        guard force || active != lastPublishedCompositionActive else { return }
+        lastPublishedCompositionActive = active
+        NotificationCenter.default.post(
+            name: .tagLauncherQuickSearchCompositionChanged,
+            object: self,
+            userInfo: ["active": active]
+        )
+    }
+
     override func keyDown(with event: NSEvent) {
+        guard !isComposingText else {
+            super.keyDown(with: event)
+            return
+        }
         switch Int(event.keyCode) {
         case kVK_UpArrow:
             onCommand?(.moveUp)

--
Gitblit v1.9.3