| | |
| | | @State private var quickSearchQuery = "" |
| | | @State private var quickSearchDocuments: [QuickSearchDocument] = [] |
| | | @State private var quickSearchResults: [QuickSearchResult] = [] |
| | | @State private var quickSearchSelectedTagName: String? = nil |
| | | @State private var quickSearchSelectedID: URL? = nil |
| | | @State private var quickSearchManualSelection = false |
| | | @State private var quickSearchFocusToken = 0 |
| | |
| | | } |
| | | .onChange(of: defaultGroupName) { _, _ in |
| | | rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames) |
| | | if quickSearchVisible { |
| | | refreshQuickSearchResults() |
| | | } |
| | | } |
| | | .onChange(of: quickSearchQuery) { _, _ in |
| | | quickSearchErrorMessage = nil |
| | |
| | | QuickSearchPanelPresentationView( |
| | | query: $quickSearchQuery, |
| | | results: quickSearchResults, |
| | | tagFilters: quickSearchTagFilters, |
| | | selectedTagName: quickSearchSelectedTagName, |
| | | selectedID: quickSearchSelectedID, |
| | | focusToken: quickSearchFocusToken, |
| | | selectionScrollToken: quickSearchSelectionScrollToken, |
| | |
| | | errorMessage: quickSearchErrorMessage, |
| | | onCommand: handleQuickSearchCommand, |
| | | onHover: selectQuickSearchResult, |
| | | onLaunch: launchQuickSearchResult |
| | | onLaunch: launchQuickSearchResult, |
| | | onTagFilterChange: selectQuickSearchTagFilter |
| | | ) |
| | | .frame(width: 0, height: 0) |
| | | .allowsHitTesting(false) |
| | |
| | | && !quickSearchResults.isEmpty |
| | | let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size)) |
| | | return QuickSearchPanelMetrics.contentHeight( |
| | | hasTagFilters: !quickSearchTagFilters.isEmpty, |
| | | hasResultList: hasResultList, |
| | | visibleRows: visibleRows |
| | | ) |
| | |
| | | private func quickSearchMaxVisibleRows(in size: CGSize) -> Int { |
| | | let bottomClearance: CGFloat = 84 |
| | | let chromeHeight = QuickSearchPanelMetrics.headerHeight |
| | | + (quickSearchTagFilters.isEmpty ? 0 : QuickSearchPanelMetrics.tagFilterBarHeight) |
| | | + QuickSearchPanelMetrics.dividerHeight |
| | | + QuickSearchPanelMetrics.resultListVerticalInset * 2 |
| | | let rowHeightWithSpacing = QuickSearchPanelMetrics.rowHeight |
| | |
| | | quickSearchVisible = true |
| | | quickSearchOpenedAt = Date() |
| | | quickSearchQuery = "" |
| | | quickSearchSelectedTagName = nil |
| | | quickSearchCompositionActive = false |
| | | quickSearchManualSelection = false |
| | | quickSearchErrorMessage = nil |
| | |
| | | quickSearchCompositionActive = false |
| | | quickSearchQuery = "" |
| | | quickSearchResults = [] |
| | | quickSearchSelectedTagName = nil |
| | | quickSearchSelectedID = nil |
| | | quickSearchManualSelection = false |
| | | quickSearchErrorMessage = nil |
| | |
| | | |
| | | private func refreshQuickSearchResults() { |
| | | let previousSelection = quickSearchSelectedID |
| | | let documents = quickSearchVisibleDocuments |
| | | let selectedTagName = validateQuickSearchSelectedTagName(against: quickSearchTagFilters) |
| | | quickSearchResults = QuickSearchEngine.search( |
| | | quickSearchQuery, |
| | | documents: quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) } |
| | | documents: documents, |
| | | selectedTagName: selectedTagName |
| | | ) |
| | | |
| | | if quickSearchResults.isEmpty { |
| | |
| | | quickSearchSelectedID = quickSearchResults.first?.id |
| | | quickSearchManualSelection = false |
| | | } |
| | | } |
| | | |
| | | private var quickSearchVisibleDocuments: [QuickSearchDocument] { |
| | | quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) } |
| | | } |
| | | |
| | | private var quickSearchTagFilters: [QuickSearchTagFilterOption] { |
| | | let availableTagNames = Set( |
| | | quickSearchVisibleDocuments |
| | | .flatMap(\.tagNames) |
| | | .filter(isQuickSearchFilterableTag) |
| | | ) |
| | | guard !availableTagNames.isEmpty else { return [] } |
| | | |
| | | let ordered = draggedTagNames |
| | | .filter { availableTagNames.contains($0) && isQuickSearchFilterableTag($0) } |
| | | let orderedSet = Set(ordered) |
| | | let remaining = availableTagNames |
| | | .filter { !orderedSet.contains($0) } |
| | | .sorted { $0.localizedStandardCompare($1) == .orderedAscending } |
| | | |
| | | return (ordered + remaining).map { tagName in |
| | | let definition = tagDefinitions[tagName] |
| | | return QuickSearchTagFilterOption( |
| | | name: tagName, |
| | | colorIndex: definition?.color ?? tagColors[tagName] ?? 0, |
| | | customColor: definition?.customColor ?? tagCustomColors[tagName] |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private func isQuickSearchFilterableTag(_ tagName: String) -> Bool { |
| | | let name = tagName.trimmingCharacters(in: .whitespacesAndNewlines) |
| | | guard !name.isEmpty, tagDefinitions[name] != nil else { return false } |
| | | return !AppContainerID.isReservedGroupName(name, defaultGroupName: defaultGroupName) |
| | | } |
| | | |
| | | private func validateQuickSearchSelectedTagName(against filters: [QuickSearchTagFilterOption]) -> String? { |
| | | guard let quickSearchSelectedTagName else { return nil } |
| | | guard filters.contains(where: { $0.name == quickSearchSelectedTagName }) else { |
| | | self.quickSearchSelectedTagName = nil |
| | | return nil |
| | | } |
| | | return quickSearchSelectedTagName |
| | | } |
| | | |
| | | private func selectQuickSearchTagFilter(_ tagName: String?) { |
| | | quickSearchSelectedTagName = tagName |
| | | quickSearchErrorMessage = nil |
| | | quickSearchManualSelection = false |
| | | quickSearchFocusToken &+= 1 |
| | | refreshQuickSearchResults() |
| | | } |
| | | |
| | | private func handleQuickSearchCommand(_ command: QuickSearchCommand) { |
| | |
| | | quickSearchFocusToken &+= 1 |
| | | } |
| | | quickSearchOpenedAt = Date() |
| | | quickSearchSelectedTagName = nil |
| | | refreshQuickSearchResults() |
| | | refreshAppsForQuickSearch() |
| | | NotificationCenter.default.post( |