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

View File

@@ -9,21 +9,22 @@ fileprivate enum AboutLinks {
} }
enum Strings { enum Strings {
static let copyright = "Copyright © 2024\nGarikMI. All rights reserved." static let copyright = "Copyright © 2024\nGarikMI. All rights reserved."
static let evaluationTitle = "License - Evaluation" static let evaluationTitle = "License - Evaluation"
static let evaluationMessage = "You are currently using evaluation license. CmdBar will quit after 20 minutes. If you already own a license, enter it below or purchase a license." static let evaluationMessage = "You are currently using evaluation license. CmdBar will quit after 20 minutes. If you already own a license, enter it below or purchase a license."
static let activate = "Activate" static let activate = "Activate"
static let proTitle = "License - Activated" static let proTitle = "License - Activated"
static let proMessage = "Thank you for purchasing CmdBar! Enjoy!" static let proMessage = "Thank you for purchasing CmdBar! Enjoy!"
static let deactivate = "Deactivate" static let deactivate = "Deactivate"
static let activating = "Activating..." static let activating = "Activating..."
} }
class AboutViewController: NSViewController, NSTextFieldDelegate { class AboutViewController: NSViewController, NSTextFieldDelegate {
private var appIconImage: NSImageView = { private var appIconImage: NSImageView = {
//let image = NSImageView(image: NSApp.applicationIconImage) //let image = NSImageView(image: NSApp.applicationIconImage)
let image = NSImageView() let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) image.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -31,25 +32,35 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
private var appNameLabel: NSTextField = { private var appNameLabel: NSTextField = {
let textField = NSTextField() let textField = NSTextField()
textField.stringValue = (Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "NOT FOUND" textField.stringValue =
(Bundle.main.infoDictionary?["CFBundleName"] as? String)
??
"NOT FOUND"
textField.isEditable = false textField.isEditable = false
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor .preferredFontDescriptor(forTextStyle: .title1).pointSize, weight: .bold) textField.font =
NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .title1).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var versionLabel: NSTextField = { private var versionLabel: NSTextField = {
let textField = NSTextField() let textField = NSTextField()
textField.stringValue = "Version \((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "-.--")" textField.stringValue =
"Version \((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "-.--")"
textField.isEditable = false textField.isEditable = false
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.textColor = NSColor.systemGray textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular) textField.font =
NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -64,7 +75,10 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.textColor = NSColor.systemGray textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular) textField.font =
NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -152,44 +166,77 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
// App image. // App image.
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 100), appIconImage.widthAnchor.constraint(equalToConstant: 100),
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1), appIconImage.heightAnchor
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20), .constraint(equalTo: appIconImage.widthAnchor,
appIconImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), multiplier: 1),
appIconImage.topAnchor
.constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing20),
appIconImage.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
]) ])
// Title // Title
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appNameLabel.topAnchor.constraint(equalTo: appIconImage.bottomAnchor, constant: ViewConstants.spacing20), appNameLabel.topAnchor
appNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: appIconImage.bottomAnchor,
constant: ViewConstants.spacing20),
appNameLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: ViewConstants.spacing2), versionLabel.topAnchor
versionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: appNameLabel.bottomAnchor,
constant: ViewConstants.spacing2),
versionLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
copyrightLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: ViewConstants.spacing10), copyrightLabel.topAnchor
copyrightLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), .constraint(equalTo: versionLabel.bottomAnchor,
constant: ViewConstants.spacing10),
copyrightLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
]) ])
// Buttons // Buttons
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
buttonsContainer.topAnchor .constraint(equalTo: copyrightLabel.bottomAnchor, constant: ViewConstants.spacing20), buttonsContainer.topAnchor
buttonsContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing20), .constraint(equalTo: copyrightLabel.bottomAnchor,
buttonsContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), constant: ViewConstants.spacing20),
buttonsContainer.bottomAnchor
.constraint(equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing20),
buttonsContainer.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
authorButton.topAnchor.constraint(equalTo: buttonsContainer.topAnchor), authorButton.topAnchor
authorButton.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor), .constraint(equalTo: buttonsContainer.topAnchor),
authorButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor), authorButton.bottomAnchor
authorButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor), .constraint(equalTo: buttonsContainer.bottomAnchor),
authorButton.leadingAnchor
.constraint(equalTo: buttonsContainer.leadingAnchor),
authorButton.trailingAnchor
.constraint(equalTo: buttonsContainer.trailingAnchor),
// privacyButton.topAnchor.constraint(equalTo: buttonsContainer.topAnchor), // privacyButton.topAnchor
// privacyButton.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor), // .constraint(equalTo: buttonsContainer.topAnchor),
// privacyButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor), // privacyButton.bottomAnchor
// .constraint(equalTo: buttonsContainer.bottomAnchor),
// privacyButton.leadingAnchor
// .constraint(equalTo: buttonsContainer.leadingAnchor),
// //
// documentationButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor), // documentationButton.firstBaselineAnchor
// documentationButton.leadingAnchor.constraint(equalTo: privacyButton.trailingAnchor,constant: ViewConstants.spacing10), // .constraint(equalTo: privacyButton.firstBaselineAnchor),
// documentationButton.leadingAnchor
// .constraint(equalTo: privacyButton.trailingAnchor,
// constant: ViewConstants.spacing10),
// //
// websiteButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor), // websiteButton.firstBaselineAnchor
// websiteButton.leadingAnchor.constraint(equalTo: documentationButton.trailingAnchor,constant: ViewConstants.spacing10), // .constraint(equalTo: privacyButton.firstBaselineAnchor),
// websiteButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor), // websiteButton.leadingAnchor
// .constraint(equalTo: documentationButton.trailingAnchor,
// constant: ViewConstants.spacing10),
// websiteButton.trailingAnchor
// .constraint(equalTo: buttonsContainer.trailingAnchor),
]) ])
} }

View File

