Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 55 additions & 8 deletions Sources/KeyboardShortcuts/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@

private static var openMenuObserver: NSObjectProtocol?
private static var closeMenuObserver: NSObjectProtocol?
private static var userDefaultsObservers = [UserDefaultsObservation]()

Check warning on line 41 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
/**
The UserDefaults instance used to store and retrieve keyboard shortcut configurations.

By default, this uses the standard UserDefaults instance. You can customize this to use a different UserDefaults instance,
for example, to store shortcuts in a specific app group or to use a custom UserDefaults instance for testing.

```swift
// Example: Using a custom UserDefaults instance
KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "com.example.appgroup")!

// Example: Using a custom UserDefaults instance for testing
KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "test")!
```

- Important: Changing this property will not migrate existing shortcuts from the previous UserDefaults instance.
- Note: All keyboard shortcut configurations are stored with the prefix "KeyboardShortcuts_" to avoid conflicts with other app data.
*/
public static var userDefaults: UserDefaults = .standard

/**
When `true`, event handlers will not be called for registered keyboard shortcuts.
Expand All @@ -59,7 +79,7 @@
}

static var allNames: Set<Name> {
UserDefaults.standard.dictionaryRepresentation()
KeyboardShortcuts.userDefaults.dictionaryRepresentation()

Check warning on line 82 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Prefer Self in Static References Violation: Use `Self` to refer to the surrounding type name (prefer_self_in_static_references)
.compactMap { key, _ in
guard key.hasPrefix(userDefaultsPrefix) else {
return nil
Expand Down Expand Up @@ -326,7 +346,7 @@
*/
public static func getShortcut(for name: Name) -> Shortcut? {
guard
let data = UserDefaults.standard.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8),
let data = KeyboardShortcuts.userDefaults.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8),

Check warning on line 349 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Prefer Self in Static References Violation: Use `Self` to refer to the surrounding type name (prefer_self_in_static_references)
let decoded = try? JSONDecoder().decode(Shortcut.self, from: data)
else {
return nil
Expand Down Expand Up @@ -410,6 +430,7 @@
*/
public static func onKeyDown(for name: Name, action: @escaping () -> Void) {
legacyKeyDownHandlers[name, default: []].append(action)
startObservingShortcut(for: name)
registerShortcutIfNeeded(for: name)
}

Expand All @@ -436,14 +457,40 @@
*/
public static func onKeyUp(for name: Name, action: @escaping () -> Void) {
legacyKeyUpHandlers[name, default: []].append(action)
startObservingShortcut(for: name)
registerShortcutIfNeeded(for: name)
}

private static let userDefaultsPrefix = "KeyboardShortcuts_"

private static func userDefaultsKey(for shortcutName: Name) -> String { "\(userDefaultsPrefix)\(shortcutName.rawValue)"
private static func userDefaultsKey(for shortcutName: Name) -> String {

Check warning on line 466 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
"\(userDefaultsPrefix)\(shortcutName.rawValue)"
}


Check warning on line 469 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
/**
Start observing UserDefaults changes for a specific shortcut name.
Only starts observation if the shortcut is not already being observed.
*/
private static func startObservingShortcut(for name: Name) {
let key = userDefaultsKey(for: name)

Check warning on line 476 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
let observation = UserDefaultsObservation(
suite: userDefaults,
name: name,
key: key
) { name, value in
if value == nil {
self.unregisterShortcutIfNeeded(for: name)
} else {
self.registerShortcutIfNeeded(for: name)
}
}

observation.start()

userDefaultsObservers.append(observation)
}

static func userDefaultsDidChange(name: Name) {
// TODO: Use proper UserDefaults observation instead of this.
NotificationCenter.default.post(name: .shortcutByNameDidChange, object: nil, userInfo: ["name": name])
Expand All @@ -459,7 +506,7 @@
}

register(shortcut)
UserDefaults.standard.set(encoded, forKey: userDefaultsKey(for: name))
KeyboardShortcuts.userDefaults.set(encoded, forKey: userDefaultsKey(for: name))

Check warning on line 509 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Prefer Self in Static References Violation: Use `Self` to refer to the surrounding type name (prefer_self_in_static_references)
userDefaultsDidChange(name: name)
}

Expand All @@ -468,7 +515,7 @@
return
}

UserDefaults.standard.set(false, forKey: userDefaultsKey(for: name))
KeyboardShortcuts.userDefaults.set(false, forKey: userDefaultsKey(for: name))

Check warning on line 518 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Prefer Self in Static References Violation: Use `Self` to refer to the surrounding type name (prefer_self_in_static_references)
unregister(shortcut)
userDefaultsDidChange(name: name)
}
Expand All @@ -478,13 +525,13 @@
return
}

UserDefaults.standard.removeObject(forKey: userDefaultsKey(for: name))
KeyboardShortcuts.userDefaults.removeObject(forKey: userDefaultsKey(for: name))

Check warning on line 528 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Prefer Self in Static References Violation: Use `Self` to refer to the surrounding type name (prefer_self_in_static_references)
unregister(shortcut)
userDefaultsDidChange(name: name)
}

static func userDefaultsContains(name: Name) -> Bool {
UserDefaults.standard.object(forKey: userDefaultsKey(for: name)) != nil
KeyboardShortcuts.userDefaults.object(forKey: userDefaultsKey(for: name)) != nil

Check warning on line 534 in Sources/KeyboardShortcuts/KeyboardShortcuts.swift

View workflow job for this annotation

GitHub Actions / lint

Prefer Self in Static References Violation: Use `Self` to refer to the surrounding type name (prefer_self_in_static_references)
}
}

Expand Down
101 changes: 101 additions & 0 deletions Sources/KeyboardShortcuts/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,104 @@ extension Character {
self = Character(content)
}
}

final class UserDefaultsObservation: NSObject {
typealias Callback = (_ name: KeyboardShortcuts.Name, _ newKeyValue: String?) -> Void

private let name: KeyboardShortcuts.Name
private let key: String
static var observationContext = 0
private weak var suite: UserDefaults?
private var isObserving = false
private let callback: Callback
private var lock = NSLock()

init(
suite: UserDefaults,
name: KeyboardShortcuts.Name,
key: String,
_ callback: @escaping Callback
) {
self.suite = suite
self.name = name
self.key = key
self.callback = callback
}

deinit {
invalidate()
}

func start() {
lock.lock()

guard !isObserving else {
return
}

suite?.addObserver(
self,
forKeyPath: key,
options: [.new],
context: &Self.observationContext
)
isObserving = true

lock.unlock()
}

func invalidate() {
lock.lock()

guard isObserving else {
return
}

suite?.removeObserver(
self,
forKeyPath: key
)
isObserving = false
suite = nil

lock.unlock()
}

override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?
) {
guard
context == &Self.observationContext
else {
super.observeValue(
forKeyPath: keyPath,
of: object,
change: change,
context: context
)
return
}

guard let selfSuite = suite else {
invalidate()
return
}

guard
selfSuite == (object as? UserDefaults),
let change
else {
return
}

guard keyPath == key else {
return
}

let encodedString = change[.newKey] as? String
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not assume String. It could be a Bool too:

Self.userDefaults.set(false, forKey: userDefaultsKey(for: name))

callback(self.name, encodedString)
}
}