-
-
Notifications
You must be signed in to change notification settings - Fork 14
/
SelectionManager.swift
167 lines (140 loc) · 4.63 KB
/
SelectionManager.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import Foundation
import Cocoa
import SwiftUI
@MainActor
final class SelectionManager<T>: ObservableObject where T: Identifiable,
T: Hashable,
T: Equatable {
typealias StoreType = (Set<T.ID>) -> Void
private(set) var lastSelection: T.ID?
@Published var selections: Set<T.ID>
@Published var selectedColor: Color = .accentColor
private let store: StoreType
init(_ selections: Set<T.ID> = [],
initialSelection: Set<T.ID> = [],
store: @escaping StoreType = { _ in }) {
self.store = store
if let firstSelection = Array(initialSelection).first {
self.selections = [firstSelection]
self.lastSelection = firstSelection
} else {
self.selections = selections
self.lastSelection = nil
}
}
func removeLastSelection() {
self.lastSelection = nil
}
func setLastSelection(_ selection: T.ID) {
self.lastSelection = selection
if self.selections.isEmpty {
self.selections = [selection]
}
}
@MainActor
func publish(_ newSelections: Set<T.ID>) {
self.selections = newSelections
store(self.selections)
}
func handle(_ direction: MoveCommandDirection,
_ data: [T],
proxy: ScrollViewProxy? = nil,
vertical: Bool = true) -> T.ID? {
switch direction {
case .up:
guard vertical else { return nil }
return moveSelection(data, proxy: proxy) { max($0 - 1, 0) }
case .down:
guard vertical else { return nil }
return moveSelection(data, proxy: proxy) { min($0 + 1, data.count - 1) }
case .left:
guard !vertical else { return nil }
return moveSelection(data, proxy: proxy) { max($0 - 1, 0) }
case .right:
guard !vertical else { return nil }
return moveSelection(data, proxy: proxy) { min($0 + 1, data.count - 1) }
default:
break
}
return nil
}
func handleOnTap(_ data: [T], element: T) {
let copyOfSelections = selections
if NSEvent.modifierFlags.contains(.shift) {
selections = onShiftTap(data, elementID: element.id, selections: copyOfSelections)
} else if NSEvent.modifierFlags.contains(.command) {
selections = onCommandTap(element, selections: copyOfSelections)
} else {
selections = onTap(element)
}
store(selections)
}
// MARK: Private methods
private func moveSelection(_ data: [T],
proxy: ScrollViewProxy? = nil,
transform: (Int) -> Int) -> T.ID? {
if let currentSelection = lastSelection ?? selections.first ?? data.first?.id,
let currentIndex = data.firstIndex(where: { $0.id == currentSelection }) {
let nextIndex = transform(currentIndex)
let currentElementID = data[currentIndex].id
let nextElementID = data[nextIndex].id
if NSEvent.modifierFlags.contains(.shift) {
if selections.contains(nextElementID) {
selections.remove(currentElementID)
} else {
selections.insert(currentElementID)
}
selections.insert(nextElementID)
} else {
selections = [nextElementID]
}
lastSelection = nextElementID
proxy?.scrollTo(nextElementID)
return nextElementID
}
return nil
}
private func onTap(_ element: T) -> Set<T.ID> {
lastSelection = element.id
return [element.id]
}
private func onCommandTap(_ element: T, selections: Set<T.ID>) -> Set<T.ID> {
var newSelections = selections
if selections.contains(element.id) {
newSelections.remove(element.id)
} else {
newSelections.insert(element.id)
}
lastSelection = element.id
return newSelections
}
private func onShiftTap(_ data: [T], elementID: T.ID, selections: Set<T.ID>) -> Set<T.ID> {
var newSelections = selections
if newSelections.contains(elementID) {
newSelections.remove(elementID)
} else {
newSelections.insert(elementID)
}
guard let lastSelection else { return newSelections }
guard var startIndex = data.firstIndex(where: { $0.id == lastSelection }),
var endIndex = data.firstIndex(where: { $0.id == elementID }) else {
return newSelections
}
if endIndex < startIndex {
let copy = startIndex
startIndex = endIndex
endIndex = copy
}
data[startIndex...endIndex].forEach { element in
if selections.contains(element.id) {
if element.id != lastSelection {
newSelections.remove(element.id)
}
} else {
newSelections.insert(element.id)
}
}
self.lastSelection = elementID
return newSelections
}
}