Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom storage for shortcuts #139

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
160 changes: 95 additions & 65 deletions Sources/KeyboardShortcuts/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AppKit.NSMenu
Global keyboard shortcuts for your macOS app.
*/
public enum KeyboardShortcuts {
public static var storageProvider: StorageProvider?
private static var registeredShortcuts = Set<Shortcut>()

private static var legacyKeyDownHandlers = [Name: [() -> Void]]()
Expand Down Expand Up @@ -137,7 +138,11 @@ public enum KeyboardShortcuts {
guard !isInitialized else {
return
}


if storageProvider == nil {

Choose a reason for hiding this comment

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

I suggest that a 'else' statement should be added to this 'if' just in case. This will help cover border effects.

storageProvider = UserDefaultsStorage()
}

openMenuObserver = NotificationCenter.default.addObserver(forName: NSMenu.didBeginTrackingNotification, object: nil, queue: nil) { _ in
isMenuOpen = true
}
Expand Down Expand Up @@ -277,29 +282,28 @@ public enum KeyboardShortcuts {
You would usually not need this as the user would be the one setting the shortcut in a settings user-interface, but it can be useful when, for example, migrating from a different keyboard shortcuts package.
*/
public static func setShortcut(_ shortcut: Shortcut?, for name: Name) {

Choose a reason for hiding this comment

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

Adding comments about the parameters will help the understanding of this method.
This has been correctly done in 'Name.swift' 👍🏻

if let shortcut {
userDefaultsSet(name: name, shortcut: shortcut)
} else {
if name.defaultShortcut != nil {
userDefaultsDisable(name: name)
} else {
userDefaultsRemove(name: name)
}
}
if let shortcut {
storageProviderSet(name: name, shortcut: shortcut)
} else {
if name.defaultShortcut != nil {
storageProviderDisable(name: name)
} else {
storageProviderRemove(name: name)
}
}
}

/**
Get the keyboard shortcut for a name.
*/
public static func getShortcut(for name: Name) -> Shortcut? {
guard
let data = UserDefaults.standard.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Shortcut.self, from: data)
else {
return nil
}

return decoded
guard
let data = storageProvider?.get(forKey: name.rawValue)?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Shortcut.self, from: data)
else {
return nil
}
return decoded
}

private static func handleOnKeyDown(_ shortcut: Shortcut) {
Expand Down Expand Up @@ -406,53 +410,48 @@ public enum KeyboardShortcuts {
registerShortcutIfNeeded(for: name)
}

private static let userDefaultsPrefix = "KeyboardShortcuts_"

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

static func userDefaultsDidChange(name: Name) {
// TODO: Use proper UserDefaults observation instead of this.
NotificationCenter.default.post(name: .shortcutByNameDidChange, object: nil, userInfo: ["name": name])
}

static func userDefaultsSet(name: Name, shortcut: Shortcut) {
guard let encoded = try? JSONEncoder().encode(shortcut).toString else {
return
}

if let oldShortcut = getShortcut(for: name) {
unregister(oldShortcut)
}

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

static func userDefaultsDisable(name: Name) {
guard let shortcut = getShortcut(for: name) else {
return
}

UserDefaults.standard.set(false, forKey: userDefaultsKey(for: name))
unregister(shortcut)
userDefaultsDidChange(name: name)
}

static func userDefaultsRemove(name: Name) {
guard let shortcut = getShortcut(for: name) else {
return
}

UserDefaults.standard.removeObject(forKey: userDefaultsKey(for: name))
unregister(shortcut)
userDefaultsDidChange(name: name)
}

static func userDefaultsContains(name: Name) -> Bool {
UserDefaults.standard.object(forKey: userDefaultsKey(for: name)) != nil
}
static func shortcutDidChange(name: Name) {
NotificationCenter.default.post(name: .shortcutByNameDidChange, object: nil, userInfo: ["name": name])
}

static func storageProviderSet(name: Name, shortcut: Shortcut) {
guard let encoded = try? JSONEncoder().encode(shortcut).toString else {
return
}

if let oldShortcut = getShortcut(for: name) {
unregister(oldShortcut)
}

register(shortcut)
storageProvider?.set(encoded, forKey: name.rawValue)
shortcutDidChange(name: name)
}

static func storageProviderDisable(name: Name) {
guard let shortcut = getShortcut(for: name) else {
return
}

storageProvider?.set(nil, forKey: name.rawValue)
unregister(shortcut)
shortcutDidChange(name: name)
}

static func storageProviderRemove(name: Name) {
guard let shortcut = getShortcut(for: name) else {
return
}

storageProvider?.remove(forKey: name.rawValue)
unregister(shortcut)
shortcutDidChange(name: name)
}

static func storageProviderContains(name: Name) -> Bool {
return storageProvider?.get(forKey: name.rawValue) != nil
}

}

extension KeyboardShortcuts {
Expand Down Expand Up @@ -589,3 +588,34 @@ extension KeyboardShortcuts {
extension Notification.Name {
static let shortcutByNameDidChange = Self("KeyboardShortcuts_shortcutByNameDidChange")
}

class UserDefaultsStorage: StorageProvider {
private static let userDefaultsPrefix = "KeyboardShortcuts_"
private static func userDefaultsKey(for shortcutName: String) -> String { "\(userDefaultsPrefix)\(shortcutName)"}

func set(_ value: String?, forKey defaultName: String) {
UserDefaults.standard.set(value, forKey: Self.userDefaultsKey(for: defaultName))
}

func remove(forKey defaultName: String) {
UserDefaults.standard.removeObject(forKey: Self.userDefaultsKey(for: defaultName))
}

func disable(forKey defaultName: String) {
UserDefaults.standard.set(false, forKey: Self.userDefaultsKey(for: defaultName))
}

func get(forKey defaultName: String) -> String? {
guard
let data = UserDefaults.standard.string(forKey: Self.userDefaultsKey(for: defaultName))
else {
return nil
}
return data
}

func contains(forKey defaultName: String) -> Bool {
return UserDefaults.standard.object(forKey: Self.userDefaultsKey(for: defaultName)) != nil
}

}
22 changes: 12 additions & 10 deletions Sources/KeyboardShortcuts/Name.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ extension KeyboardShortcuts {
- Parameter name: Name of the shortcut.
- Parameter default: Optional default key combination. Do not set this unless it's essential. Users find it annoying when random apps steal their existing keyboard shortcuts. It's generally better to show a welcome screen on the first app launch that lets the user set the shortcut.
*/
public init(_ name: String, default initialShortcut: Shortcut? = nil) {
public init(_ name: String, default initialShortcut: Shortcut? = nil) {
self.rawValue = name
self.defaultShortcut = initialShortcut

if
let initialShortcut,
!userDefaultsContains(name: self)
{
setShortcut(initialShortcut, for: self)
}

KeyboardShortcuts.initialize()
if
let initialShortcut,
!storageProviderContains(name: self)
{
setShortcut(initialShortcut, for: self)
}

KeyboardShortcuts.initialize()
}
}
}
Expand All @@ -56,3 +56,5 @@ extension KeyboardShortcuts.Name: RawRepresentable {
self.init(rawValue)
}
}


9 changes: 9 additions & 0 deletions Sources/KeyboardShortcuts/Storage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

public protocol StorageProvider {
func get(forKey defaultName: String) -> String?
mutating func set(_ value: String?, forKey defaultName: String)
mutating func disable(forKey defaultName: String)
mutating func remove(forKey defaultName: String)
func contains(forKey defaultName: String) -> Bool
}