Added settings window.
This commit is contained in:
@@ -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 {
|
} else {
|
||||||
Self.logger.debug("Failed to install event handler.")
|
// NOTE: This is the default shortcut. If you want to change
|
||||||
}
|
// it, do not forget to change it in other files
|
||||||
} else {
|
// (SettingsViewController).
|
||||||
Self.logger.debug("Failed to register hot key.")
|
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 {
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
86
src/HotKeyManager.swift
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/KeyDetectorButton.swift
Normal file
42
src/KeyDetectorButton.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Makefile
11
src/Makefile
@@ -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
107
src/MyTableCellView.swift
Normal 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
85
src/PathManager.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(_:)), to: nil, from: self)
|
NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
|
||||||
|
to: nil, from: self)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} else if commandSelector == #selector(NSResponder.insertTab(_:)) {
|
} else if commandSelector == #selector(NSResponder.insertTab(_:)) {
|
||||||
return true
|
return true
|
||||||
@@ -172,4 +226,12 @@ class SearchViewController: NSViewController, NSTextFieldDelegate {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func popoverWillShow(_ notification: Notification) {
|
||||||
|
searchInput.abortEditing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func popoverWillClose(_ notification: Notification) {
|
||||||
|
searchInput.becomeFirstResponder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
572
src/SettingsViewController.swift
Normal file
572
src/SettingsViewController.swift
Normal 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])")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user