Filesystem monitor and refactor.

This commit is contained in:
2025-02-06 17:03:16 -08:00
parent 6557993214
commit 5ed67e0cef
15 changed files with 777 additions and 481 deletions

View File

@@ -0,0 +1,211 @@
import AppKit
// TODO: Change to appropriate links.
fileprivate enum AboutLinks {
// static let website = "https://cmdbar.app"
// static let documentation = "https://cmdbar.app/documentation"
// static let privacy = "https://cmdbar.app/#privacy-policy"
static let author = "https://kolokolnikov.org"
}
enum Strings {
static let copyright = "Copyright © 2024\nGarikMI. All rights reserved."
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 activate = "Activate"
static let proTitle = "License - Activated"
static let proMessage = "Thank you for purchasing CmdBar! Enjoy!"
static let deactivate = "Deactivate"
static let activating = "Activating..."
}
class AboutViewController: NSViewController, NSTextFieldDelegate {
private var appIconImage: NSImageView = {
//let image = NSImageView(image: NSApp.applicationIconImage)
let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
private var appNameLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = (Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "NOT FOUND"
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .center
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor .preferredFontDescriptor(forTextStyle: .title1).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var versionLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = "Version \((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "-.--")"
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .center
textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var copyrightLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = Strings.copyright
textField.maximumNumberOfLines = 4
textField.cell?.truncatesLastVisibleLine = true
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .center
textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var authorButton: NSButton = {
let button = NSButton()
button.title = "Author"
button.sizeToFit()
button.bezelStyle = .rounded
button.action = #selector(author)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
// private var privacyButton: NSButton = {
// let button = NSButton()
// button.title = "Privacy Policy"
// button.sizeToFit()
// button.bezelStyle = .rounded
// button.action = #selector(privacy)
// button.translatesAutoresizingMaskIntoConstraints = false
// return button
// }()
// private var documentationButton: NSButton = {
// let button = NSButton()
// button.title = "Docs"
// button.sizeToFit()
// button.bezelStyle = .rounded
// button.action = #selector(documentation)
// button.translatesAutoresizingMaskIntoConstraints = false
// return button
// }()
//
// private var websiteButton: NSButton = {
// let button = NSButton()
// button.title = "CmdBar.app"
// button.sizeToFit()
// button.bezelStyle = .rounded
// button.action = #selector(website)
// button.translatesAutoresizingMaskIntoConstraints = false
// return button
// }()
private var buttonsContainer: NSLayoutGuide = {
let container = NSLayoutGuide()
return container
}()
override func viewDidLoad() {
super.viewDidLoad()
// Program info
view.addSubview(appIconImage)
view.addSubview(appNameLabel)
view.addSubview(versionLabel)
view.addSubview(copyrightLabel)
// Buttons
view.addLayoutGuide(buttonsContainer)
// view.addSubview(privacyButton)
// view.addSubview(documentationButton)
// view.addSubview(websiteButton)
view.addSubview(authorButton)
setupConstraints()
}
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.center()
}
override func loadView() {
self.view = NSView()
}
private func setupConstraints() {
// View.
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: 300),
view.heightAnchor.constraint(lessThanOrEqualToConstant: 500),
])
// App image.
NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 100),
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20),
appIconImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
// Title
NSLayoutConstraint.activate([
appNameLabel.topAnchor.constraint(equalTo: appIconImage.bottomAnchor, constant: ViewConstants.spacing20),
appNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: ViewConstants.spacing2),
versionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
copyrightLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: ViewConstants.spacing10),
copyrightLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
// Buttons
NSLayoutConstraint.activate([
buttonsContainer.topAnchor .constraint(equalTo: copyrightLabel.bottomAnchor, 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.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor),
authorButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor),
authorButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor),
// privacyButton.topAnchor.constraint(equalTo: buttonsContainer.topAnchor),
// privacyButton.bottomAnchor.constraint(equalTo: buttonsContainer.bottomAnchor),
// privacyButton.leadingAnchor.constraint(equalTo: buttonsContainer.leadingAnchor),
//
// documentationButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor),
// documentationButton.leadingAnchor.constraint(equalTo: privacyButton.trailingAnchor,constant: ViewConstants.spacing10),
//
// websiteButton.firstBaselineAnchor.constraint(equalTo: privacyButton.firstBaselineAnchor),
// websiteButton.leadingAnchor.constraint(equalTo: documentationButton.trailingAnchor,constant: ViewConstants.spacing10),
// websiteButton.trailingAnchor.constraint(equalTo: buttonsContainer.trailingAnchor),
])
}
@objc private func author() {
NSWorkspace.shared.open(URL(string: AboutLinks.author)!)
}
// @objc private func privacy() {
// NSWorkspace.shared.open(URL(string: AboutLinks.privacy)!)
// }
//
// @objc private func documentation() {
// NSWorkspace.shared.open(URL(string: AboutLinks.documentation)!)
// }
//
// @objc private func website() {
// NSWorkspace.shared.open(URL(string: AboutLinks.website)!)
// }
}

View File

@@ -6,9 +6,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
let fileManager = FileManager.default let fileManager = FileManager.default
let window = PopoverPanel(viewController: SearchViewController()) let window = PopoverPanel(viewController: SearchViewController())
let aboutWindow = MenulessWindow(viewController: AboutViewController())
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
PathManager.shared.rebuildIndex() aboutWindow.level = .statusBar
PathManager.shared.updateIndex()
window.delegate = self window.delegate = self
@@ -30,19 +33,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} }
HotKeyManager.shared.enable() HotKeyManager.shared.enable()
if let code = if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int,
UserDefaults.standard.object(forKey: "keyCode") as? Int, let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int
let mods =
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
{ {
HotKeyManager.shared.registerHotKey(key: code, HotKeyManager.shared.registerHotKey(key: code, modifiers: mods)
modifiers: mods)
} else { } else {
// NOTE: This is the default shortcut. If you want to change // NOTE: This is the default shortcut. If you want to change
// it, do not forget to change it in other files // it, do not forget to change it in other files
// (SettingsViewController). // (SettingsViewController).
HotKeyManager.shared.registerHotKey(key: kVK_Space, HotKeyManager.shared.registerHotKey(key: kVK_Space, modifiers: optionKey)
modifiers: optionKey)
} }
} }
@@ -52,9 +51,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} }
} }
func applicationShouldHandleReopen(_ sender: NSApplication, func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
hasVisibleWindows: Bool) -> Bool
{
if !window.isKeyWindow { if !window.isKeyWindow {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
@@ -79,4 +76,49 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
return false return false
} }
} }
public func showAboutWindow() {
NSApplication.shared.activate(ignoringOtherApps: true)
aboutWindow.makeKeyAndOrderFront(nil)
}
// NOTE: This function is triggered by DirMonitor.
public func fsEventTriggered(_ path: String, _ flags: Int) {
var shouldReload = false
print("PATH: \(path)")
// if containsFlags(key: kFSEventStreamEventFlagNone, in: flags) {
// }
// if containsFlags(key: kFSEventStreamEventFlagRootChanged, in: flags) {
// }
// if containsFlags(key: kFSEventStreamEventFlagMustScanSubDirs, in: flags) {
// }
// if containsFlags(key: kFSEventStreamEventFlagItemInodeMetaMod, in: flags) {
// }
if containsFlags(key: kFSEventStreamEventFlagItemCreated, in: flags) {
print(" CREATED")
shouldReload = true
}
if containsFlags(key: kFSEventStreamEventFlagItemRemoved, in: flags) {
print(" REMOVED")
shouldReload = true
}
if containsFlags(key: kFSEventStreamEventFlagItemCloned, in: flags) {
print(" CLONED")
shouldReload = true
}
if containsFlags(key: kFSEventStreamEventFlagItemRenamed, in: flags) {
print(" RENAMED")
shouldReload = true
}
// TODO: This should also trigger SearchViewController's search re-index.
if shouldReload {
for dir in PathManager.shared.paths {
if path.hasPrefix(dir.key) {
PathManager.shared.rebuildIndex(at: dir.key)
}
}
PathManager.shared.refreshFilesystemWatchers()
}
}
} }

