Thou shalt not cross 80 columns in thy file.
This commit is contained in:
@@ -9,21 +9,22 @@ fileprivate enum AboutLinks {
|
||||
}
|
||||
|
||||
enum Strings {
|
||||
static let copyright = "Copyright © 2024\nGarikMI. All rights reserved."
|
||||
static let evaluationTitle = "License - Evaluation"
|
||||
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..."
|
||||
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.image =
|
||||
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
|
||||
image.imageScaling = .scaleAxesIndependently
|
||||
image.translatesAutoresizingMaskIntoConstraints = false
|
||||
return image
|
||||
@@ -31,25 +32,35 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
|
||||
private var appNameLabel: 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.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
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
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var versionLabel: 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.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.font =
|
||||
NSFont.systemFont(ofSize: NSFontDescriptor
|
||||
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
|
||||
weight: .regular)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
@@ -64,7 +75,10 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .center
|
||||
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
|
||||
return textField
|
||||
}()
|
||||
@@ -152,44 +166,77 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
// 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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
// 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),
|
||||
// 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),
|
||||
// websiteButton.firstBaselineAnchor
|
||||
// .constraint(equalTo: privacyButton.firstBaselineAnchor),
|
||||
// websiteButton.leadingAnchor
|
||||
// .constraint(equalTo: documentationButton.trailingAnchor,
|
||||
// constant: ViewConstants.spacing10),
|
||||
// websiteButton.trailingAnchor
|
||||
// .constraint(equalTo: buttonsContainer.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
|
||||
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,
|
||||
!(event.eventID == kAEOpenApplication && event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem)
|
||||
!(event.eventID == kAEOpenApplication &&
|
||||
event.paramDescriptor(forKeyword: keyAEPropData)?
|
||||
.enumCodeValue == keyAELaunchedAsLogInItem)
|
||||
{
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
HotKeyManager.shared.handler = { (inHandlerCallRef, inEvent, inUserData) -> OSStatus in
|
||||
if let delegate = NSApplication.shared.delegate as? AppDelegate {
|
||||
HotKeyManager.shared.handler =
|
||||
{ (inHandlerCallRef, inEvent, inUserData) -> OSStatus in
|
||||
if let delegate = NSApplication.shared.delegate as? AppDelegate
|
||||
{
|
||||
let window = delegate.window
|
||||
if window.isKeyWindow {
|
||||
window.resignKey()
|
||||
} else {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
if let controller = window.contentViewController as? SearchViewController {
|
||||
if let controller =
|
||||
window.contentViewController as? SearchViewController
|
||||
{
|
||||
controller.centerWindow()
|
||||
}
|
||||
}
|
||||
@@ -38,13 +45,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
}
|
||||
|
||||
HotKeyManager.shared.enable()
|
||||
if let code = UserDefaults.standard.object(forKey: "keyCode") as? Int,
|
||||
let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int
|
||||
if let code =
|
||||
UserDefaults.standard.object(forKey: "keyCode") as? Int,
|
||||
let mods =
|
||||
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
|
||||
{
|
||||
HotKeyManager.shared.registerHotKey(key: code, modifiers: mods)
|
||||
} else {
|
||||
// NOTE: This is the default shortcut. If you want to change it, do not forget to change it in other files (SettingsViewController).
|
||||
HotKeyManager.shared.registerHotKey(key: kVK_Space, modifiers: optionKey)
|
||||
// NOTE: This is the default shortcut. If you want to change
|
||||
// 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 {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
@@ -85,7 +98,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
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) {
|
||||
if containsFlags(key: kFSEventStreamEventFlagItemCreated, in: flags) ||
|
||||
containsFlags(key: kFSEventStreamEventFlagItemRemoved, in: flags) ||
|
||||
|
||||
@@ -25,22 +25,34 @@ class DirMonitor {
|
||||
var context = FSEventStreamContext()
|
||||
context.info = Unmanaged.passUnretained(self).toOpaque()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
for i in 0..<numEvents {
|
||||
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.
|
||||
if !containsFlags(key: kFSEventStreamEventFlagItemIsDir, in: flags) || !url.path.hasSuffix(".app") {
|
||||
// Since this is a directory monitor, we discard file
|
||||
// events.
|
||||
if !containsFlags(key: kFSEventStreamEventFlagItemIsDir,
|
||||
in: flags) ||
|
||||
!url.path.hasSuffix(".app")
|
||||
{
|
||||
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 {
|
||||
delegate.fsEventTriggered(url.path, flags)
|
||||
}
|
||||
@@ -50,7 +62,8 @@ class DirMonitor {
|
||||
self.dirs, // [path as NSString] as NSArray,
|
||||
UInt64(kFSEventStreamEventIdSinceNow),
|
||||
2.0,
|
||||
FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents) // FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
|
||||
// FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
|
||||
FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents)
|
||||
) else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -15,23 +15,33 @@ final class EditableNSTextField: NSTextField {
|
||||
if event.type == NSEvent.EventType.keyDown {
|
||||
if modsContains(keys: OSCmd, in: modifiers) {
|
||||
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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
} else if isNumericalCode(key) { // Ignore Command + {1-9}.
|
||||
@@ -39,7 +49,9 @@ final class EditableNSTextField: NSTextField {
|
||||
}
|
||||
} else if modsContains(keys: OSCmd | OSShift, in: modifiers) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ final class LocalEventMonitor: EventMonitor {
|
||||
}
|
||||
|
||||
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() {
|
||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
|
||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask,
|
||||
handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ func modsContains(keys: UInt, in modifiers: UInt) -> 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 {
|
||||
@@ -46,8 +48,10 @@ func keyName(virtualKeyCode: UInt16) -> String? {
|
||||
|
||||
//let source =
|
||||
// TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
|
||||
let source = TISCopyInputSourceForLanguage("en-US" as CFString).takeRetainedValue();
|
||||
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
|
||||
let source = TISCopyInputSourceForLanguage("en-US" as CFString)
|
||||
.takeRetainedValue();
|
||||
guard let ptr =
|
||||
TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
|
||||
else {
|
||||
print("Could not get keyboard layout data")
|
||||
return nil
|
||||
@@ -55,9 +59,11 @@ func keyName(virtualKeyCode: UInt16) -> String? {
|
||||
let layoutData = Unmanaged<CFData>.fromOpaque(ptr)
|
||||
.takeUnretainedValue() as Data
|
||||
let osStatus = layoutData.withUnsafeBytes {
|
||||
UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode,
|
||||
UInt16(kUCKeyActionDown), modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
|
||||
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
|
||||
UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress,
|
||||
virtualKeyCode, UInt16(kUCKeyActionDown),
|
||||
modifierKeys, keyboardType,
|
||||
UInt32(kUCKeyTranslateNoDeadKeysMask), &deadKeys,
|
||||
maxNameLength, &nameLength, &nameBuffer)
|
||||
}
|
||||
guard osStatus == noErr else {
|
||||
print("Code: \(virtualKeyCode) Status: \(osStatus)")
|
||||
@@ -78,16 +84,23 @@ func keyName(virtualKeyCode: UInt16) -> String? {
|
||||
|
||||
func isDirectory(_ path: String) -> Bool {
|
||||
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
|
||||
} else {
|
||||
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)?
|
||||
.withSymbolConfiguration(NSImage.SymbolConfiguration(textStyle: size, scale: scale).applying(configuration))
|
||||
.withSymbolConfiguration(NSImage
|
||||
.SymbolConfiguration(textStyle: size, scale: scale)
|
||||
.applying(configuration))
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
@@ -4,12 +4,15 @@ import OSLog
|
||||
final class 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?
|
||||
public var handler: EventHandlerUPP?
|
||||
|
||||
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() {}
|
||||
deinit {}
|
||||
@@ -20,7 +23,8 @@ final class HotKeyManager {
|
||||
disable()
|
||||
}
|
||||
|
||||
let err = InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType, nil, &eventHandlerRef)
|
||||
let err = InstallEventHandler(GetApplicationEventTarget(), handler,
|
||||
1, &eventType, nil, &eventHandlerRef)
|
||||
if err == noErr {
|
||||
print("Installed event handler.")
|
||||
} else {
|
||||
@@ -32,7 +36,8 @@ final class HotKeyManager {
|
||||
guard eventHandlerRef != nil else { return }
|
||||
let err = RemoveEventHandler(eventHandlerRef)
|
||||
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.")
|
||||
} else {
|
||||
print("Failed to remove event handler.")
|
||||
@@ -46,7 +51,10 @@ final class HotKeyManager {
|
||||
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 {
|
||||
print("Registered hot key.")
|
||||
} else {
|
||||
@@ -59,7 +67,8 @@ final class HotKeyManager {
|
||||
guard hotKeyRef != nil else { return }
|
||||
let err = UnregisterEventHotKey(hotKeyRef)
|
||||
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.")
|
||||
} else {
|
||||
print("Failed to unregistered hot key.")
|
||||
|
||||
@@ -13,7 +13,8 @@ final class KeyDetectorButton: NSButton {
|
||||
|
||||
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 mouseUp(with event: NSEvent) {
|
||||
@@ -24,7 +25,9 @@ final class KeyDetectorButton: NSButton {
|
||||
if event.keyCode == kVK_Escape || event.keyCode == kVK_Return {
|
||||
// Ignore escape and return keys.
|
||||
} 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
|
||||
}
|
||||
} else {
|
||||
|
||||
70
src/Makefile
70
src/Makefile
@@ -7,10 +7,13 @@ XCODE_PATH = $(shell xcode-select --print-path)
|
||||
|
||||
EXEC = Grapp
|
||||
|
||||
SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift PopoverPanel.swift SearchViewController.swift \
|
||||
SettingsViewController.swift HotKeyManager.swift KeyDetectorButton.swift PathManager.swift PathsTableCellView.swift \
|
||||
ProgramsTable.swift ShadowView.swift DirMonitor.swift MenulessWindow.swift AboutViewController.swift AppDelegate.swift \
|
||||
main.swift
|
||||
SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift \
|
||||
PopoverPanel.swift SearchViewController.swift \
|
||||
SettingsViewController.swift HotKeyManager.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))
|
||||
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))
|
||||
|
||||
@@ -18,35 +21,52 @@ LIBS =
|
||||
|
||||
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
|
||||
# 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.
|
||||
# HACK: Target is getting touched because timestamps of the generated
|
||||
# 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
|
||||
# I can't imagine why timestamps wouldn't change. When clang
|
||||
# generates same exact executable, timestamps do change.
|
||||
./arm64/%.o: %.swift
|
||||
swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \
|
||||
$(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && \
|
||||
swift -frontend -c \
|
||||
-target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \
|
||||
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \
|
||||
$(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ \
|
||||
-emit-module && \
|
||||
touch $@
|
||||
|
||||
ifdef UNIVERSAL
|
||||
./x86_64/%.o: %.swift
|
||||
@swift -frontend -c -target x86_64-apple-macos$(MACOS_VERSION) $(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) \
|
||||
$(LIBS) $(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && \
|
||||
@swift -frontend -c \
|
||||
-target x86_64-apple-macos$(MACOS_VERSION) $(FLAGS) \
|
||||
-primary-file $< $(filter-out $<, $(SRCMODULES)) \
|
||||
$(LIBS) $(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ \
|
||||
-emit-module && \
|
||||
touch $@
|
||||
endif
|
||||
|
||||
./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 \
|
||||
-sectcreate __TEXT __info_plist Info.plist -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 \
|
||||
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) -o $@
|
||||
-sectcreate __TEXT __info_plist Info.plist \
|
||||
-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 \
|
||||
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
|
||||
./arm64/main.o $(filter-out ./arm64/main.o, $(ARMOBJMODULES)) -o $@
|
||||
|
||||
ifdef UNIVERSAL
|
||||
./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 \
|
||||
-sectcreate __TEXT __info_plist Info.plist -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 \
|
||||
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx ./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) -o $@
|
||||
-sectcreate __TEXT __info_plist Info.plist \
|
||||
-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 \
|
||||
Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
|
||||
./x86_64/main.o $(filter-out ./x86_64/main.o, $(X86OBJMODULES)) -o $@
|
||||
endif
|
||||
|
||||
ifdef UNIVERSAL
|
||||
@@ -64,20 +84,22 @@ $(EXEC).app: $(EXEC)
|
||||
cp Info.plist $@/Contents/ && \
|
||||
cp resources/AppIcon.icns $@/Contents/Resources/ && \
|
||||
cp $(EXEC) $@/Contents/MacOS/ && \
|
||||
$(if $(DEBUG), codesign --entitlements Grapp.entitlements -s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \
|
||||
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp -o runtime $(EXEC).app)
|
||||
$(if $(DEBUG), codesign --entitlements Grapp.entitlements \
|
||||
-s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \
|
||||
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp \
|
||||
-o runtime $(EXEC).app)
|
||||
|
||||
all: $(EXEC).app
|
||||
|
||||
kill:
|
||||
-pkill $(EXEC)
|
||||
|
||||
open: kill all
|
||||
open $(EXEC).app
|
||||
|
||||
run: kill all
|
||||
./$(EXEC)
|
||||
|
||||
open: kill all
|
||||
open $(EXEC).app
|
||||
|
||||
clean:
|
||||
rm -rf $(EXEC) $(EXEC).app arm64 x86_64
|
||||
mkdir arm64 x86_64
|
||||
|
||||
@@ -23,7 +23,9 @@ class MenulessWindow: NSWindow {
|
||||
let key = event.keyCode
|
||||
|
||||
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)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ final class PathManager {
|
||||
|
||||
private init() {
|
||||
// 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 {
|
||||
addPath(dir)
|
||||
}
|
||||
@@ -79,7 +82,9 @@ final class PathManager {
|
||||
buf.append(path.key)
|
||||
}
|
||||
|
||||
dirMonitor = DirMonitor(paths: buf, queue: DispatchQueue.global(qos: .userInitiated))
|
||||
dirMonitor =
|
||||
DirMonitor(paths: buf,
|
||||
queue: DispatchQueue.global(qos: .userInitiated))
|
||||
// _ = dirMonitor!.start()
|
||||
if dirMonitor!.start() {
|
||||
print("Started monitoring directories.")
|
||||
@@ -115,10 +120,12 @@ final class PathManager {
|
||||
let name = String(item.dropLast(4))
|
||||
|
||||
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 {
|
||||
array += indexDirs(at: path + "/" + item, deepness: deepness-1)
|
||||
array += indexDirs(at: path + "/" + item,
|
||||
deepness: deepness-1)
|
||||
}
|
||||
}
|
||||
} catch { print("Error: \(error.localizedDescription)") }
|
||||
|
||||
@@ -28,7 +28,7 @@ class PathsTableCellView: NSTableCellView, NSTextFieldDelegate,
|
||||
var selectionButton: NSButton = {
|
||||
let button = NSButton()
|
||||
button.image = systemImage("hand.point.up.fill", .headline, .large,
|
||||
.init(paletteColors: [.labelColor, .systemRed]))
|
||||
.init(paletteColors: [.labelColor, .systemRed]))
|
||||
button.isBordered = false
|
||||
button.sizeToFit()
|
||||
button.toolTip = "Select Path"
|
||||
@@ -57,8 +57,8 @@ class PathsTableCellView: NSTableCellView, NSTextFieldDelegate,
|
||||
//titleField.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
titleField.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
titleField.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
titleField.trailingAnchor.constraint(
|
||||
equalTo: selectionButton.leadingAnchor),
|
||||
titleField.trailingAnchor
|
||||
.constraint(equalTo: selectionButton.leadingAnchor),
|
||||
|
||||
selectionButton.centerYAnchor.constraint(
|
||||
equalTo: centerYAnchor),
|
||||
|
||||
@@ -7,7 +7,8 @@ class PopoverPanel: NSPanel {
|
||||
init(viewController: NSViewController) {
|
||||
super.init(
|
||||
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
|
||||
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .fullSizeContentView],
|
||||
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow,
|
||||
.fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
@@ -25,7 +26,8 @@ class PopoverPanel: NSPanel {
|
||||
titlebarAppearsTransparent = true
|
||||
|
||||
animationBehavior = .none
|
||||
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient]
|
||||
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary,
|
||||
.transient]
|
||||
isReleasedWhenClosed = false
|
||||
hidesOnDeactivate = false
|
||||
}
|
||||
@@ -35,13 +37,19 @@ class PopoverPanel: NSPanel {
|
||||
let key = event.keyCode
|
||||
|
||||
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)
|
||||
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()
|
||||
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()
|
||||
return true
|
||||
} else if key == kVK_Escape {
|
||||
|
||||
@@ -7,7 +7,8 @@ final class ProgramsTableView: NSTableView {
|
||||
class ProgramsTableRowView: NSTableRowView {
|
||||
override func drawSelection(in dirtyRect: NSRect) {
|
||||
if self.selectionHighlightStyle != .none {
|
||||
let selectionColor = NSColor.controlAccentColor.withAlphaComponent(0.8)
|
||||
let selectionColor =
|
||||
NSColor.controlAccentColor.withAlphaComponent(0.8)
|
||||
selectionColor.setFill()
|
||||
self.bounds.fill()
|
||||
}
|
||||
@@ -28,7 +29,10 @@ class ProgramsTableViewCell: NSTableCellView {
|
||||
|
||||
field.textColor = NSColor.secondaryLabelColor
|
||||
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
|
||||
return field
|
||||
}()
|
||||
@@ -36,7 +40,8 @@ class ProgramsTableViewCell: NSTableCellView {
|
||||
|
||||
public var appIconImage: 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.translatesAutoresizingMaskIntoConstraints = false
|
||||
return image
|
||||
@@ -57,7 +62,10 @@ class ProgramsTableViewCell: NSTableCellView {
|
||||
field.isBordered = false
|
||||
field.drawsBackground = false
|
||||
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
|
||||
return field
|
||||
}()
|
||||
@@ -66,7 +74,8 @@ class ProgramsTableViewCell: NSTableCellView {
|
||||
super.init(frame: frameRect)
|
||||
|
||||
// wantsLayer = true
|
||||
// layer?.backgroundColor = NSColor.yellow.withAlphaComponent(0.2).cgColor
|
||||
// layer?.backgroundColor =
|
||||
// NSColor.yellow.withAlphaComponent(0.2).cgColor
|
||||
|
||||
addSubview(indexLabel)
|
||||
addSubview(appIconImage)
|
||||
@@ -77,21 +86,33 @@ class ProgramsTableViewCell: NSTableCellView {
|
||||
NSLayoutConstraint.activate([
|
||||
indexLabel.widthAnchor.constraint(equalToConstant: 25),
|
||||
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.heightAnchor.constraint(equalToConstant: 40),
|
||||
appIconImage.topAnchor.constraint(equalTo: topAnchor),
|
||||
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.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
titleField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -ViewConstants.spacing5),
|
||||
titleField.topAnchor
|
||||
.constraint(equalTo: appIconImage.topAnchor,
|
||||
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.leadingAnchor.constraint(equalTo: titleField.leadingAnchor),
|
||||
progPathLabel.trailingAnchor.constraint(equalTo: titleField.trailingAnchor),
|
||||
progPathLabel.topAnchor
|
||||
.constraint(equalTo: titleField.bottomAnchor),
|
||||
progPathLabel.leadingAnchor
|
||||
.constraint(equalTo: titleField.leadingAnchor),
|
||||
progPathLabel.trailingAnchor
|
||||
.constraint(equalTo: titleField.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ fileprivate let windowCornerRadius = 15.0
|
||||
|
||||
fileprivate let maxItems = 20
|
||||
|
||||
class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
||||
class SearchViewController: NSViewController, NSTextFieldDelegate,
|
||||
NSPopoverDelegate, NSTableViewDataSource,
|
||||
NSTableViewDelegate
|
||||
{
|
||||
private var keyboardEvents: EventMonitor?
|
||||
|
||||
private var listIndex = 0
|
||||
@@ -46,7 +49,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
|
||||
private var contentView: NSView = {
|
||||
let view = NSView()
|
||||
// Clip all content to window's rounded frame emulated by backgroundView.
|
||||
// Clip all content to window's rounded frame emulated by
|
||||
// backgroundView.
|
||||
view.wantsLayer = true
|
||||
view.layer?.masksToBounds = true
|
||||
view.layer?.cornerRadius = windowCornerRadius
|
||||
@@ -64,14 +68,18 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
textField.focusRingType = .none
|
||||
textField.placeholderString = "Program Search"
|
||||
textField.bezelStyle = .roundedBezel
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .largeTitle).pointSize, weight: .medium)
|
||||
textField.font = NSFont
|
||||
.systemFont(ofSize: NSFontDescriptor
|
||||
.preferredFontDescriptor(forTextStyle: .largeTitle).pointSize,
|
||||
weight: .medium)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var settingsButton: NSButton = {
|
||||
let button = NSButton()
|
||||
button.image = systemImage("gear.circle.fill", .title1, .large, .init(paletteColors: [.white, .systemGray]))
|
||||
button.image = systemImage("gear.circle.fill", .title1, .large,
|
||||
.init(paletteColors: [.white, .systemGray]))
|
||||
button.isBordered = false
|
||||
button.action = #selector(openSettings)
|
||||
button.sizeToFit()
|
||||
@@ -83,7 +91,9 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
private var tableScrollView: NSScrollView = {
|
||||
let scroll = NSScrollView()
|
||||
scroll.automaticallyAdjustsContentInsets = false
|
||||
scroll.contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: ViewConstants.spacing10, right: 0)
|
||||
scroll.contentInsets = NSEdgeInsets(top: 0, left: 0,
|
||||
bottom: ViewConstants.spacing10,
|
||||
right: 0)
|
||||
scroll.drawsBackground = false
|
||||
scroll.translatesAutoresizingMaskIntoConstraints = false
|
||||
return scroll
|
||||
@@ -102,7 +112,11 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
table.allowsColumnReordering = false
|
||||
table.allowsColumnResizing = false
|
||||
table.allowsColumnSelection = false
|
||||
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Program")))
|
||||
table.addTableColumn(
|
||||
NSTableColumn(
|
||||
identifier: NSUserInterfaceItemIdentifier("Program")
|
||||
)
|
||||
)
|
||||
|
||||
table.doubleAction = #selector(tableDoubleClick)
|
||||
|
||||
@@ -123,47 +137,83 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
var tableViewHeightAnchor: NSLayoutConstraint?
|
||||
|
||||
private func setConstraints() {
|
||||
tableViewHeightAnchor = tableScrollView.heightAnchor.constraint(equalToConstant: 0)
|
||||
tableViewHeightAnchor = tableScrollView.heightAnchor
|
||||
.constraint(equalToConstant: 0)
|
||||
tableViewHeightAnchor?.isActive = true
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: -100),
|
||||
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 100),
|
||||
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -100),
|
||||
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 100),
|
||||
view.topAnchor
|
||||
.constraint(equalTo: contentView.topAnchor, constant: -100),
|
||||
view.bottomAnchor
|
||||
.constraint(equalTo: contentView.bottomAnchor,
|
||||
constant: 100),
|
||||
view.leadingAnchor
|
||||
.constraint(equalTo: contentView.leadingAnchor,
|
||||
constant: -100),
|
||||
view.trailingAnchor
|
||||
.constraint(equalTo: contentView.trailingAnchor,
|
||||
constant: 100),
|
||||
|
||||
shadowView.topAnchor.constraint(equalTo: backgroundView.topAnchor),
|
||||
shadowView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor),
|
||||
shadowView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
|
||||
shadowView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
|
||||
shadowView.topAnchor
|
||||
.constraint(equalTo: backgroundView.topAnchor),
|
||||
shadowView.bottomAnchor
|
||||
.constraint(equalTo: backgroundView.bottomAnchor),
|
||||
shadowView.leadingAnchor
|
||||
.constraint(equalTo: backgroundView.leadingAnchor),
|
||||
shadowView.trailingAnchor
|
||||
.constraint(equalTo: backgroundView.trailingAnchor),
|
||||
|
||||
backgroundView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
backgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
backgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
backgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
backgroundView.topAnchor
|
||||
.constraint(equalTo: contentView.topAnchor),
|
||||
backgroundView.bottomAnchor
|
||||
.constraint(equalTo: contentView.bottomAnchor),
|
||||
backgroundView.leadingAnchor
|
||||
.constraint(equalTo: contentView.leadingAnchor),
|
||||
backgroundView.trailingAnchor
|
||||
.constraint(equalTo: contentView.trailingAnchor),
|
||||
|
||||
searchInput.widthAnchor.constraint(equalToConstant: 400),
|
||||
searchInput.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ViewConstants.spacing10),
|
||||
searchInput.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: ViewConstants.spacing15),
|
||||
searchInput.widthAnchor
|
||||
.constraint(equalToConstant: 400),
|
||||
searchInput.topAnchor
|
||||
.constraint(equalTo: contentView.topAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
searchInput.leadingAnchor
|
||||
.constraint(equalTo: contentView.leadingAnchor,
|
||||
constant: ViewConstants.spacing15),
|
||||
|
||||
settingsButton.centerYAnchor.constraint(equalTo: searchInput.centerYAnchor),
|
||||
settingsButton.leadingAnchor.constraint(equalTo: searchInput.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
settingsButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -ViewConstants.spacing10),
|
||||
settingsButton.centerYAnchor
|
||||
.constraint(equalTo: searchInput.centerYAnchor),
|
||||
settingsButton.leadingAnchor
|
||||
.constraint(equalTo: searchInput.trailingAnchor,
|
||||
constant: ViewConstants.spacing5),
|
||||
settingsButton.trailingAnchor
|
||||
.constraint(equalTo: contentView.trailingAnchor,
|
||||
constant: -ViewConstants.spacing10),
|
||||
|
||||
tableScrollView.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
tableScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
tableScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
tableScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
||||
tableScrollView.topAnchor
|
||||
.constraint(equalTo: searchInput.bottomAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
tableScrollView.bottomAnchor
|
||||
.constraint(equalTo: contentView.bottomAnchor),
|
||||
tableScrollView.leadingAnchor
|
||||
.constraint(equalTo: contentView.leadingAnchor),
|
||||
tableScrollView.trailingAnchor
|
||||
.constraint(equalTo: contentView.trailingAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// NOTE: This needs removeObserver on deinit? Well, technically we don't care because this view controller will exist throughout
|
||||
// the whole life of the program. When the program gets killed, the OS will clear this.
|
||||
DistributedNotificationCenter.default.addObserver(self, selector: #selector(osThemeChanged(sender:)),
|
||||
name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil)
|
||||
// NOTE: This needs removeObserver on deinit? Well, technically we
|
||||
// don't care because this view controller will exist
|
||||
// throughout the whole life of the program. When the
|
||||
// program gets killed, the OS will clear this.
|
||||
DistributedNotificationCenter.default
|
||||
.addObserver(self, selector: #selector(osThemeChanged(sender:)),
|
||||
name: NSNotification
|
||||
.Name(rawValue: "AppleInterfaceThemeChangedNotification"),
|
||||
object: nil)
|
||||
|
||||
// Initialize an array of programs and reusable cells.
|
||||
for i in 0..<maxItems {
|
||||
@@ -179,47 +229,70 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
|
||||
keyboardEvents = LocalEventMonitor(mask: [.keyDown]) { [weak self] event in
|
||||
keyboardEvents = LocalEventMonitor(mask: [.keyDown])
|
||||
{ [weak self] event in
|
||||
let key = event.keyCode
|
||||
let modifiers = event.modifierFlags.rawValue
|
||||
|
||||
if let controller = self {
|
||||
if modsContains(keys: OSCtrl, in: modifiers) && key == kVK_ANSI_P ||
|
||||
if modsContains(keys: OSCtrl, in: modifiers) &&
|
||||
key == kVK_ANSI_P ||
|
||||
modsContainsNone(in: modifiers) && key == kVK_UpArrow
|
||||
{
|
||||
controller.programsTableViewSelection -= 1
|
||||
} else if modsContains(keys: OSCtrl, in: modifiers) && key == kVK_ANSI_N ||
|
||||
modsContainsNone(in: modifiers) && key == kVK_DownArrow
|
||||
} else if modsContains(keys: OSCtrl, in: modifiers) &&
|
||||
key == kVK_ANSI_N ||
|
||||
modsContainsNone(in: modifiers) && key == kVK_DownArrow
|
||||
{
|
||||
controller.programsTableViewSelection += 1
|
||||
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) && key == kVK_ANSI_P ||
|
||||
modsContains(keys: OSCmd, in: modifiers) && key == kVK_UpArrow
|
||||
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) &&
|
||||
key == kVK_ANSI_P ||
|
||||
modsContains(keys: OSCmd, in: modifiers) &&
|
||||
key == kVK_UpArrow
|
||||
{
|
||||
controller.programsTableViewSelection = 0
|
||||
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) && key == kVK_ANSI_N ||
|
||||
modsContains(keys: OSCmd, in: modifiers) && key == kVK_DownArrow
|
||||
} else if modsContains(keys: OSCtrl | OSCmd, in: modifiers) &&
|
||||
key == kVK_ANSI_N ||
|
||||
modsContains(keys: OSCmd, in: modifiers) &&
|
||||
key == kVK_DownArrow
|
||||
{
|
||||
controller.programsTableViewSelection = controller.listIndex-1
|
||||
} else if modsContains(keys: OSCmd, in: modifiers) && isNumericalCode(key) {
|
||||
if key == kVK_ANSI_1 { controller.programsTableViewSelection = 0 }
|
||||
if key == kVK_ANSI_2 { controller.programsTableViewSelection = 1 }
|
||||
if key == kVK_ANSI_3 { controller.programsTableViewSelection = 2 }
|
||||
if key == kVK_ANSI_4 { controller.programsTableViewSelection = 3 }
|
||||
if key == kVK_ANSI_5 { controller.programsTableViewSelection = 4 }
|
||||
if key == kVK_ANSI_6 { controller.programsTableViewSelection = 5 }
|
||||
if key == kVK_ANSI_7 { controller.programsTableViewSelection = 6 }
|
||||
if key == kVK_ANSI_8 { controller.programsTableViewSelection = 7 }
|
||||
if key == kVK_ANSI_9 { controller.programsTableViewSelection = 8 }
|
||||
} else if modsContains(keys: OSCmd, in: modifiers) &&
|
||||
isNumericalCode(key)
|
||||
{
|
||||
if key == kVK_ANSI_1
|
||||
{ controller.programsTableViewSelection = 0 }
|
||||
if key == kVK_ANSI_2
|
||||
{ controller.programsTableViewSelection = 1 }
|
||||
if key == kVK_ANSI_3
|
||||
{ controller.programsTableViewSelection = 2 }
|
||||
if key == kVK_ANSI_4
|
||||
{ controller.programsTableViewSelection = 3 }
|
||||
if key == kVK_ANSI_5
|
||||
{ controller.programsTableViewSelection = 4 }
|
||||
if key == kVK_ANSI_6
|
||||
{ controller.programsTableViewSelection = 5 }
|
||||
if key == kVK_ANSI_7
|
||||
{ controller.programsTableViewSelection = 6 }
|
||||
if key == kVK_ANSI_8
|
||||
{ controller.programsTableViewSelection = 7 }
|
||||
if key == kVK_ANSI_9
|
||||
{ controller.programsTableViewSelection = 8 }
|
||||
}
|
||||
|
||||
if controller.programsTableViewSelection > controller.listIndex-1 {
|
||||
controller.programsTableViewSelection = controller.listIndex-1
|
||||
if controller.programsTableViewSelection >
|
||||
controller.listIndex-1
|
||||
{
|
||||
controller.programsTableViewSelection =
|
||||
controller.listIndex-1
|
||||
} else if controller.programsTableViewSelection < 0 {
|
||||
controller.programsTableViewSelection = 0
|
||||
}
|
||||
|
||||
let select = controller.programsTableViewSelection
|
||||
self?.programsTableView.selectRowIndexes(IndexSet(integer: select), byExtendingSelection: false)
|
||||
self?.programsTableView
|
||||
.selectRowIndexes(IndexSet(integer: select),
|
||||
byExtendingSelection: false)
|
||||
self?.programsTableView.scrollRowToVisible(select)
|
||||
}
|
||||
|
||||
@@ -246,7 +319,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
|
||||
view.window?.makeFirstResponder(searchInput)
|
||||
// searchInput should select all text whenever window appears.
|
||||
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
|
||||
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil,
|
||||
from: self)
|
||||
}
|
||||
|
||||
override func viewWillAppear() {
|
||||
@@ -267,8 +341,12 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
|
||||
func centerWindow() {
|
||||
if let win = view.window, let scrn = NSScreen.main {
|
||||
let x = (scrn.visibleFrame.origin.x + scrn.visibleFrame.size.width / 2) - (win.frame.size.width / 2)
|
||||
let y = (scrn.visibleFrame.origin.y + scrn.visibleFrame.size.height * 0.9) - win.frame.size.height
|
||||
let x = (scrn.visibleFrame.origin.x +
|
||||
scrn.visibleFrame.size.width / 2) -
|
||||
(win.frame.size.width / 2)
|
||||
let y = (scrn.visibleFrame.origin.y +
|
||||
scrn.visibleFrame.size.height * 0.9) -
|
||||
win.frame.size.height
|
||||
view.window?.setFrameOrigin(NSPoint(x: x, y: y))
|
||||
}
|
||||
}
|
||||
@@ -284,7 +362,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
|
||||
@objc
|
||||
func openSettings() {
|
||||
settingsPopover.show(relativeTo: settingsButton.bounds, of: settingsButton, preferredEdge: .maxY)
|
||||
settingsPopover.show(relativeTo: settingsButton.bounds,
|
||||
of: settingsButton, preferredEdge: .maxY)
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -294,7 +373,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
}
|
||||
|
||||
private func openProgram(_ program: Program) {
|
||||
let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext)
|
||||
let url = URL(fileURLWithPath: program.path)
|
||||
.appendingPathComponent(program.name+program.ext)
|
||||
let config = NSWorkspace.OpenConfiguration()
|
||||
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: config)
|
||||
@@ -306,7 +386,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
}
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
guard let searchInput = obj.object as? EditableNSTextField else { return }
|
||||
guard let searchInput =
|
||||
obj.object as? EditableNSTextField else { return }
|
||||
|
||||
listIndex = 0
|
||||
if !searchInput.stringValue.isEmpty {
|
||||
@@ -315,11 +396,20 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
if listIndex >= maxItems { break outerloop }
|
||||
let prog = path.value[i]
|
||||
|
||||
if prog.name.lowercased().contains(searchInput.stringValue.lowercased()) {
|
||||
if prog.name.lowercased()
|
||||
.contains(searchInput.stringValue.lowercased())
|
||||
{
|
||||
programsList[listIndex].path = prog.path
|
||||
programsList[listIndex].name = prog.name
|
||||
programsList[listIndex].ext = prog.ext
|
||||
programsList[listIndex].img = NSWorkspace.shared.icon(forFile: URL(fileURLWithPath: prog.path).appendingPathComponent(prog.name+prog.ext).path)
|
||||
programsList[listIndex].ext = prog.ext
|
||||
programsList[listIndex].img =
|
||||
NSWorkspace.shared
|
||||
.icon(forFile: URL(
|
||||
fileURLWithPath: prog.path
|
||||
)
|
||||
.appendingPathComponent(
|
||||
prog.name+prog.ext).path
|
||||
)
|
||||
listIndex += 1
|
||||
}
|
||||
}
|
||||
@@ -328,25 +418,37 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
reloadProgramsTableViewData()
|
||||
|
||||
programsTableViewSelection = 0
|
||||
programsTableView.selectRowIndexes(IndexSet(integer: programsTableViewSelection), byExtendingSelection: false)
|
||||
programsTableView.selectRowIndexes(
|
||||
IndexSet(integer: programsTableViewSelection),
|
||||
byExtendingSelection: false
|
||||
)
|
||||
programsTableView.scrollRowToVisible(programsTableViewSelection)
|
||||
}
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
func control(_ control: NSControl, textView: NSTextView,
|
||||
doCommandBy commandSelector: Selector) -> Bool
|
||||
{
|
||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
||||
if listIndex > 0 {
|
||||
let program = programsList[programsTableViewSelection]
|
||||
openProgram(program)
|
||||
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
|
||||
NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
|
||||
to: nil, from: self)
|
||||
}
|
||||
return true
|
||||
} else if commandSelector == #selector(NSResponder.insertTab(_:)) {
|
||||
return true
|
||||
} else if commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.moveDown(_:)) {
|
||||
// Ignore arrows up and down because we use those to navigate the programs list.
|
||||
} else if commandSelector == #selector(NSResponder.moveUp(_:)) ||
|
||||
commandSelector == #selector(NSResponder.moveDown(_:))
|
||||
{
|
||||
// Ignore arrows up and down because we use those to navigate
|
||||
// the programs list.
|
||||
return true
|
||||
} else if commandSelector == #selector(NSResponder.moveToBeginningOfDocument(_:)) {
|
||||
// Ignore command plus up and down arrows because we use those to move to the beginning and end of the list.
|
||||
} else if commandSelector ==
|
||||
#selector(NSResponder.moveToBeginningOfDocument(_:))
|
||||
{
|
||||
// Ignore command plus up and down arrows because we use those
|
||||
// to move to the beginning and end of the list.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -365,18 +467,28 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
return listIndex
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
||||
func tableView(_ tableView: NSTableView,
|
||||
rowViewForRow row: Int) -> NSTableRowView?
|
||||
{
|
||||
return ProgramsTableRowView()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
func tableView(_ tableView: NSTableView,
|
||||
viewFor tableColumn: NSTableColumn?,
|
||||
row: Int) -> NSView?
|
||||
{
|
||||
let cell = programsListCells[row]
|
||||
let program = programsList[row]
|
||||
|
||||
let app = program.name + program.ext
|
||||
let rangeToHighlight = (app.lowercased() as NSString).range(of: searchInput.stringValue.lowercased())
|
||||
let rangeToHighlight =
|
||||
(app.lowercased() as NSString).range(
|
||||
of: searchInput.stringValue.lowercased()
|
||||
)
|
||||
let attributedString = NSMutableAttributedString(string: app)
|
||||
attributedString.addAttributes([.foregroundColor: NSColor.labelColor], range: rangeToHighlight)
|
||||
attributedString
|
||||
.addAttributes([.foregroundColor: NSColor.labelColor],
|
||||
range: rangeToHighlight)
|
||||
|
||||
cell.titleField.attributedStringValue = attributedString
|
||||
cell.progPathLabel.stringValue = program.path
|
||||
@@ -397,10 +509,14 @@ class SearchViewController: NSViewController, NSTextFieldDelegate, NSPopoverDele
|
||||
}
|
||||
|
||||
@objc func updateViewsBasedOnOSTheme() {
|
||||
if NSApp.windows.first?.effectiveAppearance.bestMatch(from: [.darkAqua, .vibrantDark]) == .darkAqua { // dark
|
||||
backgroundView.layer?.borderColor = NSColor.white.withAlphaComponent(0.2).cgColor
|
||||
if NSApp.windows.first?.effectiveAppearance
|
||||
.bestMatch(from: [.darkAqua, .vibrantDark]) == .darkAqua
|
||||
{ // dark
|
||||
backgroundView.layer?.borderColor =
|
||||
NSColor.white.withAlphaComponent(0.2).cgColor
|
||||
} else { // light
|
||||
backgroundView.layer?.borderColor = NSColor.black.withAlphaComponent(0.2).cgColor
|
||||
backgroundView.layer?.borderColor =
|
||||
NSColor.black.withAlphaComponent(0.2).cgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import Carbon
|
||||
import ServiceManagement
|
||||
|
||||
class SettingsViewController: NSViewController,
|
||||
NSTextFieldDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource,
|
||||
NSTableViewDelegate, PathsTableCellViewDelegate
|
||||
NSTextFieldDelegate, KeyDetectorButtonDelegate,
|
||||
NSTableViewDataSource, NSTableViewDelegate,
|
||||
PathsTableCellViewDelegate
|
||||
{
|
||||
private var recording = false
|
||||
|
||||
@@ -15,8 +16,10 @@ class SettingsViewController: NSViewController,
|
||||
|
||||
private var paths: [String] = []
|
||||
|
||||
// PERF: This is very slow to initialize because it creates a new process. This also cannot be done on a separate
|
||||
// thread. This sucks because the program now takes considerably longer to launch.
|
||||
// PERF: This is very slow to initialize because it creates a new
|
||||
// process. This also cannot be done on a separate thread. This
|
||||
// sucks because the program now takes considerably longer to
|
||||
// launch.
|
||||
private let dirPicker: NSOpenPanel = {
|
||||
let panel = NSOpenPanel()
|
||||
panel.message = "Select a directory to search applications in . . ."
|
||||
@@ -28,14 +31,18 @@ class SettingsViewController: NSViewController,
|
||||
|
||||
private var shortcutsLabel: NSTextField = {
|
||||
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
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var aboutButton: 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.action = #selector(showAbout)
|
||||
button.sizeToFit()
|
||||
@@ -95,7 +102,10 @@ class SettingsViewController: NSViewController,
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
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
|
||||
return textField
|
||||
}()
|
||||
@@ -110,8 +120,12 @@ class SettingsViewController: NSViewController,
|
||||
}()
|
||||
|
||||
private var pathsLabel: NSTextField = {
|
||||
let textField = NSTextField(labelWithString: "Application Directories")
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold)
|
||||
let textField =
|
||||
NSTextField(labelWithString: "Application Directories")
|
||||
textField.font = NSFont
|
||||
.systemFont(ofSize: NSFontDescriptor
|
||||
.preferredFontDescriptor(forTextStyle: .title2).pointSize,
|
||||
weight: .bold)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
@@ -135,7 +149,11 @@ class SettingsViewController: NSViewController,
|
||||
table.allowsColumnReordering = false
|
||||
table.allowsColumnResizing = false
|
||||
table.allowsColumnSelection = false
|
||||
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Paths")))
|
||||
table.addTableColumn(
|
||||
NSTableColumn(
|
||||
identifier: NSUserInterfaceItemIdentifier("Paths")
|
||||
)
|
||||
)
|
||||
|
||||
table.translatesAutoresizingMaskIntoConstraints = false
|
||||
return table
|
||||
@@ -146,8 +164,10 @@ class SettingsViewController: NSViewController,
|
||||
control.segmentCount = 2
|
||||
control.segmentStyle = .roundRect
|
||||
|
||||
control.setImage(NSImage(systemSymbolName: "plus", accessibilityDescription: nil), forSegment: 0)
|
||||
control.setImage(NSImage(systemSymbolName: "minus", accessibilityDescription: nil), forSegment: 1)
|
||||
control.setImage(NSImage(systemSymbolName: "plus",
|
||||
accessibilityDescription: nil), forSegment: 0)
|
||||
control.setImage(NSImage(systemSymbolName: "minus",
|
||||
accessibilityDescription: nil), forSegment: 1)
|
||||
|
||||
control.setToolTip("Add Path", forSegment: 0)
|
||||
control.setToolTip("Remove Path", forSegment: 1)
|
||||
@@ -212,52 +232,99 @@ class SettingsViewController: NSViewController,
|
||||
|
||||
private func setConstraints() {
|
||||
NSLayoutConstraint.activate([
|
||||
shortcutsLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10),
|
||||
shortcutsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10),
|
||||
shortcutsLabel.topAnchor
|
||||
.constraint(equalTo: view.topAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
shortcutsLabel.leadingAnchor
|
||||
.constraint(equalTo: view.leadingAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
|
||||
aboutButton.firstBaselineAnchor.constraint(equalTo: shortcutsLabel.firstBaselineAnchor),
|
||||
aboutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10),
|
||||
aboutButton.firstBaselineAnchor
|
||||
.constraint(equalTo: shortcutsLabel.firstBaselineAnchor),
|
||||
aboutButton.trailingAnchor
|
||||
.constraint(equalTo: view.trailingAnchor,
|
||||
constant: -ViewConstants.spacing10),
|
||||
|
||||
ctrlButton.topAnchor.constraint(equalTo: shortcutsLabel.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
ctrlButton.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor),
|
||||
ctrlButton.topAnchor
|
||||
.constraint(equalTo: shortcutsLabel.bottomAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
ctrlButton.leadingAnchor
|
||||
.constraint(equalTo: shortcutsLabel.leadingAnchor),
|
||||
|
||||
cmdButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
cmdButton.leadingAnchor.constraint(equalTo: ctrlButton.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
cmdButton.centerYAnchor
|
||||
.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
cmdButton.leadingAnchor
|
||||
.constraint(equalTo: ctrlButton.trailingAnchor,
|
||||
constant: ViewConstants.spacing5),
|
||||
|
||||
optButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
optButton.leadingAnchor.constraint(equalTo: cmdButton.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
optButton.centerYAnchor
|
||||
.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
optButton.leadingAnchor
|
||||
.constraint(equalTo: cmdButton.trailingAnchor,
|
||||
constant: ViewConstants.spacing5),
|
||||
|
||||
shiftButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
shiftButton.leadingAnchor.constraint(equalTo: optButton.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
shiftButton.centerYAnchor
|
||||
.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
shiftButton.leadingAnchor
|
||||
.constraint(equalTo: optButton.trailingAnchor,
|
||||
constant: ViewConstants.spacing5),
|
||||
|
||||
plusLabel.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
plusLabel.leadingAnchor.constraint(equalTo: shiftButton.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
plusLabel.centerYAnchor
|
||||
.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
plusLabel.leadingAnchor
|
||||
.constraint(equalTo: shiftButton.trailingAnchor,
|
||||
constant: ViewConstants.spacing5),
|
||||
|
||||
recordButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
recordButton.centerYAnchor.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
recordButton.leadingAnchor.constraint(equalTo: plusLabel.trailingAnchor, constant: ViewConstants.spacing5),
|
||||
recordButton.centerYAnchor
|
||||
.constraint(equalTo: ctrlButton.centerYAnchor),
|
||||
recordButton.leadingAnchor
|
||||
.constraint(equalTo: plusLabel.trailingAnchor,
|
||||
constant: ViewConstants.spacing5),
|
||||
|
||||
pathsLabel.topAnchor.constraint(equalTo: ctrlButton.bottomAnchor, constant: ViewConstants.spacing20),
|
||||
pathsLabel.leadingAnchor.constraint(equalTo: shortcutsLabel.leadingAnchor),
|
||||
pathsLabel.topAnchor
|
||||
.constraint(equalTo: ctrlButton.bottomAnchor,
|
||||
constant: ViewConstants.spacing20),
|
||||
pathsLabel.leadingAnchor
|
||||
.constraint(equalTo: shortcutsLabel.leadingAnchor),
|
||||
|
||||
tableScrollView.widthAnchor.constraint(equalToConstant: 350),
|
||||
tableScrollView.heightAnchor.constraint(equalToConstant: 150),
|
||||
tableScrollView.topAnchor.constraint(equalTo: pathsLabel.bottomAnchor),
|
||||
tableScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableScrollView.topAnchor
|
||||
.constraint(equalTo: pathsLabel.bottomAnchor),
|
||||
tableScrollView.leadingAnchor
|
||||
.constraint(equalTo: view.leadingAnchor),
|
||||
tableScrollView.trailingAnchor
|
||||
.constraint(equalTo: view.trailingAnchor),
|
||||
|
||||
pathsControl.topAnchor.constraint(equalTo: tableScrollView.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
pathsControl.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10),
|
||||
pathsControl.topAnchor
|
||||
.constraint(equalTo: tableScrollView.bottomAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
pathsControl.leadingAnchor
|
||||
.constraint(equalTo: view.leadingAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
|
||||
launchAtLoginLabel.topAnchor.constraint(equalTo: pathsControl.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
launchAtLoginLabel.trailingAnchor.constraint(equalTo: launchAtLoginToggle.leadingAnchor, constant: -ViewConstants.spacing10),
|
||||
launchAtLoginLabel.topAnchor
|
||||
.constraint(equalTo: pathsControl.bottomAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
launchAtLoginLabel.trailingAnchor
|
||||
.constraint(equalTo: launchAtLoginToggle.leadingAnchor,
|
||||
constant: -ViewConstants.spacing10),
|
||||
|
||||
launchAtLoginToggle.firstBaselineAnchor.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
|
||||
launchAtLoginToggle.trailingAnchor.constraint(equalTo: resetAllButton.leadingAnchor, constant: -ViewConstants.spacing15),
|
||||
launchAtLoginToggle.firstBaselineAnchor
|
||||
.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
|
||||
launchAtLoginToggle.trailingAnchor
|
||||
.constraint(equalTo: resetAllButton.leadingAnchor,
|
||||
constant: -ViewConstants.spacing15),
|
||||
|
||||
resetAllButton.firstBaselineAnchor.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
|
||||
resetAllButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10),
|
||||
resetAllButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing10),
|
||||
resetAllButton.firstBaselineAnchor
|
||||
.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
|
||||
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()
|
||||
|
||||
// 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
|
||||
}
|
||||
if let mods = UserDefaults.standard.object(forKey: "keyModifiers") as? Int {
|
||||
if let mods =
|
||||
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
|
||||
{
|
||||
modifiers = mods
|
||||
}
|
||||
|
||||
@@ -317,7 +388,8 @@ class SettingsViewController: NSViewController,
|
||||
override func 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(modifiers, forKey: "keyModifiers")
|
||||
@@ -377,7 +449,8 @@ class SettingsViewController: NSViewController,
|
||||
private func reset() {
|
||||
keyCode = Int(kVK_Space)
|
||||
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(modifiers, forKey: "keyModifiers")
|
||||
syncModifierButtons()
|
||||
@@ -449,11 +522,16 @@ class SettingsViewController: NSViewController,
|
||||
case 0:
|
||||
let row = paths.count
|
||||
paths.append("")
|
||||
pathsTableView.insertRows(at: IndexSet(integer: row), withAnimation: [])
|
||||
pathsTableView.insertRows(at: IndexSet(integer: row),
|
||||
withAnimation: [])
|
||||
|
||||
pathsTableView.scrollRowToVisible(row)
|
||||
pathsTableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
||||
(pathsTableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? PathsTableCellView)?.startEditing()
|
||||
pathsTableView.selectRowIndexes(IndexSet(integer: row),
|
||||
byExtendingSelection: false)
|
||||
(
|
||||
pathsTableView
|
||||
.view(atColumn: 0, row: row, makeIfNecessary: false
|
||||
) as? PathsTableCellView)?.startEditing()
|
||||
break
|
||||
case 1:
|
||||
if pathsTableView.selectedRow > -1 {
|
||||
@@ -484,9 +562,16 @@ class SettingsViewController: NSViewController,
|
||||
@objc
|
||||
private func editItem(_ sender: NSTableView) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -521,7 +606,9 @@ class SettingsViewController: NSViewController,
|
||||
|
||||
delegate.window.level = .statusBar
|
||||
delegate.window.makeKeyAndOrderFront(nil)
|
||||
if let controller = delegate.window.contentViewController as? SearchViewController {
|
||||
if let controller =
|
||||
delegate.window.contentViewController as? SearchViewController
|
||||
{
|
||||
controller.openSettings()
|
||||
}
|
||||
}
|
||||
@@ -533,7 +620,8 @@ class SettingsViewController: NSViewController,
|
||||
func tableView(_ tableView: NSTableView,
|
||||
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)
|
||||
cell.titleField.stringValue = paths[row]
|
||||
cell.delegate = self
|
||||
|
||||
Reference in New Issue
Block a user