diff --git a/.github/workflows/Swift-Build.yml b/.github/workflows/Swift-Build.yml index 8d8559b..db622b1 100644 --- a/.github/workflows/Swift-Build.yml +++ b/.github/workflows/Swift-Build.yml @@ -5,11 +5,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-12, macos-13] - xcode: ['14.2', '15.2'] + os: [macos-13, macos-14] + xcode: ['15.2', '16.2'] exclude: - - os: macos-12 - xcode: '15.2' + - os: macos-13 + xcode: '16.2' env: DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" steps: diff --git a/.github/workflows/Xcode-Build.yml b/.github/workflows/Xcode-Build.yml index 232489c..b4f4999 100644 --- a/.github/workflows/Xcode-Build.yml +++ b/.github/workflows/Xcode-Build.yml @@ -5,11 +5,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-12, macos-13] - xcode: ['14.2', '15.2'] + os: [macos-13, macos-14] + xcode: ['15.2', '16.2'] exclude: - - os: macos-12 - xcode: '15.2' + - os: macos-13 + xcode: '16.2' env: DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" steps: @@ -22,6 +22,7 @@ jobs: -scheme "$SCHEME" \ -sdk "$SDK" \ -configuration Debug \ + -parallel-testing-enabled NO \ ENABLE_TESTABILITY=YES | xcpretty -c; env: PROJECT: Sauce.xcworkspace diff --git a/Lib/Sauce.xcodeproj/project.pbxproj b/Lib/Sauce.xcodeproj/project.pbxproj index 57af1ee..9226232 100644 --- a/Lib/Sauce.xcodeproj/project.pbxproj +++ b/Lib/Sauce.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 09CDC68A210F044A007DDFE4 /* TISInputSource+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */; }; 50582477261C6F1F00AD2DD8 /* NSMenuItem+Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50582476261C6F1F00AD2DD8 /* NSMenuItem+Key.swift */; }; 5058247B261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5058247A261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift */; }; + C5FADB892D32779F00F038E3 /* KeyModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FADB882D32779B00F038E3 /* KeyModifier.swift */; }; FA1E407621107B0A0016D710 /* SpecialKeyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */; }; FAA4132A210E2C730097D522 /* Sauce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAA41320210E2C730097D522 /* Sauce.framework */; }; FAA41331210E2C730097D522 /* Sauce.h in Headers */ = {isa = PBXBuildFile; fileRef = FAA41323210E2C730097D522 /* Sauce.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -37,6 +38,7 @@ 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TISInputSource+Property.swift"; sourceTree = ""; }; 50582476261C6F1F00AD2DD8 /* NSMenuItem+Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Key.swift"; sourceTree = ""; }; 5058247A261C6FA400AD2DD8 /* NSMenuItem+KeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+KeyTests.swift"; sourceTree = ""; }; + C5FADB882D32779B00F038E3 /* KeyModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyModifier.swift; sourceTree = ""; }; FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialKeyCode.swift; sourceTree = ""; }; FAA41320210E2C730097D522 /* Sauce.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sauce.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FAA41323210E2C730097D522 /* Sauce.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Sauce.h; sourceTree = ""; }; @@ -68,6 +70,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C5FADB8C2D335E1300F038E3 /* Internal */ = { + isa = PBXGroup; + children = ( + FAA41368210E33060097D522 /* KeyboardLayout.swift */, + C5FADB882D32779B00F038E3 /* KeyModifier.swift */, + FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */, + 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */, + ); + path = Internal; + sourceTree = ""; + }; FAA41316210E2C730097D522 = { isa = PBXGroup; children = ( @@ -89,14 +102,12 @@ FAA41322210E2C730097D522 /* Sauce */ = { isa = PBXGroup; children = ( + C5FADB8C2D335E1300F038E3 /* Internal */, FAA41323210E2C730097D522 /* Sauce.h */, FAA41324210E2C730097D522 /* Info.plist */, FAA41364210E2D9B0097D522 /* Sauce.swift */, FAA4136A210E33CB0097D522 /* InputSource.swift */, - FAA41368210E33060097D522 /* KeyboardLayout.swift */, - 09CDC689210F044A007DDFE4 /* TISInputSource+Property.swift */, FAA41366210E2EFB0097D522 /* Key.swift */, - FA1E407521107B0A0016D710 /* SpecialKeyCode.swift */, 095EF0002456ED9A00174829 /* ModifierTransformer.swift */, 50582476261C6F1F00AD2DD8 /* NSMenuItem+Key.swift */, ); @@ -230,6 +241,7 @@ FAA41365210E2D9B0097D522 /* Sauce.swift in Sources */, FA1E407621107B0A0016D710 /* SpecialKeyCode.swift in Sources */, FAA41367210E2EFB0097D522 /* Key.swift in Sources */, + C5FADB892D32779F00F038E3 /* KeyModifier.swift in Sources */, 095EF0012456ED9A00174829 /* ModifierTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Lib/Sauce.xcodeproj/xcshareddata/xcschemes/Sauce.xcscheme b/Lib/Sauce.xcodeproj/xcshareddata/xcschemes/Sauce.xcscheme index 846c239..dcc6a43 100644 --- a/Lib/Sauce.xcodeproj/xcshareddata/xcschemes/Sauce.xcscheme +++ b/Lib/Sauce.xcodeproj/xcshareddata/xcschemes/Sauce.xcscheme @@ -38,7 +38,8 @@ + skipped = "NO" + parallelizable = "NO"> [Key: CGKeyCode]? { - return keyCodes(with: currentKeyboardLayoutInputSource) +internal extension KeyboardLayout { + func currentKeyCodes(carbonModifiers: Int) -> [Key: CGKeyCode]? { + return keyCodes(with: currentKeyboardLayoutInputSource, carbonModifiers: carbonModifiers) } - func currentKeyCode(for key: Key) -> CGKeyCode? { - return keyCode(with: currentKeyboardLayoutInputSource, key: key) + func currentKeyCode(for key: Key, carbonModifiers: Int) -> CGKeyCode? { + return keyCode(with: currentKeyboardLayoutInputSource, key: key, carbonModifiers: carbonModifiers) } - func keyCodes(with source: InputSource) -> [Key: CGKeyCode]? { - return mappedKeyCodes[source] + func keyCodes(with source: InputSource, carbonModifiers: Int) -> [Key: CGKeyCode]? { + return mappedKeyCodes[source]?[.init(carbonModifiers: carbonModifiers)] } - func keyCode(with source: InputSource, key: Key) -> CGKeyCode? { - return mappedKeyCodes[source]?[key] + func keyCode(with source: InputSource, key: Key, carbonModifiers: Int) -> CGKeyCode? { + return mappedKeyCodes[source]?[.init(carbonModifiers: carbonModifiers)]?[key] } } // MARK: - Key -extension KeyboardLayout { - func currentKey(for keyCode: Int) -> Key? { - return key(with: currentKeyboardLayoutInputSource, keyCode: keyCode) +internal extension KeyboardLayout { + func currentKey(for keyCode: Int, carbonModifiers: Int) -> Key? { + return key(with: currentKeyboardLayoutInputSource, keyCode: keyCode, carbonModifiers: carbonModifiers) } - func key(with source: InputSource, keyCode: Int) -> Key? { - return mappedKeyCodes[source]?.first(where: { $0.value == CGKeyCode(keyCode) })?.key + func key(with source: InputSource, keyCode: Int, carbonModifiers: Int) -> Key? { + return mappedKeyCodes[source]?[.init(carbonModifiers: carbonModifiers)]?.first(where: { $0.value == CGKeyCode(keyCode) })?.key } } // MARK: - Characters -extension KeyboardLayout { +internal extension KeyboardLayout { func currentCharacter(for keyCode: Int, carbonModifiers: Int) -> String? { return character(with: currentKeyboardLayoutInputSource, keyCode: keyCode, carbonModifiers: carbonModifiers) } @@ -89,7 +87,7 @@ extension KeyboardLayout { } // MARK: - Notifications -extension KeyboardLayout { +internal extension KeyboardLayout { private func observeNotifications() { distributedNotificationCenter.addObserver(self, selector: #selector(selectedKeyboardInputSourceChanged), @@ -145,27 +143,57 @@ private extension KeyboardLayout { func mappingKeyCodes(with source: InputSource) { guard let layoutData = TISGetInputSourceProperty(source.source, kTISPropertyUnicodeKeyLayoutData) else { return } let data = Unmanaged.fromOpaque(layoutData).takeUnretainedValue() as Data - var keyCodes = [Key: CGKeyCode]() - for i in 0..<128 { - guard let character = character(with: data, keyCode: i, carbonModifiers: 0) else { continue } - guard let key = Key(character: character, virtualKeyCode: i) else { continue } - guard keyCodes[key] == nil else { continue } - keyCodes[key] = CGKeyCode(i) + var codes = [KeyModifier: [Key: CGKeyCode]]() + KeyModifier.allCases.forEach { keyModifier in + var keyCodes = [Key: CGKeyCode]() + for i in 0..<128 { + guard let character = character(with: data, keyCode: i, carbonModifiers: keyModifier.carbonModifier) else { continue } + guard let key = Key(character: character, virtualKeyCode: i) else { continue } + guard keyCodes[key] == nil else { continue } + keyCodes[key] = CGKeyCode(i) + } + codes[keyModifier] = keyCodes } - mappedKeyCodes[source] = keyCodes + mappedKeyCodes[source] = codes } func character(with source: TISInputSource, keyCode: Int, carbonModifiers: Int) -> String? { guard let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { return nil } let data = Unmanaged.fromOpaque(layoutData).takeUnretainedValue() as Data - return character(with: data, keyCode: keyCode, carbonModifiers: carbonModifiers) + let keyModifier = KeyModifier(carbonModifiers: carbonModifiers) + var carbonModifiers = modifierTransformer.convertCharactorSupportCarbonModifiers(from: carbonModifiers) + switch keyModifier { + case .none: + return character(with: data, keyCode: keyCode, carbonModifiers: carbonModifiers) + case .withCommand: + /// Determines if it's a special keyboard environment by comparing the string output with and without the ⌘ key pressed + /// For example, with a `Dvorak - QWERTY ⌘` keyboard, entering keycode `47` returns different characters depending on whether the ⌘ key pressed or not + /// ⌘ not pressed: `v` + /// ⌘ pressed: `.` (same as entering keycode `47` on a QWERTY keyboard) + let noCommandCharacter = character(with: data, keyCode: keyCode, carbonModifiers: 0) + let commandCharacter = character(with: data, keyCode: keyCode, carbonModifiers: cmdKey) + guard noCommandCharacter != commandCharacter else { + /// If the outputs are the same, it's a regular keyboard, so return the string excluding the ⌘ key + return character(with: data, keyCode: keyCode, carbonModifiers: carbonModifiers) + } + /// Workaround: To get a string with modifiers other than ⌘ key working, obtain the keycode for the standard key layout and generate the string + guard let commandCharacter, + let key = Key(character: commandCharacter, virtualKeyCode: keyCode), + let keyCode = mappedKeyCodes[.init(source: source)]?[.none]?.first(where: { $0.key == key })?.value + else { + /// If mapping is not possible, ignore modifiers other than ⌘ and return a value as close as possible to the key input + carbonModifiers |= cmdKey + return character(with: data, keyCode: keyCode, carbonModifiers: carbonModifiers) + } + return character(with: data, keyCode: Int(keyCode), carbonModifiers: carbonModifiers) + } } func character(with layoutData: Data, keyCode: Int, carbonModifiers: Int) -> String? { // In the case of the special key code, it does not depend on the keyboard layout if let specialKeyCode = SpecialKeyCode(keyCode: keyCode) { return specialKeyCode.character } - let modifierKeyState = (modifierTransformer.convertCharactorSupportCarbonModifiers(from: carbonModifiers) >> 8) & 0xff + let modifierKeyState = (carbonModifiers >> 8) & 0xff var deadKeyState: UInt32 = 0 let maxChars = 256 var chars = [UniChar](repeating: 0, count: maxChars) diff --git a/Lib/Sauce/SpecialKeyCode.swift b/Lib/Sauce/Internal/SpecialKeyCode.swift similarity index 99% rename from Lib/Sauce/SpecialKeyCode.swift rename to Lib/Sauce/Internal/SpecialKeyCode.swift index e08a199..9221171 100644 --- a/Lib/Sauce/SpecialKeyCode.swift +++ b/Lib/Sauce/Internal/SpecialKeyCode.swift @@ -9,8 +9,8 @@ // #if os(macOS) -import Foundation import Carbon +import Foundation // swiftlint:disable identifier_name function_body_length @@ -20,7 +20,7 @@ import Carbon * * UCKeyTranslate can not convert a layout-independent keycode to string. **/ -enum SpecialKeyCode { +internal enum SpecialKeyCode { case `return` case tab case space diff --git a/Lib/Sauce/TISInputSource+Property.swift b/Lib/Sauce/Internal/TISInputSource+Property.swift similarity index 100% rename from Lib/Sauce/TISInputSource+Property.swift rename to Lib/Sauce/Internal/TISInputSource+Property.swift index 953b77d..ce8cf7f 100644 --- a/Lib/Sauce/TISInputSource+Property.swift +++ b/Lib/Sauce/Internal/TISInputSource+Property.swift @@ -9,8 +9,8 @@ // #if os(macOS) -import Foundation import Carbon +import Foundation extension TISInputSource { func value(forProperty propertyKey: CFString, type: T.Type) -> T? { diff --git a/Lib/Sauce/Key.swift b/Lib/Sauce/Key.swift index 4aa20a5..24b3208 100644 --- a/Lib/Sauce/Key.swift +++ b/Lib/Sauce/Key.swift @@ -9,8 +9,8 @@ // #if os(macOS) -import Foundation import Carbon +import Foundation // swiftlint:disable file_length function_body_length type_body_length identifier_name public enum Key: String, Codable, Equatable, Sendable { @@ -560,6 +560,5 @@ public enum Key: String, Codable, Equatable, Sendable { case .section: return CGKeyCode(kVK_ISO_Section) } } - } #endif diff --git a/Lib/Sauce/ModifierTransformer.swift b/Lib/Sauce/ModifierTransformer.swift index cb26423..a5b7d5e 100644 --- a/Lib/Sauce/ModifierTransformer.swift +++ b/Lib/Sauce/ModifierTransformer.swift @@ -9,9 +9,9 @@ // #if os(macOS) -import Foundation -import Carbon import AppKit +import Carbon +import Foundation open class ModifierTransformer {} diff --git a/Lib/Sauce/Sauce.swift b/Lib/Sauce/Sauce.swift index 38ec3ad..6844647 100644 --- a/Lib/Sauce/Sauce.swift +++ b/Lib/Sauce/Sauce.swift @@ -9,17 +9,17 @@ // #if os(macOS) -import Foundation import AppKit +import Carbon +import Foundation -extension NSNotification.Name { +public extension NSNotification.Name { static let SauceSelectedKeyboardInputSourceChanged = Notification.Name("SauceSelectedKeyboardInputSourceChanged") static let SauceEnabledKeyboardInputSourcesChanged = Notification.Name("SauceEnabledKeyboardInputSourcesChanged") static let SauceSelectedKeyboardKeyCodesChanged = Notification.Name("SauceSelectedKeyboardKeyCodesChanged") } open class Sauce { - // MARK: - Properties public static let shared = Sauce() @@ -31,7 +31,6 @@ open class Sauce { self.layout = layout self.modifierTransformar = modifierTransformar } - } // MARK: - Input Sources @@ -43,39 +42,71 @@ extension Sauce { // MARK: - KeyCodes extension Sauce { - public func keyCode(for key: Key) -> CGKeyCode { - return currentKeyCode(for: key) ?? key.QWERTYKeyCode + public func keyCode(for key: Key, carbonModifiers: Int = 0) -> CGKeyCode { + return currentKeyCode(for: key, carbonModifiers: carbonModifiers) ?? key.QWERTYKeyCode + } + + public func keyCode(for key: Key, cocoaModifiers: NSEvent.ModifierFlags) -> CGKeyCode { + return keyCode(for: key, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) + } + + public func currentKeyCode(for key: Key, carbonModifiers: Int = 0) -> CGKeyCode? { + return layout.currentKeyCode(for: key, carbonModifiers: carbonModifiers) + } + + public func currentKeyCode(for key: Key, cocoaModifiers: NSEvent.ModifierFlags) -> CGKeyCode? { + return currentKeyCode(for: key, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) } - public func currentKeyCode(for key: Key) -> CGKeyCode? { - return layout.currentKeyCode(for: key) + public func currentKeyCodes(carbonModifiers: Int = 0) -> [Key: CGKeyCode]? { + return layout.currentKeyCodes(carbonModifiers: carbonModifiers) } - public func currentKeyCodes() -> [Key: CGKeyCode]? { - return layout.currentKeyCodes() + public func currentKeyCodes(cocoaModifiers: NSEvent.ModifierFlags) -> [Key: CGKeyCode]? { + return currentKeyCodes(carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) } - public func keyCode(with source: InputSource, key: Key) -> CGKeyCode? { - return layout.keyCode(with: source, key: key) + public func keyCode(with source: InputSource, key: Key, carbonModifiers: Int = 0) -> CGKeyCode? { + return layout.keyCode(with: source, key: key, carbonModifiers: carbonModifiers) } - public func keyCodes(with source: InputSource) -> [Key: CGKeyCode]? { - return layout.keyCodes(with: source) + public func keyCode(with source: InputSource, key: Key, cocoaModifiers: NSEvent.ModifierFlags) -> CGKeyCode? { + return keyCode(with: source, key: key, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) + } + + public func keyCodes(with source: InputSource, carbonModifiers: Int = 0) -> [Key: CGKeyCode]? { + return layout.keyCodes(with: source, carbonModifiers: carbonModifiers) + } + + public func keyCodes(with source: InputSource, cocoaModifiers: NSEvent.ModifierFlags) -> [Key: CGKeyCode]? { + return keyCodes(with: source, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) } } // MARK: - Key extension Sauce { - public func key(for keyCode: Int) -> Key? { - return currentKey(for: keyCode) ?? Key(QWERTYKeyCode: keyCode) + public func key(for keyCode: Int, carbonModifiers: Int = 0) -> Key? { + return currentKey(for: keyCode, carbonModifiers: carbonModifiers) ?? Key(QWERTYKeyCode: keyCode) + } + + public func key(for keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> Key? { + return key(for: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) + } + + public func currentKey(for keyCode: Int, carbonModifiers: Int = 0) -> Key? { + return layout.currentKey(for: keyCode, carbonModifiers: carbonModifiers) + } + + public func currentKey(for keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> Key? { + return currentKey(for: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) } - public func currentKey(for keyCode: Int) -> Key? { - return layout.currentKey(for: keyCode) + public func key(with source: InputSource, keyCode: Int, carbonModifiers: Int = 0) -> Key? { + return layout.key(with: source, keyCode: keyCode, carbonModifiers: carbonModifiers) } - public func key(with source: InputSource, keyCode: Int) -> Key? { - return layout.key(with: source, keyCode: keyCode) + public func key(with source: InputSource, keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> Key? { + return key(with: source, keyCode: keyCode, carbonModifiers: modifierTransformar.carbonFlags(from: cocoaModifiers)) } } diff --git a/Lib/SauceTests/KeyboardLayoutTests.swift b/Lib/SauceTests/KeyboardLayoutTests.swift index 9196e69..7ac9050 100644 --- a/Lib/SauceTests/KeyboardLayoutTests.swift +++ b/Lib/SauceTests/KeyboardLayoutTests.swift @@ -18,6 +18,7 @@ final class KeyboardLayoutTests: XCTestCase { // MARK: - Properties private let ABCKeyboardID = "com.apple.keylayout.ABC" private let dvorakKeyboardID = "com.apple.keylayout.Dvorak" + private let dvorakQWERTYKeyboardID = "com.apple.keylayout.DVORAK-QWERTYCMD" private var japaneseKeyboardID: String { if #available(macOS 11.0, *) { return "com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese" @@ -36,6 +37,20 @@ final class KeyboardLayoutTests: XCTestCase { private let QWERTYVKeyCode = 9 private let DvorakVKeyCode = 47 // swiftlint:disable:this identifier_name + private static var selectedInputSource: InputSource? + + override static func setUp() { + super.setUp() + selectedInputSource = InputSource(source: TISCopyCurrentKeyboardLayoutInputSource().takeUnretainedValue()) + } + + override static func tearDown() { + super.tearDown() + guard let selectedInputSource else { return } + TISEnableInputSource(selectedInputSource.source) + TISSelectInputSource(selectedInputSource.source) + } + // MARK: - Tests func testKeyCodesForABCKeyboard() { let isInstalledABCKeyboard = isInstalledInputSource(id: ABCKeyboardID) @@ -43,9 +58,9 @@ final class KeyboardLayoutTests: XCTestCase { XCTAssertTrue(selectInputSource(id: ABCKeyboardID)) let notificationCenter = NotificationCenter() let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter) - let vKeyCode = keyboardLayout.currentKeyCode(for: .v) + let vKeyCode = keyboardLayout.currentKeyCode(for: .v, carbonModifiers: 0) XCTAssertEqual(vKeyCode, CGKeyCode(QWERTYVKeyCode)) - let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode) + let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode, carbonModifiers: 0) XCTAssertEqual(vKey, .v) let vCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: 0) XCTAssertEqual(vCharacter, "v") @@ -59,15 +74,37 @@ final class KeyboardLayoutTests: XCTestCase { uninstallInputSource(id: ABCKeyboardID) } + func testKeyCodesForABCKeyboardWithCommandModifier() { + let isInstalledABCKeyboard = isInstalledInputSource(id: ABCKeyboardID) + XCTAssertTrue(installInputSource(id: ABCKeyboardID)) + XCTAssertTrue(selectInputSource(id: ABCKeyboardID)) + let notificationCenter = NotificationCenter() + let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter) + let vKeyCode = keyboardLayout.currentKeyCode(for: .v, carbonModifiers: cmdKey) + XCTAssertEqual(vKeyCode, CGKeyCode(QWERTYVKeyCode)) + let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode, carbonModifiers: cmdKey) + XCTAssertEqual(vKey, .v) + let vCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: cmdKey) + XCTAssertEqual(vCharacter, "v") + let vShiftCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.shift, .command])) + XCTAssertEqual(vShiftCharacter, "V") + let vOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.option, .command])) + XCTAssertEqual(vOptionCharacter, "√") + let vShiftOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.shift, .option, .command])) + XCTAssertEqual(vShiftOptionCharacter, "◊") + guard !isInstalledABCKeyboard else { return } + uninstallInputSource(id: ABCKeyboardID) + } + func testKeyCodesForDvorakKeyboard() { let isInstalledDvorakKeyboard = isInstalledInputSource(id: dvorakKeyboardID) XCTAssertTrue(installInputSource(id: dvorakKeyboardID)) XCTAssertTrue(selectInputSource(id: dvorakKeyboardID)) let notificationCenter = NotificationCenter() let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter) - let vKeyCode = keyboardLayout.currentKeyCode(for: .v) + let vKeyCode = keyboardLayout.currentKeyCode(for: .v, carbonModifiers: 0) XCTAssertEqual(vKeyCode, CGKeyCode(DvorakVKeyCode)) - let vKey = keyboardLayout.currentKey(for: DvorakVKeyCode) + let vKey = keyboardLayout.currentKey(for: DvorakVKeyCode, carbonModifiers: 0) XCTAssertEqual(vKey, .v) let vCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: 0) XCTAssertEqual(vCharacter, "v") @@ -81,6 +118,40 @@ final class KeyboardLayoutTests: XCTestCase { uninstallInputSource(id: dvorakKeyboardID) } + /*func testKeyCodesForDvorakQWERTYKeyboard() { + let isInstalledDvorakQWERTYKeyboard = isInstalledInputSource(id: dvorakQWERTYKeyboardID) + XCTAssertTrue(installInputSource(id: dvorakQWERTYKeyboardID)) + XCTAssertTrue(selectInputSource(id: dvorakQWERTYKeyboardID)) + let notificationCenter = NotificationCenter() + let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter) + let vKeyCode = keyboardLayout.currentKeyCode(for: .v, carbonModifiers: 0) + let vCommandKeyCode = keyboardLayout.currentKeyCode(for: .v, carbonModifiers: cmdKey) + XCTAssertEqual(vKeyCode, CGKeyCode(DvorakVKeyCode)) + XCTAssertEqual(vCommandKeyCode, CGKeyCode(QWERTYVKeyCode)) + let vKey = keyboardLayout.currentKey(for: DvorakVKeyCode, carbonModifiers: 0) + let vCommandKey = keyboardLayout.currentKey(for: QWERTYVKeyCode, carbonModifiers: cmdKey) + XCTAssertEqual(vKey, .v) + XCTAssertEqual(vCommandKey, .v) + let vCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: 0) + let vCommandCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: cmdKey) + XCTAssertEqual(vCharacter, "v") + XCTAssertEqual(vCommandCharacter, "v") + let vShiftCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: .shift)) + let vCommandShiftCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.command, .shift])) + XCTAssertEqual(vShiftCharacter, "V") + XCTAssertEqual(vCommandShiftCharacter, "V") + let vOptionCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.option])) + let vCommandOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.command, .option])) + XCTAssertEqual(vOptionCharacter, "√") + XCTAssertEqual(vCommandOptionCharacter, "√") + let vShiftOptionCharacter = keyboardLayout.currentCharacter(for: DvorakVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.shift, .option])) + let vCommandShiftOptionCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: modifierTransformer.carbonFlags(from: [.command, .shift, .option])) + XCTAssertEqual(vShiftOptionCharacter, "◊") + XCTAssertEqual(vCommandShiftOptionCharacter, "◊") + guard !isInstalledDvorakQWERTYKeyboard else { return } + uninstallInputSource(id: dvorakQWERTYKeyboardID) + }*/ + func testKeyCodesJapanesesAndDvorakOnlyKeyboard() { let installedInputSources = fetchInputSource(includeAllInstalled: false) let isInstalledJapaneseKeyboard = isInstalledInputSource(id: japaneseKeyboardID) @@ -97,9 +168,9 @@ final class KeyboardLayoutTests: XCTestCase { .forEach { uninstallInputSource(id: $0.id) } let notificationCenter = NotificationCenter() let keyboardLayout = KeyboardLayout(notificationCenter: notificationCenter) - let vKeyCode = keyboardLayout.currentKeyCode(for: .v) + let vKeyCode = keyboardLayout.currentKeyCode(for: .v, carbonModifiers: 0) XCTAssertEqual(vKeyCode, CGKeyCode(QWERTYVKeyCode)) - let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode) + let vKey = keyboardLayout.currentKey(for: QWERTYVKeyCode, carbonModifiers: 0) XCTAssertEqual(vKey, .v) let vCharacter = keyboardLayout.currentCharacter(for: QWERTYVKeyCode, carbonModifiers: 0) XCTAssertEqual(vCharacter, "v")