94
src/DirMonitor.swift Normal file
View File

@@ -0,0 +1,94 @@
import AppKit
class DirMonitor {
init(paths: [String], queue: DispatchQueue) {
for path in paths {
if isDirectory(path) {
self.dirs.add(path)
}
}
self.queue = queue
}
deinit {
precondition(self.stream == nil, "released a running monitor")
}
private var dirs: NSMutableArray = []
private let queue: DispatchQueue
// var handler: ((Int, UnsafeMutablePointer<UnsafePointer<Int8>>, UnsafeBufferPointer<UInt32>, UnsafeBufferPointer<UInt64>) -> Void)?
private var stream: FSEventStreamRef? = nil
func start() -> Bool {
precondition(self.stream == nil, "started a running monitor")
if dirs.count < 1 { return false }
var context = FSEventStreamContext()
context.info = Unmanaged.passUnretained(self).toOpaque()
// func test(count: Int, paths: UnsafeMutablePointer<UnsafePointer<Int8>>, flags: UnsafeBufferPointer<UInt32>, ids: UnsafeBufferPointer<UInt64>) { }
// test(count: numEvents, paths: pathsBase, flags: flagsBuffer, ids: eventIDsBuffer)
guard let stream = FSEventStreamCreate(nil,
{
(stream, info, numEvents, eventPaths, eventFlags, eventIds) in
let pathsBase = eventPaths .assumingMemoryBound(to: UnsafePointer<CChar>.self)
let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents)
let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents)
// let eventIDsBuffer = UnsafeBufferPointer(start: eventIds, count: numEvents)
// stream -> OpaquePointer
// info -> Optional<UnsafeMutableRawPointer>
// numEvents -> Int
// eventPaths -> UnsafeMutableRawPointer
// eventFlags -> UnsafePointer<UInt32>
// eventIds -> UnsafePointer<UInt64>
// pathsBase -> UnsafeMutablePointer<UnsafePointer<Int8>>
// pathsBuffer -> UnsafeBufferPointer<UnsafePointer<Int8>>
// flagsBuffer -> UnsafeBufferPointer<UInt32>
// eventIDsBuffer -> UnsafeBufferPointer<UInt64>
for i in 0..<numEvents {
let flags = Int(flagsBuffer[i])
// NOTE: Since this is a directory monitor, we discard file events.
if !containsFlags(key: kFSEventStreamEventFlagItemIsDir, in: flags) {
continue
}
let url: URL = URL(fileURLWithFileSystemRepresentation: pathsBuffer[i], isDirectory: true, relativeTo: nil)
// NOTE: The delegate callback should always be called on main thread!
DispatchQueue.main.async {
delegate.fsEventTriggered(url.path, flags)
}
}
},
&context,
self.dirs, // [path as NSString] as NSArray,
UInt64(kFSEventStreamEventIdSinceNow),
1.0,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents) // FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
) else {
return false
}
self.stream = stream
FSEventStreamSetDispatchQueue(stream, queue)
guard FSEventStreamStart(stream) else {
FSEventStreamInvalidate(stream)
self.stream = nil
return false
}
return true
}
func stop() {
guard let stream = self.stream else { return }
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
self.stream = nil
}
}

View File

@@ -15,32 +15,31 @@ 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(_:)), if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) {
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(_:)), if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) {
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(_:)), if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) {
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:")), if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) {
to: nil, from: self) return true
{ return true } }
} else if key == kVK_ANSI_A { } else if key == kVK_ANSI_A {
if NSApp.sendAction( if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) {
#selector(NSResponder.selectAll(_:)), to: nil, return true
from: self) }
{ return true }
} }
} 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, if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) {
from: self) return true
{ return true } }
} }
} }
} }

View File

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

View File

