diff options
| author | Dave DeLong | 2019-08-29 19:28:24 -0600 | 
|---|---|---|
| committer | Dave DeLong | 2019-08-29 19:28:24 -0600 | 
| commit | 605ad85dca9803c36364380a3b8c8cae0866f349 (patch) | |
| tree | f03622b6da8b5e706b756278dc64b83e09fa7cad | |
| parent | e0481f648e0bc7e55d183622b00510b6721152d8 (diff) | |
| download | DDHotKey-605ad85dca9803c36364380a3b8c8cae0866f349.tar.bz2 | |
Starting to convert to Swift
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Package.swift | 28 | ||||
| -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 | ||||
| -rw-r--r-- | Tests/DDHotKeyTests/DDHotKeyTests.swift | 18 | ||||
| -rw-r--r-- | Tests/DDHotKeyTests/XCTestManifests.swift | 9 | ||||
| -rw-r--r-- | Tests/LinuxMain.swift | 7 | 
9 files changed, 467 insertions, 0 deletions
| @@ -2,3 +2,5 @@ build  **/*.mode1v3  **/*.perspectivev3  **/*.pbxuser +.build +DDHotKey.xcodeproj diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..573352b --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( +    name: "DDHotKey", +    products: [ +        // Products define the executables and libraries produced by a package, and make them visible to other packages. +        .library( +            name: "DDHotKey", +            targets: ["DDHotKey"]), +    ], +    dependencies: [ +        // Dependencies declare other packages that this package depends on. +        // .package(url: /* package url */, from: "1.0.0"), +    ], +    targets: [ +        // Targets are the basic building blocks of a package. A target can define a module or a test suite. +        // Targets can depend on other targets in this package, and on products in packages which this package depends on. +        .target( +            name: "DDHotKey", +            dependencies: []), +        .testTarget( +            name: "DDHotKeyTests", +            dependencies: ["DDHotKey"]), +    ] +) 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] +} diff --git a/Tests/DDHotKeyTests/DDHotKeyTests.swift b/Tests/DDHotKeyTests/DDHotKeyTests.swift new file mode 100644 index 0000000..5e523af --- /dev/null +++ b/Tests/DDHotKeyTests/DDHotKeyTests.swift @@ -0,0 +1,18 @@ +import XCTest +import Carbon +@testable import DDHotKey + +final class DDHotKeyTests: XCTestCase { +     +    func testExample() { +        // This is an example of a functional test case. +        // Use XCTAssert and related functions to verify your tests produce the correct +        // results. +        let s = string(for: kVK_ANSI_A) +        print("\(s)") +    } + +    static var allTests = [ +        ("testExample", testExample), +    ] +} diff --git a/Tests/DDHotKeyTests/XCTestManifests.swift b/Tests/DDHotKeyTests/XCTestManifests.swift new file mode 100644 index 0000000..70843ac --- /dev/null +++ b/Tests/DDHotKeyTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { +    return [ +        testCase(DDHotKeyTests.allTests), +    ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..ebc7760 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import DDHotKeyTests + +var tests = [XCTestCaseEntry]() +tests += DDHotKeyTests.allTests() +XCTMain(tests) | 
