Added settings window.

This commit is contained in:
2025-01-03 16:05:03 -08:00
parent d6e097506a
commit 83bb184fe3
13 changed files with 1162 additions and 129 deletions

View File

@@ -3,29 +3,12 @@ import Carbon
import ServiceManagement import ServiceManagement
import OSLog import OSLog
struct Program {
let path: String
let name: String
let ext: String
}
func appActivatedHandler(nextHandler: EventHandlerCallRef?, theEvent: EventRef?, userData: UnsafeMutableRawPointer?) -> OSStatus {
print("App was activated!")
return noErr
}
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
fileprivate static let logger = Logger( fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!, subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: AppDelegate.self) category: String(describing: AppDelegate.self)
) )
var paths = ["/Applications", "/System/Applications",
"/System/Applications/Utilities",
"/Applications/Xcode.app/Contents/Applications",
"/System/Library/CoreServices/Applications"]
var programs: [Program] = []
let fileManager = FileManager.default let fileManager = FileManager.default
let window = PopoverPanel(viewController: SearchViewController()) let window = PopoverPanel(viewController: SearchViewController())
@@ -33,45 +16,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
Self.logger.debug("applicationDidFinishLaunching") Self.logger.debug("applicationDidFinishLaunching")
NSRunningApplication.current.hide() PathManager.shared.rebuildIndex()
window.delegate = self window.delegate = self
//GlobalEventTap.shared.enable()
for path in paths {
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"))
}
}
}
} catch {
print("Error reading directory: \(error.localizedDescription)")
}
}
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
// TODO: Implement Unregister and Uninstall. HotKeyManager.shared.handler =
// TODO: A user should be able to enter hot keys to trigger. { (inHandlerCallRef, inEvent, inUserData) -> OSStatus in
// We either can use local event monitor or let user choose
// from list.
var hotKeyRef: EventHotKeyRef?
let hotKeyID: EventHotKeyID = EventHotKeyID(signature: OSType("grap".fourCharCodeValue), id: 1)
// GetEventDispatcherTarget
var err = RegisterEventHotKey(UInt32(kVK_Space), UInt32(optionKey), hotKeyID, GetApplicationEventTarget(), UInt32(kEventHotKeyNoOptions), &hotKeyRef)
//let handler = NewEventHandlerUPP()
// Handler get executed on main thread.
let handler: EventHandlerUPP = { (inHandlerCallRef, inEvent, inUserData) -> OSStatus in
AppDelegate.logger.debug("Shortcut handler fired off.") AppDelegate.logger.debug("Shortcut handler fired off.")
if let delegate = NSApplication.shared.delegate as? AppDelegate { if let delegate =
NSApplication.shared.delegate as? AppDelegate
{
let window = delegate.window let window = delegate.window
if window.isKeyWindow { if window.isKeyWindow {
window.resignKey() window.resignKey()
@@ -79,24 +35,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
} }
return noErr return noErr
} }
var eventHandlerRef: EventHandlerRef? = nil
if err == noErr { HotKeyManager.shared.enable()
Self.logger.debug("Registered hot key.") if let code =
UserDefaults.standard.object(forKey: "keyCode") as? Int,
var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)) let mods =
err = InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType, nil, &eventHandlerRef) UserDefaults.standard.object(forKey: "keyModifiers") as? Int
{
if err == noErr { HotKeyManager.shared.registerHotKey(key: code,
Self.logger.debug("Event handler installed.") modifiers: mods)
} else {
Self.logger.debug("Failed to install event handler.")
}
} else { } else {
Self.logger.debug("Failed to register hot key.") // NOTE: This is the default shortcut. If you want to change
// it, do not forget to change it in other files
// (SettingsViewController).
HotKeyManager.shared.registerHotKey(key: kVK_Space,
modifiers: optionKey)
} }
} }
@@ -115,7 +70,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} }
} }
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool { func applicationShouldHandleReopen(_ sender: NSApplication,
hasVisibleWindows: Bool) -> Bool
{
Self.logger.debug("Application reopened.") Self.logger.debug("Application reopened.")
if !window.isKeyWindow { if !window.isKeyWindow {
@@ -124,6 +81,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
return true return true
} }
public func toggleLaunchAtLogin() {
let service = SMAppService.mainApp
if service.status == .enabled {
try? service.unregister()
} else {
try? service.register()
}
}
public func willLaunchAtLogin() -> Bool {
let service = SMAppService.mainApp
if service.status == .enabled {
return true
} else {
return false
}
}
} }
extension String { extension String {

View File

@@ -3,28 +3,48 @@ import os
final class EditableNSTextField: NSTextField { final class EditableNSTextField: NSTextField {
private let commandKey = NSEvent.ModifierFlags.command.rawValue private let commandKey = NSEvent.ModifierFlags.command.rawValue
private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue |
NSEvent.ModifierFlags.shift.rawValue
override func performKeyEquivalent(with event: NSEvent) -> Bool { override func performKeyEquivalent(with event: NSEvent) -> Bool {
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey { if (event.modifierFlags.rawValue &
NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue)
== commandKey
{
switch event.charactersIgnoringModifiers! { switch event.charactersIgnoringModifiers! {
case "x": case "x":
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true } if NSApp.sendAction(#selector(NSText.cut(_:)),
to: nil, from: self)
{ return true }
case "c": case "c":
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true } if NSApp.sendAction(#selector(NSText.copy(_:)),
to: nil, from: self)
{ return true }
case "v": case "v":
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true } if NSApp.sendAction(#selector(NSText.paste(_:)),
to: nil, from: self)
{ return true }
case "z": case "z":
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true } if NSApp.sendAction(Selector(("undo:")),
to: nil, from: self)
{ return true }
case "a": case "a":
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true } if NSApp.sendAction(
#selector(NSResponder.selectAll(_:)), to: nil,
from: self)
{ return true }
default: default:
break break
} }
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey { } else if (event.modifierFlags.rawValue &
NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue)
== commandShiftKey
{
if event.charactersIgnoringModifiers == "Z" { if event.charactersIgnoringModifiers == "Z" {
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true } if NSApp.sendAction(Selector(("redo:")), to: nil,
from: self)
{ return true }
} }
} }
} }

