Skip to content

Commit

Permalink
make ItemFilter injectable
Browse files Browse the repository at this point in the history
  • Loading branch information
DivineDominion committed Aug 26, 2023
1 parent 4e1b1b8 commit 853901e
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 25 deletions.
33 changes: 9 additions & 24 deletions Sources/FloatingFilter/FilterInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ class FilterInteractor {

private var state: State {
didSet {
self.view.showItems(state.filteredItems)
self.view.showItems(state.filteredItems(filter: filter))
}
}

let view: FilteredItemView
let filter: ItemFilter

init(view: FilteredItemView) {
init(
view: FilteredItemView,
filter: @escaping ItemFilter
) {
self.view = view
self.filter = filter
self.state = State(allItems: [], filterPhrase: "")
}
}
Expand All @@ -41,28 +46,8 @@ extension FilterInteractor: FilterChangeDelegate {
}

extension FilterInteractor.State {
fileprivate var filteredItems: [Item] {
fileprivate func filteredItems(filter: ItemFilter) -> [Item] {
guard !filterPhrase.isEmpty else { return allItems }
return allItems.sortedByNormalizedFuzzyMatch(pattern: self.filterPhrase)
}
}

extension Sequence where Iterator.Element == Item {
fileprivate func sortedByNormalizedFuzzyMatch(pattern: String) -> [Element] {
// These magic numbers are totally experimental
let fuzziness = 0.3
let threshold = 0.4

return self
.map { (element: $0, score: $0.title.score(word: pattern, fuzziness: fuzziness)) }
.filter { $0.score > threshold }
.sorted(by: { $0.score > $1.score })
.map { $0.element }
}
}

extension Item {
fileprivate var normalizedTitle: String {
return self.title.lowercased()
return filter(filterPhrase, allItems)
}
}
34 changes: 33 additions & 1 deletion Sources/FloatingFilter/FloatingFilterModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,44 @@ import AppKit
private var windowHoldingService = WindowHoldingService()

public typealias SelectionCallback = (_ selectedItems: [Item]) -> Void
public typealias ItemFilter = (_ needle: String, _ haystack: [Item]) -> [Item]

public struct FloatingFilterModule {
/// Uses the default filter that scores all ``Item``s by `pattern` fuzzily, discards matches below a certain
/// threshold, and sorts by score.
public static func defaultFuzzyFilter(
fuzziness: Double = 0.3,
threshold: Double = 0.4
) -> ItemFilter {
return { needle, haystack in
let needle = needle.localizedLowercase

return haystack
.map { (element: $0, score: $0.title.localizedLowercase.score(word: needle, fuzziness: fuzziness)) }
.filter { $0.score > threshold }
.sorted(by: { $0.score > $1.score })
.map { $0.element }
}
}

private init() { }

/// - Parameters:
/// - items: Filter-able items, sorted, to show in the floating filter window.
/// - filterPlaceholderText: Placeholder for the FloatingFilter text field
/// - windowLevel: Level to show the filter window on. Default `.floating` is intended for use
/// as a utility panel.
/// - closeWhenLosingFocus: Whether the filter should disappear when users e.g. activate another app
/// before completing the operation. Defaults to `true` to be used as a temporary utility.
/// - filter: Narrows down all filterable `items` to find `needle`. The default uses a fuzzy string
/// matching algorithm.
/// - selectionCallback: Output port for confirmed selections in the filter window.
public static func showFilterWindow(
items: [Item],
filterPlaceholderText: String = NSLocalizedString("Filter Items", comment: "Placeholder for the FloatingFilter text field"),
windowLevel: NSWindow.Level = .floating,
closeWhenLosingFocus: Bool = true,
filter: @escaping ItemFilter = FloatingFilterModule.defaultFuzzyFilter(),
selection selectionCallback: @escaping SelectionCallback
) {
let windowController = FilterWindowController()
Expand All @@ -34,7 +63,10 @@ public struct FloatingFilterModule {
window.makeKeyAndOrderFront(nil)
}

let interactor = FilterInteractor(view: windowController)
let interactor = FilterInteractor(
view: windowController,
filter: filter
)
interactor.showItems(items)

windowController.filterChangeDelegate = interactor
Expand Down

0 comments on commit 853901e

Please sign in to comment.