Ariver
2026-07-02 b2a06ec5ebc86c0241e635782ae5a032ab5aad0a
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] = [
@@ -844,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() {
@@ -871,7 +881,7 @@
            let panel = QuickSearchPanel(
                contentRect: .zero,
                styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
                styleMask: [.borderless, .fullSizeContentView],
                backing: .buffered,
                defer: false
            )
@@ -893,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(
@@ -942,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
    }
}
@@ -1432,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 {
@@ -1440,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() {
@@ -1564,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
@@ -1572,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
@@ -1590,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)
            }
        }
@@ -1607,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)
@@ -1644,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)