View File

@@ -13,7 +13,7 @@ class EventMonitor {
} }
func start() { func start() {
fatalError("start must be implemented by a subclass of EventMonitor") fatalError("start must be implemented by a subclass")
} }
func stop() { func stop() {
@@ -35,7 +35,8 @@ final class LocalEventMonitor: EventMonitor {
} }
override func start() { override func start() {
monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler) monitor = NSEvent.addLocalMonitorForEvents(matching: mask,
handler: handler)
} }
} }
@@ -50,6 +51,7 @@ final class GlobalEventMonitor: EventMonitor {
} }
override func start() { override func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) monitor = NSEvent.addGlobalMonitorForEvents(matching: mask,
handler: handler)
} }
} }

View File

@@ -15,12 +15,14 @@ fileprivate func handleGlobalEvents(proxy: CGEventTapProxy,
case .keyDown: case .keyDown:
//logger.debug(".keyDown") //logger.debug(".keyDown")
if (event.flags.rawValue & CGEventFlags.maskAlternate.rawValue) == CGEventFlags.maskAlternate.rawValue && let keyCode = "keyCode: \(event.getIntegerValueField(.keyboardEventKeycode))"
(event.flags.rawValue & (CGEventFlags.maskShift.rawValue | CGEventFlags.maskControl.rawValue | CGEventFlags.maskCommand.rawValue)) == 0 { logger.debug("\(keyCode, privacy: .public)")
logger.debug("maskAlternate")
}
logger.debug("Option rawValue=\(CGEventFlags.maskAlternate.rawValue)") //if (event.flags.rawValue & CGEventFlags.maskAlternate.rawValue) == CGEventFlags.maskAlternate.rawValue &&
// (event.flags.rawValue & (CGEventFlags.maskShift.rawValue | CGEventFlags.maskControl.rawValue | CGEventFlags.maskCommand.rawValue)) == 0 {
// logger.debug("maskAlternate")
//}
//logger.debug("Option rawValue=\(CGEventFlags.maskAlternate.rawValue)")
// var keyCode = event.getIntegerValueField(.keyboardEventKeycode) // var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
//if keyCode == 49 { //if keyCode == 49 {

View File

@@ -1,6 +1,67 @@
import AppKit import AppKit
import Carbon
import OSLog
func systemImage(_ name: String, _ size: NSFont.TextStyle, _ scale: NSImage.SymbolScale, _ configuration: NSImage.SymbolConfiguration) -> NSImage? { fileprivate let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String("Helpers")
)
struct Program {
let path: String
let name: String
let ext: String
}
func keyName(virtualKeyCode: UInt16) -> String? {
let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
//let source =
// TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
let source = TISCopyInputSourceForLanguage("en-US" as CFString)
.takeRetainedValue();
guard let ptr = TISGetInputSourceProperty(source,
kTISPropertyUnicodeKeyLayoutData)
else {
logger.log("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged<CFData>.fromOpaque(ptr)
.takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate(
$0.bindMemory(to: UCKeyboardLayout.self).baseAddress,
virtualKeyCode, UInt16(kUCKeyActionDown), modifierKeys,
keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
logger.debug("Code: \(virtualKeyCode) Status: \(osStatus)")
return nil
}
// NOTE: This is way too specific. This will need an additional func
// flag or be re-written to a more generic version if it's going
// to be used for something other than hot key representation.
var character = String(utf16CodeUnits: nameBuffer, count: nameLength)
if character == " " {
character = ""
} else {
character = character.uppercased()
}
return character
}
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) NSImage.SymbolConfiguration(textStyle: size, scale: scale)

86
src/HotKeyManager.swift Normal file
View File

@@ -0,0 +1,86 @@
import Carbon
import OSLog
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()
private var eventType = EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
private var eventHandlerRef: EventHandlerRef?
public var handler: EventHandlerUPP?
private var hotKeyRef: EventHotKeyRef?
private let hotKeyID: EventHotKeyID = EventHotKeyID(
signature: OSType("grap".fourCharCodeValue), id: 1)
private init() {}
deinit {}
// TODO: Handle errors.
public func enable() {
if eventHandlerRef != nil {
disable()
}
let err = InstallEventHandler(
GetApplicationEventTarget(), handler, 1, &eventType,
nil, &eventHandlerRef)
if err == noErr {
Self.logger.debug("Installed event handler.")
} else {
Self.logger.error("Failed to install event handler.")
}
}
public func disable() {
guard eventHandlerRef != nil else { return }
let err = RemoveEventHandler(eventHandlerRef)
if err == noErr {
eventHandlerRef = nil // WARNING: Does it remove no matter
// what on error?
Self.logger.debug("Removed event handler.")
} else {
Self.logger.error("Failed to remove event handler.")
}
}
// TODO: Handle errors.
// NOTE: Multiple modifiers should be ORed.
public func registerHotKey(key: Int, modifiers: Int) {
// GetEventDispatcherTarget
if hotKeyRef != nil {
unregisterHotKey()
}
let err = RegisterEventHotKey(
UInt32(key), UInt32(modifiers), hotKeyID,
GetApplicationEventTarget(), UInt32(kEventHotKeyNoOptions),
&hotKeyRef)
if err == noErr {
Self.logger.debug("Registered hot key.")
} else {
Self.logger.error("Failed to register hot key.")
}
}
// TODO: Handle errors.
public func unregisterHotKey() {
guard hotKeyRef != nil else { return }
let err = UnregisterEventHotKey(hotKeyRef)
if err == noErr {
hotKeyRef = nil // WARNING: Does it unregister no matter
// what on error?
Self.logger.debug("Successfully unregestered hot key.")
} else {
Self.logger.error("Failed to unregester hot key.")
}
}
}

View File

@@ -0,0 +1,42 @@
import AppKit
import Carbon
import OSLog
protocol KeyDetectorButtonDelegate: AnyObject {
func keyWasSet(to keyCode: Int)
}
final class KeyDetectorButton: NSButton {
var defaultKey: Int?
weak var delegate: KeyDetectorButtonDelegate?
override var acceptsFirstResponder: Bool { true }
// This removes default bahavior from NSButton, thus allowing mouse up
// events.
override func mouseDown(with event: NSEvent) {}
override func mouseUp(with event: NSEvent) {
self.window?.makeFirstResponder(self)
}
override func keyDown(with event: NSEvent) {
if event.keyCode == kVK_Escape || event.keyCode == kVK_Return {
} else if event.keyCode == kVK_Delete {
if let key = defaultKey,
let character = keyName(virtualKeyCode: UInt16(key))
{
title = character
}
} else {
if let character =
keyName(virtualKeyCode: UInt16(event.keyCode))
{
title = character
}
delegate?.keyWasSet(to: Int(event.keyCode))
}
self.window?.makeFirstResponder(nil)
}
}

View File

@@ -9,6 +9,8 @@ EXEC = Grapp
SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift \ SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift \
GlobalEventTap.swift PopoverPanel.swift SearchViewController.swift \ GlobalEventTap.swift PopoverPanel.swift SearchViewController.swift \
SettingsViewController.swift HotKeyManager.swift \
KeyDetectorButton.swift PathManager.swift MyTableCellView.swift \
AppDelegate.swift main.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))
@@ -17,17 +19,24 @@ LIBS =
FRAMEWORKS = -framework AppKit -framework ServiceManagement FRAMEWORKS = -framework AppKit -framework ServiceManagement
# HACK: Target is getting touched because timestamps of the generated
# object file don't change unless there's an actual change in the
# outputted object code. This results in this target running every
# single time. I'm not sure whether that's the exact reason, but
# I can't imagine why timestamps wouldn't change. When clang
# generates same exact executable, timestamps do change.
./arm64/%.o: %.swift ./arm64/%.o: %.swift
swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \ swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) -sdk $(SDK) \ -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) -sdk $(SDK) \
-module-name $(EXEC) -o $@ -emit-module -module-name $(EXEC) -o $@ -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)) $(LIBS) $(FRAMEWORKS) \ $(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) \
-sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module -sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module
@touch $@
endif endif
./arm64/$(EXEC): $(ARMOBJMODULES) ./arm64/$(EXEC): $(ARMOBJMODULES)

