@@ -48,11 +48,14 @@ public final class IOKeyEventMonitor {
4848 notificationCenter = CFNotificationCenterGetDistributedCenter ( )
4949 let deviceMatch : CFMutableDictionary = [ kIOHIDDeviceUsageKey: usage, kIOHIDDeviceUsagePageKey: usagePage] as NSMutableDictionary
5050 IOHIDManagerSetDeviceMatching ( hidManager, deviceMatch)
51+
5152 loadMappings ( )
5253 }
5354
5455 deinit {
5556 self . saveMappings ( )
57+ stopObservingSettingsChanges ( )
58+
5659 let context = UnsafeMutableRawPointer ( Unmanaged . passUnretained ( self ) . toOpaque ( ) )
5760 IOHIDManagerRegisterInputValueCallback ( hidManager, Optional . none, context)
5861 CFNotificationCenterRemoveObserver ( notificationCenter, context, CFNotificationName ( kTISNotifySelectedKeyboardInputSourceChanged) , nil )
@@ -66,6 +69,8 @@ public final class IOKeyEventMonitor {
6669
6770 IOHIDManagerScheduleWithRunLoop ( hidManager, CFRunLoopGetMain ( ) , CFRunLoopMode . defaultMode!. rawValue)
6871 IOHIDManagerOpen ( hidManager, IOOptionBits ( kIOHIDOptionsTypeNone) )
72+
73+ startObservingSettingsChanges ( )
6974 }
7075
7176 private func observeIputSourceChangedNotification( context: UnsafeMutableRawPointer ) {
@@ -169,7 +174,12 @@ public extension IOKeyEventMonitor {
169174 }
170175
171176 func onKeyboardEvent( keyboard: String , conformsToKeyboard: Bool ? = nil ) {
172- guard lastActiveKeyboard != keyboard else { return }
177+ guard lastActiveKeyboard != keyboard else {
178+ if verbosity >= TRACE {
179+ print ( " change: ignoring event from keyboard \( keyboard) because active device hasn't changed " )
180+ }
181+ return
182+ }
173183
174184 if verbosity >= TRACE {
175185 print ( " change: keyboard changed from \( lastActiveKeyboard ?? " nil " ) to \( keyboard) " )
@@ -200,52 +210,6 @@ public extension IOKeyEventMonitor {
200210 }
201211}
202212
203- // MARK: - Persistence
204-
205- extension IOKeyEventMonitor {
206- func loadMappings( ) {
207- let selectableIsProperties = [
208- kTISPropertyInputSourceIsEnableCapable: true ,
209- kTISPropertyInputSourceCategory: kTISCategoryKeyboardInputSource ?? " " as CFString ,
210- ] as CFDictionary
211- let inputSources = TISCreateInputSourceList ( selectableIsProperties, false ) . takeUnretainedValue ( ) as! [ TISInputSource ]
212-
213- let inputSourcesById = inputSources. reduce ( [ String: TISInputSource] ( ) ) {
214- dict, inputSource -> [ String : TISInputSource ] in
215- var dict = dict
216- if let id = unmanagedStringToString ( TISGetInputSourceProperty ( inputSource, kTISPropertyInputSourceID) ) {
217- dict [ id] = inputSource
218- }
219- return dict
220- }
221-
222- if let mappings = defaults. dictionary ( forKey: MAPPINGS_DEFAULTS_KEY) {
223- for (keyboardId, inputSourceId) in mappings {
224- kb2is [ keyboardId] = inputSourcesById [ String ( describing: inputSourceId) ]
225- }
226- }
227-
228- if let enabledMappings = defaults. dictionary ( forKey: MAPPING_ENABLED_KEY) as? [ String : Bool ] {
229- deviceEnabled = enabledMappings
230- }
231- }
232-
233- func saveMappings( ) {
234- let mappings = kb2is. mapValues ( is2Id)
235- defaults. set ( mappings, forKey: MAPPINGS_DEFAULTS_KEY)
236- defaults. set ( deviceEnabled, forKey: MAPPING_ENABLED_KEY)
237- }
238-
239- public func clearAllSettings( ) {
240- kb2is. removeAll ( )
241- deviceEnabled. removeAll ( )
242- lastActiveKeyboard = nil
243- defaults. removeObject ( forKey: MAPPINGS_DEFAULTS_KEY)
244- defaults. removeObject ( forKey: MAPPING_ENABLED_KEY)
245- defaults. synchronize ( )
246- }
247- }
248-
249213// MARK: - Device Management
250214
251215public extension IOKeyEventMonitor {
@@ -304,6 +268,126 @@ public extension IOKeyEventMonitor {
304268 }
305269}
306270
271+ // MARK: - Persistence
272+
273+ extension IOKeyEventMonitor {
274+ func loadMappings( ) {
275+ let selectableIsProperties = [
276+ kTISPropertyInputSourceIsEnableCapable: true ,
277+ kTISPropertyInputSourceCategory: kTISCategoryKeyboardInputSource ?? " " as CFString ,
278+ ] as CFDictionary
279+ let inputSources = TISCreateInputSourceList ( selectableIsProperties, false ) . takeUnretainedValue ( ) as! [ TISInputSource ]
280+
281+ let inputSourcesById = inputSources. reduce ( [ String: TISInputSource] ( ) ) {
282+ dict, inputSource -> [ String : TISInputSource ] in
283+ var dict = dict
284+ if let id = unmanagedStringToString ( TISGetInputSourceProperty ( inputSource, kTISPropertyInputSourceID) ) {
285+ dict [ id] = inputSource
286+ }
287+ return dict
288+ }
289+
290+ if let mappings = defaults. dictionary ( forKey: MAPPINGS_DEFAULTS_KEY) {
291+ for (keyboardId, inputSourceId) in mappings {
292+ kb2is [ keyboardId] = inputSourcesById [ String ( describing: inputSourceId) ]
293+ }
294+ }
295+
296+ if let enabledMappings = defaults. dictionary ( forKey: MAPPING_ENABLED_KEY) as? [ String : Bool ] {
297+ deviceEnabled = enabledMappings
298+ }
299+ }
300+
301+ func saveMappings( ) {
302+ withPausedSettingsObserver {
303+ let mappings = kb2is. mapValues ( is2Id)
304+ defaults. set ( mappings, forKey: MAPPINGS_DEFAULTS_KEY)
305+ defaults. set ( deviceEnabled, forKey: MAPPING_ENABLED_KEY)
306+ defaults. synchronize ( )
307+
308+ postSettingsChangedNotification ( )
309+ }
310+
311+ if verbosity >= TRACE {
312+ print ( " Saved keyboard mappings to UserDefaults " )
313+ }
314+ }
315+
316+ public func clearAllSettings( ) {
317+ kb2is. removeAll ( )
318+ deviceEnabled. removeAll ( )
319+ lastActiveKeyboard = nil
320+ defaults. removeObject ( forKey: MAPPINGS_DEFAULTS_KEY)
321+ defaults. removeObject ( forKey: MAPPING_ENABLED_KEY)
322+ defaults. synchronize ( )
323+ }
324+ }
325+
326+ // MARK: - Settings Change Notifications
327+
328+ private extension IOKeyEventMonitor {
329+ private func startObservingSettingsChanges( ) {
330+ if verbosity >= TRACE {
331+ print ( " Starting UserDefaults observation " )
332+ }
333+
334+ let context = UnsafeMutableRawPointer ( Unmanaged . passUnretained ( self ) . toOpaque ( ) )
335+ let callback : CFNotificationCallback = { _, observer, _, _, _ in
336+ let selfPtr = Unmanaged < IOKeyEventMonitor > . fromOpaque ( observer!) . takeUnretainedValue ( )
337+ if selfPtr. verbosity >= TRACE {
338+ print ( " Received settings change notification " )
339+ }
340+ selfPtr. onSettingsChanged ( )
341+ }
342+
343+ CFNotificationCenterAddObserver (
344+ notificationCenter,
345+ context,
346+ callback,
347+ " com.autokbisw.settingsChanged " as CFString ,
348+ nil ,
349+ . deliverImmediately
350+ )
351+ }
352+
353+ private func stopObservingSettingsChanges( ) {
354+ let context = UnsafeMutableRawPointer ( Unmanaged . passUnretained ( self ) . toOpaque ( ) )
355+ CFNotificationCenterRemoveObserver (
356+ notificationCenter,
357+ context,
358+ CFNotificationName ( " com.autokbisw.settingsChanged " as CFString ) ,
359+ nil
360+ )
361+ }
362+
363+ private func postSettingsChangedNotification( ) {
364+ if verbosity >= TRACE {
365+ print ( " Posting settings changed notification " )
366+ }
367+
368+ CFNotificationCenterPostNotification (
369+ notificationCenter,
370+ CFNotificationName ( " com.autokbisw.settingsChanged " as CFString ) ,
371+ nil ,
372+ nil ,
373+ true
374+ )
375+ }
376+
377+ private func withPausedSettingsObserver< T> ( _ operation: ( ) -> T ) -> T {
378+ stopObservingSettingsChanges ( )
379+ defer { startObservingSettingsChanges ( ) }
380+ return operation ( )
381+ }
382+
383+ private func onSettingsChanged( ) {
384+ loadMappings ( )
385+ if verbosity >= TRACE {
386+ print ( " Reloaded mappings due to UserDefaults change " )
387+ }
388+ }
389+ }
390+
307391// MARK: - Utilities
308392
309393extension IOKeyEventMonitor {
0 commit comments