From ed43ee83789cb99b1cb95db340fee939dd0dedec Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 05 Jun 2026 02:07:17 +0800
Subject: [PATCH] Add final 7.8.14 App Store submission package
---
Apptag/QuickSearch.swift | 382 ++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 356 insertions(+), 26 deletions(-)
diff --git a/Apptag/QuickSearch.swift b/Apptag/QuickSearch.swift
index bac6813..b933fd5 100644
--- a/Apptag/QuickSearch.swift
+++ b/Apptag/QuickSearch.swift
@@ -15,6 +15,13 @@
static let globalVisible = "globalVisible"
}
+enum QuickSearchDismissSource {
+ static let keyboard = "keyboard"
+ static let mouse = "mouse"
+ static let backdrop = "backdrop"
+ static let programmatic = "programmatic"
+}
+
// MARK: - Hotkeys
struct LauncherHotkey: Equatable {
@@ -729,6 +736,271 @@
case dismiss
}
+enum QuickSearchPanelMetrics {
+ static let width: CGFloat = 760
+ static let shadowOutset: CGFloat = 56
+ static let rowHeight: CGFloat = 74
+ static let rowSpacing: CGFloat = 2
+ static let resultListVerticalInset: CGFloat = 10
+ static let headerHeight: CGFloat = 84
+ static let dividerHeight: CGFloat = 1
+ static let messageRowHeight: CGFloat = 86
+
+ static func contentHeight(hasResultList: Bool, visibleRows: Int) -> CGFloat {
+ if hasResultList {
+ return headerHeight
+ + dividerHeight
+ + CGFloat(max(1, visibleRows)) * (rowHeight + rowSpacing)
+ + resultListVerticalInset * 2
+ }
+ return headerHeight + dividerHeight + messageRowHeight
+ }
+}
+
+struct QuickSearchPanelPresentationView: NSViewRepresentable {
+ @Binding var query: String
+ let results: [QuickSearchResult]
+ let selectedID: URL?
+ let focusToken: Int
+ let selectionScrollToken: Int
+ let isLoading: Bool
+ let maxVisibleRows: Int
+ let panelTopY: CGFloat
+ let panelHeight: CGFloat
+ let errorMessage: String?
+ let onCommand: (QuickSearchCommand) -> Void
+ let onHover: (QuickSearchResult) -> Void
+ let onLaunch: (QuickSearchResult) -> Void
+
+ func makeNSView(context: Context) -> QuickSearchPanelAnchorView {
+ QuickSearchPanelAnchorView()
+ }
+
+ func updateNSView(_ view: QuickSearchPanelAnchorView, context: Context) {
+ let contentSize = NSSize(width: QuickSearchPanelMetrics.width, height: panelHeight)
+ let windowSize = NSSize(
+ width: contentSize.width + QuickSearchPanelMetrics.shadowOutset * 2,
+ height: contentSize.height + QuickSearchPanelMetrics.shadowOutset * 2
+ )
+ let rootView = QuickSearchPanelWindowContent(
+ windowSize: windowSize,
+ contentSize: contentSize,
+ content: AnyView(
+ QuickSearchOverlayView(
+ query: $query,
+ results: results,
+ selectedID: selectedID,
+ focusToken: focusToken,
+ selectionScrollToken: selectionScrollToken,
+ isLoading: isLoading,
+ maxVisibleRows: maxVisibleRows,
+ errorMessage: errorMessage,
+ onCommand: onCommand,
+ onHover: onHover,
+ onLaunch: onLaunch
+ )
+ )
+ )
+ context.coordinator.apply(
+ QuickSearchPanelConfiguration(
+ panelTopY: panelTopY,
+ windowSize: windowSize,
+ contentSize: contentSize,
+ rootView: AnyView(rootView)
+ ),
+ anchorView: view
+ )
+ }
+
+ static func dismantleNSView(_ nsView: QuickSearchPanelAnchorView, coordinator: Coordinator) {
+ coordinator.closePanel()
+ }
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator()
+ }
+
+ final class Coordinator {
+ private var panel: QuickSearchPanel?
+ private var hostingView: NSHostingView<AnyView>?
+ private weak var parentWindow: NSWindow?
+ private var deferredApplyPending = false
+
+ func apply(_ configuration: QuickSearchPanelConfiguration, anchorView: QuickSearchPanelAnchorView) {
+ guard let parentWindow = anchorView.window else {
+ scheduleDeferredApply(configuration, anchorView: anchorView)
+ return
+ }
+
+ deferredApplyPending = false
+ let panel = ensurePanel(parentWindow: parentWindow)
+ let frame = windowFrame(
+ parentWindow: parentWindow,
+ panelTopY: configuration.panelTopY,
+ contentSize: configuration.contentSize,
+ windowSize: configuration.windowSize
+ )
+
+ hostingView?.rootView = configuration.rootView
+ hostingView?.frame = NSRect(origin: .zero, size: configuration.windowSize)
+ panel.contentView?.frame = NSRect(origin: .zero, size: configuration.windowSize)
+ panel.collectionBehavior = collectionBehavior(parentWindow: parentWindow)
+ panel.level = parentWindow.level
+
+ if panel.parent !== parentWindow {
+ if let previousParent = panel.parent {
+ previousParent.removeChildWindow(panel)
+ }
+ parentWindow.addChildWindow(panel, ordered: .above)
+ self.parentWindow = parentWindow
+ }
+
+ if panel.frame != frame {
+ panel.setFrame(frame, display: true)
+ }
+ panel.makeKeyAndOrderFront(nil)
+ panel.orderFrontRegardless()
+ }
+
+ func closePanel() {
+ if let panel {
+ if let parent = panel.parent {
+ parent.removeChildWindow(panel)
+ }
+ panel.orderOut(nil)
+ }
+ panel = nil
+ hostingView = nil
+ parentWindow = nil
+ deferredApplyPending = false
+ }
+
+ private func ensurePanel(parentWindow: NSWindow) -> QuickSearchPanel {
+ if let panel {
+ return panel
+ }
+
+ let panel = QuickSearchPanel(
+ contentRect: .zero,
+ styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
+ backing: .buffered,
+ defer: false
+ )
+ panel.isFloatingPanel = true
+ panel.hidesOnDeactivate = false
+ panel.isOpaque = false
+ panel.backgroundColor = .clear
+ panel.hasShadow = false
+ panel.titlebarAppearsTransparent = true
+ panel.titleVisibility = .hidden
+ panel.isReleasedWhenClosed = false
+ panel.collectionBehavior = collectionBehavior(parentWindow: parentWindow)
+
+ let hostingView = NSHostingView(rootView: AnyView(EmptyView()))
+ hostingView.wantsLayer = true
+ hostingView.layer?.backgroundColor = NSColor.clear.cgColor
+ panel.contentView = hostingView
+
+ self.panel = panel
+ self.hostingView = hostingView
+ return panel
+ }
+
+ private func scheduleDeferredApply(
+ _ configuration: QuickSearchPanelConfiguration,
+ anchorView: QuickSearchPanelAnchorView
+ ) {
+ guard !deferredApplyPending else { return }
+ deferredApplyPending = true
+ DispatchQueue.main.async { [weak self, weak anchorView] in
+ guard let self, let anchorView else { return }
+ self.deferredApplyPending = false
+ self.apply(configuration, anchorView: anchorView)
+ }
+ }
+
+ private func windowFrame(
+ parentWindow: NSWindow,
+ panelTopY: CGFloat,
+ contentSize: NSSize,
+ windowSize: NSSize
+ ) -> NSRect {
+ let parentFrame = parentWindow.frame
+ return NSRect(
+ x: parentFrame.minX + (parentFrame.width - contentSize.width) / 2 - QuickSearchPanelMetrics.shadowOutset,
+ y: parentFrame.maxY - panelTopY - contentSize.height - QuickSearchPanelMetrics.shadowOutset,
+ width: windowSize.width,
+ height: windowSize.height
+ )
+ }
+
+ private func collectionBehavior(parentWindow: NSWindow) -> NSWindow.CollectionBehavior {
+ var behavior: NSWindow.CollectionBehavior = [
+ .fullScreenAuxiliary,
+ .stationary,
+ .transient,
+ .ignoresCycle
+ ]
+ if parentWindow.collectionBehavior.contains(.canJoinAllSpaces) {
+ behavior.insert(.canJoinAllSpaces)
+ }
+ return behavior
+ }
+ }
+}
+
+private final class QuickSearchPanel: NSPanel {
+ override var canBecomeKey: Bool { true }
+ override var canBecomeMain: Bool { false }
+
+ override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
+ frameRect
+ }
+}
+
+final class QuickSearchPanelAnchorView: NSView {
+ override init(frame frameRect: NSRect) {
+ super.init(frame: frameRect)
+ setup()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setup()
+ }
+
+ private func setup() {
+ wantsLayer = true
+ layer?.backgroundColor = NSColor.clear.cgColor
+ }
+}
+
+struct QuickSearchPanelConfiguration {
+ let panelTopY: CGFloat
+ let windowSize: NSSize
+ let contentSize: NSSize
+ let rootView: AnyView
+}
+
+private struct QuickSearchPanelWindowContent: View {
+ let windowSize: NSSize
+ let contentSize: NSSize
+ let content: AnyView
+
+ var body: some View {
+ VStack(spacing: 0) {
+ content
+ .frame(width: contentSize.width, height: contentSize.height, alignment: .top)
+ Spacer(minLength: 0)
+ }
+ .padding(.top, QuickSearchPanelMetrics.shadowOutset)
+ .padding(.horizontal, QuickSearchPanelMetrics.shadowOutset)
+ .padding(.bottom, QuickSearchPanelMetrics.shadowOutset)
+ .frame(width: windowSize.width, height: windowSize.height, alignment: .top)
+ .background(Color.clear)
+ }
+}
+
struct QuickSearchOverlayView: View {
@Binding var query: String
let results: [QuickSearchResult]
@@ -744,13 +1016,17 @@
@Environment(\.colorScheme) private var colorScheme
- private let panelWidth: CGFloat = 760
- private let rowHeight: CGFloat = 74
+ private let panelWidth: CGFloat = QuickSearchPanelMetrics.width
+ private let rowHeight: CGFloat = QuickSearchPanelMetrics.rowHeight
private var panelBackgroundColor: Color {
colorScheme == .dark
? Color(red: 0.105, green: 0.110, blue: 0.125).opacity(0.97)
: Color.white.opacity(0.97)
+ }
+
+ private var panelShape: RoundedRectangle {
+ RoundedRectangle(cornerRadius: 34, style: .continuous)
}
var body: some View {
@@ -806,19 +1082,24 @@
onHover: onHover,
onLaunch: onLaunch
)
- .frame(height: CGFloat(min(results.count, maxVisibleRows)) * (rowHeight + 2) + 20)
+ .frame(
+ height: CGFloat(min(results.count, maxVisibleRows))
+ * (rowHeight + QuickSearchPanelMetrics.rowSpacing)
+ + QuickSearchPanelMetrics.resultListVerticalInset * 2
+ )
}
}
.frame(width: panelWidth)
.background(
- RoundedRectangle(cornerRadius: 34, style: .continuous)
- .fill(panelBackgroundColor)
- .shadow(color: .black.opacity(0.18), radius: 36, y: 18)
+ panelShape.fill(panelBackgroundColor)
)
+ .clipShape(panelShape)
.overlay(
- RoundedRectangle(cornerRadius: 34, style: .continuous)
- .stroke(Color.primary.opacity(0.10), lineWidth: 1)
+ panelShape.stroke(Color.primary.opacity(0.10), lineWidth: 1)
)
+ .compositingGroup()
+ .shadow(color: .black.opacity(colorScheme == .dark ? 0.46 : 0.26), radius: 30, x: 0, y: 18)
+ .shadow(color: .black.opacity(colorScheme == .dark ? 0.24 : 0.12), radius: 8, x: 0, y: 3)
.accessibilityElement(children: .contain)
.accessibilityLabel(tr("quickSearch.title"))
}
@@ -1023,6 +1304,18 @@
}
final class QuickSearchResultRowView: NSView {
+ private enum TagBadgeLayout {
+ static let columnWidth: CGFloat = 196
+ static let minWidth: CGFloat = 80
+ static let maxWidth: CGFloat = 196
+ static let height: CGFloat = 32
+ static let horizontalInset: CGFloat = 16
+ static let labelSafetyWidth: CGFloat = 8
+ static let trailingInset: CGFloat = 18
+ static let textGap: CGFloat = 18
+ static let minimumMainTextWidth: CGFloat = 260
+ }
+
private let iconView = NSImageView()
private let titleLabel = NSTextField(labelWithString: "")
private let detailLabel = NSTextField(labelWithString: "")
@@ -1082,25 +1375,44 @@
height: iconSize
)
- let tagMaxWidth: CGFloat = 128
- let tagHeight: CGFloat = 32
- let trailingInset: CGFloat = 18
+ let textX = iconX + iconSize + 16
+ let trailingInset = TagBadgeLayout.trailingInset
+ let rightLimit = bounds.width - trailingInset
var tagFrame = NSRect.zero
- if !tagBackgroundView.isHidden {
- let labelWidth = min(tagMaxWidth - 24, max(22, tagLabel.intrinsicContentSize.width))
- let tagWidth = min(tagMaxWidth, labelWidth + 24)
+ let hasTag = !tagLabel.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ let availableBadgeColumnWidth = rightLimit - textX - TagBadgeLayout.minimumMainTextWidth - TagBadgeLayout.textGap
+ let tagColumnWidth = min(TagBadgeLayout.columnWidth, max(0, availableBadgeColumnWidth))
+ let canShowTag = hasTag && tagColumnWidth >= TagBadgeLayout.minWidth
+ tagBackgroundView.isHidden = !canShowTag
+
+ if canShowTag {
+ let measuredLabelWidth = measuredTagLabelWidth()
+ let measuredBadgeWidth = measuredLabelWidth
+ + TagBadgeLayout.horizontalInset * 2
+ + TagBadgeLayout.labelSafetyWidth
+ let tagWidth = min(
+ min(TagBadgeLayout.maxWidth, tagColumnWidth),
+ max(TagBadgeLayout.minWidth, measuredBadgeWidth)
+ )
tagFrame = NSRect(
- x: bounds.width - trailingInset - tagWidth,
- y: (bounds.height - tagHeight) / 2,
+ x: rightLimit - tagWidth,
+ y: (bounds.height - TagBadgeLayout.height) / 2,
width: tagWidth,
- height: tagHeight
+ height: TagBadgeLayout.height
)
tagBackgroundView.frame = tagFrame
- tagLabel.frame = NSRect(x: 12, y: 6, width: tagWidth - 24, height: 20)
+ tagLabel.frame = NSRect(
+ x: TagBadgeLayout.horizontalInset,
+ y: 6,
+ width: tagWidth - TagBadgeLayout.horizontalInset * 2,
+ height: 20
+ )
+ } else {
+ tagBackgroundView.frame = .zero
+ tagLabel.frame = .zero
}
- let textX = iconX + iconSize + 16
- let textRight = tagBackgroundView.isHidden ? bounds.width - trailingInset : tagFrame.minX - 16
+ let textRight = tagBackgroundView.isHidden ? bounds.width - trailingInset : tagFrame.minX - TagBadgeLayout.textGap
let textWidth = max(40, textRight - textX)
if detailLabel.isHidden {
titleLabel.frame = NSRect(x: textX, y: (bounds.height - 26) / 2, width: textWidth, height: 26)
@@ -1109,6 +1421,11 @@
titleLabel.frame = NSRect(x: textX, y: 14, width: textWidth, height: 25)
detailLabel.frame = NSRect(x: textX, y: 42, width: textWidth, height: 21)
}
+ }
+
+ private func measuredTagLabelWidth() -> CGFloat {
+ let font = tagLabel.font ?? NSFont.systemFont(ofSize: 14, weight: .semibold)
+ return ceil((tagLabel.stringValue as NSString).size(withAttributes: [.font: font]).width)
}
override func updateTrackingAreas() {
@@ -1135,7 +1452,9 @@
override func mouseDown(with event: NSEvent) {
if let result {
- onLaunch(result)
+ DispatchQueue.main.async { [onLaunch] in
+ onLaunch(result)
+ }
} else {
super.mouseDown(with: event)
}
@@ -1176,6 +1495,9 @@
tagLabel.lineBreakMode = .byTruncatingTail
tagLabel.maximumNumberOfLines = 1
tagLabel.backgroundColor = .clear
+ tagLabel.allowsDefaultTighteningForTruncation = true
+ tagLabel.cell?.usesSingleLineMode = true
+ tagLabel.cell?.lineBreakMode = .byTruncatingTail
}
private func updateColors() {
@@ -1274,11 +1596,17 @@
}
private func requestFocus(_ field: QuickSearchNativeTextField) {
- DispatchQueue.main.async { [weak field] in
- guard let field,
- field.window?.firstResponder !== field
- else { return }
- field.window?.makeFirstResponder(field)
+ for delay in [0.0, 0.03, 0.08, 0.16] {
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak field] in
+ guard let field,
+ let window = field.window,
+ window.isVisible,
+ window.firstResponder !== field
+ else { return }
+ window.makeKeyAndOrderFront(nil)
+ window.orderFrontRegardless()
+ window.makeFirstResponder(field)
+ }
}
}
@@ -1328,6 +1656,8 @@
private final class QuickSearchNativeTextField: NSTextField {
var onCommand: ((QuickSearchCommand) -> Void)?
+ override var acceptsFirstResponder: Bool { true }
+
override func keyDown(with event: NSEvent) {
switch Int(event.keyCode) {
case kVK_UpArrow:
--
Gitblit v1.9.3