@@ -15,21 +15,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
window.delegate = self window.delegate = self
// NOTE: Here we check wether the program was launched by the system. If it was not, then display the window. // NOTE: Here we check wether the program was launched by the
// system. If it was not, then display the window.
if let event = NSAppleEventManager.shared().currentAppleEvent, if let event = NSAppleEventManager.shared().currentAppleEvent,
!(event.eventID == kAEOpenApplication && event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem) !(event.eventID == kAEOpenApplication &&
event.paramDescriptor(forKeyword: keyAEPropData)?
.enumCodeValue == keyAELaunchedAsLogInItem)
{ {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
HotKeyManager.shared.handler = { (inHandlerCallRef, inEvent, inUserData) -> OSStatus in HotKeyManager.shared.handler =
if let delegate = NSApplication.shared.delegate as? AppDelegate { { (inHandlerCallRef, inEvent, inUserData) -> OSStatus in
if let delegate = NSApplication.shared.delegate as? AppDelegate
{
let window = delegate.window let window = delegate.window
if window.isKeyWindow { if window.isKeyWindow {
window.resignKey() window.resignKey()
} else { } else {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
if let controller = window.contentViewController as? SearchViewController { if let controller =
window.contentViewController as? SearchViewController
{
controller.centerWindow() controller.centerWindow()
} }
} }
@@ -38,13 +45,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} }
HotKeyManager.shared.enable() HotKeyManager.shared.enable()
if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int, if let code =
let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int UserDefaults.standard.object(forKey: "keyCode") as? Int,
let mods =
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
{ {
HotKeyManager.shared.registerHotKey(key: code, modifiers: mods) HotKeyManager.shared.registerHotKey(key: code, modifiers: mods)
} else { } else {
// NOTE: This is the default shortcut. If you want to change it, do not forget to change it in other files (SettingsViewController). // NOTE: This is the default shortcut. If you want to change
HotKeyManager.shared.registerHotKey(key: kVK_Space, modifiers: optionKey) // it, do not forget to change it in other files
// (SettingsViewController).
HotKeyManager.shared.registerHotKey(key: kVK_Space,
modifiers: optionKey)
} }
} }
@@ -54,7 +66,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} }
} }
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool { func applicationShouldHandleReopen(_ sender: NSApplication,
hasVisibleWindows: Bool) -> Bool {
if !window.isKeyWindow { if !window.isKeyWindow {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
@@ -85,7 +98,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
aboutWindow.makeKeyAndOrderFront(nil) aboutWindow.makeKeyAndOrderFront(nil)
} }
// NOTE: This function act like a callback is triggered by DirMonitor when file system events occur. // NOTE: This function act like a callback is triggered by DirMonitor
// when file system events occur.
public func fsEventTriggered(_ path: String, _ flags: Int) { public func fsEventTriggered(_ path: String, _ flags: Int) {
if containsFlags(key: kFSEventStreamEventFlagItemCreated, in: flags) || if containsFlags(key: kFSEventStreamEventFlagItemCreated, in: flags) ||
containsFlags(key: kFSEventStreamEventFlagItemRemoved, in: flags) || containsFlags(key: kFSEventStreamEventFlagItemRemoved, in: flags) ||

View File

@@ -25,22 +25,34 @@ class DirMonitor {
var context = FSEventStreamContext() var context = FSEventStreamContext()
context.info = Unmanaged.passUnretained(self).toOpaque() context.info = Unmanaged.passUnretained(self).toOpaque()
guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in guard let stream = FSEventStreamCreate(nil,
let pathsBase = eventPaths .assumingMemoryBound(to: UnsafePointer<CChar>.self) { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in
let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents) let pathsBase = eventPaths
let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents) .assumingMemoryBound(to: UnsafePointer<CChar>.self)
// let eventIDsBuffer = UnsafeBufferPointer(start: eventIds, count: numEvents) let pathsBuffer =
UnsafeBufferPointer(start: pathsBase, count: numEvents)
let flagsBuffer =
UnsafeBufferPointer(start: eventFlags, count: numEvents)
// let eventIDsBuffer =
// UnsafeBufferPointer(start: eventIds, count: numEvents)
for i in 0..<numEvents { for i in 0..<numEvents {
let flags = Int(flagsBuffer[i]) let flags = Int(flagsBuffer[i])
let url: URL = URL(fileURLWithFileSystemRepresentation: pathsBuffer[i], isDirectory: true, relativeTo: nil) let url: URL =
URL(fileURLWithFileSystemRepresentation: pathsBuffer[i],
isDirectory: true, relativeTo: nil)
// Since this is a directory monitor, we discard file events. // Since this is a directory monitor, we discard file
if !containsFlags(key: kFSEventStreamEventFlagItemIsDir, in: flags) || !url.path.hasSuffix(".app") { // events.
if !containsFlags(key: kFSEventStreamEventFlagItemIsDir,
in: flags) ||
!url.path.hasSuffix(".app")
{
continue continue
} }
// NOTE: The delegate callback should always be called on main thread! // NOTE: The delegate callback should always be called
// on main thread!
DispatchQueue.main.async { DispatchQueue.main.async {
delegate.fsEventTriggered(url.path, flags) delegate.fsEventTriggered(url.path, flags)
} }
@@ -50,7 +62,8 @@ class DirMonitor {
self.dirs, // [path as NSString] as NSArray, self.dirs, // [path as NSString] as NSArray,
UInt64(kFSEventStreamEventIdSinceNow), UInt64(kFSEventStreamEventIdSinceNow),
2.0, 2.0,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents) // FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone) // FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents)
) else { ) else {
return false return false
} }

View File

@@ -15,23 +15,33 @@ final class EditableNSTextField: NSTextField {
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if modsContains(keys: OSCmd, in: modifiers) { if modsContains(keys: OSCmd, in: modifiers) {
if key == kVK_ANSI_X { if key == kVK_ANSI_X {
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil,
from: self)
{
return true return true
} }
} else if key == kVK_ANSI_C { } else if key == kVK_ANSI_C {
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil,
from: self)
{
return true return true
} }
} else if key == kVK_ANSI_V { } else if key == kVK_ANSI_V {
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil,
from: self)
{
return true return true
} }
} else if key == kVK_ANSI_Z { } else if key == kVK_ANSI_Z {
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { if NSApp.sendAction(Selector(("undo:")), to: nil,
from: self)
{
return true return true
} }
} else if key == kVK_ANSI_A { } else if key == kVK_ANSI_A {
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { if NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
to: nil, from: self)
{
return true return true
} }
} else if isNumericalCode(key) { // Ignore Command + {1-9}. } else if isNumericalCode(key) { // Ignore Command + {1-9}.
@@ -39,7 +49,9 @@ final class EditableNSTextField: NSTextField {
} }
} else if modsContains(keys: OSCmd | OSShift, in: modifiers) { } else if modsContains(keys: OSCmd | OSShift, in: modifiers) {
if key == kVK_ANSI_Z { if key == kVK_ANSI_Z {
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { if NSApp.sendAction(Selector(("redo:")), to: nil,
from: self)
{
return true return true
} }
} }

View File

@@ -35,7 +35,8 @@ final class LocalEventMonitor: EventMonitor {
} }
override func start() { override func start() {
monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler) monitor = NSEvent.addLocalMonitorForEvents(matching: mask,
handler: handler)
} }
} }
@@ -50,6 +51,7 @@ final class GlobalEventMonitor: EventMonitor {
} }
override func start() { override func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) monitor = NSEvent.addGlobalMonitorForEvents(matching: mask,
handler: handler)
} }
} }

