Files
Grapp/src/SettingsViewController.swift
2025-02-08 15:07:59 -08:00

544 lines
19 KiB
Swift

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