# CutBox.app

## Coverage Report

#### Created: 2024-03-12 03:40

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

    }