.../Source/App/SearchAndPreview/SearchViewController.swift
Line | Count | Source (jump to first uncovered line) |
1 | | // |
2 | | // SearchViewController.swift |
3 | | // CutBox |
4 | | // |
5 | | // Created by Jason Milkins on 3/4/18. |
6 | | // Copyright © 2018-2023 ocodo. All rights reserved. |
7 | | // |
8 | | |
9 | | import Cocoa |
10 | | import RxSwift |
11 | | |
12 | | /// Controller for `SearchAndPreviewView` |
13 | | /// |
14 | | /// Subscribes any `SearchViewEvents` (from `SearchAndPreviewView`), |
15 | | /// performing the required actions, using init injected: `searchView:SearchAndPreviewView`, |
16 | | /// `historyService:HistoryService` and `prefs:CutBoxPreferencesService`. |
17 | | class SearchViewController: NSObject { |
18 | | var searchView: SearchAndPreviewView |
19 | | var historyService: HistoryService |
20 | | var prefs: CutBoxPreferencesService |
21 | | var fakeKey: FakeKey |
22 | | |
23 | 75 | var orderedSelection: OrderedSet<Int> = OrderedSet<Int>() |
24 | | |
25 | | var searchPopup: PopupController |
26 | | |
27 | | /// Event stream from SearchAndPreviewView |
28 | 82 | var events: PublishSubject<SearchViewEvents> { |
29 | 82 | return self.searchView.events |
30 | 82 | } |
31 | | |
32 | | /// Row indexes of selected items |
33 | 2 | var selectedItems: IndexSet? { |
34 | 2 | return self.searchView.selectedItems |
35 | 2 | } |
36 | | |
37 | | /// Strings of selected items |
38 | 6 | var selectedClips: [String] { |
39 | 6 | if self.historyService.items.isEmpty { |
40 | 2 | return [] |
41 | 4 | } |
42 | 4 | |
43 | 4 | return self.orderedSelection.all() |
44 | 4 | .map { self.historyService.items[$0] } |
45 | 6 | } |
46 | | |
47 | 75 | let disposeBag = DisposeBag() |
48 | | |
49 | | /// Setup controller, initialize the search view and popup. |
50 | | /// Connect the history service and tell it to start polling the pasteboard. |
51 | | /// Connect the preferences service to self and members. |
52 | | init(historyService: HistoryService = HistoryService.shared, |
53 | | cutBoxPreferences: CutBoxPreferencesService = CutBoxPreferencesService.shared, |
54 | | fakeKey: FakeKey = FakeKey(), |
55 | | searchView: SearchAndPreviewView = SearchAndPreviewView.fromNib()! |
56 | 75 | ) { |
57 | 75 | self.historyService = historyService |
58 | 75 | self.prefs = cutBoxPreferences |
59 | 75 | self.fakeKey = fakeKey |
60 | 75 | |
61 | 75 | self.searchView = searchView |
62 | 75 | self.searchPopup = PopupController(content: self.searchView) |
63 | 75 | |
64 | 75 | self.historyService.beginPolling() |
65 | 75 | |
66 | 75 | super.init() |
67 | 75 | |
68 | 75 | configureSearchPopupAndView() |
69 | 75 | } |
70 | | |
71 | | /// Setup the context menu for items shown in the search view |
72 | 75 | func setupClipItemsContextMenu() { |
73 | 75 | self.searchView.setupClipItemsContextMenu() |
74 | 75 | self.searchView.menuDelegate = self |
75 | 75 | } |
76 | | |
77 | | /// Toggle display of the search popup |
78 | 0 | func togglePopup() { |
79 | 0 | self.historyService.favoritesOnly = false |
80 | 0 | self.searchView.applyTheme() |
81 | 0 | self.searchPopup.togglePopup() |
82 | 0 | } |
83 | | |
84 | | /// Close the search popup |
85 | 2 | func closeSearchPopup() { |
86 | 2 | self.historyService.favoritesOnly = false |
87 | 2 | self.searchPopup.closePopup() |
88 | 2 | } |
89 | | |
90 | | /// Open the search popup |
91 | 0 | func openSearchPopup() { |
92 | 0 | self.historyService.favoritesOnly = false |
93 | 0 | self.searchPopup.openPopup() |
94 | 0 | } |
95 | | |
96 | | /// Hide the app (objc selector) |
97 | 2 | @objc func hideApp() { |
98 | 2 | NSApp.hide(self) |
99 | 2 | } |
100 | | |
101 | | /// send a fake paste (Cmd V) to macOS |
102 | 1 | @objc func fakePaste() { |
103 | 1 | fakeKey.send(fakeKey: "V", useCommandFlag: true) |
104 | 1 | } |
105 | | |
106 | 1 | private func justClose() { |
107 | 1 | self.closeSearchPopup() |
108 | 1 | perform(#selector(hideApp), with: self, afterDelay: 0.1) |
109 | 1 | } |
110 | | |
111 | 1 | private func closeAndPaste(useJS: Bool = false) { |
112 | 1 | self.pasteSelectedClipToPasteboard(useJS) |
113 | 1 | self.closeSearchPopup() |
114 | 1 | perform(#selector(hideApp), with: self, afterDelay: 0.1) |
115 | 1 | perform(#selector(fakePaste), with: self, afterDelay: 0.25) |
116 | 1 | } |
117 | | |
118 | | /// Remove selected items and refresh the search view |
119 | 1 | func removeSelectedItems() { |
120 | 1 | if let selection = self.selectedItems { |
121 | 1 | self.historyService.remove(selected: selection) |
122 | 1 | self.searchView.reloadData() |
123 | 1 | } |
124 | 1 | } |
125 | | |
126 | | /// Toggle favorite status on selected items |
127 | 1 | func toggleFavoriteItems() { |
128 | 1 | if let selection = self.selectedItems { |
129 | 1 | self.historyService.toggleFavorite(items: selection) |
130 | 1 | self.searchView.reloadData() |
131 | 1 | self.searchView.selectRowIndexes(selection, byExtendingSelection: false) |
132 | 1 | } |
133 | 1 | } |
134 | | |
135 | | /// Send the selected clip text to the pasteboard |
136 | 1 | func pasteSelectedClipToPasteboard(_ useJS: Bool) { |
137 | 1 | guard !self.selectedClips.isEmpty else { |
138 | 1 | return |
139 | 1 | } |
140 | 0 |
|
141 | 0 | pasteToPasteboard(self.selectedClips) |
142 | 0 | } |
143 | | |
144 | 0 | private func pasteToPasteboard(_ clips: [String]) { |
145 | 0 | let clip = prefs.prepareClips(clips) |
146 | 0 |
|
147 | 0 | NSPasteboard.general.clearContents() |
148 | 0 | NSPasteboard.general.setString(clip, forType: .string) |
149 | 0 | } |
150 | | |
151 | 0 | private func pasteToPasteboard(_ clip: String) { |
152 | 0 | NSPasteboard.general.clearContents() |
153 | 0 | NSPasteboard.general.setString(clip, forType: .string) |
154 | 0 | } |
155 | | |
156 | 2 | private func resetSearchText() { |
157 | 2 | self.searchView.searchText?.string = "" |
158 | 2 | self.searchView.filterTextPublisher.onNext("") |
159 | 2 | self.searchView.reloadData() |
160 | 2 | } |
161 | | |
162 | 75 | private func setupSearchViewAndFilterBinding() { |
163 | 75 | self.searchView.itemsDataSource = self |
164 | 75 | self.searchView.itemsDelegate = self |
165 | 75 | |
166 | 75 | self.searchView.filterTextPublisher |
167 | 75 | .map { $0.isEmpty } |
168 | 75 | .subscribe(onNext: { |
169 | 2 | if self.prefs.useCompactUI { |
170 | 0 | self.searchView.hideSearchResults($0) |
171 | 2 | } else { |
172 | 2 | self.searchView.hideSearchResults(false) |
173 | 2 | } |
174 | 2 | }) |
175 | 75 | .disposed(by: disposeBag) |
176 | 75 | |
177 | 75 | self.searchView.filterTextPublisher |
178 | 75 | .subscribe(onNext: { |
179 | 2 | self.historyService.filterText = $0 |
180 | 2 | self.searchView.reloadData() |
181 | 2 | self.searchView.scrollRowToVisible(0) |
182 | 2 | }) |
183 | 75 | .disposed(by: self.disposeBag) |
184 | 75 | |
185 | 75 | self.prefs.events |
186 | 5.76k | .compactMap { |
187 | 5.76k | if case .hidePreviewSettingChanged(let isOn) = $0 { |
188 | 1.15k | return isOn |
189 | 4.60k | } |
190 | 4.60k | return nil |
191 | 5.76k | } |
192 | 1.15k | .subscribe(onNext: { |
193 | 1.15k | self.searchView.hidePreview($0) |
194 | 1.15k | }) |
195 | 75 | .disposed(by: self.disposeBag) |
196 | 75 | } |
197 | | |
198 | | /// Update the preview with selected clip(s) |
199 | 4 | func updateSearchItemPreview() { |
200 | 4 | let preview = prefs.prepareClips(selectedClips) |
201 | 4 | self.searchView.previewString = preview.truncate(limit: 50_000) |
202 | 4 | } |
203 | | |
204 | | // swiftlint:disable cyclomatic_complexity |
205 | | // swiftlint:disable function_body_length |
206 | 75 | private func setupSearchTextEventBindings() { |
207 | 75 | self.events |
208 | 76 | .subscribe(onNext: onNext) $s15CutBoxUnitTests20SearchViewControllerC05setupE17TextEventBindings33_EB8AC02C75C5D49A81C9F81D503666B0LLyyFyAA0eF6EventsOcACcfu_ Line | Count | Source | 208 | 75 | .subscribe(onNext: onNext) |
$s15CutBoxUnitTests20SearchViewControllerC05setupE17TextEventBindings33_EB8AC02C75C5D49A81C9F81D503666B0LLyyFyAA0eF6EventsOcACcfu_yAGcfu0_ Line | Count | Source | 208 | 1 | .subscribe(onNext: onNext) |
|
209 | 75 | .disposed(by: self.disposeBag) |
210 | 75 | } |
211 | | |
212 | 18 | func onNext(event: SearchViewEvents) { |
213 | 18 | switch event { |
214 | 18 | case .setSearchMode(let mode): |
215 | 2 | self.historyService.searchMode = mode |
216 | 2 | self.searchView.reloadData() |
217 | 2 | self.searchView.setSearchModeButton(mode: mode) |
218 | 18 | |
219 | 18 | case .toggleSearchMode: |
220 | 2 | let mode = self.historyService.toggleSearchMode() |
221 | 2 | self.searchView.reloadData() |
222 | 2 | self.searchView.setSearchModeButton(mode: mode) |
223 | 18 | |
224 | 18 | case .setTimeFilter(let seconds): |
225 | 1 | self.historyService.setTimeFilter(seconds: seconds) |
226 | 1 | self.searchView.reloadData() |
227 | 18 | |
228 | 18 | case .toggleTimeFilter: |
229 | 1 | self.searchView.toggleTimeFilter() |
230 | 1 | self.historyService.setTimeFilter(seconds: nil) |
231 | 1 | self.searchView.reloadData() |
232 | 18 | |
233 | 18 | case .cycleTheme: |
234 | 1 | self.prefs.cycleTheme() |
235 | 1 | self.searchView.applyTheme() |
236 | 1 | self.reloadDataWithExistingSelection() |
237 | 18 | |
238 | 18 | case .toggleWrappingStrings: |
239 | 1 | self.prefs.useWrappingStrings.toggle() |
240 | 1 | self.updateSearchItemPreview() |
241 | 18 | |
242 | 18 | case .toggleJoinStrings: |
243 | 1 | self.prefs.useJoinString.toggle() |
244 | 1 | self.updateSearchItemPreview() |
245 | 18 | |
246 | 18 | case .toggleSearchScope: |
247 | 1 | self.historyService.favoritesOnly.toggle() |
248 | 1 | self.searchView.reloadData() |
249 | 1 | self.searchView.setSearchScopeButton(favoritesOnly: self.historyService.favoritesOnly) |
250 | 18 | |
251 | 18 | case .togglePreview: |
252 | 1 | self.prefs.hidePreview.toggle() |
253 | 18 | |
254 | 18 | case .scaleTextDown: |
255 | 1 | self.prefs.scaleTextDown() |
256 | 1 | self.reloadDataWithExistingSelection() |
257 | 18 | |
258 | 18 | case .scaleTextUp: |
259 | 1 | self.prefs.scaleTextUp() |
260 | 1 | self.reloadDataWithExistingSelection() |
261 | 18 | |
262 | 18 | case .scaleTextNormalize: |
263 | 1 | self.prefs.scaleTextNormalize() |
264 | 1 | self.reloadDataWithExistingSelection() |
265 | 18 | |
266 | 18 | case .toggleFavorite: |
267 | 1 | self.toggleFavoriteItems() |
268 | 18 | |
269 | 18 | case .justClose: |
270 | 1 | self.justClose() |
271 | 18 | |
272 | 18 | case .closeAndPasteSelected: |
273 | 1 | self.closeAndPaste() |
274 | 18 | |
275 | 18 | case .removeSelected: |
276 | 1 | self.removeSelectedItems() |
277 | 18 | |
278 | 18 | default: |
279 | 0 | break |
280 | 18 | } |
281 | 18 | } |
282 | | // swiftlint:enable cyclomatic_complexity |
283 | | // swiftlint:enable function_body_length |
284 | | |
285 | 4 | private func reloadDataWithExistingSelection() { |
286 | 4 | if let selected = self.searchView.selectedRowIndexes { |
287 | 4 | self.searchView.updateLayer() |
288 | 4 | self.searchView.reloadData() |
289 | 4 | self.searchView.setTextScale() |
290 | 4 | self.searchView.selectRowIndexes(selected, byExtendingSelection: false) |
291 | 4 | } |
292 | 4 | } |
293 | | |
294 | 75 | private func configureSearchPopupAndView() { |
295 | 75 | setupSearchTextEventBindings() |
296 | 75 | setupSearchViewAndFilterBinding() |
297 | 75 | setupClipItemsContextMenu() |
298 | 75 | |
299 | 75 | self.searchView.placeHolderTextString = "search_placeholder".l7n |
300 | 75 | |
301 | 75 | self.searchPopup.proportionalTopPadding = 0.15 |
302 | 75 | self.searchPopup.proportionalWidth = 0.6 |
303 | 75 | self.searchPopup.proportionalHeight = 0.6 |
304 | 75 | |
305 | 75 | self.searchPopup.willOpenPopup = self.searchPopup.proportionalResizePopup $s15CutBoxUnitTests20SearchViewControllerC09configuree8PopupAndF033_EB8AC02C75C5D49A81C9F81D503666B0LLyyFyycAA0iG0Ccfu_ Line | Count | Source | 305 | 75 | self.searchPopup.willOpenPopup = self.searchPopup.proportionalResizePopup |
Unexecuted instantiation: $s15CutBoxUnitTests20SearchViewControllerC09configuree8PopupAndF033_EB8AC02C75C5D49A81C9F81D503666B0LLyyFyycAA0iG0Ccfu_yycfu0_ |
306 | 75 | |
307 | 75 | self.searchPopup.didOpenPopup = { |
308 | 0 | guard let window = self.searchView.window |
309 | 0 | else { fatalError("No window found for popup") } |
310 | 0 |
|
311 | 0 | self.resetSearchText() |
312 | 0 |
|
313 | 0 | // Focus search text |
314 | 0 | window.makeFirstResponder(self.searchView.searchText) |
315 | 0 | } |
316 | 75 | |
317 | 77 | self.searchPopup.willClosePopup = self.resetSearchText $s15CutBoxUnitTests20SearchViewControllerC09configuree8PopupAndF033_EB8AC02C75C5D49A81C9F81D503666B0LLyyFyycACcfu1_ Line | Count | Source | 317 | 75 | self.searchPopup.willClosePopup = self.resetSearchText |
$s15CutBoxUnitTests20SearchViewControllerC09configuree8PopupAndF033_EB8AC02C75C5D49A81C9F81D503666B0LLyyFyycACcfu1_yycfu2_ Line | Count | Source | 317 | 2 | self.searchPopup.willClosePopup = self.resetSearchText |
|
318 | 75 | } |
319 | | } |