@@ -15,9 +15,13 @@ func modsContainsNone(in modifiers: UInt) -> Bool {
return (modifiers & OSMods) == 0 return (modifiers & OSMods) == 0
} }
func containsFlags(key: Int, in flags: Int) -> Bool {
return (flags & key) == key
}
enum ViewConstants { enum ViewConstants {
static let spacing2: CGFloat = 2 static let spacing2: CGFloat = 2
static let spacing5: CGFloat = 2 static let spacing5: CGFloat = 2
static let spacing10: CGFloat = 10 static let spacing10: CGFloat = 10
static let spacing15: CGFloat = 15 static let spacing15: CGFloat = 15
static let spacing20: CGFloat = 20 static let spacing20: CGFloat = 20
@@ -38,10 +42,8 @@ func keyName(virtualKeyCode: UInt16) -> String? {
//let source = //let source =
// TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue() // TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
let source = TISCopyInputSourceForLanguage("en-US" as CFString) let source = TISCopyInputSourceForLanguage("en-US" as CFString).takeRetainedValue();
.takeRetainedValue(); guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
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
@@ -49,11 +51,9 @@ 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( UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode,
$0.bindMemory(to: UCKeyboardLayout.self).baseAddress, UInt16(kUCKeyActionDown), modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
virtualKeyCode, UInt16(kUCKeyActionDown), modifierKeys, &deadKeys, maxNameLength, &nameLength, &nameBuffer)
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)")
@@ -72,15 +72,18 @@ func keyName(virtualKeyCode: UInt16) -> String? {
return character return character
} }
func systemImage(_ name: String, _ size: NSFont.TextStyle, func isDirectory(_ path: String) -> Bool {
_ scale: NSImage.SymbolScale, var isDirectory: ObjCBool = false
_ configuration: NSImage.SymbolConfiguration) -> NSImage? if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue {
{ return true
} else {
return false
}
}
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( .withSymbolConfiguration(NSImage.SymbolConfiguration(textStyle: size, scale: scale).applying(configuration))
NSImage.SymbolConfiguration(textStyle: size, scale: scale)
.applying(configuration)
)
} }
func isDirectory(atPath path: String) -> Bool { func isDirectory(atPath path: String) -> Bool {
@@ -92,16 +95,16 @@ func isDirectory(atPath path: String) -> Bool {
} }
extension String { extension String {
/// This converts string to UInt as a fourCharCode // This converts string to UInt as a fourCharCode
public var fourCharCodeValue: Int { public var fourCharCodeValue: Int {
var result: Int = 0 var result: Int = 0
if let data = self.data(using: String.Encoding.macOSRoman) { if let data = self.data(using: String.Encoding.macOSRoman) {
data.withUnsafeBytes({ (rawBytes) in data.withUnsafeBytes { (rawBytes) in
let bytes = rawBytes.bindMemory(to: UInt8.self) let bytes = rawBytes.bindMemory(to: UInt8.self)
for i in 0 ..< data.count { for i in 0 ..< data.count {
result = result << 8 + Int(bytes[i]) result = result << 8 + Int(bytes[i])
} }
}) }
} }
return result return result
} }

View File

@@ -2,26 +2,16 @@ import Carbon
import OSLog import OSLog
final class HotKeyManager { final class HotKeyManager {
fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
//category: String(describing: HotKeyManager.self)
category: String(describing: AppDelegate.self)
)
static let shared = HotKeyManager() static let shared = HotKeyManager()
private var eventType = EventTypeSpec( private var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
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( private let hotKeyID: EventHotKeyID = EventHotKeyID(signature: OSType("grap".fourCharCodeValue), id: 1)
signature: OSType("grap".fourCharCodeValue), id: 1)
private init() {} private init() {}
deinit {} deinit {}
// TODO: Handle errors. // TODO: Handle errors.
@@ -30,13 +20,11 @@ final class HotKeyManager {
disable() disable()
} }
let err = InstallEventHandler( let err = InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType, nil, &eventHandlerRef)
GetApplicationEventTarget(), handler, 1, &eventType,
nil, &eventHandlerRef)
if err == noErr { if err == noErr {
Self.logger.debug("Installed event handler.") print("Installed event handler.")
} else { } else {
Self.logger.error("Failed to install event handler.") print("Failed to install event handler.")
} }
} }
@@ -44,30 +32,25 @@ 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 eventHandlerRef = nil // WARNING: Does it remove no matter what on error?
// what on error? print("Removed event handler.")
Self.logger.debug("Removed event handler.")
} else { } else {
Self.logger.error("Failed to remove event handler.") print("Failed to remove event handler.")
} }
} }
// TODO: Handle errors. // TODO: Handle errors.
// NOTE: Multiple modifiers should be ORed. // NOTE: Multiple modifiers should be ORed.
public func registerHotKey(key: Int, modifiers: Int) { public func registerHotKey(key: Int, modifiers: Int) {
// GetEventDispatcherTarget
if hotKeyRef != nil { if hotKeyRef != nil {
unregisterHotKey() unregisterHotKey()
} }
let err = RegisterEventHotKey( let err = RegisterEventHotKey(UInt32(key), UInt32(modifiers), hotKeyID, GetApplicationEventTarget(), UInt32(kEventHotKeyNoOptions), &hotKeyRef)
UInt32(key), UInt32(modifiers), hotKeyID,
GetApplicationEventTarget(), UInt32(kEventHotKeyNoOptions),
&hotKeyRef)
if err == noErr { if err == noErr {
Self.logger.debug("Registered hot key.") print("Registered hot key.")
} else { } else {
Self.logger.error("Failed to register hot key.") print("Failed to register hot key.")
} }
} }
@@ -76,11 +59,10 @@ 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 hotKeyRef = nil // WARNING: Does it unregister no matter what on error?
// what on error? print("Successfully unregistered hot key.")
Self.logger.debug("Successfully unregestered hot key.")
} else { } else {
Self.logger.error("Failed to unregester hot key.") print("Failed to unregistered hot key.")
} }
} }
} }

View File

