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