diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift index d5064a2..93772f3 100644 --- a/Example/Example/AppDelegate.swift +++ b/Example/Example/AppDelegate.swift @@ -19,7 +19,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { // ⌘ + Control + B - guard let keyCombo = KeyCombo(keyCode: 11, carbonModifiers: 4352) else { return } + guard let keyCombo = KeyCombo(key: .b, cocoaModifiers: [.command, .control]) else { return } let hotKey = HotKey(identifier: "CommandControlB", keyCombo: keyCombo, target: self, @@ -27,7 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { hotKey.register() // Shift + Control + A - guard let keyCombo2 = KeyCombo(keyCode: 0, cocoaModifiers: [.shift, .control]) else { return } + guard let keyCombo2 = KeyCombo(key: .a, cocoaModifiers: [.shift, .control]) else { return } let hotKey2 = HotKey(identifier: "ShiftControlA", keyCombo: keyCombo2, target: self, @@ -43,7 +43,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { hotKey3.register() // Shift Double Tap - guard let keyCombo4 = KeyCombo(doubledCocoaModifiers: .shift) else { return } + guard let keyCombo4 = KeyCombo(doubledCarbonModifiers: shiftKey) else { return } let hotKey4 = HotKey(identifier: "ShiftDoubleTap", keyCombo: keyCombo4, target: self, diff --git a/Lib/Magnet.xcodeproj/project.pbxproj b/Lib/Magnet.xcodeproj/project.pbxproj index 7d40032..2f24108 100644 --- a/Lib/Magnet.xcodeproj/project.pbxproj +++ b/Lib/Magnet.xcodeproj/project.pbxproj @@ -9,16 +9,17 @@ /* Begin PBXBuildFile section */ 091D85D22459554600930473 /* Sauce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 091D85CF2459553A00930473 /* Sauce.framework */; }; 091D85D32459554600930473 /* Sauce.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 091D85CF2459553A00930473 /* Sauce.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - FA3AA2142315A51F007EAA1F /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AA2132315A51F007EAA1F /* CollectionExtension.swift */; }; + 091D85EA245A917500930473 /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E6245A917500930473 /* NSEventExtension.swift */; }; + 091D85EB245A917500930473 /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E7245A917500930473 /* IntExtension.swift */; }; + 091D85EC245A917500930473 /* KeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E8245A917500930473 /* KeyExtension.swift */; }; + 091D85ED245A917500930473 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E9245A917500930473 /* CollectionExtension.swift */; }; FA3AA2162315A6A3007EAA1F /* CollectionExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */; }; - FA7145471D147F1B004A04D4 /* KeyTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7145461D147F1B004A04D4 /* KeyTransformer.swift */; }; FAEC34B31C9059DF004177E2 /* Magnet.h in Headers */ = {isa = PBXBuildFile; fileRef = FAEC34B21C9059DF004177E2 /* Magnet.h */; settings = {ATTRIBUTES = (Public, ); }; }; FAEC34BA1C9059DF004177E2 /* Magnet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAEC34AF1C9059DF004177E2 /* Magnet.framework */; }; FAEC34BF1C9059DF004177E2 /* MagnetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34BE1C9059DF004177E2 /* MagnetTests.swift */; }; FAEC34D81C905B4D004177E2 /* HotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34D71C905B4D004177E2 /* HotKey.swift */; }; FAEC34DA1C905B5A004177E2 /* KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34D91C905B5A004177E2 /* KeyCombo.swift */; }; FAEC34DE1C905B7C004177E2 /* HotKeyCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */; }; - FAFA5B711D1FD1FA003CB2DC /* KeyCodeTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFA5B701D1FD1FA003CB2DC /* KeyCodeTransformer.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -61,9 +62,11 @@ /* Begin PBXFileReference section */ 091D85C92459553A00930473 /* Sauce.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Sauce.xcodeproj; path = ../Carthage/Checkouts/Sauce/Lib/Sauce.xcodeproj; sourceTree = ""; }; - FA3AA2132315A51F007EAA1F /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; + 091D85E6245A917500930473 /* NSEventExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; + 091D85E7245A917500930473 /* IntExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; + 091D85E8245A917500930473 /* KeyExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyExtension.swift; sourceTree = ""; }; + 091D85E9245A917500930473 /* CollectionExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensionTests.swift; sourceTree = ""; }; - FA7145461D147F1B004A04D4 /* KeyTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyTransformer.swift; sourceTree = ""; }; FAEC34AF1C9059DF004177E2 /* Magnet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Magnet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FAEC34B21C9059DF004177E2 /* Magnet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Magnet.h; sourceTree = ""; }; FAEC34B41C9059DF004177E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -73,7 +76,6 @@ FAEC34D71C905B4D004177E2 /* HotKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKey.swift; sourceTree = ""; }; FAEC34D91C905B5A004177E2 /* KeyCombo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCombo.swift; sourceTree = ""; }; FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeyCenter.swift; sourceTree = ""; }; - FAFA5B701D1FD1FA003CB2DC /* KeyCodeTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCodeTransformer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -113,6 +115,17 @@ name = Products; sourceTree = ""; }; + 091D85E5245A915A00930473 /* Extensions */ = { + isa = PBXGroup; + children = ( + 091D85E9245A917500930473 /* CollectionExtension.swift */, + 091D85E7245A917500930473 /* IntExtension.swift */, + 091D85E8245A917500930473 /* KeyExtension.swift */, + 091D85E6245A917500930473 /* NSEventExtension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; FAEC34A51C9059DF004177E2 = { isa = PBXGroup; children = ( @@ -135,12 +148,10 @@ FAEC34B11C9059DF004177E2 /* Magnet */ = { isa = PBXGroup; children = ( + 091D85E5245A915A00930473 /* Extensions */, FAEC34D71C905B4D004177E2 /* HotKey.swift */, FAEC34D91C905B5A004177E2 /* KeyCombo.swift */, FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */, - FA7145461D147F1B004A04D4 /* KeyTransformer.swift */, - FAFA5B701D1FD1FA003CB2DC /* KeyCodeTransformer.swift */, - FA3AA2132315A51F007EAA1F /* CollectionExtension.swift */, FAEC34B21C9059DF004177E2 /* Magnet.h */, FAEC34B41C9059DF004177E2 /* Info.plist */, ); @@ -293,11 +304,12 @@ buildActionMask = 2147483647; files = ( FAEC34DA1C905B5A004177E2 /* KeyCombo.swift in Sources */, - FA3AA2142315A51F007EAA1F /* CollectionExtension.swift in Sources */, FAEC34DE1C905B7C004177E2 /* HotKeyCenter.swift in Sources */, + 091D85EA245A917500930473 /* NSEventExtension.swift in Sources */, FAEC34D81C905B4D004177E2 /* HotKey.swift in Sources */, - FAFA5B711D1FD1FA003CB2DC /* KeyCodeTransformer.swift in Sources */, - FA7145471D147F1B004A04D4 /* KeyTransformer.swift in Sources */, + 091D85EC245A917500930473 /* KeyExtension.swift in Sources */, + 091D85ED245A917500930473 /* CollectionExtension.swift in Sources */, + 091D85EB245A917500930473 /* IntExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Lib/Magnet/CollectionExtension.swift b/Lib/Magnet/Extensions/CollectionExtension.swift similarity index 82% rename from Lib/Magnet/CollectionExtension.swift rename to Lib/Magnet/Extensions/CollectionExtension.swift index 5fa55f1..71b2c6d 100644 --- a/Lib/Magnet/CollectionExtension.swift +++ b/Lib/Magnet/Extensions/CollectionExtension.swift @@ -10,7 +10,7 @@ import Foundation -extension Collection where Element == Bool { +public extension Collection where Element == Bool { var trueCount: Int { return filter { $0 }.count } diff --git a/Lib/Magnet/Extensions/IntExtension.swift b/Lib/Magnet/Extensions/IntExtension.swift new file mode 100644 index 0000000..e872725 --- /dev/null +++ b/Lib/Magnet/Extensions/IntExtension.swift @@ -0,0 +1,31 @@ +// +// IntExtension.swift +// +// Magnet +// GitHub: https://github.com/clipy +// HP: https://clipy-app.com +// +// Copyright © 2015-2020 Clipy Project. +// + +import Cocoa +import Carbon + +extension Int { + func convertSupportCococaModifiers() -> NSEvent.ModifierFlags { + var cocoaFlags: NSEvent.ModifierFlags = NSEvent.ModifierFlags(rawValue: 0) + if (self & cmdKey) != 0 { + cocoaFlags.insert(.command) + } + if (self & optionKey) != 0 { + cocoaFlags.insert(.option) + } + if (self & controlKey) != 0 { + cocoaFlags.insert(.control) + } + if (self & shiftKey) != 0 { + cocoaFlags.insert(.shift) + } + return cocoaFlags + } +} diff --git a/Lib/Magnet/Extensions/KeyExtension.swift b/Lib/Magnet/Extensions/KeyExtension.swift new file mode 100644 index 0000000..48e9698 --- /dev/null +++ b/Lib/Magnet/Extensions/KeyExtension.swift @@ -0,0 +1,42 @@ +// +// KeyExtension.swift +// +// Magnet +// GitHub: https://github.com/clipy +// HP: https://clipy-app.com +// +// Copyright © 2015-2020 Clipy Project. +// + +import Foundation +import Sauce + +public extension Key { + var isFunctionKey: Bool { + switch self { + case .f1, + .f2, + .f3, + .f4, + .f5, + .f6, + .f7, + .f8, + .f9, + .f10, + .f11, + .f12, + .f13, + .f14, + .f15, + .f16, + .f17, + .f18, + .f19, + .f20: + return true + default: + return false + } + } +} diff --git a/Lib/Magnet/Extensions/NSEventExtension.swift b/Lib/Magnet/Extensions/NSEventExtension.swift new file mode 100644 index 0000000..b00e409 --- /dev/null +++ b/Lib/Magnet/Extensions/NSEventExtension.swift @@ -0,0 +1,62 @@ +// +// NSEventExtension.swift +// +// Magnet +// GitHub: https://github.com/clipy +// HP: https://clipy-app.com +// +// Copyright © 2015-2020 Clipy Project. +// + +import Cocoa +import Carbon + +public extension NSEvent.ModifierFlags { + var containsSupportModifiers: Bool { + return contains(.command) || contains(.option) || contains(.control) || contains(.shift) || contains(.function) + } + var isSingleFlags: Bool { + let commandSelected = contains(.command) + let optionSelected = contains(.option) + let controlSelected = contains(.control) + let shiftSelected = contains(.shift) + return [commandSelected, optionSelected, controlSelected, shiftSelected].trueCount == 1 + } + + func filterUnsupportModifiers() -> NSEvent.ModifierFlags { + var filterdModifierFlags = NSEvent.ModifierFlags(rawValue: 0) + if contains(.command) { + filterdModifierFlags.insert(.command) + } + if contains(.option) { + filterdModifierFlags.insert(.option) + } + if contains(.control) { + filterdModifierFlags.insert(.control) + } + if contains(.shift) { + filterdModifierFlags.insert(.shift) + } + return filterdModifierFlags + } + + func carbonModifiers(isSupportFunctionKey: Bool = false) -> Int { + var carbonModifiers: Int = 0 + if contains(.command) { + carbonModifiers |= cmdKey + } + if contains(.option) { + carbonModifiers |= optionKey + } + if contains(.control) { + carbonModifiers |= controlKey + } + if contains(.shift) { + carbonModifiers |= shiftKey + } + if contains(.function) && isSupportFunctionKey { + carbonModifiers |= Int(NSEvent.ModifierFlags.function.rawValue) + } + return carbonModifiers + } +} diff --git a/Lib/Magnet/HotKeyCenter.swift b/Lib/Magnet/HotKeyCenter.swift index d6ed0aa..c2b8629 100644 --- a/Lib/Magnet/HotKeyCenter.swift +++ b/Lib/Magnet/HotKeyCenter.swift @@ -17,75 +17,84 @@ public final class HotKeyCenter { public static let shared = HotKeyCenter() private var hotKeys = [String: HotKey]() - private var hotKeyMap = [NSNumber: HotKey]() private var hotKeyCount: UInt32 = 0 private var tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0) private var multiModifiers = false private var isInstalledFlagsChangedEvent = false + private let notificationCenter: NotificationCenter // MARK: - Initialize - init() { + init(notificationCenter: NotificationCenter = .default) { + self.notificationCenter = notificationCenter + installHotKeyPressedEventHandler() observeApplicationTerminate() } + deinit { + notificationCenter.removeObserver(self) + } + } // MARK: - Register & Unregister public extension HotKeyCenter { + @discardableResult func register(with hotKey: HotKey) -> Bool { guard !hotKeys.keys.contains(hotKey.identifier) else { return false } guard !hotKeys.values.contains(hotKey) else { return false } - if !hotKey.keyCombo.doubledModifiers { - // Normal HotKey - let hotKeyId = EventHotKeyID(signature: UTGetOSTypeFromString("Magnet" as CFString), id: hotKeyCount) - var carbonHotKey: EventHotKeyRef? - let error = RegisterEventHotKey(UInt32(hotKey.keyCombo.keyCode), - UInt32(hotKey.keyCombo.modifiers), - hotKeyId, - GetEventDispatcherTarget(), - 0, - &carbonHotKey) - if error != 0 { return false } - - hotKey.hotKeyId = hotKeyId.id - hotKey.hotKeyRef = carbonHotKey - } else { - installEventHandler() + hotKeys[hotKey.identifier] = hotKey + guard !hotKey.keyCombo.doubledModifiers else { + // In the case of a double-tap shortcut, start independent monitoring + installModifierEventHandlerIfNeeded() + return true } - - let kId = NSNumber(value: hotKeyCount as UInt32) - hotKeyMap[kId] = hotKey + // Normal macOS shortcut + /* + * Discussion: + * When registering a hotkey, a KeyCode that conforms to the + * keyboard layout at the time of registration is registered. + * To register a `v` on the QWERTY keyboard, `9` is registered, + * and to register a `v` on the Dvorak keyboard, `47` is registered. + * Therefore, if you change the keyboard layout after registering + * a hot key, the hot key is not assigned to the correct key. + * To solve this problem, you need to re-register the hotkeys + * when you change the layout, but it's not supported by the + * Apple Genuine app either, so it's not supported now. + */ + let hotKeyId = EventHotKeyID(signature: UTGetOSTypeFromString("Magnet" as CFString), id: hotKeyCount) + var carbonHotKey: EventHotKeyRef? + let error = RegisterEventHotKey(UInt32(hotKey.keyCombo.currentKeyCode), + UInt32(hotKey.keyCombo.modifiers), + hotKeyId, + GetEventDispatcherTarget(), + 0, + &carbonHotKey) + if error != 0 { + unregister(with: hotKey) + return false + } + hotKey.hotKeyId = hotKeyId.id + hotKey.hotKeyRef = carbonHotKey hotKeyCount += 1 - hotKeys[hotKey.identifier] = hotKey - return true } func unregister(with hotKey: HotKey) { - guard hotKeys.values.contains(hotKey) else { return } - - if !hotKey.keyCombo.doubledModifiers { - // Notmal HotKey - guard let carbonHotKey = hotKey.hotKeyRef else { return } + if let carbonHotKey = hotKey.hotKeyRef { UnregisterEventHotKey(carbonHotKey) } - hotKeys.removeValue(forKey: hotKey.identifier) - hotKey.hotKeyId = nil hotKey.hotKeyRef = nil - - hotKeyMap - .filter { $1 == hotKey } - .map { $0.0 } - .forEach { hotKeyMap.removeValue(forKey: $0) } } - func unregisterHotKey(with identifier: String) { - guard let hotKey = hotKeys[identifier] else { return } + @discardableResult + func unregisterHotKey(with identifier: String) -> Bool { + guard let hotKey = hotKeys[identifier] else { return false } unregister(with: hotKey) + return true } func unregisterAll() { @@ -96,10 +105,10 @@ public extension HotKeyCenter { // MARK: - Terminate extension HotKeyCenter { private func observeApplicationTerminate() { - NotificationCenter.default.addObserver(self, - selector: #selector(HotKeyCenter.applicationWillTerminate), - name: NSApplication.willTerminateNotification, - object: nil) + notificationCenter.addObserver(self, + selector: #selector(HotKeyCenter.applicationWillTerminate), + name: NSApplication.willTerminateNotification, + object: nil) } @objc func applicationWillTerminate() { @@ -109,17 +118,17 @@ extension HotKeyCenter { // MARK: - HotKey Events private extension HotKeyCenter { - func installEventHandler() { - guard !isInstalledFlagsChangedEvent else { return } - isInstalledFlagsChangedEvent = true - // Press HotKey Event + func installHotKeyPressedEventHandler() { var pressedEventType = EventTypeSpec() pressedEventType.eventClass = OSType(kEventClassKeyboard) pressedEventType.eventKind = OSType(kEventHotKeyPressed) InstallEventHandler(GetEventDispatcherTarget(), { _, inEvent, _ -> OSStatus in return HotKeyCenter.shared.sendCarbonEvent(inEvent!) }, 1, &pressedEventType, nil, nil) + } + func installModifierEventHandlerIfNeeded() { + guard !isInstalledFlagsChangedEvent else { return } // Press Modifiers Event let mask = CGEventMask((1 << CGEventType.flagsChanged.rawValue)) let event = CGEvent.tapCreate(tap: .cghidEventTap, @@ -128,10 +137,11 @@ private extension HotKeyCenter { eventsOfInterest: mask, callback: { _, _, event, _ in return HotKeyCenter.shared.sendModifiersEvent(event) }, userInfo: nil) - if event == nil { return } - let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event!, 0) + guard let modifiersEvent = event else { return } + isInstalledFlagsChangedEvent = true + let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, modifiersEvent, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), source, CFRunLoopMode.commonModes) - CGEvent.tapEnable(tap: event!, enable: true) + CGEvent.tapEnable(tap: modifiersEvent, enable: true) } func sendCarbonEvent(_ event: EventRef) -> OSStatus { @@ -150,16 +160,13 @@ private extension HotKeyCenter { assert(hotKeyId.signature == UTGetOSTypeFromString("Magnet" as CFString), "Invalid hot key id") - let kId = NSNumber(value: hotKeyId.id as UInt32) - let hotKey = hotKeyMap[kId] - + let hotKey = hotKeys.values.first(where: { $0.hotKeyId == hotKeyId.id }) switch GetEventKind(event) { case EventParamName(kEventHotKeyPressed): hotKeyDown(hotKey) default: assert(false, "Unknown event kind") } - return noErr } @@ -195,7 +202,7 @@ private extension HotKeyCenter { (tappedModifierKey.contains(.shift) && shiftTapped) || (tappedModifierKey.contains(.control) && controlTapped) || (tappedModifierKey.contains(.option) && altTapped) { - doubleTapped(with: KeyTransformer.carbonFlags(from: tappedModifierKey)) + doubleTapped(with: tappedModifierKey.carbonModifiers()) tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0) } else { if commandTapped { diff --git a/Lib/Magnet/KeyCodeTransformer.swift b/Lib/Magnet/KeyCodeTransformer.swift deleted file mode 100644 index 7011650..0000000 --- a/Lib/Magnet/KeyCodeTransformer.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// KeyCodeTransformer.swift -// -// Magnet -// GitHub: https://github.com/clipy -// HP: https://clipy-app.com -// -// Copyright © 2015-2020 Clipy Project. -// - -import Cocoa -import Carbon - -open class KeyCodeTransformer { - // MARK: - Properties - public static let shared = KeyCodeTransformer() -} - -// MARK: - Transform -extension KeyCodeTransformer { - public func transformValue(_ keyCode: Int, carbonModifiers: Int) -> String { - return transformValue(keyCode, modifiers: carbonModifiers) - } - - public func transformValue(_ keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> String { - return transformValue(keyCode, modifiers: KeyTransformer.carbonFlags(from: cocoaModifiers)) - } - - fileprivate func transformValue(_ keyCode: Int, modifiers: Int) -> String { - // Return Special KeyCode - if let unmappedString = transformSpecialKeyCode(keyCode) { - return unmappedString - } - - let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource().takeUnretainedValue() - let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) - let dataRef = unsafeBitCast(layoutData, to: CFData.self) - - let keyLayout = unsafeBitCast(CFDataGetBytePtr(dataRef), to: UnsafePointer.self) - - let keyTranslateOptions = OptionBits(CoreServices.kUCKeyTranslateNoDeadKeysBit) - var deadKeyState: UInt32 = 0 - let maxChars = 256 - var chars = [UniChar](repeating: 0, count: maxChars) - var length = 0 - - let error = CoreServices.UCKeyTranslate(keyLayout, - UInt16(keyCode), - UInt16(CoreServices.kUCKeyActionDisplay), - UInt32(modifiers), - UInt32(LMGetKbdType()), - keyTranslateOptions, - &deadKeyState, - maxChars, - &length, - &chars) - - if error != noErr { return "" } - - return NSString(characters: &chars, length: length).uppercased - } - - fileprivate func transformSpecialKeyCode(_ keyCode: Int) -> String? { - return specialKeyCodeStrings[keyCode] - } -} - -// MARK: - Mapping -private extension KeyCodeTransformer { - var specialKeyCodeStrings: [Int: String] { - return [ - 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_Space: "Space", - kVK_Delete: string(from: 0x232B), // ⌫ - kVK_ForwardDelete: string(from: 0x2326), // ⌦ - kVK_ANSI_Keypad0: string(from: 0x2327), // ⌧ - kVK_LeftArrow: string(from: 0x2190), // ← - kVK_RightArrow: string(from: 0x2192), // → - kVK_UpArrow: string(from: 0x2191), // ↑ - kVK_DownArrow: string(from: 0x2193), // ↓ - kVK_End: string(from: 0x2198), // ↘ - kVK_Home: string(from: 0x2196), // ↖ - kVK_Escape: string(from: 0x238B), // ⎋ - kVK_PageDown: string(from: 0x21DF), // ⇟ - kVK_PageUp: string(from: 0x21DE), // ⇞ - kVK_Return: string(from: 0x21A9), // ↩ - kVK_ANSI_KeypadEnter: string(from: 0x2305), // ⌅ - kVK_Tab: string(from: 0x21E5), // ⇥ - kVK_Help: "?⃝" - ] - } -} - -// MARK: - Charactor -private extension KeyCodeTransformer { - func string(from char: unichar) -> String { - return String(format: "%C", char) - } -} diff --git a/Lib/Magnet/KeyCombo.swift b/Lib/Magnet/KeyCombo.swift index 49fcab9..e21d58a 100644 --- a/Lib/Magnet/KeyCombo.swift +++ b/Lib/Magnet/KeyCombo.swift @@ -10,84 +10,138 @@ import Cocoa import Carbon +import Sauce public final class KeyCombo: NSObject, NSCopying, NSCoding, Codable { // MARK: - Properties - public let keyCode: Int + public let QWERTYKeyCode: Int public let modifiers: Int public let doubledModifiers: Bool public var characters: String { - if doubledModifiers { return "" } - return KeyCodeTransformer.shared.transformValue(keyCode, carbonModifiers: modifiers) + guard !doubledModifiers else { return "" } + guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { return "" } + return Sauce.shared.character(by: Int(Sauce.shared.keyCode(by: key)), carbonModifiers: modifiers) ?? "" } + public var currentKeyCode: CGKeyCode { + guard !doubledModifiers else { return 0 } + return Sauce.shared.keyCode(by: key) + } + + private let key: Key // MARK: - Initialize - public init?(keyCode: Int, carbonModifiers: Int) { - if keyCode < 0 || carbonModifiers < 0 { return nil } + public convenience init?(QWERTYKeyCode: Int, carbonModifiers: Int) { + self.init(QWERTYKeyCode: QWERTYKeyCode, cocoaModifiers: carbonModifiers.convertSupportCococaModifiers()) + } - if KeyTransformer.containsFunctionKey(keyCode) { - self.modifiers = Int(UInt(carbonModifiers) | NSEvent.ModifierFlags.function.rawValue) - } else { - self.modifiers = carbonModifiers - } - self.keyCode = keyCode - self.doubledModifiers = false + public convenience init?(QWERTYKeyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) { + guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { return nil } + self.init(key: key, cocoaModifiers: cocoaModifiers) } - public init?(keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) { - if keyCode < 0 || !KeyTransformer.supportedCocoaFlags(cocoaModifiers) { return nil } + public convenience init?(key: Key, carbonModifiers: Int) { + self.init(key: key, cocoaModifiers: carbonModifiers.convertSupportCococaModifiers()) + } - if KeyTransformer.containsFunctionKey(keyCode) { - self.modifiers = Int(UInt(KeyTransformer.carbonFlags(from: cocoaModifiers)) | NSEvent.ModifierFlags.function.rawValue) - } else { - self.modifiers = KeyTransformer.carbonFlags(from: cocoaModifiers) + public init?(key: Key, cocoaModifiers: NSEvent.ModifierFlags) { + var filterdCocoaModifiers = cocoaModifiers.filterUnsupportModifiers() + // In the case of the function key, will need to add the modifier manually + if key.isFunctionKey { + filterdCocoaModifiers.insert(.function) } - self.keyCode = keyCode + guard filterdCocoaModifiers.containsSupportModifiers else { return nil } + self.modifiers = filterdCocoaModifiers.carbonModifiers(isSupportFunctionKey: true) + self.QWERTYKeyCode = Int(key.QWERTYKeyCode) self.doubledModifiers = false + self.key = key } - public init?(doubledCarbonModifiers modifiers: Int) { - if !KeyTransformer.singleCarbonFlags(modifiers) { return nil } - - self.keyCode = 0 - self.modifiers = modifiers - self.doubledModifiers = true + public convenience init?(doubledCarbonModifiers modifiers: Int) { + self.init(doubledCocoaModifiers: modifiers.convertSupportCococaModifiers()) } public init?(doubledCocoaModifiers modifiers: NSEvent.ModifierFlags) { - if !KeyTransformer.singleCocoaFlags(modifiers) { return nil } - - self.keyCode = 0 - self.modifiers = KeyTransformer.carbonFlags(from: modifiers) + guard modifiers.isSingleFlags else { return nil } + self.modifiers = modifiers.carbonModifiers() + self.QWERTYKeyCode = 0 self.doubledModifiers = true + self.key = .a } + // MARK: - NSCoping public func copy(with zone: NSZone?) -> Any { if doubledModifiers { - return KeyCombo(doubledCarbonModifiers: modifiers)! + return KeyCombo(doubledCarbonModifiers: modifiers) as Any } else { - return KeyCombo(keyCode: keyCode, carbonModifiers: modifiers)! + return KeyCombo(QWERTYKeyCode: QWERTYKeyCode, carbonModifiers: modifiers) as Any } } + // MARK: - NSCoding public init?(coder aDecoder: NSCoder) { - self.keyCode = aDecoder.decodeInteger(forKey: "keyCode") + // Changed KeyCode to QWERTYKeyCode from v3.0.0 + let containsKeyCode = aDecoder.containsValue(forKey: "keyCode") + if containsKeyCode { + self.QWERTYKeyCode = aDecoder.decodeInteger(forKey: "keyCode") + } else { + self.QWERTYKeyCode = aDecoder.decodeInteger(forKey: "QWERTYKeyCode") + } + guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { return nil } self.modifiers = aDecoder.decodeInteger(forKey: "modifiers") self.doubledModifiers = aDecoder.decodeBool(forKey: "doubledModifiers") + self.key = key } public func encode(with aCoder: NSCoder) { - aCoder.encode(keyCode, forKey: "keyCode") + aCoder.encode(QWERTYKeyCode, forKey: "QWERTYKeyCode") aCoder.encode(modifiers, forKey: "modifiers") aCoder.encode(doubledModifiers, forKey: "doubledModifiers") } + // MARK: - Codable + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.contains(.keyCode) { + // Changed KeyCode to QWERTYKeyCode from v3.0.0 + self.QWERTYKeyCode = try container.decode(Int.self, forKey: .keyCode) + } else { + self.QWERTYKeyCode = try container.decode(Int.self, forKey: .QWERTYKeyCode) + } + guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { + throw KeyCombo.InitializeError() + } + self.modifiers = try container.decode(Int.self, forKey: .modifiers) + self.doubledModifiers = try container.decode(Bool.self, forKey: .doubledModifiers) + self.key = key + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(QWERTYKeyCode, forKey: .QWERTYKeyCode) + try container.encode(modifiers, forKey: .modifiers) + try container.encode(doubledModifiers, forKey: .doubledModifiers) + } + + // MARK: - Coding Keys + private enum CodingKeys: String, CodingKey { + case keyCode + case QWERTYKeyCode + case modifiers + case doubledModifiers + } + // MARK: - Equatable public override func isEqual(_ object: Any?) -> Bool { guard let keyCombo = object as? KeyCombo else { return false } - return keyCode == keyCombo.keyCode && + return QWERTYKeyCode == keyCombo.QWERTYKeyCode && modifiers == keyCombo.modifiers && doubledModifiers == keyCombo.doubledModifiers } + +} + +// MARK: - Error +public extension KeyCombo { + struct InitializeError: Error {} } diff --git a/Lib/Magnet/KeyTransformer.swift b/Lib/Magnet/KeyTransformer.swift deleted file mode 100644 index 45babfa..0000000 --- a/Lib/Magnet/KeyTransformer.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// KeyTransformer.swift -// -// Magnet -// GitHub: https://github.com/clipy -// HP: https://clipy-app.com -// -// Copyright © 2015-2020 Clipy Project. -// - -import Cocoa -import Carbon - -public final class KeyTransformer {} - -// MARK: - Cocoa & Carbon -public extension KeyTransformer { - static func cocoaFlags(from carbonFlags: Int) -> NSEvent.ModifierFlags { - var cocoaFlags: NSEvent.ModifierFlags = NSEvent.ModifierFlags(rawValue: 0) - - if (carbonFlags & cmdKey) != 0 { - cocoaFlags.insert(.command) - } - if (carbonFlags & optionKey) != 0 { - cocoaFlags.insert(.option) - } - if (carbonFlags & controlKey) != 0 { - cocoaFlags.insert(.control) - } - if (carbonFlags & shiftKey) != 0 { - cocoaFlags.insert(.shift) - } - - return cocoaFlags - } - - static func carbonFlags(from cocoaFlags: NSEvent.ModifierFlags) -> Int { - var carbonFlags: Int = 0 - - if cocoaFlags.contains(.command) { - carbonFlags |= cmdKey - } - if cocoaFlags.contains(.option) { - carbonFlags |= optionKey - } - if cocoaFlags.contains(.control) { - carbonFlags |= controlKey - } - if cocoaFlags.contains(.shift) { - carbonFlags |= shiftKey - } - - return carbonFlags - } - - static func supportedCarbonFlags(_ carbonFlags: Int) -> Bool { - return cocoaFlags(from: carbonFlags).rawValue != 0 - } - - static func supportedCocoaFlags(_ cocoaFlogs: NSEvent.ModifierFlags) -> Bool { - return carbonFlags(from: cocoaFlogs) != 0 - } - - static func singleCarbonFlags(_ carbonFlags: Int) -> Bool { - let commandSelected = (carbonFlags & cmdKey) != 0 - let optionSelected = (carbonFlags & optionKey) != 0 - let controlSelected = (carbonFlags & controlKey) != 0 - let shiftSelected = (carbonFlags & shiftKey) != 0 - return [commandSelected, optionSelected, controlSelected, shiftSelected].trueCount == 1 - } - - static func singleCocoaFlags(_ cocoaFlags: NSEvent.ModifierFlags) -> Bool { - let commandSelected = cocoaFlags.contains(.command) - let optionSelected = cocoaFlags.contains(.option) - let controlSelected = cocoaFlags.contains(.control) - let shiftSelected = cocoaFlags.contains(.shift) - return [commandSelected, optionSelected, controlSelected, shiftSelected].trueCount == 1 - } -} - -// MARK: - Function -public extension KeyTransformer { - static func containsFunctionKey(_ keyCode: Int) -> Bool { - switch keyCode { - case kVK_F1, - kVK_F2, - kVK_F3, - kVK_F4, - kVK_F5, - kVK_F6, - kVK_F7, - kVK_F8, - kVK_F9, - kVK_F10, - kVK_F11, - kVK_F12, - kVK_F13, - kVK_F14, - kVK_F15, - kVK_F16, - kVK_F17, - kVK_F18, - kVK_F19, - kVK_F20: - return true - default: - return false - } - } -} - -// MARK: - Modifiers -public extension KeyTransformer { - static func modifiersToString(_ carbonModifiers: Int) -> [String] { - var strings = [String]() - - if (carbonModifiers & cmdKey) != 0 { - strings.append("⌘") - } - if (carbonModifiers & optionKey) != 0 { - strings.append("⌥") - } - if (carbonModifiers & controlKey) != 0 { - strings.append("⌃") - } - if (carbonModifiers & shiftKey) != 0 { - strings.append("⇧") - } - - return strings - } - - static func modifiersToString(_ cocoaModifiers: NSEvent.ModifierFlags) -> [String] { - var strings = [String]() - - if cocoaModifiers.contains(.command) { - strings.append("⌘") - } - if cocoaModifiers.contains(.option) { - strings.append("⌥") - } - if cocoaModifiers.contains(.control) { - strings.append("⌃") - } - if cocoaModifiers.contains(.shift) { - strings.append("⇧") - } - - return strings - } -}