@@ -13,8 +13,7 @@ final class KeyDetectorButton: NSButton {
override var acceptsFirstResponder: Bool { true } override var acceptsFirstResponder: Bool { true }
// This removes default bahavior from NSButton, thus allowing mouse up // This removes default bahavior from NSButton, thus allowing mouse up events.
// 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) {
@@ -23,16 +22,13 @@ final class KeyDetectorButton: NSButton {
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
if event.keyCode == kVK_Escape || event.keyCode == kVK_Return { if event.keyCode == kVK_Escape || event.keyCode == kVK_Return {
// Ignore escape and return keys.
} else if event.keyCode == kVK_Delete { } else if event.keyCode == kVK_Delete {
if let key = defaultKey, if let key = defaultKey, let character = keyName(virtualKeyCode: UInt16(key)) {
let character = keyName(virtualKeyCode: UInt16(key))
{
title = character title = character
} }
} else { } else {
if let character = if let character = keyName(virtualKeyCode: UInt16(event.keyCode)) {
keyName(virtualKeyCode: UInt16(event.keyCode))
{
title = character title = character
} }
delegate?.keyWasSet(to: Int(event.keyCode)) delegate?.keyWasSet(to: Int(event.keyCode))

View File

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

33
src/MenulessWindow.swift Normal file
View File

@@ -0,0 +1,33 @@
import AppKit
import Carbon
class MenulessWindow: NSWindow {
init(viewController: NSViewController) {
super.init(
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
super.contentViewController = viewController
title = ""
titlebarAppearsTransparent = true
collectionBehavior = [.managed, .fullScreenNone]
isReleasedWhenClosed = false
hidesOnDeactivate = false
}
override func performKeyEquivalent(with event: NSEvent) -> Bool {
let modifiers = event.modifierFlags.rawValue
let key = event.keyCode
if event.type == NSEvent.EventType.keyDown {
if modsContains(keys: OSCmd, in: modifiers) && key == kVK_ANSI_W {
performClose(nil)
}
}
return super.performKeyEquivalent(with: event)
}
}

View File

@@ -3,86 +3,133 @@ import AppKit
struct Program { struct Program {
let path: String let path: String
let name: String let name: String
let ext: String let ext : String
var img: NSImage? var img : NSImage?
} }
final class PathManager { final class PathManager {
static let shared = PathManager() static let shared = PathManager()
// TODO: Filesystem events to watch changes on these directories and private var dirMonitor: DirMonitor?
// rebuild index when needed.
// NOTE: These are default paths where MacOS's default programs are // NOTE: These are default paths where MacOS's default programs are
// stored. This list should be updated if something changes in // stored. This list should be updated if something changes in
// newer MacOS version. // newer MacOS version.
static let defaultPaths = ["/Applications", "/System/Applications", static let defaultPaths = [
"/System/Applications/Utilities", "/System/Library/CoreServices", "/Applications",
"/System/Applications",
"/System/Applications/Utilities",
"/System/Library/CoreServices",
"/Applications/Xcode.app/Contents/Applications", "/Applications/Xcode.app/Contents/Applications",
"/System/Library/CoreServices/Applications"] "/System/Library/CoreServices/Applications"
var userPaths: [String] = [] ]
private(set) var programs: [Program] = [] private(set) var paths: [String: [Program]] = [:]
private let fileManager = FileManager.default private let fileManager = FileManager.default
private init() { private init() {
if let paths = // UserDefaults.standard.removeObject(forKey: "programPaths")
UserDefaults.standard.stringArray(forKey: "programPaths") if let dirs = UserDefaults.standard.stringArray(forKey: "programPaths"), !dirs.isEmpty {
{ for dir in dirs {
for path in paths { addPath(dir)
addPath(path)
} }
} else { } else {
userPaths += Self.defaultPaths for path in PathManager.defaultPaths {
addPath(path)
}
} }
} }
deinit {} deinit {}
public func addPath(_ path: String) { public func addPath(_ path: String) {
if !userPaths.contains(path) { if isDirectory(path) {
userPaths.append(path) paths[path] = []
} }
} }
public func removePath(_ path: String) { public func removePath(_ path: String) {
userPaths.removeAll { $0 == path } paths.removeValue(forKey: path)
} }
public func removeEmpty() { public func resetPaths() {
userPaths.removeAll { $0.isEmpty } paths = [:]
for path in PathManager.defaultPaths {
addPath(path)
}
}
public func contains(_ name: String) -> Bool {
for path in paths {
for prog in path.value {
if prog.name == name {
return true
}
}
}
return false
}
public func refreshFilesystemWatchers() {
dirMonitor?.stop()
dirMonitor = nil
var buf: [String] = []
for path in paths {
buf.append(path.key)
}
dirMonitor = DirMonitor(paths: buf, queue: DispatchQueue.global(qos: .userInitiated))
// _ = dirMonitor!.start()
if dirMonitor!.start() {
print("Started monitoring directories.")
} else {
print("Failed to start monitoring directories.")
}
} }
public func savePaths() { public func savePaths() {
UserDefaults.standard.set(userPaths, forKey: "programPaths") var buf: [String] = []
} for path in paths {
buf.append(path.key)
public func reset() {
userPaths = []
userPaths += Self.defaultPaths
savePaths()
}
public func rebuildIndex() {
programs.removeAll(keepingCapacity: true)
for path in userPaths {
do {
let items = try fileManager.contentsOfDirectory(
atPath: path)
for item in items {
let name = String(item.dropLast(4))
if item.hasSuffix(".app") {
if !programs.contains(where: { name == $0.name }) {
programs.append(
Program(
path: path, name: name, ext: ".app",
img: nil))
}
}
}
} catch {
print("Error reading directory: \(error.localizedDescription)")
}
} }
UserDefaults.standard.set(buf, forKey: "programPaths")
}
// PERF: Optimize some more. Do not rebuild the entire array, instead
// remove or add only needed programs. Thereby, limiting the
// amount of allocations.
public func rebuildIndex(at path: String) {
paths[path] = indexDirs(at: path, deepness: 2)
}
public func indexDirs(at path: String, deepness: Int) -> [Program] {
var array: [Program] = []
do {
var items = try fileManager.contentsOfDirectory(atPath: path)
items = items.filter({ isDirectory((path + "/" + $0)) })
for item in items {
let name = String(item.dropLast(4))
if item.hasSuffix(".app"), !contains(name) {
array.append(Program(path: path, name: name, ext: ".app", img: nil))
}
if deepness > 0 {
array += indexDirs(at: path + "/" + item, deepness: deepness-1)
}
}
} catch { print("Error: \(error.localizedDescription)") }
return array
}
public func updateIndex() {
print("updateIndex()")
for path in paths {
rebuildIndex(at: path.key)
}
refreshFilesystemWatchers()
} }
} }

View File

@@ -52,7 +52,7 @@ class PopoverPanel: NSPanel {
in: modifiers) && in: modifiers) &&
key == kVK_ANSI_R key == kVK_ANSI_R
{ {
PathManager.shared.rebuildIndex() PathManager.shared.updateIndex()
return true return true
} else if key == kVK_Escape { } else if key == kVK_Escape {
resignKey() resignKey()

View File

@@ -7,8 +7,7 @@ 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 let selectionColor = NSColor.controlAccentColor.withAlphaComponent(0.8)
.withAlphaComponent(0.8)
selectionColor.setFill() selectionColor.setFill()
self.bounds.fill() self.bounds.fill()
} }
@@ -22,8 +21,7 @@ class ProgramsTableViewCell: NSTableCellView {
public var appIconImage: NSImageView = { public var appIconImage: NSImageView = {
let image = NSImageView() let image = NSImageView()
image.image = image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -40,9 +38,7 @@ class ProgramsTableViewCell: NSTableCellView {
public var progPathLabel: NSTextField = { public var progPathLabel: NSTextField = {
let textField = NSTextField(labelWithString: "") let textField = NSTextField(labelWithString: "")
textField.cell?.lineBreakMode = .byTruncatingTail textField.cell?.lineBreakMode = .byTruncatingTail
textField.font = NSFont.systemFont( textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .caption1).pointSize, weight: .medium)
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .caption1).pointSize, weight: .medium)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -59,23 +55,15 @@ class ProgramsTableViewCell: NSTableCellView {
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: leadingAnchor, appIconImage.leadingAnchor.constraint(equalTo: leadingAnchor, constant: ViewConstants.spacing5),
constant: ViewConstants.spacing5),
titleField.topAnchor.constraint( titleField.topAnchor.constraint(equalTo: appIconImage.topAnchor, constant: ViewConstants.spacing2),
equalTo: appIconImage.topAnchor, titleField.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing5),
constant: ViewConstants.spacing2),
titleField.leadingAnchor.constraint(
equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing5),
titleField.trailingAnchor.constraint(equalTo: trailingAnchor), titleField.trailingAnchor.constraint(equalTo: trailingAnchor),
progPathLabel.topAnchor.constraint( progPathLabel.topAnchor.constraint(equalTo: titleField.bottomAnchor),
equalTo: titleField.bottomAnchor), progPathLabel.leadingAnchor.constraint(equalTo: titleField.leadingAnchor),
progPathLabel.leadingAnchor.constraint( progPathLabel.trailingAnchor.constraint(equalTo: titleField.trailingAnchor),
equalTo: titleField.leadingAnchor),
progPathLabel.trailingAnchor.constraint(
equalTo: titleField.trailingAnchor),
]) ])
} }

