import AppKit import Carbon import ServiceManagement import OSLog class SettingsViewController: NSViewController, NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource, NSTableViewDelegate, MyTableCellViewDelegate { fileprivate static let logger = Logger( subsystem: Bundle.main.bundleIdentifier!, category: String(describing: SettingsViewController.self) ) 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) // NOTE: PERF: This is very slow to initialize because it creates a // 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 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 = true table.allowsColumnReordering = false table.allowsColumnResizing = false table.allowsColumnSelection = false table.addTableColumn(NSTableColumn( identifier: NSUserInterfaceItemIdentifier("Paths"))) //rowHeight cgfloat must see doc 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 launchAtLoginButton: NSButton = { let button = NSButton() button.title = "Launch at login - OFF" button.action = #selector(launchAtLogin) button.setButtonType(.toggle) button.sizeToFit() button.bezelStyle = .rounded button.isBordered = false button.translatesAutoresizingMaskIntoConstraints = false return button }() 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(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(launchAtLoginButton) 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), 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: 100), 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), launchAtLoginButton.topAnchor.constraint( equalTo: pathsControl.bottomAnchor, constant: ViewConstants.spacing10), launchAtLoginButton.trailingAnchor.constraint( equalTo: resetAllButton.leadingAnchor, constant: -ViewConstants.spacing10), resetAllButton.centerYAnchor.constraint( equalTo: launchAtLoginButton.centerYAnchor), 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 launchAtLoginButton.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(_:)) addSubviews() setConstraints() } override func viewWillAppear() { super.viewWillAppear() // PERF: Maybe we shouldn't fetch it on every appearance? // Only do it in AppDelegate? if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int { keyCode = code } if let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int { modifiers = mods } syncModifierButtons() launchAtLoginStatus() } override func viewDidAppear() { super.viewDidAppear() self.view.window?.center() } override func viewWillDisappear() { super.viewWillDisappear() HotKeyManager.shared.registerHotKey(key: keyCode, modifiers: modifiers) UserDefaults.standard.set(keyCode, forKey: "keyCode") UserDefaults.standard.set(modifiers, forKey: "keyModifiers") PathManager.shared.savePaths() PathManager.shared.rebuildIndex() } override func loadView() { self.view = NSView() } @objc private func handleModifiers() { // NOTE: 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() { delegate.toggleLaunchAtLogin() launchAtLoginStatus() } private func launchAtLoginStatus() { if delegate.willLaunchAtLogin() { launchAtLoginButton.title = "Launch at login - ON" launchAtLoginButton.state = .on } else { launchAtLoginButton.title = "Launch at login - OFF" launchAtLoginButton.state = .off } } @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.reset() 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) { // PERF: All of this could be written better. let selectedSegment = sender.selectedSegment switch selectedSegment { case 0: PathManager.shared.addPath("") pathsTableView.reloadData() let row = PathManager.shared.userPaths.count-1 pathsTableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) pathsTableView.scrollRowToVisible(row) (pathsTableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? MyTableCellView)? .startEditing() break case 1: var toRemove: [String] = [] for row in pathsTableView.selectedRowIndexes { toRemove.append(PathManager.shared.userPaths[row]) } PathManager.shared.userPaths.removeAll( where: { toRemove.contains($0) }) pathsTableView.reloadData() 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? MyTableCellView { cell.startEditing() } } func titleFieldFinishedEditing(tag: Int, text: String) { PathManager.shared.userPaths[tag] = text if PathManager.shared.userPaths[tag].isEmpty { PathManager.shared.userPaths.remove(at: tag) pathsTableView.reloadData() } pathsTableView.deselectAll(nil) } 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 if dirPicker.runModal() == .OK { if let url = dirPicker.url { PathManager.shared.userPaths[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 PathManager.shared.userPaths.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 = MyTableCellView(frame: rect) cell.titleField.stringValue = PathManager.shared.userPaths[row] cell.delegate = self cell.id = row return cell } func tableViewSelectionDidChange(_ notification: Notification) { /* let selectedRow = tableView.selectedRow if selectedRow >= 0 { print("Selected: \(items[selectedRow])") } */ } }