9
9
//
10
10
11
11
#if os(macOS)
12
- import Foundation
13
12
import Carbon
13
+ import Foundation
14
14
15
- final class KeyboardLayout {
16
-
15
+ internal final class KeyboardLayout {
17
16
// MARK: - Properties
18
17
private var currentKeyboardLayoutInputSource : InputSource
19
18
private var currentASCIICapableInputSource : InputSource
20
- private var mappedKeyCodes = [ InputSource: [ Key: CGKeyCode] ] ( )
19
+ private var mappedKeyCodes = [ InputSource: [ KeyModifier : [ Key: CGKeyCode] ] ] ( )
21
20
private( set) var inputSources = [ InputSource] ( )
22
21
23
22
private let distributedNotificationCenter : DistributedNotificationCenter
@@ -40,41 +39,40 @@ final class KeyboardLayout {
40
39
distributedNotificationCenter. removeObserver ( self )
41
40
notificationCenter. removeObserver ( self )
42
41
}
43
-
44
42
}
45
43
46
44
// MARK: - KeyCodes
47
- extension KeyboardLayout {
48
- func currentKeyCodes( ) -> [ Key : CGKeyCode ] ? {
49
- return keyCodes ( with: currentKeyboardLayoutInputSource)
45
+ internal extension KeyboardLayout {
46
+ func currentKeyCodes( carbonModifiers : Int ) -> [ Key : CGKeyCode ] ? {
47
+ return keyCodes ( with: currentKeyboardLayoutInputSource, carbonModifiers : carbonModifiers )
50
48
}
51
49
52
- func currentKeyCode( for key: Key ) -> CGKeyCode ? {
53
- return keyCode ( with: currentKeyboardLayoutInputSource, key: key)
50
+ func currentKeyCode( for key: Key , carbonModifiers : Int ) -> CGKeyCode ? {
51
+ return keyCode ( with: currentKeyboardLayoutInputSource, key: key, carbonModifiers : carbonModifiers )
54
52
}
55
53
56
- func keyCodes( with source: InputSource ) -> [ Key : CGKeyCode ] ? {
57
- return mappedKeyCodes [ source]
54
+ func keyCodes( with source: InputSource , carbonModifiers : Int ) -> [ Key : CGKeyCode ] ? {
55
+ return mappedKeyCodes [ source] ? [ . init ( carbonModifiers : carbonModifiers ) ]
58
56
}
59
57
60
- func keyCode( with source: InputSource , key: Key ) -> CGKeyCode ? {
61
- return mappedKeyCodes [ source] ? [ key]
58
+ func keyCode( with source: InputSource , key: Key , carbonModifiers : Int ) -> CGKeyCode ? {
59
+ return mappedKeyCodes [ source] ? [ . init ( carbonModifiers : carbonModifiers ) ] ? [ key]
62
60
}
63
61
}
64
62
65
63
// MARK: - Key
66
- extension KeyboardLayout {
67
- func currentKey( for keyCode: Int ) -> Key ? {
68
- return key ( with: currentKeyboardLayoutInputSource, keyCode: keyCode)
64
+ internal extension KeyboardLayout {
65
+ func currentKey( for keyCode: Int , carbonModifiers : Int ) -> Key ? {
66
+ return key ( with: currentKeyboardLayoutInputSource, keyCode: keyCode, carbonModifiers : carbonModifiers )
69
67
}
70
68
71
- func key( with source: InputSource , keyCode: Int ) -> Key ? {
72
- return mappedKeyCodes [ source] ? . first ( where: { $0. value == CGKeyCode ( keyCode) } ) ? . key
69
+ func key( with source: InputSource , keyCode: Int , carbonModifiers : Int ) -> Key ? {
70
+ return mappedKeyCodes [ source] ? [ . init ( carbonModifiers : carbonModifiers ) ] ? . first ( where: { $0. value == CGKeyCode ( keyCode) } ) ? . key
73
71
}
74
72
}
75
73
76
74
// MARK: - Characters
77
- extension KeyboardLayout {
75
+ internal extension KeyboardLayout {
78
76
func currentCharacter( for keyCode: Int , carbonModifiers: Int ) -> String ? {
79
77
return character ( with: currentKeyboardLayoutInputSource, keyCode: keyCode, carbonModifiers: carbonModifiers)
80
78
}
@@ -89,7 +87,7 @@ extension KeyboardLayout {
89
87
}
90
88
91
89
// MARK: - Notifications
92
- extension KeyboardLayout {
90
+ internal extension KeyboardLayout {
93
91
private func observeNotifications( ) {
94
92
distributedNotificationCenter. addObserver ( self ,
95
93
selector: #selector( selectedKeyboardInputSourceChanged) ,
@@ -145,27 +143,57 @@ private extension KeyboardLayout {
145
143
func mappingKeyCodes( with source: InputSource ) {
146
144
guard let layoutData = TISGetInputSourceProperty ( source. source, kTISPropertyUnicodeKeyLayoutData) else { return }
147
145
let data = Unmanaged < CFData > . fromOpaque ( layoutData) . takeUnretainedValue ( ) as Data
148
- var keyCodes = [ Key: CGKeyCode] ( )
149
- for i in 0 ..< 128 {
150
- guard let character = character ( with: data, keyCode: i, carbonModifiers: 0 ) else { continue }
151
- guard let key = Key ( character: character, virtualKeyCode: i) else { continue }
152
- guard keyCodes [ key] == nil else { continue }
153
- keyCodes [ key] = CGKeyCode ( i)
146
+ var codes = [ KeyModifier: [ Key: CGKeyCode] ] ( )
147
+ KeyModifier . allCases. forEach { keyModifier in
148
+ var keyCodes = [ Key: CGKeyCode] ( )
149
+ for i in 0 ..< 128 {
150
+ guard let character = character ( with: data, keyCode: i, carbonModifiers: keyModifier. carbonModifier) else { continue }
151
+ guard let key = Key ( character: character, virtualKeyCode: i) else { continue }
152
+ guard keyCodes [ key] == nil else { continue }
153
+ keyCodes [ key] = CGKeyCode ( i)
154
+ }
155
+ codes [ keyModifier] = keyCodes
154
156
}
155
- mappedKeyCodes [ source] = keyCodes
157
+ mappedKeyCodes [ source] = codes
156
158
}
157
159
158
160
func character( with source: TISInputSource , keyCode: Int , carbonModifiers: Int ) -> String ? {
159
161
guard let layoutData = TISGetInputSourceProperty ( source, kTISPropertyUnicodeKeyLayoutData) else { return nil }
160
162
let data = Unmanaged < CFData > . fromOpaque ( layoutData) . takeUnretainedValue ( ) as Data
161
- return character ( with: data, keyCode: keyCode, carbonModifiers: carbonModifiers)
163
+ let keyModifier = KeyModifier ( carbonModifiers: carbonModifiers)
164
+ var carbonModifiers = modifierTransformer. convertCharactorSupportCarbonModifiers ( from: carbonModifiers)
165
+ switch keyModifier {
166
+ case . none:
167
+ return character ( with: data, keyCode: keyCode, carbonModifiers: carbonModifiers)
168
+ case . withCommand:
169
+ /// Determines if it's a special keyboard environment by comparing the string output with and without the ⌘ key pressed
170
+ /// For example, with a `Dvorak - QWERTY ⌘` keyboard, entering keycode `47` returns different characters depending on whether the ⌘ key pressed or not
171
+ /// ⌘ not pressed: `v`
172
+ /// ⌘ pressed: `.` (same as entering keycode `47` on a QWERTY keyboard)
173
+ let noCommandCharacter = character ( with: data, keyCode: keyCode, carbonModifiers: 0 )
174
+ let commandCharacter = character ( with: data, keyCode: keyCode, carbonModifiers: cmdKey)
175
+ guard noCommandCharacter != commandCharacter else {
176
+ /// If the outputs are the same, it's a regular keyboard, so return the string excluding the ⌘ key
177
+ return character ( with: data, keyCode: keyCode, carbonModifiers: carbonModifiers)
178
+ }
179
+ /// Workaround: To get a string with modifiers other than ⌘ key working, obtain the keycode for the standard key layout and generate the string
180
+ guard let commandCharacter,
181
+ let key = Key ( character: commandCharacter, virtualKeyCode: keyCode) ,
182
+ let keyCode = mappedKeyCodes [ . init( source: source) ] ? [ . none] ? . first ( where: { $0. key == key } ) ? . value
183
+ else {
184
+ /// If mapping is not possible, ignore modifiers other than ⌘ and return a value as close as possible to the key input
185
+ carbonModifiers |= cmdKey
186
+ return character ( with: data, keyCode: keyCode, carbonModifiers: carbonModifiers)
187
+ }
188
+ return character ( with: data, keyCode: Int ( keyCode) , carbonModifiers: carbonModifiers)
189
+ }
162
190
}
163
191
164
192
func character( with layoutData: Data , keyCode: Int , carbonModifiers: Int ) -> String ? {
165
193
// In the case of the special key code, it does not depend on the keyboard layout
166
194
if let specialKeyCode = SpecialKeyCode ( keyCode: keyCode) { return specialKeyCode. character }
167
195
168
- let modifierKeyState = ( modifierTransformer . convertCharactorSupportCarbonModifiers ( from : carbonModifiers) >> 8 ) & 0xff
196
+ let modifierKeyState = ( carbonModifiers >> 8 ) & 0xff
169
197
var deadKeyState : UInt32 = 0
170
198
let maxChars = 256
171
199
var chars = [ UniChar] ( repeating: 0 , count: maxChars)
0 commit comments