View File

@@ -6,12 +6,16 @@ import Carbon
// elements inside of it. // elements inside of it.
fileprivate let windowCornerRadius = 15.0 fileprivate let windowCornerRadius = 15.0
struct ProgramWeighted {
let program: Program
let weight: Int
}
class SearchViewController: NSViewController, NSTextFieldDelegate, class SearchViewController: NSViewController, NSTextFieldDelegate,
NSPopoverDelegate, NSTableViewDataSource, NSTableViewDelegate NSPopoverDelegate, NSTableViewDataSource, NSTableViewDelegate
{ {
private var keyboardEvents: EventMonitor? private var keyboardEvents: EventMonitor?
private var foundProgram: Program? = nil
private var programsList: [Program] = [] private var programsList: [Program] = []
private var programsTableViewSelection = 0 private var programsTableViewSelection = 0
@@ -38,8 +42,7 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
effect.wantsLayer = true effect.wantsLayer = true
effect.layer?.masksToBounds = true effect.layer?.masksToBounds = true
effect.layer?.borderColor = NSColor.labelColor effect.layer?.borderColor = NSColor.labelColor.withAlphaComponent(0.1).cgColor
.withAlphaComponent(0.1).cgColor
effect.layer?.borderWidth = 1 effect.layer?.borderWidth = 1
effect.layer?.cornerRadius = windowCornerRadius effect.layer?.cornerRadius = windowCornerRadius
@@ -68,17 +71,14 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
textField.focusRingType = .none textField.focusRingType = .none
textField.placeholderString = "Program Search" textField.placeholderString = "Program Search"
textField.bezelStyle = .roundedBezel textField.bezelStyle = .roundedBezel
textField.font = NSFont.systemFont( textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .largeTitle).pointSize, weight: .medium)
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, button.image = systemImage("gear.circle.fill", .title1, .large, .init(paletteColors: [.labelColor, .systemGray]))
.init(paletteColors: [.labelColor, .systemGray]))
button.isBordered = false button.isBordered = false
button.action = #selector(openSettings) button.action = #selector(openSettings)
button.sizeToFit() button.sizeToFit()
@@ -90,10 +90,7 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
private var tableScrollView: NSScrollView = { private var tableScrollView: NSScrollView = {
let scroll = NSScrollView() let scroll = NSScrollView()
scroll.automaticallyAdjustsContentInsets = false scroll.automaticallyAdjustsContentInsets = false
scroll.contentInsets = NSEdgeInsets( scroll.contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: ViewConstants.spacing10, right: 0)
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
@@ -133,64 +130,37 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
var tableViewHeightAnchor: NSLayoutConstraint? var tableViewHeightAnchor: NSLayoutConstraint?
private func setConstraints() { private func setConstraints() {
tableViewHeightAnchor = tableScrollView.heightAnchor tableViewHeightAnchor = tableScrollView.heightAnchor.constraint(equalToConstant: 0)
.constraint(equalToConstant: 0)
tableViewHeightAnchor?.isActive = true tableViewHeightAnchor?.isActive = true
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: contentView.topAnchor, view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: -100),
constant: -100), view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 100),
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -100),
constant: 100), view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 100),
view.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor, constant: -100),
view.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor, constant: 100),
shadowView.topAnchor.constraint( shadowView.topAnchor.constraint(equalTo: backgroundView.topAnchor),
equalTo: backgroundView.topAnchor), shadowView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor),
shadowView.bottomAnchor.constraint( shadowView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
equalTo: backgroundView.bottomAnchor), shadowView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
shadowView.leadingAnchor.constraint(
equalTo: backgroundView.leadingAnchor),
shadowView.trailingAnchor.constraint(
equalTo: backgroundView.trailingAnchor),
backgroundView.topAnchor.constraint( backgroundView.topAnchor.constraint(equalTo: contentView.topAnchor),
equalTo: contentView.topAnchor), backgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
backgroundView.bottomAnchor.constraint( backgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
equalTo: contentView.bottomAnchor), backgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
backgroundView.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor),
backgroundView.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor),
searchInput.widthAnchor.constraint(equalToConstant: 350), searchInput.widthAnchor.constraint(equalToConstant: 350),
searchInput.topAnchor.constraint( searchInput.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ViewConstants.spacing10),
equalTo: contentView.topAnchor, searchInput.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: ViewConstants.spacing15),
constant: ViewConstants.spacing10),
searchInput.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor,
constant: ViewConstants.spacing15),
settingsButton.centerYAnchor.constraint( settingsButton.centerYAnchor.constraint(equalTo: searchInput.centerYAnchor),
equalTo: searchInput.centerYAnchor), settingsButton.leadingAnchor.constraint(equalTo: searchInput.trailingAnchor, constant: ViewConstants.spacing5),
settingsButton.leadingAnchor.constraint( settingsButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -ViewConstants.spacing10),
equalTo: searchInput.trailingAnchor,
constant: ViewConstants.spacing5),
settingsButton.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor,
constant: -ViewConstants.spacing10),
tableScrollView.topAnchor.constraint( tableScrollView.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: ViewConstants.spacing10),
equalTo: searchInput.bottomAnchor, tableScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
constant: ViewConstants.spacing10), tableScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
tableScrollView.bottomAnchor.constraint( tableScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
equalTo: contentView.bottomAnchor),
tableScrollView.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor),
tableScrollView.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor)
]) ])
} }
@@ -200,45 +170,34 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
view.wantsLayer = true view.wantsLayer = true
view.layer?.backgroundColor = NSColor.clear.cgColor view.layer?.backgroundColor = NSColor.clear.cgColor
keyboardEvents = LocalEventMonitor(mask: [.keyDown], handler: keyboardEvents = LocalEventMonitor(mask: [.keyDown]) { [weak self] event in
{ [weak self] event in
let key = event.keyCode let key = event.keyCode
let modifiers = event.modifierFlags.rawValue let modifiers = event.modifierFlags.rawValue
// TODO: Implement helper functions for modifiers.
if let controller = self { if let controller = self {
if modsContains(keys: OSCtrl, in: modifiers) && if modsContains(keys: OSCtrl, in: modifiers) && key == kVK_ANSI_P ||
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) && } else if modsContains(keys: OSCtrl, in: modifiers) && key == kVK_ANSI_N ||
key == kVK_ANSI_N || modsContainsNone(in: modifiers) && key == kVK_DownArrow
modsContainsNone(in: modifiers) &&
(key == kVK_DownArrow)
{ {
controller.programsTableViewSelection += 1 controller.programsTableViewSelection += 1
} }
if controller.programsTableViewSelection > if controller.programsTableViewSelection > controller.programsList.count-1 {
controller.programsList.count-1 controller.programsTableViewSelection = controller.programsList.count-1
{
controller.programsTableViewSelection =
controller.programsList.count-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( self?.programsTableView.selectRowIndexes(IndexSet(integer: select), byExtendingSelection: false)
IndexSet(integer: select),
byExtendingSelection: false)
self?.programsTableView.scrollRowToVisible(select) self?.programsTableView.scrollRowToVisible(select)
} }
return event return event
}) }
settingsPopover.delegate = self settingsPopover.delegate = self
@@ -290,7 +249,7 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
@objc @objc
func openSettings() { func openSettings() {
// HACK: This is an interseting behavior. When NSPopover appears // HACK: This is an interesting behavior. When NSPopover appears
// the first time, it always displays in the wrong location; // the first time, it always displays in the wrong location;
// however, showing it twice does result in the right // however, showing it twice does result in the right
// location. // location.
@@ -307,19 +266,16 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
} }
private func openProgram(_ program: Program) { private func openProgram(_ program: Program) {
let url = URL(fileURLWithPath: program.path) let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext)
.appendingPathComponent(program.name+program.ext)
let config = NSWorkspace.OpenConfiguration() let config = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: url, // NOTE: This needs a window! Do not just copy-paste
configuration: config) // this block elsewhere.
{ [weak self] application, error in NSWorkspace.shared.openApplication(at: url, configuration: config) { [weak self] application, error in
if let error = error { if let error = error {
print("\(error.localizedDescription)") print("\(error.localizedDescription)")
} else { } else {
print("Program opened successfully") print("Program opened successfully")
// NOTE: This needs a window! Do not just copy-paste
// this block elsewhere.
DispatchQueue.main.async { DispatchQueue.main.async {
if let window = self?.view.window { if let window = self?.view.window {
window.resignKey() window.resignKey()
@@ -331,51 +287,46 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
func controlTextDidChange(_ obj: Notification) { func controlTextDidChange(_ obj: Notification) {
guard let searchInput = obj.object as? EditableNSTextField guard let searchInput = obj.object as? EditableNSTextField
else { return } else { return }
let programs = PathManager.shared.programs
programsList = [] programsList = []
for i in programs.indices {
var program = programs[i] if !searchInput.stringValue.isEmpty {
if programsList.count >= 10 { for path in PathManager.shared.paths {
break if programsList.count >= 10 { break }
}
if program.name.lowercased().contains( for i in path.value.indices {
searchInput.stringValue.lowercased()) var prog = path.value[i]
{ if programsList.count >= 10 { break }
let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext) if prog.name.lowercased().contains(searchInput.stringValue.lowercased())
let image = NSWorkspace.shared.icon(forFile: url.path) {
program.img = image let url = URL(fileURLWithPath: prog.path).appendingPathComponent(prog.name+prog.ext)
programsList.append(program) let image = NSWorkspace.shared.icon(forFile: url.path)
prog.img = image
programsList.append(prog)
}
}
} }
} }
reloadProgramsTableViewData() reloadProgramsTableViewData()
programsTableViewSelection = 0 programsTableViewSelection = 0
programsTableView.selectRowIndexes( programsTableView.selectRowIndexes(IndexSet(integer: programsTableViewSelection), byExtendingSelection: false)
IndexSet(integer: programsTableViewSelection),
byExtendingSelection: false)
programsTableView.scrollRowToVisible(programsTableViewSelection) programsTableView.scrollRowToVisible(programsTableViewSelection)
} }
func control(_ control: NSControl, textView: NSTextView, func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
doCommandBy commandSelector: Selector) -> Bool
{
if commandSelector == #selector(NSResponder.insertNewline(_:)) { if commandSelector == #selector(NSResponder.insertNewline(_:)) {
if programsList.count > 0 { if programsList.count > 0 {
let program = programsList[programsTableViewSelection] let program = programsList[programsTableViewSelection]
openProgram(program) openProgram(program)
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
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(_:)) || } else if commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.moveDown(_:)) {
commandSelector == #selector(NSResponder.moveDown(_:))
{
// Ignore arrows keys up or down because we use those to // Ignore arrows keys up or down because we use those to
// navigate the programs list. // navigate the programs list.
return true return true

View File

@@ -2,10 +2,9 @@ import AppKit
import Carbon import Carbon
import ServiceManagement import ServiceManagement
class SettingsViewController: NSViewController, NSTextFieldDelegate, class SettingsViewController: NSViewController,
KeyDetectorButtonDelegate, NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource,
NSTableViewDataSource, NSTableViewDelegate, NSTableViewDelegate, PathsTableCellViewDelegate
PathsTableCellViewDelegate
{ {
private var recording = false private var recording = false
@@ -14,6 +13,8 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
private var keyCode = Int(kVK_Space) private var keyCode = Int(kVK_Space)
private var modifiers = Int(optionKey) private var modifiers = Int(optionKey)
private var paths: [String] = []
// NOTE: PERF: This is very slow to initialize because it creates a // NOTE: PERF: This is very slow to initialize because it creates a
// a new process. This also cannot be done on a separate // a new process. This also cannot be done on a separate
// thread. This sucks because the program now takes // thread. This sucks because the program now takes
@@ -29,13 +30,22 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
private var shortcutsLabel: NSTextField = { private var shortcutsLabel: NSTextField = {
let textField = NSTextField(labelWithString: "Shortcut") let textField = NSTextField(labelWithString: "Shortcut")
textField.font = NSFont.systemFont( textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold)
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .title2).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var aboutButton: NSButton = {
let button = NSButton()
button.image = systemImage("info.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemGray]))
button.isBordered = false
button.action = #selector(showAbout)
button.sizeToFit()
button.toolTip = "About"
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var ctrlButton: NSButton = { private var ctrlButton: NSButton = {
let button = NSButton() let button = NSButton()
button.title = "" button.title = ""
@@ -87,9 +97,7 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .center textField.alignment = .center
textField.font = NSFont.systemFont( textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold)
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .body).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -104,11 +112,8 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
}() }()
private var pathsLabel: NSTextField = { private var pathsLabel: NSTextField = {
let textField = let textField = NSTextField(labelWithString: "Application Directories")
NSTextField(labelWithString: "Application Directories") 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
}() }()
@@ -132,8 +137,7 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
table.allowsColumnReordering = false table.allowsColumnReordering = false
table.allowsColumnResizing = false table.allowsColumnResizing = false
table.allowsColumnSelection = false table.allowsColumnSelection = false
table.addTableColumn(NSTableColumn( table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Paths")))
identifier: NSUserInterfaceItemIdentifier("Paths")))
//rowHeight cgfloat must see doc //rowHeight cgfloat must see doc
@@ -146,12 +150,8 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
control.segmentCount = 2 control.segmentCount = 2
control.segmentStyle = .roundRect control.segmentStyle = .roundRect
control.setImage( control.setImage(NSImage(systemSymbolName: "plus", accessibilityDescription: nil), forSegment: 0)
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)
@@ -187,6 +187,7 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
private func addSubviews() { private func addSubviews() {
view.addSubview(shortcutsLabel) view.addSubview(shortcutsLabel)
view.addSubview(aboutButton)
view.addSubview(ctrlButton) view.addSubview(ctrlButton)
view.addSubview(cmdButton) view.addSubview(cmdButton)
view.addSubview(optButton) view.addSubview(optButton)
@@ -204,87 +205,49 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
private func setConstraints() { private func setConstraints() {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
shortcutsLabel.topAnchor.constraint( shortcutsLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10),
equalTo: view.topAnchor, shortcutsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10),
constant: ViewConstants.spacing10),
shortcutsLabel.leadingAnchor.constraint(
equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
ctrlButton.topAnchor.constraint( aboutButton.firstBaselineAnchor.constraint(equalTo: shortcutsLabel.firstBaselineAnchor),
equalTo: shortcutsLabel.bottomAnchor, aboutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10),
constant: ViewConstants.spacing10),
ctrlButton.leadingAnchor.constraint(
equalTo: shortcutsLabel.leadingAnchor),
cmdButton.centerYAnchor.constraint( ctrlButton.topAnchor.constraint(equalTo: shortcutsLabel.bottomAnchor, constant: ViewConstants.spacing10),
equalTo: ctrlButton.centerYAnchor), ctrlButton.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor),
cmdButton.leadingAnchor.constraint(
equalTo: ctrlButton.trailingAnchor,
constant: ViewConstants.spacing5),
optButton.centerYAnchor.constraint( cmdButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
equalTo: ctrlButton.centerYAnchor), cmdButton.leadingAnchor.constraint(equalTo: ctrlButton.trailingAnchor, constant: ViewConstants.spacing5),
optButton.leadingAnchor.constraint(
equalTo: cmdButton.trailingAnchor,
constant: ViewConstants.spacing5),
shiftButton.centerYAnchor.constraint( optButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
equalTo: ctrlButton.centerYAnchor), optButton.leadingAnchor.constraint(equalTo: cmdButton.trailingAnchor, constant: ViewConstants.spacing5),
shiftButton.leadingAnchor.constraint(
equalTo: optButton.trailingAnchor,
constant: ViewConstants.spacing5),
plusLabel.centerYAnchor.constraint( shiftButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
equalTo: ctrlButton.centerYAnchor), shiftButton.leadingAnchor.constraint(equalTo: optButton.trailingAnchor, constant: ViewConstants.spacing5),
plusLabel.leadingAnchor.constraint(
equalTo: shiftButton.trailingAnchor, plusLabel.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
constant: ViewConstants.spacing5), plusLabel.leadingAnchor.constraint(equalTo: shiftButton.trailingAnchor, constant: ViewConstants.spacing5),
recordButton.widthAnchor.constraint(equalToConstant: 40), recordButton.widthAnchor.constraint(equalToConstant: 40),
recordButton.centerYAnchor.constraint( recordButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
equalTo: ctrlButton.centerYAnchor), recordButton.leadingAnchor.constraint(equalTo: plusLabel.trailingAnchor, constant: ViewConstants.spacing5),
recordButton.leadingAnchor.constraint(
equalTo: plusLabel.trailingAnchor,
constant: ViewConstants.spacing5),
pathsLabel.topAnchor.constraint( pathsLabel.topAnchor.constraint(equalTo: ctrlButton.bottomAnchor, constant: ViewConstants.spacing20),
equalTo: ctrlButton.bottomAnchor, pathsLabel.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor),
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( tableScrollView.topAnchor.constraint(equalTo: pathsLabel.bottomAnchor),
equalTo: pathsLabel.bottomAnchor), tableScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableScrollView.leadingAnchor.constraint( tableScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
equalTo: view.leadingAnchor),
tableScrollView.trailingAnchor.constraint(
equalTo: view.trailingAnchor),
pathsControl.topAnchor.constraint( pathsControl.topAnchor.constraint(equalTo: tableScrollView.bottomAnchor, constant: ViewConstants.spacing10),
equalTo: tableScrollView.bottomAnchor, pathsControl.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10),
constant: ViewConstants.spacing10),
pathsControl.leadingAnchor.constraint(
equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
launchAtLoginButton.topAnchor.constraint( launchAtLoginButton.topAnchor.constraint(equalTo: pathsControl.bottomAnchor, constant: ViewConstants.spacing10),
equalTo: pathsControl.bottomAnchor, launchAtLoginButton.trailingAnchor.constraint(equalTo: resetAllButton.leadingAnchor, constant: -ViewConstants.spacing10),
constant: ViewConstants.spacing10),
launchAtLoginButton.trailingAnchor.constraint(
equalTo: resetAllButton.leadingAnchor,
constant: -ViewConstants.spacing10),
resetAllButton.centerYAnchor.constraint( resetAllButton.centerYAnchor.constraint(equalTo: launchAtLoginButton.centerYAnchor),
equalTo: launchAtLoginButton.centerYAnchor), resetAllButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10),
resetAllButton.trailingAnchor.constraint( resetAllButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing10),
equalTo: view.trailingAnchor,
constant: -ViewConstants.spacing10),
resetAllButton.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10),
]) ])
} }
@@ -320,26 +283,20 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
// PERF: Maybe we shouldn't fetch it on every appearance? // PERF: Maybe we shouldn't fetch it on every appearance?
// Only do it in AppDelegate? // Only do it in AppDelegate?
if let code = if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int {
UserDefaults.standard.object(forKey: "keyCode") as? Int
{
keyCode = code keyCode = code
} }
if let mods = if let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int {
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
{
modifiers = mods modifiers = mods
} }
pathsTableView.reloadData() loadPaths()
syncModifierButtons() syncModifierButtons()
launchAtLoginStatus() launchAtLoginStatus()
} }
override func viewDidAppear() { override func viewDidAppear() {
super.viewDidAppear() super.viewDidAppear()
self.view.window?.center()
} }
override func viewWillDisappear() { override func viewWillDisappear() {
@@ -351,19 +308,34 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
UserDefaults.standard.set(keyCode, forKey: "keyCode") UserDefaults.standard.set(keyCode, forKey: "keyCode")
UserDefaults.standard.set(modifiers, forKey: "keyModifiers") UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
PathManager.shared.removeEmpty() // Merge PathManagers paths and user paths.
// WARNING: This seems a bit error prone.
for path in paths {
if !PathManager.shared.contains(path) {
PathManager.shared.addPath(path)
}
}
for path in PathManager.shared.paths {
if !paths.contains(path.key) {
PathManager.shared.removePath(path.key)
}
}
PathManager.shared.updateIndex()
PathManager.shared.savePaths() PathManager.shared.savePaths()
PathManager.shared.rebuildIndex()
} }
override func loadView() { override func loadView() {
self.view = NSView() self.view = NSView()
} }
@objc
private func showAbout() {
delegate.showAboutWindow()
}
@objc @objc
private func handleModifiers() { private func handleModifiers() {
// NOTE: Revert to default modifier if none of the modifier // Revert to default modifier if none of the modifier buttons are on.
// buttons are on.
if cmdButton.state != .on, optButton.state != .on, if cmdButton.state != .on, optButton.state != .on,
ctrlButton.state != .on, shiftButton.state != .on ctrlButton.state != .on, shiftButton.state != .on
{ {
@@ -391,14 +363,23 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
@objc @objc
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()
PathManager.shared.reset() PathManager.shared.resetPaths()
loadPaths()
}
private func loadPaths() {
paths = []
for path in PathManager.shared.paths {
paths.append(path.key)
}
paths.sort()
pathsTableView.reloadData() pathsTableView.reloadData()
} }
@@ -454,25 +435,17 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
let selectedSegment = sender.selectedSegment let selectedSegment = sender.selectedSegment
switch selectedSegment { switch selectedSegment {
case 0: case 0:
// NOTE: Seems a bit error prone. let row = paths.count
var row = PathManager.shared.userPaths.count-1 paths.append("")
if !PathManager.shared.userPaths[row].isEmpty { pathsTableView.insertRows(at: IndexSet(integer: row), withAnimation: [])
row += 1
PathManager.shared.addPath("")
pathsTableView.insertRows(at: IndexSet(integer: row),
withAnimation: [])
}
pathsTableView.scrollRowToVisible(row) pathsTableView.scrollRowToVisible(row)
pathsTableView.selectRowIndexes(IndexSet(integer: row), pathsTableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
byExtendingSelection: false) (pathsTableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? PathsTableCellView)?.startEditing()
(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 {
PathManager.shared.userPaths paths.remove(at: pathsTableView.selectedRow)
.remove(at: pathsTableView.selectedRow)
pathsTableView.reloadData() pathsTableView.reloadData()
} }
break break
@@ -484,23 +457,18 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
@objc @objc
private func editItem(_ sender: NSTableView) { private func editItem(_ sender: NSTableView) {
pathsTableView.deselectAll(nil) pathsTableView.deselectAll(nil)
pathsTableView.selectRowIndexes( pathsTableView.selectRowIndexes(IndexSet(integer: pathsTableView.clickedRow), byExtendingSelection: false)
IndexSet(integer: pathsTableView.clickedRow),
byExtendingSelection: false)
if let cell = pathsTableView.view(atColumn: 0, if let cell = pathsTableView.view(atColumn: 0, row: pathsTableView.clickedRow, makeIfNecessary: false) as? PathsTableCellView {
row: pathsTableView.clickedRow,
makeIfNecessary: false) as? PathsTableCellView
{
cell.startEditing() cell.startEditing()
} }
} }
func titleFieldFinishedEditing(tag: Int, text: String) { func titleFieldFinishedEditing(tag: Int, text: String) {
if text.isEmpty { if text.isEmpty {
PathManager.shared.userPaths.remove(at: tag) paths.remove(at: tag)
} else { } else {
PathManager.shared.userPaths[tag] = text paths[tag] = text
} }
pathsTableView.reloadData() pathsTableView.reloadData()
} }
@@ -515,34 +483,32 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
func selectionButtonClicked(tag: Int) { func selectionButtonClicked(tag: Int) {
NSRunningApplication.current.activate(options: .activateAllWindows) NSRunningApplication.current.activate(options: .activateAllWindows)
delegate.window.level = .normal delegate.window.level = .normal
delegate.aboutWindow.performClose(nil)
if dirPicker.runModal() == .OK { if dirPicker.runModal() == .OK {
if let url = dirPicker.url { if let url = dirPicker.url {
PathManager.shared.userPaths[tag] = url.path paths[tag] = url.path
pathsTableView.reloadData() pathsTableView.reloadData()
} }
} }
delegate.window.level = .statusBar delegate.window.level = .statusBar
delegate.window.makeKeyAndOrderFront(nil) delegate.window.makeKeyAndOrderFront(nil)
if let controller = if let controller = delegate.window.contentViewController as? SearchViewController {
delegate.window.contentViewController as? SearchViewController
{
controller.openSettings() controller.openSettings()
} }
} }
func numberOfRows(in tableView: NSTableView) -> Int { func numberOfRows(in tableView: NSTableView) -> Int {
return PathManager.shared.userPaths.count return paths.count
} }
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, let rect = NSRect(x: 0, y: 0, width: tableColumn!.width, height: 20)
width: tableColumn!.width, height: 20)
let cell = PathsTableCellView(frame: rect) let cell = PathsTableCellView(frame: rect)
cell.titleField.stringValue = PathManager.shared.userPaths[row] cell.titleField.stringValue = paths[row]
cell.delegate = self cell.delegate = self
cell.id = row cell.id = row
return cell return cell