.../Source/App/CutBoxController.swift
Line | Count | Source (jump to first uncovered line) |
1 | | // |
2 | | // CutBoxController.swift |
3 | | // CutBox |
4 | | // |
5 | | // Created by Jason Milkins on 17/3/18. |
6 | | // Copyright © 2018-2023 ocodo. All rights reserved. |
7 | | // |
8 | | |
9 | | import Cocoa |
10 | | import RxSwift |
11 | | |
12 | | typealias StatusItemDescriptor = (Int, String, String?, String?) |
13 | | |
14 | | class CutBoxController: NSObject { |
15 | | @IBOutlet weak var statusMenu: CutBoxBaseMenu! |
16 | 24 | var statusItem: NSStatusItem = cutBoxGetStatusItem() |
17 | | var useCompactUI: CutBoxBaseMenuItem! |
18 | | var hidePreview: CutBoxBaseMenuItem! |
19 | | var searchModeSelectorsDict: [String: CutBoxBaseMenuItem]? |
20 | | var searchViewController: SearchViewController |
21 | | var jsFuncSearchViewController: JSFuncSearchViewController |
22 | | var preferencesController: PreferencesTabViewController |
23 | 24 | var aboutPanel: AboutPanel = AboutPanel.fromNib()! |
24 | 24 | var hotKeyService = HotKeyService.shared |
25 | 24 | var prefs = CutBoxPreferencesService.shared |
26 | 24 | var historyService = HistoryService.shared |
27 | 24 | var disposeBag = DisposeBag() |
28 | 24 | var dialogFactory = DialogFactory() |
29 | 24 | var nsAppProvider = CutBoxNSAppProvider() |
30 | | |
31 | 1 | @objc func searchClicked(_ sender: CutBoxBaseMenuItem) { |
32 | 1 | self.searchViewController.togglePopup() |
33 | 1 | } |
34 | | |
35 | 2 | @objc func clearHistoryClicked(_ sender: CutBoxBaseMenuItem?) { |
36 | 2 | if dialogFactory.suppressibleConfirmationDialog( |
37 | 2 | messageText: "confirm_warning_clear_history_title".l7n, |
38 | 2 | informativeText: "confirm_warning_clear_history".l7n, |
39 | 2 | dialogName: .clearHistoryWarning) { |
40 | 1 | self.searchViewController.historyService.clear() |
41 | 2 | } |
42 | 2 | } |
43 | | |
44 | 1 | @objc func openPreferences(_ sender: CutBoxBaseMenuItem) { |
45 | 1 | NSApplication.shared.activate(ignoringOtherApps: true) |
46 | 1 | preferencesController.open() |
47 | 1 | } |
48 | | |
49 | 1 | @objc func openAboutPanel(_ sender: CutBoxBaseMenuItem) { |
50 | 1 | aboutPanel.makeKeyAndOrderFront(self) |
51 | 1 | aboutPanel.center() |
52 | 1 | } |
53 | | |
54 | 1 | @objc func quitClicked(_ sender: CutBoxBaseMenuItem) { |
55 | 1 | nsAppProvider.terminate(sender) |
56 | 1 | } |
57 | | |
58 | 2 | @objc func useCompactUIClicked(_ sender: CutBoxBaseMenuItem) { |
59 | 2 | self.prefs.useCompactUI.toggle() |
60 | 2 | } |
61 | | |
62 | 2 | @objc func hidePreviewClicked(_ sender: CutBoxBaseMenuItem) { |
63 | 2 | self.prefs.hidePreview.toggle() |
64 | 2 | } |
65 | | |
66 | 1 | @objc func searchModeSelect(_ sender: CutBoxBaseMenuItem) { |
67 | 1 | searchModeSelectAxID(sender.accessibilityIdentifier()) |
68 | 1 | } |
69 | | |
70 | 1 | @objc func reloadJavascript(_ sender: CutBoxBaseMenuItem) { |
71 | 1 | self.prefs.loadJavascript() |
72 | 1 | } |
73 | | |
74 | 1 | @objc func reloadThemes(_ sender: CutBoxBaseMenuItem) { |
75 | 1 | self.prefs.loadThemes() |
76 | 1 | } |
77 | | |
78 | 24 | override init() { |
79 | 24 | self.searchViewController = SearchViewController() |
80 | 24 | self.jsFuncSearchViewController = JSFuncSearchViewController() |
81 | 24 | self.preferencesController = PreferencesTabViewController() |
82 | 24 | super.init() |
83 | 24 | } |
84 | | |
85 | 0 | override func awakeFromNib() { |
86 | 0 | self.setup() |
87 | 0 | } |
88 | | |
89 | 1 | func setup() { |
90 | 1 | self.hotKeyService.configure() |
91 | 1 | self.setMenuItems() |
92 | 1 | self.setupEventBindings() |
93 | 1 | } |
94 | | } |
95 | | |
96 | | extension CutBoxController { |
97 | 5 | var statusMenuItems: [StatusItemDescriptor] { |
98 | 5 | [ |
99 | 5 | (0, "cutbox_menu_search_cutbox".l7n, nil, "searchClicked:"), |
100 | 5 | (1, "---", nil, nil), |
101 | 5 | (2, "cutbox_menu_fuzzy_match".l7n, "fuzzyMatch", "searchModeSelect:"), |
102 | 5 | (3, "cutbox_menu_regexp_any_case_match".l7n, "regexpAnyCase", "searchModeSelect:"), |
103 | 5 | (4, "cutbox_menu_regexp_case_match".l7n, "regexpStrictCase", "searchModeSelect:"), |
104 | 5 | (5, "cutbox_menu_substring_match".l7n, "substringMatch", "searchModeSelect:"), |
105 | 5 | (6, "---", nil, nil), |
106 | 5 | (7, "cutbox_menu_compactui".l7n, nil, "useCompactUIClicked:"), |
107 | 5 | (8, "cutbox_menu_hide_preview".l7n, nil, "hidePreviewClicked:"), |
108 | 5 | (9, "---", nil, nil), |
109 | 5 | (10, "cutbox_menu_preferences".l7n, nil, "openPreferences:"), |
110 | 5 | (11, "cutbox_menu_clear_history".l7n, nil, "clearHistoryClicked:"), |
111 | 5 | (12, "---", nil, nil), |
112 | 5 | (13, "preferences_javascript_transform_reload".l7n, nil, "reloadJavascript:"), |
113 | 5 | (14, "preferences_color_theme_reload_themes".l7n, nil, "reloadThemes:"), |
114 | 5 | (15, "---", nil, nil), |
115 | 5 | |
116 | 5 | // 16: Sparkle: Check for Updates |
117 | 5 | // It will find and fill the empty slot automatically. |
118 | 5 | |
119 | 5 | (17, "cutbox_menu_about".l7n, nil, "openAboutPanel:"), |
120 | 5 | (18, "cutbox_menu_quit".l7n, nil, "quitClicked:") |
121 | 5 | // So number all the indexes correctly, leaving a gap. |
122 | 5 | ] |
123 | 5 | } |
124 | | |
125 | 5 | func setMenuItems() { |
126 | 5 | let menu = self.statusMenu! |
127 | 5 | self.statusItem.menu = menu |
128 | 5 | |
129 | 5 | let icon = CutBoxImageRef.statusIcon.image(with: "") |
130 | 5 | icon.isTemplate = true // best for dark mode |
131 | 5 | self.statusItem.button?.image = icon |
132 | 90 | self.statusMenuItems.forEach { addMenuItems(menu: menu, descriptor: $0) } |
133 | 5 | self.statusItem.menu = menu |
134 | 5 | |
135 | 5 | setModeSelectors(menu.items) |
136 | 5 | |
137 | 5 | self.useCompactUI = menu.item(at: 7)! |
138 | 5 | self.hidePreview = menu.item(at: 8)! |
139 | 5 | |
140 | 5 | setCompactUIMenuItem() |
141 | 5 | setHidePreviewMenuItem() |
142 | 5 | } |
143 | | |
144 | 90 | func addMenuItems(menu: CutBoxBaseMenu, descriptor: StatusItemDescriptor) { |
145 | 90 | let title = descriptor.1 |
146 | 90 | let index = descriptor.0 |
147 | 90 | |
148 | 90 | if title == "---" { |
149 | 25 | menu.insertItem(CutBoxBaseMenuItem.separator(), at: index) |
150 | 90 | } else { |
151 | 65 | let axID = descriptor.2 |
152 | 65 | let action = Selector(descriptor.3!) |
153 | 65 | |
154 | 65 | let item: CutBoxBaseMenuItem = CutBoxBaseMenuItem( |
155 | 65 | title: title, |
156 | 65 | action: action, |
157 | 65 | keyEquivalent: "") |
158 | 65 | |
159 | 65 | item.target = self |
160 | 65 | if axID != nil { |
161 | 20 | item.setAccessibilityIdentifier(axID) |
162 | 65 | } |
163 | 65 | menu.insertItem(item, at: index) |
164 | 90 | } |
165 | 90 | } |
166 | | } |
167 | | |
168 | | extension CutBoxController { |
169 | 2 | func setupEventBindings() { |
170 | 2 | self.setHotKeyServiceEventBindings() |
171 | 2 | self.setSearchEventBindings() |
172 | 2 | self.setPreferencesEventBindings() |
173 | 2 | } |
174 | | |
175 | 2 | func setHotKeyServiceEventBindings() { |
176 | 2 | self.hotKeyService.events |
177 | 2 | .subscribe(onNext: onNext) $s15CutBoxUnitTests0aB10ControllerC29setHotKeyServiceEventBindingsyyFyAA0gH6EventsOcACcfu_ Line | Count | Source | 177 | 2 | .subscribe(onNext: onNext) |
Unexecuted instantiation: $s15CutBoxUnitTests0aB10ControllerC29setHotKeyServiceEventBindingsyyFyAA0gH6EventsOcACcfu_yAFcfu0_ |
178 | 2 | .disposed(by: disposeBag) |
179 | 2 | } |
180 | | |
181 | 2 | func setSearchEventBindings() { |
182 | 2 | self.searchViewController.events |
183 | 2 | .subscribe(onNext: onNext) $s15CutBoxUnitTests0aB10ControllerC22setSearchEventBindingsyyFyAA0G10ViewEventsOcACcfu_ Line | Count | Source | 183 | 2 | .subscribe(onNext: onNext) |
Unexecuted instantiation: $s15CutBoxUnitTests0aB10ControllerC22setSearchEventBindingsyyFyAA0G10ViewEventsOcACcfu_yAFcfu0_ |
184 | 2 | .disposed(by: disposeBag) |
185 | 2 | } |
186 | | |
187 | 2 | func setPreferencesEventBindings() { |
188 | 2 | self.prefs.events |
189 | 2 | .subscribe(onNext: onNext) $s15CutBoxUnitTests0aB10ControllerC27setPreferencesEventBindingsyyFyAA0abgH0OcACcfu_ Line | Count | Source | 189 | 2 | .subscribe(onNext: onNext) |
Unexecuted instantiation: $s15CutBoxUnitTests0aB10ControllerC27setPreferencesEventBindingsyyFyAA0abgH0OcACcfu_yAFcfu0_ |
190 | 2 | .disposed(by: disposeBag) |
191 | 2 | } |
192 | | } |
193 | | |
194 | | extension CutBoxController { |
195 | 1 | func onNext(event: HotKeyEvents) { |
196 | 1 | switch event { |
197 | 1 | case .search: |
198 | 1 | self.searchViewController.togglePopup() |
199 | 1 | } |
200 | 1 | } |
201 | | |
202 | 5 | func onNext(event: SearchViewEvents) { |
203 | 5 | switch event { |
204 | 5 | case .openPreferences: |
205 | 1 | self.preferencesController.open() |
206 | 5 | case .toggleSearchMode: |
207 | 1 | self.checkSearchModeItem() |
208 | 5 | case .setSearchMode(let mode): |
209 | 1 | self.checkSearchModeItem(mode.rawValue) |
210 | 5 | case .clearHistory: |
211 | 1 | self.clearHistoryClicked(nil) |
212 | 5 | case .selectJavascriptFunction: |
213 | 1 | if self.jsFuncSearchViewController.hasFuncs { |
214 | 0 | self.openJavascriptPopup() |
215 | 5 | } |
216 | 5 | default: |
217 | 0 | break |
218 | 5 | } |
219 | 5 | } |
220 | | |
221 | 4 | func onNext(event: CutBoxPreferencesEvent) { |
222 | 4 | switch event { |
223 | 4 | case .javascriptReloaded: |
224 | 0 | break |
225 | 4 | case .historyLimitChanged(let limit): |
226 | 1 | self.historyService.historyLimit = limit |
227 | 4 | case .compactUISettingChanged(let isOn): |
228 | 1 | self.useCompactUI.state = isOn ? .on : .off |
229 | 4 | case .hidePreviewSettingChanged(let isOn): |
230 | 1 | self.hidePreview.state = isOn ? .on : .off |
231 | 4 | case .historyClearByOffset(let offset): |
232 | 1 | self.clearHistoryByTimeOffset(offset: offset) |
233 | 4 | default: |
234 | 0 | break |
235 | 4 | } |
236 | 4 | } |
237 | | } |
238 | | |
239 | | extension CutBoxController { |
240 | 1 | func clearHistoryByTimeOffset(offset: TimeInterval) { |
241 | 1 | self.historyService.clearHistoryByTimeOffset(offset: offset) |
242 | 1 | } |
243 | | } |
244 | | |
245 | | extension CutBoxController { |
246 | 1 | func openJavascriptPopup() { |
247 | 1 | let items = self.searchViewController.selectedClips |
248 | 1 | self.jsFuncSearchViewController.selectedClips = items |
249 | 1 | |
250 | 1 | self.jsFuncSearchViewController |
251 | 1 | .jsFuncView |
252 | 1 | .applyTheme() |
253 | 1 | |
254 | 1 | self.jsFuncSearchViewController |
255 | 1 | .jsFuncPopup |
256 | 1 | .togglePopup() |
257 | 1 | } |
258 | | } |
259 | | |
260 | | extension CutBoxController { |
261 | 5 | func setCompactUIMenuItem() { |
262 | 5 | self.useCompactUI.title = "preferences_use_compact_ui".l7n |
263 | 5 | self.useCompactUI.state = self.prefs.useCompactUI ? .on : .off |
264 | 5 | } |
265 | | |
266 | 5 | func setHidePreviewMenuItem() { |
267 | 5 | self.hidePreview.title = "preferences_hide_preview".l7n |
268 | 5 | self.hidePreview.state = self.prefs.hidePreview ? .on : .off |
269 | 5 | } |
270 | | } |
271 | | |
272 | | extension CutBoxController { |
273 | 5 | func setModeSelectors(_ items: [NSMenuItem]) { |
274 | 5 | self.searchModeSelectorsDict = Dictionary( |
275 | 5 | uniqueKeysWithValues: HistorySearchMode |
276 | 20 | .allCases.map { $0.rawValue } |
277 | 20 | .map { ($0, items.find(axID: $0)) }) |
278 | 5 | |
279 | 5 | checkSearchModeItem( |
280 | 5 | HistoryService |
281 | 5 | .shared.searchMode.rawValue |
282 | 5 | ) |
283 | 5 | } |
284 | | |
285 | 1 | func searchModeSelectAxID(_ axID: String) { |
286 | 1 | let mode = HistorySearchMode.searchMode(from: axID) |
287 | 1 | self.searchViewController.events.onNext(.setSearchMode(mode)) |
288 | 1 | } |
289 | | |
290 | 2 | func checkSearchModeItem() { |
291 | 2 | let rawValue = historyService.searchMode.rawValue |
292 | 8 | self.searchModeSelectorsDict?.values.forEach { $0.state = .off } |
293 | 2 | self.searchModeSelectorsDict?[rawValue]?.state = .on |
294 | 2 | } |
295 | | |
296 | 6 | func checkSearchModeItem(_ axID: String) { |
297 | 24 | self.searchModeSelectorsDict?.values.forEach { $0.state = .off } |
298 | 6 | self.searchModeSelectorsDict?[axID]?.state = .on |
299 | 6 | } |
300 | | } |