107
src/MyTableCellView.swift Normal file
View File

@@ -0,0 +1,107 @@
import AppKit
protocol MyTableCellViewDelegate: AnyObject {
func selectionButtonClicked(tag: Int)
func titleFieldTextChanged(tag: Int, text: String)
func titleFieldFinishedEditing(tag: Int, text: String)
}
class MyTableCellView: NSTableCellView, NSTextFieldDelegate {
var id: Int = -1
weak var delegate: MyTableCellViewDelegate?
private(set) var isEditing = false
public var titleField: NSTextField = {
let field = NSTextField()
field.isEditable = false
field.maximumNumberOfLines = 1
field.lineBreakMode = .byTruncatingTail
field.isBezeled = false
field.drawsBackground = false
field.translatesAutoresizingMaskIntoConstraints = false
return field
}()
var selectionButton: NSButton = {
let button = NSButton()
button.image = systemImage("hand.point.up.fill", .headline, .large,
.init(paletteColors: [.white, .systemRed]))
button.isBordered = false
button.sizeToFit()
button.toolTip = "Select Path"
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
titleField.delegate = self
selectionButton.target = self
selectionButton.action = #selector(makeSelection)
addSubview(titleField)
addSubview(selectionButton)
titleField.setContentHuggingPriority(.defaultLow, for: .horizontal)
titleField.setContentCompressionResistancePriority(.defaultLow,
for: .horizontal)
NSLayoutConstraint.activate([
//titleField.topAnchor.constraint(equalTo: topAnchor),
//titleField.bottomAnchor.constraint(equalTo: bottomAnchor),
titleField.centerYAnchor.constraint(equalTo: centerYAnchor),
titleField.leadingAnchor.constraint(equalTo: leadingAnchor),
titleField.trailingAnchor.constraint(
equalTo: selectionButton.leadingAnchor),
selectionButton.centerYAnchor.constraint(
equalTo: centerYAnchor),
selectionButton.trailingAnchor.constraint(
equalTo: trailingAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc
private func makeSelection() {
delegate?.selectionButtonClicked(tag: id)
}
public func startEditing() {
isEditing = true
titleField.isEditable = true
window?.makeFirstResponder(titleField)
}
public func stopEditing() {
isEditing = false
titleField.isEditable = false
window?.makeFirstResponder(nil)
}
func controlTextDidChange(_ obj: Notification) {
delegate?.titleFieldTextChanged(tag: id,
text: titleField.stringValue)
}
func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool
{
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
stopEditing()
delegate?.titleFieldFinishedEditing(tag: id,
text: titleField.stringValue)
return true
} else if commandSelector == #selector(NSResponder.insertTab(_:)) {
return true
}
return false
}
}

85
src/PathManager.swift Normal file
View File

@@ -0,0 +1,85 @@
import AppKit
import OSLog
final class PathManager {
fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: PathManager.self)
)
static let shared = PathManager()
// TODO: Filesystem events to watch changes on these directories and
// rebuild index when needed.
// NOTE: These are default paths where MacOS's default programs are
// stored. This list should be updated if something changes in
// newer MacOS version.
static let defaultPaths = ["/Applications", "/System/Applications",
"/System/Applications/Utilities", "/System/Library/CoreServices",
"/Applications/Xcode.app/Contents/Applications",
"/System/Library/CoreServices/Applications"]
var userPaths: [String] = []
private(set) var programs: [Program] = []
private let fileManager = FileManager.default
private init() {
if let paths =
UserDefaults.standard.stringArray(forKey: "programPaths")
{
for path in paths {
addPath(path)
}
} else {
userPaths += Self.defaultPaths
}
}
deinit {}
public func addPath(_ path: String) {
if !userPaths.contains(path) {
userPaths.append(path)
}
}
public func removePath(_ path: String) {
userPaths.removeAll { $0 == path }
}
public func removeEmpty() {
userPaths.removeAll { $0.isEmpty }
}
public func savePaths() {
UserDefaults.standard.set(userPaths, forKey: "programPaths")
}
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"))
}
}
}
} catch {
Self.logger.error("Error reading directory: \(error.localizedDescription, privacy: .public)")
}
}
}
}

