Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
62 changes: 54 additions & 8 deletions Sources/KeyboardShortcuts/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ public enum KeyboardShortcuts {

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

public static var customDefaults: UserDefaults? {
get {
_customDefaults
}

set {
_customDefaults = newValue
}
}

static var _customDefaults: UserDefaults? {
didSet {
userDefaults = _customDefaults ?? .standard
}
}

static var userDefaults: UserDefaults = .standard

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

static var allNames: Set<Name> {
UserDefaults.standard.dictionaryRepresentation()
KeyboardShortcuts.userDefaults.dictionaryRepresentation()
.compactMap { key, _ in
guard key.hasPrefix(userDefaultsPrefix) else {
return nil
Expand Down Expand Up @@ -326,7 +345,7 @@ public enum KeyboardShortcuts {
*/
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),
let decoded = try? JSONDecoder().decode(Shortcut.self, from: data)
else {
return nil
Expand Down Expand Up @@ -410,6 +429,7 @@ public enum KeyboardShortcuts {
*/
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 +456,40 @@ public enum KeyboardShortcuts {
*/
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 {
"\(userDefaultsPrefix)\(shortcutName.rawValue)"
}


/**
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)

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 +505,7 @@ public enum KeyboardShortcuts {
}

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

Expand All @@ -468,7 +514,7 @@ public enum KeyboardShortcuts {
return
}

UserDefaults.standard.set(false, forKey: userDefaultsKey(for: name))
KeyboardShortcuts.userDefaults.set(false, forKey: userDefaultsKey(for: name))
unregister(shortcut)
userDefaultsDidChange(name: name)
}
Expand All @@ -478,13 +524,13 @@ public enum KeyboardShortcuts {
return
}

UserDefaults.standard.removeObject(forKey: userDefaultsKey(for: name))
KeyboardShortcuts.userDefaults.removeObject(forKey: userDefaultsKey(for: name))
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
}
}

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 @@
self = Character(content)
}
}

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

Check warning on line 565 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
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()

Check warning on line 573 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
init(
suite: UserDefaults,
name: KeyboardShortcuts.Name,
key: String,
_ callback: @escaping Callback
) {
self.suite = suite
self.name = name
self.key = key
self.callback = callback
}

Check warning on line 585 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
deinit {
invalidate()
}

Check warning on line 589 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
func start() {
lock.lock()

Check warning on line 592 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
guard !isObserving else {
return
}

Check warning on line 596 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
suite?.addObserver(
self,
forKeyPath: key,
options: [.new],
context: &Self.observationContext
)
isObserving = true

Check warning on line 604 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
lock.unlock()
}

func invalidate() {
lock.lock()

guard isObserving else {
return
}

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

lock.unlock()
}

override func observeValue(

Check warning on line 625 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later (block_based_kvo)
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,

Check warning on line 628 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Colon Spacing Violation: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)

Check warning on line 628 in Sources/KeyboardShortcuts/Utilities.swift

View workflow job for this annotation

GitHub Actions / lint

Discouraged Optional Collection Violation: Prefer empty collection over optional collection (discouraged_optional_collection)
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)
}
}