Filesystem monitor and refactor.
This commit is contained in:
211
src/AboutViewController.swift
Normal file
211
src/AboutViewController.swift
Normal 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)!)
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -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
94
src/DirMonitor.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ import Cocoa
|
|||||||
class EventMonitor {
|
class EventMonitor {
|
||||||
fileprivate let mask: NSEvent.EventTypeMask
|
fileprivate let mask: NSEvent.EventTypeMask
|
||||||
fileprivate var monitor: Any?
|
fileprivate var monitor: Any?
|
||||||
|
|
||||||
fileprivate init(mask: NSEvent.EventTypeMask) {
|
fileprivate init(mask: NSEvent.EventTypeMask) {
|
||||||
self.mask = mask
|
self.mask = mask
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
fatalError("start must be implemented by a subclass")
|
fatalError("start must be implemented by a subclass")
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
if monitor != nil {
|
if monitor != nil {
|
||||||
NSEvent.removeMonitor(monitor!)
|
NSEvent.removeMonitor(monitor!)
|
||||||
@@ -26,32 +26,30 @@ class EventMonitor {
|
|||||||
|
|
||||||
final class LocalEventMonitor: EventMonitor {
|
final class LocalEventMonitor: EventMonitor {
|
||||||
typealias Handler = (NSEvent) -> NSEvent?
|
typealias Handler = (NSEvent) -> NSEvent?
|
||||||
|
|
||||||
private let handler: Handler
|
private let handler: Handler
|
||||||
|
|
||||||
init(mask: NSEvent.EventTypeMask, handler: @escaping Handler) {
|
init(mask: NSEvent.EventTypeMask, handler: @escaping Handler) {
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
super.init(mask: mask)
|
super.init(mask: mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func start() {
|
override func start() {
|
||||||
monitor = NSEvent.addLocalMonitorForEvents(matching: mask,
|
monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler)
|
||||||
handler: handler)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class GlobalEventMonitor: EventMonitor {
|
final class GlobalEventMonitor: EventMonitor {
|
||||||
typealias Handler = (NSEvent) -> Void
|
typealias Handler = (NSEvent) -> Void
|
||||||
|
|
||||||
private let handler: Handler
|
private let handler: Handler
|
||||||
|
|
||||||
init(mask: NSEvent.EventTypeMask, handler: @escaping Handler) {
|
init(mask: NSEvent.EventTypeMask, handler: @escaping Handler) {
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
super.init(mask: mask)
|
super.init(mask: mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func start() {
|
override func start() {
|
||||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask,
|
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
|
||||||
handler: handler)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
80
src/Makefile
80
src/Makefile
@@ -7,63 +7,46 @@ 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))
|
||||||
|
|
||||||
LIBS =
|
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
33
src/MenulessWindow.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ class PopoverPanel: NSPanel {
|
|||||||
{
|
{
|
||||||
resignKey()
|
resignKey()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
} else if modsContains(keys: OSCmd | OSShift,
|
} else if modsContains(keys: OSCmd | OSShift,
|
||||||
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()
|
||||||
|
|||||||
@@ -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),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -414,7 +365,7 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
|
|||||||
// searchInput.stringValue
|
// searchInput.stringValue
|
||||||
|
|
||||||
let app = program.name + program.ext
|
let app = program.name + program.ext
|
||||||
let rangeToHighlight =
|
let rangeToHighlight =
|
||||||
(app.lowercased() as NSString)
|
(app.lowercased() as NSString)
|
||||||
.range(of: searchInput.stringValue.lowercased())
|
.range(of: searchInput.stringValue.lowercased())
|
||||||
let attributedString = NSMutableAttributedString(string: app)
|
let attributedString = NSMutableAttributedString(string: app)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user