View File

@@ -12,15 +12,16 @@ class PopoverPanel: NSPanel {
init(viewController: NSViewController) { init(viewController: NSViewController) {
super.init( super.init(
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100), contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
styleMask: [.titled, .nonactivatingPanel, .utilityWindow, .fullSizeContentView], styleMask: [.titled, .nonactivatingPanel, .utilityWindow,
.fullSizeContentView],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
super.contentViewController = viewController super.contentViewController = viewController
title = "" title = ""
//isMovable = false isMovable = true
isMovableByWindowBackground = false isMovableByWindowBackground = true
isFloatingPanel = true isFloatingPanel = true
isOpaque = false isOpaque = false
level = .statusBar level = .statusBar
@@ -28,7 +29,8 @@ class PopoverPanel: NSPanel {
titlebarAppearsTransparent = true titlebarAppearsTransparent = true
animationBehavior = .none animationBehavior = .none
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient] collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary,
.transient]
isReleasedWhenClosed = false isReleasedWhenClosed = false
hidesOnDeactivate = false hidesOnDeactivate = false
@@ -41,13 +43,21 @@ class PopoverPanel: NSPanel {
Self.logger.debug("performKeyEquivalent keyCode=\(event.keyCode)") Self.logger.debug("performKeyEquivalent keyCode=\(event.keyCode)")
let commandKey = NSEvent.ModifierFlags.command.rawValue let commandKey = NSEvent.ModifierFlags.command.rawValue
// TODO: Make these depend on virtual keycodes, instead of
// characters.
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey, if (event.modifierFlags.rawValue &
event.charactersIgnoringModifiers! == "w" { NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue)
== commandKey,
event.charactersIgnoringModifiers! == "w"
{
resignKey() resignKey()
return true return true
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey, } else if (event.modifierFlags.rawValue &
event.charactersIgnoringModifiers! == "q" { NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue)
== commandKey,
event.charactersIgnoringModifiers! == "q"
{
NSApplication.shared.terminate(self) NSApplication.shared.terminate(self)
return true return true
} else if event.keyCode == 53 { } else if event.keyCode == 53 {

View File

@@ -8,7 +8,9 @@ fileprivate enum ViewConstants {
static let spacing40: CGFloat = 40 static let spacing40: CGFloat = 40
} }
class SearchViewController: NSViewController, NSTextFieldDelegate { class SearchViewController: NSViewController, NSTextFieldDelegate,
NSPopoverDelegate
{
fileprivate static let logger = Logger( fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!, subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: SearchViewController.self) category: String(describing: SearchViewController.self)
@@ -16,10 +18,17 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
var foundProgram: Program? = nil var foundProgram: Program? = nil
private var settingsPopover: NSPopover = {
let popover = NSPopover()
popover.contentViewController = SettingsViewController()
popover.behavior = .transient
return popover
}()
private var appIconImage: NSImageView = { private var appIconImage: NSImageView = {
//let image = NSImageView(image: NSApp.applicationIconImage)
let image = NSImageView() let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) image.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -40,14 +49,17 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
textField.isBezeled = false textField.isBezeled = false
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .left textField.alignment = .left
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) textField.font = NSFont.systemFont(
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .body).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var settingsButton: NSButton = { private var settingsButton: NSButton = {
let button = NSButton() let button = NSButton()
button.image = systemImage("gearshape.fill", .title2, .large, .init(paletteColors: [.white, .systemRed])) button.image = systemImage("gearshape.fill", .title2, .large,
.init(paletteColors: [.white, .systemRed]))
button.isBordered = false button.isBordered = false
button.action = #selector(openSettings) button.action = #selector(openSettings)
button.sizeToFit() button.sizeToFit()
@@ -56,7 +68,6 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
return button return button
}() }()
private func addSubviews() { private func addSubviews() {
view.addSubview(appIconImage) view.addSubview(appIconImage)
view.addSubview(searchInput) view.addSubview(searchInput)
@@ -67,23 +78,42 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
private func setConstraints() { private func setConstraints() {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 70), appIconImage.widthAnchor.constraint(equalToConstant: 70),
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1), appIconImage.heightAnchor.constraint(
equalTo: appIconImage.widthAnchor, multiplier: 1),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20), appIconImage.topAnchor.constraint(equalTo: view.topAnchor,
appIconImage.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing10), constant: ViewConstants.spacing20),
appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), appIconImage.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10),
appIconImage.leadingAnchor.constraint(
equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
searchInput.widthAnchor.constraint(equalToConstant: 300), searchInput.widthAnchor.constraint(equalToConstant: 300),
searchInput.topAnchor.constraint(equalTo: appIconImage.topAnchor), searchInput.topAnchor.constraint(
searchInput.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10), equalTo: appIconImage.topAnchor),
searchInput.leadingAnchor.constraint(
equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing10),
settingsButton.firstBaselineAnchor.constraint(equalTo: searchInput.firstBaselineAnchor), settingsButton.firstBaselineAnchor.constraint(
settingsButton.leadingAnchor.constraint(equalTo: searchInput.trailingAnchor, constant: ViewConstants.spacing10), equalTo: searchInput.firstBaselineAnchor),
settingsButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing10), settingsButton.leadingAnchor.constraint(
equalTo: searchInput.trailingAnchor,
constant: ViewConstants.spacing10),
settingsButton.trailingAnchor.constraint(
equalTo: view.trailingAnchor,
constant: -ViewConstants.spacing10),
programsLabel.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: ViewConstants.spacing10), programsLabel.topAnchor.constraint(
programsLabel.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10), equalTo: searchInput.bottomAnchor,
programsLabel.trailingAnchor.constraint(equalTo: searchInput.trailingAnchor), constant: ViewConstants.spacing10),
programsLabel.leadingAnchor.constraint(
equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing10),
programsLabel.trailingAnchor.constraint(
equalTo: searchInput.trailingAnchor),
]) ])
} }
@@ -91,6 +121,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
settingsPopover.delegate = self
searchInput.delegate = self searchInput.delegate = self
addSubviews() addSubviews()
@@ -101,27 +133,39 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
super.viewDidAppear() super.viewDidAppear()
self.view.window?.center() self.view.window?.center()
// searchInput should select all text whenever window appears.
NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
to: nil, from: self)
} }
override func loadView() { override func loadView() {
self.view = NSView() self.view = NSView()
} }
//private func fetchIcon() {
// for key in resultPaths.keys {
// }
//}
@objc @objc
private func openSettings() { func openSettings() {
// HACK: This is an interseting behavior. When NSPopover appears
// the first time, it always displays in the wrong location;
// however, showing it twice does result in the right
// location.
settingsPopover.show(relativeTo: settingsButton.bounds,
of: settingsButton, preferredEdge: .maxY)
settingsPopover.show(relativeTo: settingsButton.bounds,
of: settingsButton, preferredEdge: .maxY)
} }
func controlTextDidChange(_ obj: Notification) { func controlTextDidChange(_ obj: Notification) {
guard let searchInput = obj.object as? EditableNSTextField
else { return }
var list = "" var list = ""
let programs = delegate.programs let programs = PathManager.shared.programs
for program in programs { for program in programs {
if program.name.lowercased().contains(searchInput.stringValue.lowercased()) { if program.name.lowercased().contains(
searchInput.stringValue.lowercased())
{
if !list.isEmpty { if !list.isEmpty {
list += ", " list += ", "
} }
@@ -134,37 +178,47 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
} }
if let program = foundProgram { if let program = foundProgram {
programsLabel.stringValue = program.name + program.ext programsLabel.stringValue =
program.name + program.ext + " (\(program.path))"
let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext) let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext)
appIconImage.image = NSWorkspace.shared.icon(forFile: url.path) appIconImage.image = NSWorkspace.shared.icon(forFile: url.path)
} else { } else {
programsLabel.stringValue = "" programsLabel.stringValue = ""
appIconImage.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) appIconImage.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
} }
} }
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool
{
if commandSelector == #selector(NSResponder.insertNewline(_:)) { if commandSelector == #selector(NSResponder.insertNewline(_:)) {
if let program = foundProgram { if let program = foundProgram {
let url = URL(fileURLWithPath: program.path).appendingPathComponent(program.name+program.ext) let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext)
let config = NSWorkspace.OpenConfiguration() let config = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: url, configuration: config, completionHandler: { [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 {
Self.logger.debug("Failed to open application: \(error.localizedDescription)") Self.logger.debug("\(error.localizedDescription)")
} else { } else {
Self.logger.debug("Application opened successfully") Self.logger.debug("Program opened successfully")
DispatchQueue.main.async { DispatchQueue.main.async {
if let window = self?.view.window { if let window = self?.view.window {
window.resignKey() window.resignKey()
} }
} }
} }
}) }
} }
// TODO: Send this whenever SearchViewController becomes visible. 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
@@ -172,4 +226,12 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
return false return false
} }
func popoverWillShow(_ notification: Notification) {
searchInput.abortEditing()
}
func popoverWillClose(_ notification: Notification) {
searchInput.becomeFirstResponder()
}
} }

