CutBox.app

Coverage Report

Created: 2024-03-12 03:40

.../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
}