import AppKit import Carbon import ServiceManagement class SettingsViewController: NSViewController, NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource, NSTableViewDelegate, PathsTableCellViewDelegate { private var recording = false // NOTE: This is the default shortcut. If you were to change it, don't // forget to change other places in this file and delegate, too. private var keyCode = Int(kVK_Space) private var modifiers = Int(optionKey) private var paths: [String] = [] // PERF: This is very slow to initialize because it creates a new process. This also cannot be done on a separate // thread. This sucks because the program now takes considerably longer to launch. private let dirPicker: NSOpenPanel = { let panel = NSOpenPanel() panel.message = "Select a directory to search applications in . . ." panel.canChooseDirectories = true panel.canChooseFiles = false panel.allowsMultipleSelection = false return panel }() private var shortcutsLabel: NSTextField = { let textField = NSTextField(labelWithString: "Shortcut") textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() private var aboutButton: NSButton = { let button = NSButton() button.image = systemImage("info.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemGray])) button.isBordered = false button.action = #selector(showAbout) button.sizeToFit() button.toolTip = "About" button.translatesAutoresizingMaskIntoConstraints = false return button }() private var ctrlButton: NSButton = { let button = NSButton() button.title = "⌃" button.action = #selector(handleModifiers) button.setButtonType(.pushOnPushOff) button.sizeToFit() button.bezelStyle = .rounded button.translatesAutoresizingMaskIntoConstraints = false return button }() private var cmdButton: NSButton = { let button = NSButton() button.title = "⌘" button.action = #selector(handleModifiers) button.setButtonType(.pushOnPushOff) button.sizeToFit() button.bezelStyle = .rounded button.translatesAutoresizingMaskIntoConstraints = false return button }() private var optButton: NSButton = { let button = NSButton() button.title = "⌥" button.action = #selector(handleModifiers) button.setButtonType(.pushOnPushOff) button.sizeToFit() button.bezelStyle = .rounded button.translatesAutoresizingMaskIntoConstraints = false return button }() private var shiftButton: NSButton = { let button = NSButton() button.title = "⇧" button.action = #selector(handleModifiers) button.setButtonType(.pushOnPushOff) button.sizeToFit() button.bezelStyle = .rounded button.translatesAutoresizingMaskIntoConstraints = false return button }() private var plusLabel: NSTextField = { let textField = NSTextField() textField.stringValue = "+" textField.isEditable = false textField.isBezeled = false textField.drawsBackground = false textField.alignment = .center textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() private var recordButton: KeyDetectorButton = { let button = KeyDetectorButton() button.title = "Record" button.sizeToFit() button.bezelStyle = .rounded button.translatesAutoresizingMaskIntoConstraints = false return button }() private var pathsLabel: NSTextField = { let textField = NSTextField(labelWithString: "Application Directories") textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() private var tableScrollView: NSScrollView = { let scroll = NSScrollView() scroll.drawsBackground = false scroll.translatesAutoresizingMaskIntoConstraints = false return scroll }() private var pathsTableView: NSTableView = { let table = NSTableView() table.backgroundColor = .clear table.doubleAction = #selector(editItem) table.headerView = nil table.allowsMultipleSelection = false table.allowsColumnReordering = false table.allowsColumnResizing = false table.allowsColumnSelection = false table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Paths"))) table.translatesAutoresizingMaskIntoConstraints = false return table }() private var pathsControl: NSSegmentedControl = { let control = NSSegmentedControl() control.segmentCount = 2 control.segmentStyle = .roundRect control.setImage(NSImage(systemSymbolName: "plus", accessibilityDescription: nil), forSegment: 0) control.setImage(NSImage(systemSymbolName: "minus", accessibilityDescription: nil), forSegment: 1) control.setToolTip("Add Path", forSegment: 0) control.setToolTip("Remove Path", forSegment: 1) control.trackingMode = .momentary control.translatesAutoresizingMaskIntoConstraints = false return control }() private var launchAtLoginLabel: NSTextField = { let textField = NSTextField(labelWithString: "Launch at login") textField.translatesAutoresizingMaskIntoConstraints = false return textField }() private var launchAtLoginToggle: NSSegmentedControl = { let control = NSSegmentedControl() control.segmentCount = 2 control.segmentStyle = .roundRect control.setLabel("Off", forSegment: 0) control.setLabel("On", forSegment: 1) control.setToolTip("Off", forSegment: 0) control.setToolTip("On", forSegment: 1) control.translatesAutoresizingMaskIntoConstraints = false return control }() private var resetAllButton: NSButton = { let button = NSButton() button.title = "Reset" button.action = #selector(reset) button.setButtonType(.momentaryLight) button.sizeToFit() button.bezelStyle = .rounded button.isBordered = false button.translatesAutoresizingMaskIntoConstraints = false return button }() private func addSubviews() { view.addSubview(shortcutsLabel) view.addSubview(aboutButton) view.addSubview(ctrlButton) view.addSubview(cmdButton) view.addSubview(optButton) view.addSubview(shiftButton) view.addSubview(plusLabel) view.addSubview(recordButton) view.addSubview(pathsLabel) view.addSubview(tableScrollView) view.addSubview(pathsControl) view.addSubview(launchAtLoginLabel) view.addSubview(launchAtLoginToggle) view.addSubview(resetAllButton) } private func setConstraints() { NSLayoutConstraint.activate([ shortcutsLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10), shortcutsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), aboutButton.firstBaselineAnchor.constraint(equalTo: shortcutsLabel.firstBaselineAnchor), aboutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10), ctrlButton.topAnchor.constraint(equalTo: shortcutsLabel.bottomAnchor, constant: ViewConstants.spacing10), ctrlButton.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor), cmdButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), cmdButton.leadingAnchor.constraint(equalTo: ctrlButton.trailingAnchor, constant: ViewConstants.spacing5), optButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), optButton.leadingAnchor.constraint(equalTo: cmdButton.trailingAnchor, constant: ViewConstants.spacing5), shiftButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), shiftButton.leadingAnchor.constraint(equalTo: optButton.trailingAnchor, constant: ViewConstants.spacing5), plusLabel.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), plusLabel.leadingAnchor.constraint(equalTo: shiftButton.trailingAnchor, constant: ViewConstants.spacing5), recordButton.widthAnchor.constraint(equalToConstant: 40), recordButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), recordButton.leadingAnchor.constraint(equalTo: plusLabel.trailingAnchor, constant: ViewConstants.spacing5), pathsLabel.topAnchor.constraint(equalTo: ctrlButton.bottomAnchor, constant: ViewConstants.spacing20), pathsLabel.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor), tableScrollView.widthAnchor.constraint(equalToConstant: 350), tableScrollView.heightAnchor.constraint(equalToConstant: 150), tableScrollView.topAnchor.constraint(equalTo: pathsLabel.bottomAnchor), tableScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), pathsControl.topAnchor.constraint(equalTo: tableScrollView.bottomAnchor, constant: ViewConstants.spacing10), pathsControl.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), launchAtLoginLabel.topAnchor.constraint(equalTo: pathsControl.bottomAnchor, constant: ViewConstants.spacing10), launchAtLoginLabel.trailingAnchor.constraint(equalTo: launchAtLoginToggle.leadingAnchor, constant: -ViewConstants.spacing10), launchAtLoginToggle.firstBaselineAnchor.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor), launchAtLoginToggle.trailingAnchor.constraint(equalTo: resetAllButton.leadingAnchor, constant: -ViewConstants.spacing15), resetAllButton.firstBaselineAnchor.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor), resetAllButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10), resetAllButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing10), ]) } override func viewDidLoad() { super.viewDidLoad() tableScrollView.documentView = pathsTableView cmdButton.target = self optButton.target = self ctrlButton.target = self shiftButton.target = self recordButton.delegate = self launchAtLoginLabel.target = self resetAllButton.target = self recordButton.defaultKey = kVK_Space recordButton.target = self pathsTableView.dataSource = self pathsTableView.delegate = self pathsControl.target = self pathsControl.action = #selector(affectPaths(_:)) pathsControl.target = self pathsControl.action = #selector(affectPaths(_:)) launchAtLoginToggle.target = self launchAtLoginToggle.action = #selector(affectLaunchAtLogin(_:)) addSubviews() setConstraints() } override func viewWillAppear() { super.viewWillAppear() // Fetch the saved key codes and modifiers. if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int { keyCode = code } if let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int { modifiers = mods } loadPaths() syncModifierButtons() launchAtLoginStatus() } override func viewDidAppear() { super.viewDidAppear() } override func viewWillDisappear() { super.viewWillDisappear() HotKeyManager.shared.registerHotKey(key: keyCode, modifiers: modifiers) UserDefaults.standard.set(keyCode, forKey: "keyCode") UserDefaults.standard.set(modifiers, forKey: "keyModifiers") // Merge PathManagers paths and user paths. // WARNING: This seems a bit error prone. for path in paths { if !PathManager.shared.contains(path) { PathManager.shared.addPath(path) } } for path in PathManager.shared.paths { if !paths.contains(path.key) { PathManager.shared.removePath(path.key) } } PathManager.shared.updateIndex() PathManager.shared.savePaths() } override func loadView() { self.view = NSView() } @objc private func showAbout() { delegate.showAboutWindow() } @objc private func handleModifiers() { // Revert to default modifier if none of the modifier buttons are on. if cmdButton.state != .on, optButton.state != .on, ctrlButton.state != .on, shiftButton.state != .on { optButton.state = .on } detectModifers() } @objc private func launchAtLogin(isOn status: Bool) { delegate.toggleLaunchAtLogin(isOn: status) launchAtLoginStatus() } private func launchAtLoginStatus() { if delegate.willLaunchAtLogin() { launchAtLoginToggle.setSelected(true, forSegment: 1) } else { launchAtLoginToggle.setSelected(true, forSegment: 0) } } @objc private func reset() { keyCode = Int(kVK_Space) modifiers = Int(optionKey) HotKeyManager.shared.registerHotKey(key: keyCode, modifiers: modifiers) UserDefaults.standard.set(keyCode, forKey: "keyCode") UserDefaults.standard.set(modifiers, forKey: "keyModifiers") syncModifierButtons() PathManager.shared.resetPaths() loadPaths() } private func loadPaths() { paths = [] for path in PathManager.shared.paths { paths.append(path.key) } paths.sort() pathsTableView.reloadData() } private func detectModifers() { var mods = 0 if cmdButton.state == .on { mods |= cmdKey } if optButton.state == .on { mods |= optionKey } if ctrlButton.state == .on { mods |= controlKey } if shiftButton.state == .on { mods |= shiftKey } if mods == 0 { mods |= optionKey } else { modifiers = mods } } private func syncModifierButtons() { ctrlButton.state = .off cmdButton.state = .off optButton.state = .off shiftButton.state = .off if modifiers & controlKey != 0 { ctrlButton.state = .on } if modifiers & cmdKey != 0 { cmdButton.state = .on } if modifiers & optionKey != 0 { optButton.state = .on } if modifiers & shiftKey != 0 { shiftButton.state = .on } if let character = keyName(virtualKeyCode: UInt16(keyCode)) { recordButton.title = character } } @objc private func affectPaths(_ sender: NSSegmentedControl) { let selectedSegment = sender.selectedSegment switch selectedSegment { case 0: let row = paths.count paths.append("") pathsTableView.insertRows(at: IndexSet(integer: row), withAnimation: []) pathsTableView.scrollRowToVisible(row) pathsTableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) (pathsTableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? PathsTableCellView)?.startEditing() break case 1: if pathsTableView.selectedRow > -1 { paths.remove(at: pathsTableView.selectedRow) pathsTableView.reloadData() } break default: break } } @objc private func affectLaunchAtLogin(_ sender: NSSegmentedControl) { let selectedSegment = sender.selectedSegment switch selectedSegment { case 0: launchAtLogin(isOn: false) break case 1: launchAtLogin(isOn: true) break default: break } } @objc private func editItem(_ sender: NSTableView) { pathsTableView.deselectAll(nil) pathsTableView.selectRowIndexes(IndexSet(integer: pathsTableView.clickedRow), byExtendingSelection: false) if let cell = pathsTableView.view(atColumn: 0, row: pathsTableView.clickedRow, makeIfNecessary: false) as? PathsTableCellView { cell.startEditing() } } func titleFieldFinishedEditing(tag: Int, text: String) { if text.isEmpty { paths.remove(at: tag) } else { paths[tag] = text } pathsTableView.reloadData() } func titleFieldTextChanged(tag: Int, text: String) { } func keyWasSet(to keyCode: Int) { self.keyCode = Int(keyCode) } func selectionButtonClicked(tag: Int) { NSRunningApplication.current.activate(options: .activateAllWindows) delegate.window.level = .normal delegate.aboutWindow.performClose(nil) if dirPicker.runModal() == .OK { if let url = dirPicker.url { paths[tag] = url.path pathsTableView.reloadData() } } delegate.window.level = .statusBar delegate.window.makeKeyAndOrderFront(nil) if let controller = delegate.window.contentViewController as? SearchViewController { controller.openSettings() } } func numberOfRows(in tableView: NSTableView) -> Int { return paths.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let rect = NSRect(x: 0, y: 0, width: tableColumn!.width, height: 20) let cell = PathsTableCellView(frame: rect) cell.titleField.stringValue = paths[row] cell.delegate = self cell.id = row return cell } }