Thou shalt not cross 80 columns in thy file.

This commit is contained in:
2025-03-20 13:06:24 -07:00
parent 1a3e52efc8
commit ef4d469941
16 changed files with 640 additions and 263 deletions
+193 -77
View File
@@ -6,7 +6,10 @@ fileprivate let windowCornerRadius = 15.0
fileprivate let maxItems = 20
class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDelegate, NSTableViewDataSource, NSTableViewDelegate {
class SearchViewController: NSViewController, NSTextFieldDelegate,
NSPopoverDelegate, NSTableViewDataSource,
NSTableViewDelegate
{
private var keyboardEvents: EventMonitor?
private var listIndex = 0
@@ -46,7 +49,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
private var contentView: NSView = {
let view = NSView()
// Clip all content to window's rounded frame emulated by backgroundView.
// Clip all content to window's rounded frame emulated by
// backgroundView.
view.wantsLayer = true
view.layer?.masksToBounds = true
view.layer?.cornerRadius = windowCornerRadius
@@ -64,14 +68,18 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
textField.focusRingType = .none
textField.placeholderString = "Program Search"
textField.bezelStyle = .roundedBezel
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .largeTitle).pointSize, weight: .medium)
textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .largeTitle).pointSize,
weight: .medium)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var settingsButton: NSButton = {
let button = NSButton()
button.image = systemImage("gear.circle.fill", .title1, .large, .init(paletteColors: [.white, .systemGray]))
button.image = systemImage("gear.circle.fill", .title1, .large,
.init(paletteColors: [.white, .systemGray]))
button.isBordered = false
button.action = #selector(openSettings)
button.sizeToFit()
@@ -83,7 +91,9 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
private var tableScrollView: NSScrollView = {
let scroll = NSScrollView()
scroll.automaticallyAdjustsContentInsets = false
scroll.contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: ViewConstants.spacing10, right: 0)
scroll.contentInsets = NSEdgeInsets(top: 0, left: 0,
bottom: ViewConstants.spacing10,
right: 0)
scroll.drawsBackground = false
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
@@ -102,7 +112,11 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
table.allowsColumnReordering = false
table.allowsColumnResizing = false
table.allowsColumnSelection = false
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Program")))
table.addTableColumn(
NSTableColumn(
identifier: NSUserInterfaceItemIdentifier("Program")
)
)
table.doubleAction = #selector(tableDoubleClick)
@@ -123,47 +137,83 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
var tableViewHeightAnchor: NSLayoutConstraint?
private func setConstraints() {
tableViewHeightAnchor = tableScrollView.heightAnchor.constraint(equalToConstant: 0)
tableViewHeightAnchor = tableScrollView.heightAnchor
.constraint(equalToConstant: 0)
tableViewHeightAnchor?.isActive = true
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: -100),
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 100),
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -100),
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 100),
view.topAnchor
.constraint(equalTo: contentView.topAnchor, constant: -100),
view.bottomAnchor
.constraint(equalTo: contentView.bottomAnchor,
constant: 100),
view.leadingAnchor
.constraint(equalTo: contentView.leadingAnchor,
constant: -100),
view.trailingAnchor
.constraint(equalTo: contentView.trailingAnchor,
constant: 100),
shadowView.topAnchor.constraint(equalTo: backgroundView.topAnchor),
shadowView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor),
shadowView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
shadowView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
shadowView.topAnchor
.constraint(equalTo: backgroundView.topAnchor),
shadowView.bottomAnchor
.constraint(equalTo: backgroundView.bottomAnchor),
shadowView.leadingAnchor
.constraint(equalTo: backgroundView.leadingAnchor),
shadowView.trailingAnchor
.constraint(equalTo: backgroundView.trailingAnchor),
backgroundView.topAnchor.constraint(equalTo: contentView.topAnchor),
backgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
backgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
backgroundView.topAnchor
.constraint(equalTo: contentView.topAnchor),
backgroundView.bottomAnchor
.constraint(equalTo: contentView.bottomAnchor),
backgroundView.leadingAnchor
.constraint(equalTo: contentView.leadingAnchor),
backgroundView.trailingAnchor
.constraint(equalTo: contentView.trailingAnchor),
searchInput.widthAnchor.constraint(equalToConstant: 400),
searchInput.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ViewConstants.spacing10),
searchInput.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: ViewConstants.spacing15),
searchInput.widthAnchor
.constraint(equalToConstant: 400),
searchInput.topAnchor
.constraint(equalTo: contentView.topAnchor,
constant: ViewConstants.spacing10),
searchInput.leadingAnchor
.constraint(equalTo: contentView.leadingAnchor,
constant: ViewConstants.spacing15),
settingsButton.centerYAnchor.constraint(equalTo: searchInput.centerYAnchor),
settingsButton.leadingAnchor.constraint(equalTo: searchInput.trailingAnchor, constant: ViewConstants.spacing5),
settingsButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -ViewConstants.spacing10),
settingsButton.centerYAnchor
.constraint(equalTo: searchInput.centerYAnchor),
settingsButton.leadingAnchor
.constraint(equalTo: searchInput.trailingAnchor,
constant: ViewConstants.spacing5),
settingsButton.trailingAnchor
.constraint(equalTo: contentView.trailingAnchor,
constant: -ViewConstants.spacing10),
tableScrollView.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: ViewConstants.spacing10),
tableScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
tableScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
tableScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
tableScrollView.topAnchor
.constraint(equalTo: searchInput.bottomAnchor,
constant: ViewConstants.spacing10),
tableScrollView.bottomAnchor
.constraint(equalTo: contentView.bottomAnchor),
tableScrollView.leadingAnchor
.constraint(equalTo: contentView.leadingAnchor),
tableScrollView.trailingAnchor
.constraint(equalTo: contentView.trailingAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
// NOTE: This needs removeObserver on deinit? Well, technically we don't care because this view controller will exist throughout
// the whole life of the program. When the program gets killed, the OS will clear this.
DistributedNotificationCenter.default.addObserver(self, selector: #selector(osThemeChanged(sender:)),
name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil)
// NOTE: This needs removeObserver on deinit? Well, technically we
// don't care because this view controller will exist
// throughout the whole life of the program. When the
// program gets killed, the OS will clear this.
DistributedNotificationCenter.default
.addObserver(self, selector: #selector(osThemeChanged(sender:)),
name: NSNotification
.Name(rawValue: "AppleInterfaceThemeChangedNotification"),
object: nil)
// Initialize an array of programs and reusable cells.
for i in 0..<maxItems {
@@ -179,47 +229,70 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.clear.cgColor
keyboardEvents = LocalEventMonitor(mask: [.keyDown]) { [weak self] event in
keyboardEvents = LocalEventMonitor(mask: [.keyDown])
{ [weak self] event in
let key = event.keyCode
let modifiers = event.modifierFlags.rawValue
if let controller = self {
if modsContains(keys: OSCtrl, in: modifiers) && key == kVK_ANSI_P ||
if modsContains(keys: OSCtrl, in: modifiers) &&
key == kVK_ANSI_P ||
modsContainsNone(in: modifiers) && key == kVK_UpArrow
{
controller.programsTableViewSelection -= 1
} else if modsContains(keys: OSCtrl, in: modifiers) && key == kVK_ANSI_N ||
modsContainsNone(in: modifiers) && key == kVK_DownArrow
} else if modsContains(keys: OSCtrl, in: modifiers) &&
key == kVK_ANSI_N ||
modsContainsNone(in: modifiers) && key == kVK_DownArrow
{
controller.programsTableViewSelection += 1
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) && key == kVK_ANSI_P ||
modsContains(keys: OSCmd, in: modifiers) && key == kVK_UpArrow
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) &&
key == kVK_ANSI_P ||
modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_UpArrow
{
controller.programsTableViewSelection = 0
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) && key == kVK_ANSI_N ||
modsContains(keys: OSCmd, in: modifiers) && key == kVK_DownArrow
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) &&
key == kVK_ANSI_N ||
modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_DownArrow
{
controller.programsTableViewSelection = controller.listIndex-1
} else if modsContains(keys: OSCmd, in: modifiers) && isNumericalCode(key) {
if key == kVK_ANSI_1 { controller.programsTableViewSelection = 0 }
if key == kVK_ANSI_2 { controller.programsTableViewSelection = 1 }
if key == kVK_ANSI_3 { controller.programsTableViewSelection = 2 }
if key == kVK_ANSI_4 { controller.programsTableViewSelection = 3 }
if key == kVK_ANSI_5 { controller.programsTableViewSelection = 4 }
if key == kVK_ANSI_6 { controller.programsTableViewSelection = 5 }
if key == kVK_ANSI_7 { controller.programsTableViewSelection = 6 }
if key == kVK_ANSI_8 { controller.programsTableViewSelection = 7 }
if key == kVK_ANSI_9 { controller.programsTableViewSelection = 8 }
} else if modsContains(keys: OSCmd, in: modifiers) &&
isNumericalCode(key)
{
if key == kVK_ANSI_1
{ controller.programsTableViewSelection = 0 }
if key == kVK_ANSI_2
{ controller.programsTableViewSelection = 1 }
if key == kVK_ANSI_3
{ controller.programsTableViewSelection = 2 }
if key == kVK_ANSI_4
{ controller.programsTableViewSelection = 3 }
if key == kVK_ANSI_5
{ controller.programsTableViewSelection = 4 }
if key == kVK_ANSI_6
{ controller.programsTableViewSelection = 5 }
if key == kVK_ANSI_7
{ controller.programsTableViewSelection = 6 }
if key == kVK_ANSI_8
{ controller.programsTableViewSelection = 7 }
if key == kVK_ANSI_9
{ controller.programsTableViewSelection = 8 }
}
if controller.programsTableViewSelection > controller.listIndex-1 {
controller.programsTableViewSelection = controller.listIndex-1
if controller.programsTableViewSelection >
controller.listIndex-1
{
controller.programsTableViewSelection =
controller.listIndex-1
} else if controller.programsTableViewSelection < 0 {
controller.programsTableViewSelection = 0
}
let select = controller.programsTableViewSelection
self?.programsTableView.selectRowIndexes(IndexSet(integer: select), byExtendingSelection: false)
self?.programsTableView
.selectRowIndexes(IndexSet(integer: select),
byExtendingSelection: false)
self?.programsTableView.scrollRowToVisible(select)
}
@@ -246,7 +319,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
view.window?.makeFirstResponder(searchInput)
// searchInput should select all text whenever window appears.
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil,
from: self)
}
override func viewWillAppear() {
@@ -267,8 +341,12 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
func centerWindow() {
if let win = view.window, let scrn = NSScreen.main {
let x = (scrn.visibleFrame.origin.x + scrn.visibleFrame.size.width / 2) - (win.frame.size.width / 2)
let y = (scrn.visibleFrame.origin.y + scrn.visibleFrame.size.height * 0.9) - win.frame.size.height
let x = (scrn.visibleFrame.origin.x +
scrn.visibleFrame.size.width / 2) -
(win.frame.size.width / 2)
let y = (scrn.visibleFrame.origin.y +
scrn.visibleFrame.size.height * 0.9) -
win.frame.size.height
view.window?.setFrameOrigin(NSPoint(x: x, y: y))
}
}
@@ -284,7 +362,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
@objc
func openSettings() {
settingsPopover.show(relativeTo: settingsButton.bounds, of: settingsButton, preferredEdge: .maxY)
settingsPopover.show(relativeTo: settingsButton.bounds,
of: settingsButton, preferredEdge: .maxY)
}
@objc
@@ -294,7 +373,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
}
private func openProgram(_ program: Program) {
let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext)
let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext)
let config = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: url, configuration: config)
@@ -306,7 +386,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
}
func controlTextDidChange(_ obj: Notification) {
guard let searchInput = obj.object as? EditableNSTextField else { return }
guard let searchInput =
obj.object as? EditableNSTextField else { return }
listIndex = 0
if !searchInput.stringValue.isEmpty {
@@ -315,11 +396,20 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
if listIndex >= maxItems { break outerloop }
let prog = path.value[i]
if prog.name.lowercased().contains(searchInput.stringValue.lowercased()) {
if prog.name.lowercased()
.contains(searchInput.stringValue.lowercased())
{
programsList[listIndex].path = prog.path
programsList[listIndex].name = prog.name
programsList[listIndex].ext = prog.ext
programsList[listIndex].img = NSWorkspace.shared.icon(forFile: URL(fileURLWithPath: prog.path).appendingPathComponent(prog.name+prog.ext).path)
programsList[listIndex].ext = prog.ext
programsList[listIndex].img =
NSWorkspace.shared
.icon(forFile: URL(
fileURLWithPath: prog.path
)
.appendingPathComponent(
prog.name+prog.ext).path
)
listIndex += 1
}
}
@@ -328,25 +418,37 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
reloadProgramsTableViewData()
programsTableViewSelection = 0
programsTableView.selectRowIndexes(IndexSet(integer: programsTableViewSelection), byExtendingSelection: false)
programsTableView.selectRowIndexes(
IndexSet(integer: programsTableViewSelection),
byExtendingSelection: false
)
programsTableView.scrollRowToVisible(programsTableViewSelection)
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool
{
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
if listIndex > 0 {
let program = programsList[programsTableViewSelection]
openProgram(program)
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
to: nil, from: self)
}
return true
} else if commandSelector == #selector(NSResponder.insertTab(_:)) {
return true
} else if commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.moveDown(_:)) {
// Ignore arrows up and down because we use those to navigate the programs list.
} else if commandSelector == #selector(NSResponder.moveUp(_:)) ||
commandSelector == #selector(NSResponder.moveDown(_:))
{
// Ignore arrows up and down because we use those to navigate
// the programs list.
return true
} else if commandSelector == #selector(NSResponder.moveToBeginningOfDocument(_:)) {
// Ignore command plus up and down arrows because we use those to move to the beginning and end of the list.
} else if commandSelector ==
#selector(NSResponder.moveToBeginningOfDocument(_:))
{
// Ignore command plus up and down arrows because we use those
// to move to the beginning and end of the list.
return true
}
@@ -365,18 +467,28 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
return listIndex
}
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
func tableView(_ tableView: NSTableView,
rowViewForRow row: Int) -> NSTableRowView?
{
return ProgramsTableRowView()
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
func tableView(_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?,
row: Int) -> NSView?
{
let cell = programsListCells[row]
let program = programsList[row]
let app = program.name + program.ext
let rangeToHighlight = (app.lowercased() as NSString).range(of: searchInput.stringValue.lowercased())
let rangeToHighlight =
(app.lowercased() as NSString).range(
of: searchInput.stringValue.lowercased()
)
let attributedString = NSMutableAttributedString(string: app)
attributedString.addAttributes([.foregroundColor: NSColor.labelColor], range: rangeToHighlight)
attributedString
.addAttributes([.foregroundColor: NSColor.labelColor],
range: rangeToHighlight)
cell.titleField.attributedStringValue = attributedString
cell.progPathLabel.stringValue = program.path
@@ -397,10 +509,14 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
}
@objc func updateViewsBasedOnOSTheme() {
if NSApp.windows.first?.effectiveAppearance.bestMatch(from: [.darkAqua, .vibrantDark]) == .darkAqua { // dark
backgroundView.layer?.borderColor = NSColor.white.withAlphaComponent(0.2).cgColor
if NSApp.windows.first?.effectiveAppearance
.bestMatch(from: [.darkAqua, .vibrantDark]) == .darkAqua
{ // dark
backgroundView.layer?.borderColor =
NSColor.white.withAlphaComponent(0.2).cgColor
} else { // light
backgroundView.layer?.borderColor = NSColor.black.withAlphaComponent(0.2).cgColor
backgroundView.layer?.borderColor =
NSColor.black.withAlphaComponent(0.2).cgColor
}
}
}