diff options
Diffstat (limited to 'Sources')
| -rw-r--r-- | Sources/DDHotKey/DDHotKey.swift | 48 | ||||
| -rw-r--r-- | Sources/DDHotKey/DDHotKeyCenter.swift | 154 | ||||
| -rw-r--r-- | Sources/DDHotKey/DDHotKeyTranslation.swift | 59 | ||||
| -rw-r--r-- | Sources/DDHotKey/DDHotKeyUtilities.swift | 142 |
4 files changed, 403 insertions, 0 deletions
diff --git a/Sources/DDHotKey/DDHotKey.swift b/Sources/DDHotKey/DDHotKey.swift new file mode 100644 index 0000000..0f3c93e --- /dev/null +++ b/Sources/DDHotKey/DDHotKey.swift @@ -0,0 +1,48 @@ +// +// DDHotKey.swift +// DDHotKey +// +// Created by Dave DeLong on 8/28/19. +// + +import Cocoa +import Carbon + +public class DDHotKey { + internal var hotKeyID: UInt32? + internal var hotKeyRef: EventHotKeyRef? + + internal let uuid = UUID() + internal let keyCode: CGKeyCode + internal let modifiers: NSEvent.ModifierFlags + internal let handler: (NSEvent) -> Void + + public init(keyCode: CGKeyCode, modifiers: NSEvent.ModifierFlags, handler: @escaping (NSEvent) -> Void) { + self.keyCode = keyCode + self.modifiers = modifiers + self.handler = handler + } + + public convenience init?(shortcut: String, handler: @escaping (NSEvent) -> Void) { + guard let (keyCode, modifiers) = keyCodeAndModifiers(from: shortcut) else { + return nil + } + + self.init(keyCode: keyCode, modifiers: modifiers, handler: handler) + } + + internal func invoke(with event: NSEvent) { + handler(event) + } +} + +public struct DDHotKeyOld: Hashable { + var hotKeyID: UInt32 = 0 + var keyCode: UInt16 = 0 + var text = "Hello, World!" + + func invoke(with event: NSEvent) -> OSStatus { + return noErr + } +} + diff --git a/Sources/DDHotKey/DDHotKeyCenter.swift b/Sources/DDHotKey/DDHotKeyCenter.swift new file mode 100644 index 0000000..84a456d --- /dev/null +++ b/Sources/DDHotKey/DDHotKeyCenter.swift @@ -0,0 +1,154 @@ +// +// DDHotKeyCenter.swift +// DDHotKey +// +// Created by Dave DeLong on 8/28/19. +// + +import Cocoa +import Carbon + +public class DDHotKeyCenter { + + public enum RegistrationError: Error { + case alreadyRegistered + case tooManyHotKeys + case conflictsWithExistingHotKey(DDHotKey) + case unableToRegisterHotKey(OSStatus) + } + + public enum UnregistrationError: Error { + case notRegistered + case unknownHotKey + case unableToUnregisterHotKey(OSStatus) + } + + public static let shared = DDHotKeyCenter() + + private var registered = Dictionary<UUID, DDHotKey>() + private var nextID: UInt32 = 1 + + private init() { + + var spec = EventTypeSpec() + spec.eventClass = OSType(kEventClassKeyboard) + spec.eventKind = UInt32(kEventHotKeyReleased) + InstallEventHandler(GetApplicationEventTarget(), + handler, + 1, + &spec, + nil, + nil) + } + + internal func hotKeysMatching(_ filter: (DDHotKey) -> Bool) -> Array<DDHotKey> { + return registered.values.filter(filter) + } + + public func registeredHotKeys() -> Array<DDHotKey> { + return Array(registered.values) + } + + public func register(hotKey: DDHotKey) throws /* RegistrationError */ { + // cannot register a hot key that is already registered + if hotKey.hotKeyID != nil { + throw RegistrationError.alreadyRegistered + } + + guard nextID < UInt32.max else { + throw RegistrationError.tooManyHotKeys + } + + // cannot register a hot key that has the same invocation as an existing hotkey + let matching = registered.values.filter { $0.keyCode == hotKey.keyCode && $0.modifiers == hotKey.modifiers } + if matching.isEmpty == false { + throw RegistrationError.conflictsWithExistingHotKey(matching[0]) + } + + let hotKeyID = EventHotKeyID(signature: OSType(fourCharCode: "htk1"), id: nextID) + let flags = carbonModifiers(from: hotKey.modifiers) + + var hotKeyRef: EventHotKeyRef? + let error = RegisterEventHotKey(UInt32(hotKey.keyCode), flags, hotKeyID, GetEventDispatcherTarget(), 0, &hotKeyRef) + + if error != noErr { + throw RegistrationError.unableToRegisterHotKey(error) + } + + hotKey.hotKeyRef = hotKeyRef + hotKey.hotKeyID = nextID + registered[hotKey.uuid] = hotKey + + nextID += 1 + } + + public func unregister(hotKey: DDHotKey) throws /* UnregistrationError */ { + guard let ref = hotKey.hotKeyRef, hotKey.hotKeyID != nil else { + throw UnregistrationError.notRegistered + } + + guard let existing = registered[hotKey.uuid], existing === hotKey else { + throw UnregistrationError.unknownHotKey + } + + let status = UnregisterEventHotKey(ref) + + guard status == noErr else { + throw UnregistrationError.unableToUnregisterHotKey(status) + } + + registered.removeValue(forKey: hotKey.uuid) + } +} + + +fileprivate var handler: EventHandlerProcPtr = { (callRef, eventRef, context) -> OSStatus in + return autoreleasepool { () -> OSStatus in + var hotKeyID = EventHotKeyID() + GetEventParameter(eventRef, + EventParamName(kEventParamDirectObject), + EventParamType(typeEventHotKeyID), + nil, + MemoryLayout<EventHotKeyID>.size, + nil, + &hotKeyID); + + let keyID = hotKeyID.id + let matches = DDHotKeyCenter.shared.hotKeysMatching { $0.hotKeyID == keyID } + if matches.count != 1 { + print("Unable to find a single hotkey with id \(keyID)") + return OSStatus(errAborted) + } + let matching = matches[matches.startIndex] + + guard let ref = eventRef else { + print("Missing EventRef to handle hotkey with id \(keyID)") + return OSStatus(errAborted) + } + + guard let event = NSEvent(eventRef: UnsafeRawPointer(ref)) else { + print("Unable to create NSEvent from EventRef \(ref) for the hotkey with id \(keyID)") + return OSStatus(errAborted) + } + + let keyEvent = NSEvent.keyEvent(with: .keyUp, + location: event.locationInWindow, + modifierFlags: event.modifierFlags, + timestamp: event.timestamp, + windowNumber: -1, + context: nil, + characters: "", + charactersIgnoringModifiers: "", + isARepeat: false, + keyCode: matching.keyCode) + + guard let key = keyEvent else { + print("Unable to create key event from NSEvent \(event) for the hotkey with id \(keyID)") + return OSStatus(errAborted) + } + + matching.invoke(with: key) + return noErr + } + +} diff --git a/Sources/DDHotKey/DDHotKeyTranslation.swift b/Sources/DDHotKey/DDHotKeyTranslation.swift new file mode 100644 index 0000000..ce66d0d --- /dev/null +++ b/Sources/DDHotKey/DDHotKeyTranslation.swift @@ -0,0 +1,59 @@ +// +// DDHotKeyTranslation.swift +// DDHotKey +// +// Created by Dave DeLong on 8/28/19. +// + +import Cocoa +import Carbon + +fileprivate let standardModifiers: Dictionary<Int, NSEvent.ModifierFlags> = [ + kVK_Option: .option, + kVK_Shift: .shift, + kVK_Command: .command, + kVK_Control: .control +] + +internal func keyCodeAndModifiers(from string: String) -> (CGKeyCode, NSEvent.ModifierFlags)? { + var flags = NSEvent.ModifierFlags() + var keyCode: Int? + + for character in string { + guard let code = keycode(for: String(character)) else { return nil } + if let modifier = standardModifiers[code] { + flags.insert(modifier) + } else if keyCode == nil { + keyCode = code + } else { + return nil + } + } + + guard let code = keyCode else { return nil } + + return (CGKeyCode(code), flags) +} + +internal func stringFrom(keyCode: CGKeyCode, modifiers: NSEvent.ModifierFlags) -> String { + var final = "" + if modifiers.contains(.control) { + final += string(for: kVK_Control)! + } + if modifiers.contains(.option) { + final += string(for: kVK_Option)! + } + if modifiers.contains(.shift) { + final += string(for: kVK_Shift)! + } + if modifiers.contains(.command) { + final += string(for: kVK_Command)! + } + + if standardModifiers[Int(keyCode)] != nil { return final } + + if let mapped = string(for: Int(keyCode), carbonModifiers: carbonModifiers(from: modifiers)) { + final += mapped + } + return final +} diff --git a/Sources/DDHotKey/DDHotKeyUtilities.swift b/Sources/DDHotKey/DDHotKeyUtilities.swift new file mode 100644 index 0000000..cc2fa04 --- /dev/null +++ b/Sources/DDHotKey/DDHotKeyUtilities.swift @@ -0,0 +1,142 @@ +// +// DDHotKeyUtilities.swift +// DDHotKey +// +// Created by Dave DeLong on 8/28/19. +// + +import Carbon +import Cocoa + +internal extension OSType { + + init(fourCharCode: String) { + + let scalars = Array(fourCharCode.unicodeScalars)[0 ..< 4] + var sum: UInt32 = 0 + for scalar in scalars { + sum = (sum << 8) + scalar.value + } + self = sum + } + +} + +fileprivate let characterMap: Dictionary<Int, String> = [ + kVK_Return: "↩", + kVK_Tab: "⇥", + kVK_Space: "⎵", + kVK_Delete: "⌫", + kVK_Escape: "⎋", + kVK_Command: "⌘", + kVK_Shift: "⇧", + kVK_CapsLock: "⇪", + kVK_Option: "⌥", + kVK_Control: "⌃", + kVK_RightShift: "⇧", + kVK_RightOption: "⌥", + kVK_RightControl: "⌃", + kVK_VolumeUp: "🔊", + kVK_VolumeDown: "🔈", + kVK_Mute: "🔇", + kVK_Function: "\u{2318}", + kVK_F1: "F1", + kVK_F2: "F2", + kVK_F3: "F3", + kVK_F4: "F4", + kVK_F5: "F5", + kVK_F6: "F6", + kVK_F7: "F7", + kVK_F8: "F8", + kVK_F9: "F9", + kVK_F10: "F10", + kVK_F11: "F11", + kVK_F12: "F12", + kVK_F13: "F13", + kVK_F14: "F14", + kVK_F15: "F15", + kVK_F16: "F16", + kVK_F17: "F17", + kVK_F18: "F18", + kVK_F19: "F19", + kVK_F20: "F20", + // kVK_Help: "", + kVK_ForwardDelete: "⌦", + kVK_Home: "↖", + kVK_End: "↘", + kVK_PageUp: "⇞", + kVK_PageDown: "⇟", + kVK_LeftArrow: "←", + kVK_RightArrow: "→", + kVK_DownArrow: "↓", + kVK_UpArrow: "↑", +] + +internal func carbonModifiers(from cocoaModifiers: NSEvent.ModifierFlags) -> UInt32 { + var newFlags: Int = 0 + if cocoaModifiers.contains(.control) { newFlags |= controlKey } + if cocoaModifiers.contains(.command) { newFlags |= cmdKey } + if cocoaModifiers.contains(.shift) { newFlags |= shiftKey } + if cocoaModifiers.contains(.option) { newFlags |= optionKey } + if cocoaModifiers.contains(.capsLock) { newFlags |= alphaLock } + return UInt32(newFlags) +} + +fileprivate let keycodesToCharacters: Dictionary<Int, String> = { + var map = characterMap + for code in 0 ..< 65536 { + if map[code] != nil { continue } + if let character = string(for: code, useCache: false) { + map[code] = character + } + } + return map +}() + +fileprivate let charactersToKeycodes: Dictionary<String, Int> = { + var map = Dictionary<String, Int>(minimumCapacity: keycodesToCharacters.count) + for (code, string) in keycodesToCharacters { + map[string] = code + } + return map +}() + +fileprivate let layoutData: CFData? = { + var currentKeyboard = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue() + if currentKeyboard == nil { + currentKeyboard = TISCopyCurrentASCIICapableKeyboardInputSource()?.takeRetainedValue() + } + guard let keyboard = currentKeyboard else { return nil } + + guard let ptr = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData) else { return nil } + return Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() +}() + +internal func string(for keyCode: Int, carbonModifiers: UInt32 = UInt32(alphaLock >> 8), useCache: Bool = true) -> String? { + if useCache == true { + if let mapped = keycodesToCharacters[keyCode] { return mapped } + } + + guard let data = layoutData else { return nil } + guard let bytePtr = CFDataGetBytePtr(data) else { return nil } + + var deadKeyState: UInt32 = 0 + var actualStringLength: Int = 0 + var characters = Array<UniChar>(repeating: 0, count: 255) + + let status = bytePtr.withMemoryRebound(to: UCKeyboardLayout.self, capacity: 1) { pointer -> OSStatus in + return UCKeyTranslate(pointer, + UInt16(keyCode), UInt16(kUCKeyActionDown), carbonModifiers, + UInt32(LMGetKbdType()), 0, + &deadKeyState, 255, + &actualStringLength, &characters) + } + + guard status == noErr else { return nil } + guard actualStringLength > 0 else { return nil } + return String(utf16CodeUnits: &characters, count: actualStringLength) +} + +internal func keycode(for string: String) -> Int? { + return charactersToKeycodes[string] +} |