View File

@@ -0,0 +1,572 @@
import AppKit
import Carbon
import ServiceManagement
import OSLog
fileprivate enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing5: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing20: CGFloat = 20
static let spacing40: CGFloat = 40
}
class SettingsViewController: NSViewController, NSTextFieldDelegate,
KeyDetectorButtonDelegate, NSTableViewDataSource, NSTableViewDelegate,
MyTableCellViewDelegate
{
fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: SettingsViewController.self)
)
private var recording = false
// NOTE: This is the default shortcut. If you were to change it, don't
// forget to change other places in this file and delegate, too.
private var keyCode = Int(kVK_Space)
private var modifiers = Int(optionKey)
// NOTE: PERF: This is very slow to initialize because it creates a
// a new process. This also cannot be done on a separate
// thread. This sucks because the program now takes
// considerably longer to launch.
private let dirPicker: NSOpenPanel = {
let panel = NSOpenPanel()
panel.message = "Select a directory to search applications in . . ."
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.allowsMultipleSelection = false
return panel
}()
private var shortcutsLabel: NSTextField = {
let textField = NSTextField(labelWithString: "Shortcut")
textField.font = NSFont.systemFont(
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .title2).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var ctrlButton: NSButton = {
let button = NSButton()
button.title = ""
button.action = #selector(handleModifiers)
button.setButtonType(.pushOnPushOff)
button.sizeToFit()
button.bezelStyle = .rounded
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var cmdButton: NSButton = {
let button = NSButton()
button.title = ""
button.action = #selector(handleModifiers)
button.setButtonType(.pushOnPushOff)
button.sizeToFit()
button.bezelStyle = .rounded
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var optButton: NSButton = {
let button = NSButton()
button.title = ""
button.action = #selector(handleModifiers)
button.setButtonType(.pushOnPushOff)
button.sizeToFit()
button.bezelStyle = .rounded
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var shiftButton: NSButton = {
let button = NSButton()
button.title = ""
button.action = #selector(handleModifiers)
button.setButtonType(.pushOnPushOff)
button.sizeToFit()
button.bezelStyle = .rounded
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var plusLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = "+"
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .center
textField.font = NSFont.systemFont(
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .body).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var recordButton: KeyDetectorButton = {
let button = KeyDetectorButton()
button.title = "Record"
button.sizeToFit()
button.bezelStyle = .rounded
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var pathsLabel: NSTextField = {
let textField =
NSTextField(labelWithString: "Application Directories")
textField.font = NSFont.systemFont(
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .title2).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var tableScrollView: NSScrollView = {
let scroll = NSScrollView()
scroll.drawsBackground = false
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private var pathsTableView: NSTableView = {
let table = NSTableView()
table.backgroundColor = .clear
table.doubleAction = #selector(editItem)
table.headerView = nil
table.allowsMultipleSelection = true
table.allowsColumnReordering = false
table.allowsColumnResizing = false
table.allowsColumnSelection = false
table.addTableColumn(NSTableColumn(
identifier: NSUserInterfaceItemIdentifier("Paths")))
//rowHeight cgfloat must see doc
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private var pathsControl: NSSegmentedControl = {
let control = NSSegmentedControl()
control.segmentCount = 2
control.segmentStyle = .roundRect
control.setImage(
NSImage(systemSymbolName: "plus",
accessibilityDescription: nil), forSegment: 0)
control.setImage(
NSImage(systemSymbolName: "minus",
accessibilityDescription: nil), forSegment: 1)
control.setToolTip("Add Path", forSegment: 0)
control.setToolTip("Remove Path", forSegment: 1)
control.trackingMode = .momentary
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
private var launchAtLoginButton: NSButton = {
let button = NSButton()
button.title = "Launch at login - OFF"
button.action = #selector(launchAtLogin)
button.setButtonType(.toggle)
button.sizeToFit()
button.bezelStyle = .rounded
button.isBordered = false
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var resetAllButton: NSButton = {
let button = NSButton()
button.title = "Reset"
button.action = #selector(reset)
button.setButtonType(.momentaryLight)
button.sizeToFit()
button.bezelStyle = .rounded
button.isBordered = false
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private func addSubviews() {
view.addSubview(shortcutsLabel)
view.addSubview(ctrlButton)
view.addSubview(cmdButton)
view.addSubview(optButton)
view.addSubview(shiftButton)
view.addSubview(plusLabel)
view.addSubview(recordButton)
view.addSubview(pathsLabel)
view.addSubview(tableScrollView)
view.addSubview(pathsControl)
view.addSubview(launchAtLoginButton)
view.addSubview(resetAllButton)
}
private func setConstraints() {
NSLayoutConstraint.activate([
shortcutsLabel.topAnchor.constraint(
equalTo: view.topAnchor,
constant: ViewConstants.spacing10),
shortcutsLabel.leadingAnchor.constraint(
equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
ctrlButton.topAnchor.constraint(
equalTo: shortcutsLabel.bottomAnchor,
constant: ViewConstants.spacing10),
ctrlButton.leadingAnchor.constraint(
equalTo: shortcutsLabel.leadingAnchor),
cmdButton.centerYAnchor.constraint(
equalTo: ctrlButton.centerYAnchor),
cmdButton.leadingAnchor.constraint(
equalTo: ctrlButton.trailingAnchor,
constant: ViewConstants.spacing5),
optButton.centerYAnchor.constraint(
equalTo: ctrlButton.centerYAnchor),
optButton.leadingAnchor.constraint(
equalTo: cmdButton.trailingAnchor,
constant: ViewConstants.spacing5),
shiftButton.centerYAnchor.constraint(
equalTo: ctrlButton.centerYAnchor),
shiftButton.leadingAnchor.constraint(
equalTo: optButton.trailingAnchor,
constant: ViewConstants.spacing5),
plusLabel.centerYAnchor.constraint(
equalTo: ctrlButton.centerYAnchor),
plusLabel.leadingAnchor.constraint(
equalTo: shiftButton.trailingAnchor,
constant: ViewConstants.spacing5),
recordButton.widthAnchor.constraint(equalToConstant: 40),
recordButton.centerYAnchor.constraint(
equalTo: ctrlButton.centerYAnchor),
recordButton.leadingAnchor.constraint(
equalTo: plusLabel.trailingAnchor,
constant: ViewConstants.spacing5),
pathsLabel.topAnchor.constraint(
equalTo: ctrlButton.bottomAnchor,
constant: ViewConstants.spacing20),
pathsLabel.leadingAnchor.constraint(
equalTo: shortcutsLabel.leadingAnchor),
tableScrollView.widthAnchor.constraint(equalToConstant: 350),
tableScrollView.heightAnchor.constraint(equalToConstant: 100),
tableScrollView.topAnchor.constraint(
equalTo: pathsLabel.bottomAnchor),
tableScrollView.leadingAnchor.constraint(
equalTo: view.leadingAnchor),
tableScrollView.trailingAnchor.constraint(
equalTo: view.trailingAnchor),
pathsControl.topAnchor.constraint(
equalTo: tableScrollView.bottomAnchor,
constant: ViewConstants.spacing10),
pathsControl.leadingAnchor.constraint(
equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
launchAtLoginButton.topAnchor.constraint(
equalTo: pathsControl.bottomAnchor,
constant: ViewConstants.spacing10),
launchAtLoginButton.trailingAnchor.constraint(
equalTo: resetAllButton.leadingAnchor,
constant: -ViewConstants.spacing10),
resetAllButton.centerYAnchor.constraint(
equalTo: launchAtLoginButton.centerYAnchor),
resetAllButton.trailingAnchor.constraint(
equalTo: view.trailingAnchor,
constant: -ViewConstants.spacing10),
resetAllButton.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10),
])
}
override func viewDidLoad() {
super.viewDidLoad()
tableScrollView.documentView = pathsTableView
cmdButton.target = self
optButton.target = self
ctrlButton.target = self
shiftButton.target = self
recordButton.delegate = self
launchAtLoginButton.target = self
resetAllButton.target = self
recordButton.defaultKey = kVK_Space
recordButton.target = self
pathsTableView.dataSource = self
pathsTableView.delegate = self
pathsTableView.delegate = self
pathsControl.target = self
pathsControl.action = #selector(affectPaths(_:))
addSubviews()
setConstraints()
}
override func viewWillAppear() {
super.viewWillAppear()
// PERF: Maybe we shouldn't fetch it on every appearance?
// Only do it in AppDelegate?
if let code =
UserDefaults.standard.object(forKey: "keyCode") as? Int
{
keyCode = code
}
if let mods =
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
{
modifiers = mods
}
syncModifierButtons()
launchAtLoginStatus()
}
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.center()
}
override func viewWillDisappear() {
super.viewWillDisappear()
HotKeyManager.shared.registerHotKey(key: keyCode,
modifiers: modifiers)
UserDefaults.standard.set(keyCode, forKey: "keyCode")
UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
PathManager.shared.savePaths()
PathManager.shared.rebuildIndex()
}
override func loadView() {
self.view = NSView()
}
@objc
private func handleModifiers() {
// NOTE: Revert to default modifier if none of the modifier
// buttons are on.
if cmdButton.state != .on, optButton.state != .on,
ctrlButton.state != .on, shiftButton.state != .on
{
optButton.state = .on
}
detectModifers()
}
@objc
private func launchAtLogin() {
delegate.toggleLaunchAtLogin()
launchAtLoginStatus()
}
private func launchAtLoginStatus() {
if delegate.willLaunchAtLogin() {
launchAtLoginButton.title = "Launch at login - ON"
launchAtLoginButton.state = .on
} else {
launchAtLoginButton.title = "Launch at login - OFF"
launchAtLoginButton.state = .off
}
}
@objc
private func reset() {
keyCode = Int(kVK_Space)
modifiers = Int(optionKey)
HotKeyManager.shared.registerHotKey(key: keyCode, modifiers: modifiers)
UserDefaults.standard.set(keyCode, forKey: "keyCode")
UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
syncModifierButtons()
PathManager.shared.reset()
pathsTableView.reloadData()
}
private func detectModifers() {
var mods = 0
if cmdButton.state == .on {
mods |= cmdKey
}
if optButton.state == .on {
mods |= optionKey
}
if ctrlButton.state == .on {
mods |= controlKey
}
if shiftButton.state == .on {
mods |= shiftKey
}
if mods == 0 {
mods |= optionKey
} else {
modifiers = mods
}
}
private func syncModifierButtons() {
ctrlButton.state = .off
cmdButton.state = .off
optButton.state = .off
shiftButton.state = .off
if modifiers & controlKey != 0 {
ctrlButton.state = .on
}
if modifiers & cmdKey != 0 {
cmdButton.state = .on
}
if modifiers & optionKey != 0 {
optButton.state = .on
}
if modifiers & shiftKey != 0 {
shiftButton.state = .on
}
if let character = keyName(virtualKeyCode: UInt16(keyCode)) {
recordButton.title = character
}
}
@objc
private func affectPaths(_ sender: NSSegmentedControl) {
// PERF: All of this could be written better.
let selectedSegment = sender.selectedSegment
switch selectedSegment {
case 0:
PathManager.shared.addPath("")
pathsTableView.reloadData()
let row = PathManager.shared.userPaths.count-1
pathsTableView.selectRowIndexes(IndexSet(integer: row),
byExtendingSelection: false)
pathsTableView.scrollRowToVisible(row)
(pathsTableView.view(atColumn: 0, row: row,
makeIfNecessary: false) as? MyTableCellView)?
.startEditing()
break
case 1:
var toRemove: [String] = []
for row in pathsTableView.selectedRowIndexes {
toRemove.append(PathManager.shared.userPaths[row])
}
PathManager.shared.userPaths.removeAll(
where: { toRemove.contains($0) })
pathsTableView.reloadData()
break
default:
break
}
}
@objc
private func editItem(_ sender: NSTableView) {
pathsTableView.deselectAll(nil)
pathsTableView.selectRowIndexes(
IndexSet(integer: pathsTableView.clickedRow),
byExtendingSelection: false)
if let cell = pathsTableView.view(atColumn: 0,
row: pathsTableView.clickedRow,
makeIfNecessary: false) as? MyTableCellView
{
cell.startEditing()
}
}
func titleFieldFinishedEditing(tag: Int, text: String) {
PathManager.shared.userPaths[tag] = text
if PathManager.shared.userPaths[tag].isEmpty {
PathManager.shared.userPaths.remove(at: tag)
pathsTableView.reloadData()
}
pathsTableView.deselectAll(nil)
}
func titleFieldTextChanged(tag: Int, text: String) {
}
func keyWasSet(to keyCode: Int) {
self.keyCode = Int(keyCode)
}
func selectionButtonClicked(tag: Int) {
NSRunningApplication.current.activate(options: .activateAllWindows)
delegate.window.level = .normal
if dirPicker.runModal() == .OK {
if let url = dirPicker.url {
PathManager.shared.userPaths[tag] = url.path
pathsTableView.reloadData()
}
}
delegate.window.level = .statusBar
delegate.window.makeKeyAndOrderFront(nil)
if let controller =
delegate.window.contentViewController as? SearchViewController
{
controller.openSettings()
}
}
func numberOfRows(in tableView: NSTableView) -> Int {
return PathManager.shared.userPaths.count
}
func tableView(_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{
let rect = NSRect(x: 0, y: 0,
width: tableColumn!.width, height: 20)
let cell = MyTableCellView(frame: rect)
cell.titleField.stringValue = PathManager.shared.userPaths[row]
cell.delegate = self
cell.id = row
return cell
}
func tableViewSelectionDidChange(_ notification: Notification) {
/*
let selectedRow = tableView.selectedRow
if selectedRow >= 0 {
print("Selected: \(items[selectedRow])")
}
*/
}
}