View File

@@ -12,7 +12,9 @@ func modsContains(keys: UInt, in modifiers: UInt) -> Bool {
} }
func isNumericalCode(_ key: UInt16) -> Bool { func isNumericalCode(_ key: UInt16) -> Bool {
return (key == kVK_ANSI_1 || key == kVK_ANSI_2 || key == kVK_ANSI_3 || key == kVK_ANSI_4 || key == kVK_ANSI_5 || key == kVK_ANSI_6 || key == kVK_ANSI_7 || key == kVK_ANSI_8 || key == kVK_ANSI_9) return (key == kVK_ANSI_1 || key == kVK_ANSI_2 || key == kVK_ANSI_3 ||
key == kVK_ANSI_4 || key == kVK_ANSI_5 || key == kVK_ANSI_6 ||
key == kVK_ANSI_7 || key == kVK_ANSI_8 || key == kVK_ANSI_9)
} }
func modsContainsNone(in modifiers: UInt) -> Bool { func modsContainsNone(in modifiers: UInt) -> Bool {
@@ -46,8 +48,10 @@ func keyName(virtualKeyCode: UInt16) -> String? {
//let source = //let source =
// TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue() // TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
let source = TISCopyInputSourceForLanguage("en-US" as CFString).takeRetainedValue(); let source = TISCopyInputSourceForLanguage("en-US" as CFString)
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) .takeRetainedValue();
guard let ptr =
TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
else { else {
print("Could not get keyboard layout data") print("Could not get keyboard layout data")
return nil return nil
@@ -55,9 +59,11 @@ func keyName(virtualKeyCode: UInt16) -> String? {
let layoutData = Unmanaged<CFData>.fromOpaque(ptr) let layoutData = Unmanaged<CFData>.fromOpaque(ptr)
.takeUnretainedValue() as Data .takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes { let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode, UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress,
UInt16(kUCKeyActionDown), modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask), virtualKeyCode, UInt16(kUCKeyActionDown),
&deadKeys, maxNameLength, &nameLength, &nameBuffer) modifierKeys, keyboardType,
UInt32(kUCKeyTranslateNoDeadKeysMask), &deadKeys,
maxNameLength, &nameLength, &nameBuffer)
} }
guard osStatus == noErr else { guard osStatus == noErr else {
print("Code: \(virtualKeyCode) Status: \(osStatus)") print("Code: \(virtualKeyCode) Status: \(osStatus)")
@@ -78,16 +84,23 @@ func keyName(virtualKeyCode: UInt16) -> String? {
func isDirectory(_ path: String) -> Bool { func isDirectory(_ path: String) -> Bool {
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue { if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory),
isDirectory.boolValue
{
return true return true
} else { } else {
return false return false
} }
} }
func systemImage(_ name: String, _ size: NSFont.TextStyle, _ scale: NSImage.SymbolScale, _ configuration: NSImage.SymbolConfiguration) -> NSImage? { func systemImage(_ name: String, _ size: NSFont.TextStyle,
_ scale: NSImage.SymbolScale,
_ configuration: NSImage.SymbolConfiguration) -> NSImage?
{
return NSImage(systemSymbolName: name, accessibilityDescription: nil)? return NSImage(systemSymbolName: name, accessibilityDescription: nil)?
.withSymbolConfiguration(NSImage.SymbolConfiguration(textStyle: size, scale: scale).applying(configuration)) .withSymbolConfiguration(NSImage
.SymbolConfiguration(textStyle: size, scale: scale)
.applying(configuration))
} }
extension String { extension String {

View File

@@ -4,12 +4,15 @@ import OSLog
final class HotKeyManager { final class HotKeyManager {
static let shared = HotKeyManager() static let shared = HotKeyManager()
private var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)) private var eventType =
EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
private var eventHandlerRef: EventHandlerRef? private var eventHandlerRef: EventHandlerRef?
public var handler: EventHandlerUPP? public var handler: EventHandlerUPP?
private var hotKeyRef: EventHotKeyRef? private var hotKeyRef: EventHotKeyRef?
private let hotKeyID: EventHotKeyID = EventHotKeyID(signature: OSType("grap".fourCharCodeValue), id: 1) private let hotKeyID: EventHotKeyID =
EventHotKeyID(signature: OSType("grap".fourCharCodeValue), id: 1)
private init() {} private init() {}
deinit {} deinit {}
@@ -20,7 +23,8 @@ final class HotKeyManager {
disable() disable()
} }
let err = InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType, nil, &eventHandlerRef) let err = InstallEventHandler(GetApplicationEventTarget(), handler,
1, &eventType, nil, &eventHandlerRef)
if err == noErr { if err == noErr {
print("Installed event handler.") print("Installed event handler.")
} else { } else {
@@ -32,7 +36,8 @@ final class HotKeyManager {
guard eventHandlerRef != nil else { return } guard eventHandlerRef != nil else { return }
let err = RemoveEventHandler(eventHandlerRef) let err = RemoveEventHandler(eventHandlerRef)
if err == noErr { if err == noErr {
eventHandlerRef = nil // WARNING: Does it remove no matter what on error? // WARNING: Does it remove no matter what on error?
eventHandlerRef = nil
print("Removed event handler.") print("Removed event handler.")
} else { } else {
print("Failed to remove event handler.") print("Failed to remove event handler.")
@@ -46,7 +51,10 @@ final class HotKeyManager {
unregisterHotKey() unregisterHotKey()
} }
let err = RegisterEventHotKey(UInt32(key), UInt32(modifiers), hotKeyID, GetApplicationEventTarget(), UInt32(kEventHotKeyNoOptions), &hotKeyRef) let err = RegisterEventHotKey(UInt32(key), UInt32(modifiers),
hotKeyID, GetApplicationEventTarget(),
UInt32(kEventHotKeyNoOptions),
&hotKeyRef)
if err == noErr { if err == noErr {
print("Registered hot key.") print("Registered hot key.")
} else { } else {
@@ -59,7 +67,8 @@ final class HotKeyManager {
guard hotKeyRef != nil else { return } guard hotKeyRef != nil else { return }
let err = UnregisterEventHotKey(hotKeyRef) let err = UnregisterEventHotKey(hotKeyRef)
if err == noErr { if err == noErr {
hotKeyRef = nil // WARNING: Does it unregister no matter what on error? // WARNING: Does it unregister no matter what on error?
hotKeyRef = nil
print("Successfully unregistered hot key.") print("Successfully unregistered hot key.")
} else { } else {
print("Failed to unregistered hot key.") print("Failed to unregistered hot key.")

View File

@@ -13,7 +13,8 @@ final class KeyDetectorButton: NSButton {
override var acceptsFirstResponder: Bool { true } override var acceptsFirstResponder: Bool { true }
// This removes default bahavior from NSButton, thus allowing mouse up events. // This removes default bahavior from NSButton, thus allowing mouse up
// events.
override func mouseDown(with event: NSEvent) {} override func mouseDown(with event: NSEvent) {}
override func mouseUp(with event: NSEvent) { override func mouseUp(with event: NSEvent) {
@@ -24,7 +25,9 @@ final class KeyDetectorButton: NSButton {
if event.keyCode == kVK_Escape || event.keyCode == kVK_Return { if event.keyCode == kVK_Escape || event.keyCode == kVK_Return {
// Ignore escape and return keys. // Ignore escape and return keys.
} else if event.keyCode == kVK_Delete { } else if event.keyCode == kVK_Delete {
if let key = defaultKey, let character = keyName(virtualKeyCode: UInt16(key)) { if let key = defaultKey,
let character = keyName(virtualKeyCode: UInt16(key))
{
title = character title = character
} }
} else { } else {

View File

@@ -7,10 +7,13 @@ XCODE_PATH = $(shell xcode-select --print-path)
EXEC = Grapp EXEC = Grapp
SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift PopoverPanel.swift SearchViewController.swift \ SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift \
SettingsViewController.swift HotKeyManager.swift KeyDetectorButton.swift PathManager.swift PathsTableCellView.swift \ PopoverPanel.swift SearchViewController.swift \
ProgramsTable.swift ShadowView.swift DirMonitor.swift MenulessWindow.swift AboutViewController.swift AppDelegate.swift \ SettingsViewController.swift HotKeyManager.swift \
main.swift KeyDetectorButton.swift PathManager.swift \
PathsTableCellView.swift ProgramsTable.swift ShadowView.swift \
DirMonitor.swift MenulessWindow.swift AboutViewController.swift \
AppDelegate.swift main.swift
ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o)) ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o))
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o)) X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))
@@ -18,35 +21,52 @@ LIBS =
FRAMEWORKS = -framework AppKit -framework ServiceManagement FRAMEWORKS = -framework AppKit -framework ServiceManagement
# HACK: Target is getting touched because timestamps of the generated object file don't change unless there's an actual change in the # HACK: Target is getting touched because timestamps of the generated
# outputted object code. This results in this target running every single time. I'm not sure whether that's the exact reason, but # object file don't change unless there's an actual change in the
# I can't imagine why timestamps wouldn't change. When clang generates same exact executable, timestamps do change. # outputted object code. This results in this target running every
# single time. I'm not sure whether that's the exact reason, but
# I can't imagine why timestamps wouldn't change. When clang
# generates same exact executable, timestamps do change.
./arm64/%.o: %.swift ./arm64/%.o: %.swift
swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \ swift -frontend -c \
$(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && \ -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \
$(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ \
-emit-module && \
touch $@ touch $@
ifdef UNIVERSAL ifdef UNIVERSAL
./x86_64/%.o: %.swift ./x86_64/%.o: %.swift
@swift -frontend -c -target x86_64-apple-macos$(MACOS_VERSION) $(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) \ @swift -frontend -c \
$(LIBS) $(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && \ -target x86_64-apple-macos$(MACOS_VERSION) $(FLAGS) \
-primary-file $< $(filter-out $<, $(SRCMODULES)) \
$(LIBS) $(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ \
-emit-module && \
touch $@ touch $@
endif endif
./arm64/$(EXEC): $(ARMOBJMODULES) ./arm64/$(EXEC): $(ARMOBJMODULES)
@ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch arm64 -macos_version_min $(MACOS_VERSION).0 \ @ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch arm64 \
-macos_version_min $(MACOS_VERSION).0 \
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \ /Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
-sectcreate __TEXT __info_plist Info.plist -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ -sectcreate __TEXT __info_plist Info.plist \
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift -no_objc_category_merging -L $(XCODE_PATH) -rpath \ -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) -o $@ /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift \
-no_objc_category_merging -L $(XCODE_PATH) -rpath \
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) -o $@
ifdef UNIVERSAL ifdef UNIVERSAL
./x86_64/$(EXEC): $(X86OBJMODULES) ./x86_64/$(EXEC): $(X86OBJMODULES)
@ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch x86_64 -macos_version_min $(MACOS_VERSION).0 \ @ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch x86_64 \
-macos_version_min $(MACOS_VERSION).0 \
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \ /Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
-sectcreate __TEXT __info_plist Info.plist -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ -sectcreate __TEXT __info_plist Info.plist \
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift -no_objc_category_merging -L $(XCODE_PATH) -rpath \ -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx \
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) -o $@ -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/swift \
-no_objc_category_merging -L $(XCODE_PATH) -rpath \
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) -o $@
endif endif
ifdef UNIVERSAL ifdef UNIVERSAL
@@ -64,20 +84,22 @@ $(EXEC).app: $(EXEC)
cp Info.plist $@/Contents/ && \ cp Info.plist $@/Contents/ && \
cp resources/AppIcon.icns $@/Contents/Resources/ && \ cp resources/AppIcon.icns $@/Contents/Resources/ && \
cp $(EXEC) $@/Contents/MacOS/ && \ cp $(EXEC) $@/Contents/MacOS/ && \
$(if $(DEBUG), codesign --entitlements Grapp.entitlements -s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \ $(if $(DEBUG), codesign --entitlements Grapp.entitlements \
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp -o runtime $(EXEC).app) -s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp \
-o runtime $(EXEC).app)
all: $(EXEC).app all: $(EXEC).app
kill: kill:
-pkill $(EXEC) -pkill $(EXEC)
open: kill all
open $(EXEC).app
run: kill all run: kill all
./$(EXEC) ./$(EXEC)
open: kill all
open $(EXEC).app
clean: clean:
rm -rf $(EXEC) $(EXEC).app arm64 x86_64 rm -rf $(EXEC) $(EXEC).app arm64 x86_64
mkdir arm64 x86_64 mkdir arm64 x86_64

View File

@@ -23,7 +23,9 @@ class MenulessWindow: NSWindow {
let key = event.keyCode let key = event.keyCode
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if modsContains(keys: OSCmd, in: modifiers) && key == kVK_ANSI_W { if modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_ANSI_W
{
performClose(nil) performClose(nil)
return true return true
} }

View File

@@ -29,7 +29,10 @@ final class PathManager {
private init() { private init() {
// UserDefaults.standard.removeObject(forKey: "programPaths") // UserDefaults.standard.removeObject(forKey: "programPaths")
if let dirs = UserDefaults.standard.stringArray(forKey: "programPaths"), !dirs.isEmpty { if let dirs =
UserDefaults.standard.stringArray(forKey: "programPaths"),
!dirs.isEmpty
{
for dir in dirs { for dir in dirs {
addPath(dir) addPath(dir)
} }
@@ -79,7 +82,9 @@ final class PathManager {
buf.append(path.key) buf.append(path.key)
} }
dirMonitor = DirMonitor(paths: buf, queue: DispatchQueue.global(qos: .userInitiated)) dirMonitor =
DirMonitor(paths: buf,
queue: DispatchQueue.global(qos: .userInitiated))
// _ = dirMonitor!.start() // _ = dirMonitor!.start()
if dirMonitor!.start() { if dirMonitor!.start() {
print("Started monitoring directories.") print("Started monitoring directories.")
@@ -115,10 +120,12 @@ final class PathManager {
let name = String(item.dropLast(4)) let name = String(item.dropLast(4))
if item.hasSuffix(".app"), !contains(name) { if item.hasSuffix(".app"), !contains(name) {
array.append(Program(path: path, name: name, ext: ".app", img: nil)) array.append(Program(path: path, name: name,
ext: ".app", img: nil))
} }
if deepness > 0 { if deepness > 0 {
array += indexDirs(at: path + "/" + item, deepness: deepness-1) array += indexDirs(at: path + "/" + item,
deepness: deepness-1)
} }
} }
} catch { print("Error: \(error.localizedDescription)") } } catch { print("Error: \(error.localizedDescription)") }

View File

@@ -28,7 +28,7 @@ class PathsTableCellView: NSTableCellView, NSTextFieldDelegate,
var selectionButton: NSButton = { var selectionButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("hand.point.up.fill", .headline, .large, button.image = systemImage("hand.point.up.fill", .headline, .large,
.init(paletteColors: [.labelColor, .systemRed])) .init(paletteColors: [.labelColor, .systemRed]))
button.isBordered = false button.isBordered = false
button.sizeToFit() button.sizeToFit()
button.toolTip = "Select Path" button.toolTip = "Select Path"
@@ -57,8 +57,8 @@ class PathsTableCellView: NSTableCellView, NSTextFieldDelegate,
//titleField.bottomAnchor.constraint(equalTo: bottomAnchor), //titleField.bottomAnchor.constraint(equalTo: bottomAnchor),
titleField.centerYAnchor.constraint(equalTo: centerYAnchor), titleField.centerYAnchor.constraint(equalTo: centerYAnchor),
titleField.leadingAnchor.constraint(equalTo: leadingAnchor), titleField.leadingAnchor.constraint(equalTo: leadingAnchor),
titleField.trailingAnchor.constraint( titleField.trailingAnchor
equalTo: selectionButton.leadingAnchor), .constraint(equalTo: selectionButton.leadingAnchor),
selectionButton.centerYAnchor.constraint( selectionButton.centerYAnchor.constraint(
equalTo: centerYAnchor), equalTo: centerYAnchor),

View File

@@ -7,7 +7,8 @@ class PopoverPanel: NSPanel {
init(viewController: NSViewController) { init(viewController: NSViewController) {
super.init( super.init(
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100), contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .fullSizeContentView], styleMask: [.borderless, .nonactivatingPanel, .utilityWindow,
.fullSizeContentView],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
@@ -25,7 +26,8 @@ class PopoverPanel: NSPanel {
titlebarAppearsTransparent = true titlebarAppearsTransparent = true
animationBehavior = .none animationBehavior = .none
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient] collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary,
.transient]
isReleasedWhenClosed = false isReleasedWhenClosed = false
hidesOnDeactivate = false hidesOnDeactivate = false
} }
@@ -35,13 +37,19 @@ class PopoverPanel: NSPanel {
let key = event.keyCode let key = event.keyCode
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if modsContains(keys: OSCmd, in: modifiers) && key == kVK_ANSI_Q { if modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_ANSI_Q
{
NSApplication.shared.terminate(self) NSApplication.shared.terminate(self)
return true return true
} else if modsContains(keys: OSCmd, in: modifiers) && key == kVK_ANSI_W { } else if modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_ANSI_W
{
resignKey() resignKey()
return true return true
} else if modsContains(keys: OSCmd | OSShift, in: modifiers) && key == kVK_ANSI_R { } else if modsContains(keys: OSCmd | OSShift, in: modifiers) &&
key == kVK_ANSI_R
{
PathManager.shared.updateIndex() PathManager.shared.updateIndex()
return true return true
} else if key == kVK_Escape { } else if key == kVK_Escape {

View File

@@ -7,7 +7,8 @@ final class ProgramsTableView: NSTableView {
class ProgramsTableRowView: NSTableRowView { class ProgramsTableRowView: NSTableRowView {
override func drawSelection(in dirtyRect: NSRect) { override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none { if self.selectionHighlightStyle != .none {
let selectionColor = NSColor.controlAccentColor.withAlphaComponent(0.8) let selectionColor =
NSColor.controlAccentColor.withAlphaComponent(0.8)
selectionColor.setFill() selectionColor.setFill()
self.bounds.fill() self.bounds.fill()
} }
@@ -28,7 +29,10 @@ class ProgramsTableViewCell: NSTableCellView {
field.textColor = NSColor.secondaryLabelColor field.textColor = NSColor.secondaryLabelColor
field.cell?.lineBreakMode = .byTruncatingTail field.cell?.lineBreakMode = .byTruncatingTail
field.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .caption1).pointSize, weight: .bold) field.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .caption1).pointSize,
weight: .bold)
field.translatesAutoresizingMaskIntoConstraints = false field.translatesAutoresizingMaskIntoConstraints = false
return field return field
}() }()
@@ -36,7 +40,8 @@ class ProgramsTableViewCell: NSTableCellView {
public var appIconImage: NSImageView = { public var appIconImage: NSImageView = {
let image = NSImageView() let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) image.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -57,7 +62,10 @@ class ProgramsTableViewCell: NSTableCellView {
field.isBordered = false field.isBordered = false
field.drawsBackground = false field.drawsBackground = false
field.lineBreakMode = .byTruncatingTail field.lineBreakMode = .byTruncatingTail
field.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .caption1).pointSize, weight: .medium) field.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .caption1).pointSize,
weight: .medium)
field.translatesAutoresizingMaskIntoConstraints = false field.translatesAutoresizingMaskIntoConstraints = false
return field return field
}() }()
@@ -66,7 +74,8 @@ class ProgramsTableViewCell: NSTableCellView {
super.init(frame: frameRect) super.init(frame: frameRect)
// wantsLayer = true // wantsLayer = true
// layer?.backgroundColor = NSColor.yellow.withAlphaComponent(0.2).cgColor // layer?.backgroundColor =
// NSColor.yellow.withAlphaComponent(0.2).cgColor
addSubview(indexLabel) addSubview(indexLabel)
addSubview(appIconImage) addSubview(appIconImage)
@@ -77,21 +86,33 @@ class ProgramsTableViewCell: NSTableCellView {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
indexLabel.widthAnchor.constraint(equalToConstant: 25), indexLabel.widthAnchor.constraint(equalToConstant: 25),
indexLabel.centerYAnchor.constraint(equalTo: centerYAnchor), indexLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
indexLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: ViewConstants.spacing5), indexLabel.leadingAnchor
.constraint(equalTo: leadingAnchor,
constant: ViewConstants.spacing5),
appIconImage.widthAnchor.constraint(equalToConstant: 40), appIconImage.widthAnchor.constraint(equalToConstant: 40),
appIconImage.heightAnchor.constraint(equalToConstant: 40), appIconImage.heightAnchor.constraint(equalToConstant: 40),
appIconImage.topAnchor.constraint(equalTo: topAnchor), appIconImage.topAnchor.constraint(equalTo: topAnchor),
appIconImage.bottomAnchor.constraint(equalTo: bottomAnchor), appIconImage.bottomAnchor.constraint(equalTo: bottomAnchor),
appIconImage.leadingAnchor.constraint(equalTo: indexLabel.trailingAnchor), appIconImage.leadingAnchor
.constraint(equalTo: indexLabel.trailingAnchor),
titleField.topAnchor.constraint(equalTo: appIconImage.topAnchor, constant: ViewConstants.spacing2), titleField.topAnchor
titleField.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing5), .constraint(equalTo: appIconImage.topAnchor,
titleField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -ViewConstants.spacing5), constant: ViewConstants.spacing2),
titleField.leadingAnchor
.constraint(equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing5),
titleField.trailingAnchor
.constraint(equalTo: trailingAnchor,
constant: -ViewConstants.spacing5),
progPathLabel.topAnchor.constraint(equalTo: titleField.bottomAnchor), progPathLabel.topAnchor
progPathLabel.leadingAnchor.constraint(equalTo: titleField.leadingAnchor), .constraint(equalTo: titleField.bottomAnchor),
progPathLabel.trailingAnchor.constraint(equalTo: titleField.trailingAnchor), progPathLabel.leadingAnchor
.constraint(equalTo: titleField.leadingAnchor),
progPathLabel.trailingAnchor
.constraint(equalTo: titleField.trailingAnchor),
]) ])
} }

View File

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

View File

@@ -3,8 +3,9 @@ import Carbon
import ServiceManagement import ServiceManagement
class SettingsViewController: NSViewController, class SettingsViewController: NSViewController,
NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource, NSTextFieldDelegate, KeyDetectorButtonDelegate,
NSTableViewDelegate, PathsTableCellViewDelegate NSTableViewDataSource, NSTableViewDelegate,
PathsTableCellViewDelegate
{ {
private var recording = false private var recording = false
@@ -15,8 +16,10 @@ class SettingsViewController: NSViewController,
private var paths: [String] = [] 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 // PERF: This is very slow to initialize because it creates a new
// thread. This sucks because the program now takes considerably longer to launch. // 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 = { private let dirPicker: NSOpenPanel = {
let panel = NSOpenPanel() let panel = NSOpenPanel()
panel.message = "Select a directory to search applications in . . ." panel.message = "Select a directory to search applications in . . ."
@@ -28,14 +31,18 @@ class SettingsViewController: NSViewController,
private var shortcutsLabel: NSTextField = { private var shortcutsLabel: NSTextField = {
let textField = NSTextField(labelWithString: "Shortcut") let textField = NSTextField(labelWithString: "Shortcut")
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold) textField.font =
NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .title2).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var aboutButton: NSButton = { private var aboutButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("info.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemGray])) button.image = systemImage("info.circle.fill", .title2, .large,
.init(paletteColors: [.white, .systemGray]))
button.isBordered = false button.isBordered = false
button.action = #selector(showAbout) button.action = #selector(showAbout)
button.sizeToFit() button.sizeToFit()
@@ -95,7 +102,10 @@ class SettingsViewController: NSViewController,
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -110,8 +120,12 @@ class SettingsViewController: NSViewController,
}() }()
private var pathsLabel: NSTextField = { private var pathsLabel: NSTextField = {
let textField = NSTextField(labelWithString: "Application Directories") let textField =
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold) NSTextField(labelWithString: "Application Directories")
textField.font = NSFont
.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .title2).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -135,7 +149,11 @@ class SettingsViewController: NSViewController,
table.allowsColumnReordering = false table.allowsColumnReordering = false
table.allowsColumnResizing = false table.allowsColumnResizing = false
table.allowsColumnSelection = false table.allowsColumnSelection = false
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Paths"))) table.addTableColumn(
NSTableColumn(
identifier: NSUserInterfaceItemIdentifier("Paths")
)
)
table.translatesAutoresizingMaskIntoConstraints = false table.translatesAutoresizingMaskIntoConstraints = false
return table return table
@@ -146,8 +164,10 @@ class SettingsViewController: NSViewController,
control.segmentCount = 2 control.segmentCount = 2
control.segmentStyle = .roundRect control.segmentStyle = .roundRect
control.setImage(NSImage(systemSymbolName: "plus", accessibilityDescription: nil), forSegment: 0) control.setImage(NSImage(systemSymbolName: "plus",
control.setImage(NSImage(systemSymbolName: "minus", accessibilityDescription: nil), forSegment: 1) accessibilityDescription: nil), forSegment: 0)
control.setImage(NSImage(systemSymbolName: "minus",
accessibilityDescription: nil), forSegment: 1)
control.setToolTip("Add Path", forSegment: 0) control.setToolTip("Add Path", forSegment: 0)
control.setToolTip("Remove Path", forSegment: 1) control.setToolTip("Remove Path", forSegment: 1)
@@ -212,52 +232,99 @@ class SettingsViewController: NSViewController,
private func setConstraints() { private func setConstraints() {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
shortcutsLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10), shortcutsLabel.topAnchor
shortcutsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing10),
shortcutsLabel.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
aboutButton.firstBaselineAnchor.constraint(equalTo: shortcutsLabel.firstBaselineAnchor), aboutButton.firstBaselineAnchor
aboutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10), .constraint(equalTo: shortcutsLabel.firstBaselineAnchor),
aboutButton.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: -ViewConstants.spacing10),
ctrlButton.topAnchor.constraint(equalTo: shortcutsLabel.bottomAnchor, constant: ViewConstants.spacing10), ctrlButton.topAnchor
ctrlButton.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor), .constraint(equalTo: shortcutsLabel.bottomAnchor,
constant: ViewConstants.spacing10),
ctrlButton.leadingAnchor
.constraint(equalTo: shortcutsLabel.leadingAnchor),
cmdButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), cmdButton.centerYAnchor
cmdButton.leadingAnchor.constraint(equalTo: ctrlButton.trailingAnchor, constant: ViewConstants.spacing5), .constraint(equalTo: ctrlButton.centerYAnchor),
cmdButton.leadingAnchor
.constraint(equalTo: ctrlButton.trailingAnchor,
constant: ViewConstants.spacing5),
optButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), optButton.centerYAnchor
optButton.leadingAnchor.constraint(equalTo: cmdButton.trailingAnchor, constant: ViewConstants.spacing5), .constraint(equalTo: ctrlButton.centerYAnchor),
optButton.leadingAnchor
.constraint(equalTo: cmdButton.trailingAnchor,
constant: ViewConstants.spacing5),
shiftButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), shiftButton.centerYAnchor
shiftButton.leadingAnchor.constraint(equalTo: optButton.trailingAnchor, constant: ViewConstants.spacing5), .constraint(equalTo: ctrlButton.centerYAnchor),
shiftButton.leadingAnchor
.constraint(equalTo: optButton.trailingAnchor,
constant: ViewConstants.spacing5),
plusLabel.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), plusLabel.centerYAnchor
plusLabel.leadingAnchor.constraint(equalTo: shiftButton.trailingAnchor, constant: ViewConstants.spacing5), .constraint(equalTo: ctrlButton.centerYAnchor),
plusLabel.leadingAnchor
.constraint(equalTo: shiftButton.trailingAnchor,
constant: ViewConstants.spacing5),
recordButton.widthAnchor.constraint(equalToConstant: 40), recordButton.widthAnchor.constraint(equalToConstant: 40),
recordButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor), recordButton.centerYAnchor
recordButton.leadingAnchor.constraint(equalTo: plusLabel.trailingAnchor, constant: ViewConstants.spacing5), .constraint(equalTo: ctrlButton.centerYAnchor),
recordButton.leadingAnchor
.constraint(equalTo: plusLabel.trailingAnchor,
constant: ViewConstants.spacing5),
pathsLabel.topAnchor.constraint(equalTo: ctrlButton.bottomAnchor, constant: ViewConstants.spacing20), pathsLabel.topAnchor
pathsLabel.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor), .constraint(equalTo: ctrlButton.bottomAnchor,
constant: ViewConstants.spacing20),
pathsLabel.leadingAnchor
.constraint(equalTo: shortcutsLabel.leadingAnchor),
tableScrollView.widthAnchor.constraint(equalToConstant: 350), tableScrollView.widthAnchor.constraint(equalToConstant: 350),
tableScrollView.heightAnchor.constraint(equalToConstant: 150), tableScrollView.heightAnchor.constraint(equalToConstant: 150),
tableScrollView.topAnchor.constraint(equalTo: pathsLabel.bottomAnchor), tableScrollView.topAnchor
tableScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), .constraint(equalTo: pathsLabel.bottomAnchor),
tableScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableScrollView.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
tableScrollView.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
pathsControl.topAnchor.constraint(equalTo: tableScrollView.bottomAnchor, constant: ViewConstants.spacing10), pathsControl.topAnchor
pathsControl.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), .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.topAnchor
launchAtLoginLabel.trailingAnchor.constraint(equalTo: launchAtLoginToggle.leadingAnchor, constant: -ViewConstants.spacing10), .constraint(equalTo: pathsControl.bottomAnchor,
constant: ViewConstants.spacing10),
launchAtLoginLabel.trailingAnchor
.constraint(equalTo: launchAtLoginToggle.leadingAnchor,
constant: -ViewConstants.spacing10),
launchAtLoginToggle.firstBaselineAnchor.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor), launchAtLoginToggle.firstBaselineAnchor
launchAtLoginToggle.trailingAnchor.constraint(equalTo: resetAllButton.leadingAnchor, constant: -ViewConstants.spacing15), .constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
launchAtLoginToggle.trailingAnchor
.constraint(equalTo: resetAllButton.leadingAnchor,
constant: -ViewConstants.spacing15),
resetAllButton.firstBaselineAnchor.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor), resetAllButton.firstBaselineAnchor
resetAllButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10), .constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
resetAllButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing10), resetAllButton.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: -ViewConstants.spacing10),
resetAllButton.bottomAnchor
.constraint(equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10),
]) ])
} }
@@ -298,10 +365,14 @@ class SettingsViewController: NSViewController,
super.viewWillAppear() super.viewWillAppear()
// Fetch the saved key codes and modifiers. // Fetch the saved key codes and modifiers.
if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int { if let code =
UserDefaults.standard.object(forKey: "keyCode") as? Int
{
keyCode = code keyCode = code
} }
if let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int { if let mods =
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
{
modifiers = mods modifiers = mods
} }
@@ -317,7 +388,8 @@ class SettingsViewController: NSViewController,
override func viewWillDisappear() { override func viewWillDisappear() {
super.viewWillDisappear() super.viewWillDisappear()
HotKeyManager.shared.registerHotKey(key: keyCode, modifiers: modifiers) HotKeyManager.shared.registerHotKey(key: keyCode,
modifiers: modifiers)
UserDefaults.standard.set(keyCode, forKey: "keyCode") UserDefaults.standard.set(keyCode, forKey: "keyCode")
UserDefaults.standard.set(modifiers, forKey: "keyModifiers") UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
@@ -377,7 +449,8 @@ class SettingsViewController: NSViewController,
private func reset() { private func reset() {
keyCode = Int(kVK_Space) keyCode = Int(kVK_Space)
modifiers = Int(optionKey) modifiers = Int(optionKey)
HotKeyManager.shared.registerHotKey(key: keyCode, modifiers: modifiers) HotKeyManager.shared.registerHotKey(key: keyCode,
modifiers: modifiers)
UserDefaults.standard.set(keyCode, forKey: "keyCode") UserDefaults.standard.set(keyCode, forKey: "keyCode")
UserDefaults.standard.set(modifiers, forKey: "keyModifiers") UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
syncModifierButtons() syncModifierButtons()
@@ -449,11 +522,16 @@ class SettingsViewController: NSViewController,
case 0: case 0:
let row = paths.count let row = paths.count
paths.append("") paths.append("")
pathsTableView.insertRows(at: IndexSet(integer: row), withAnimation: []) pathsTableView.insertRows(at: IndexSet(integer: row),
withAnimation: [])
pathsTableView.scrollRowToVisible(row) pathsTableView.scrollRowToVisible(row)
pathsTableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) pathsTableView.selectRowIndexes(IndexSet(integer: row),
(pathsTableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? PathsTableCellView)?.startEditing() byExtendingSelection: false)
(
pathsTableView
.view(atColumn: 0, row: row, makeIfNecessary: false
) as? PathsTableCellView)?.startEditing()
break break
case 1: case 1:
if pathsTableView.selectedRow > -1 { if pathsTableView.selectedRow > -1 {
@@ -484,9 +562,16 @@ class SettingsViewController: NSViewController,
@objc @objc
private func editItem(_ sender: NSTableView) { private func editItem(_ sender: NSTableView) {
pathsTableView.deselectAll(nil) pathsTableView.deselectAll(nil)
pathsTableView.selectRowIndexes(IndexSet(integer: pathsTableView.clickedRow), byExtendingSelection: false) pathsTableView.selectRowIndexes(
IndexSet(integer: pathsTableView.clickedRow),
byExtendingSelection: false
)
if let cell = pathsTableView.view(atColumn: 0, row: pathsTableView.clickedRow, makeIfNecessary: false) as? PathsTableCellView { if let cell = pathsTableView.view(atColumn: 0,
row: pathsTableView.clickedRow,
makeIfNecessary: false) as?
PathsTableCellView
{
cell.startEditing() cell.startEditing()
} }
} }
@@ -521,7 +606,9 @@ class SettingsViewController: NSViewController,
delegate.window.level = .statusBar delegate.window.level = .statusBar
delegate.window.makeKeyAndOrderFront(nil) delegate.window.makeKeyAndOrderFront(nil)
if let controller = delegate.window.contentViewController as? SearchViewController { if let controller =
delegate.window.contentViewController as? SearchViewController
{
controller.openSettings() controller.openSettings()
} }
} }
@@ -533,7 +620,8 @@ class SettingsViewController: NSViewController,
func tableView(_ tableView: NSTableView, func tableView(_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{ {
let rect = NSRect(x: 0, y: 0, width: tableColumn!.width, height: 20) let rect = NSRect(x: 0, y: 0, width: tableColumn!.width,
height: 20)
let cell = PathsTableCellView(frame: rect) let cell = PathsTableCellView(frame: rect)
cell.titleField.stringValue = paths[row] cell.titleField.stringValue = paths[row]
cell.delegate = self cell.delegate = self