Skip to content

Commit

Permalink
Add modern listener method
Browse files Browse the repository at this point in the history
Fixes #46
  • Loading branch information
sindresorhus committed Mar 30, 2022
1 parent 7e251f3 commit 0dcedd5
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 43 deletions.
9 changes: 5 additions & 4 deletions Example/KeyboardShortcutsExample/MainScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,6 @@ private struct DoubleShortcut: View {
isPressed1 = true
}

KeyboardShortcuts.onKeyUp(for: .testShortcut1) {
isPressed1 = false
}

KeyboardShortcuts.onKeyDown(for: .testShortcut2) {
isPressed2 = true
}
Expand All @@ -117,6 +113,11 @@ private struct DoubleShortcut: View {
isPressed2 = false
}
}
.task {
for await _ in KeyboardShortcuts.on(.keyUp, for: .testShortcut1) {
isPressed1 = false
}
}
}
}

Expand Down
192 changes: 154 additions & 38 deletions Sources/KeyboardShortcuts/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,31 @@ public enum KeyboardShortcuts {

private static var registeredShortcuts = Set<Shortcut>()

// Not currently used. For the future.
private static var keyDownHandlers = [Shortcut: [KeyAction]]()
private static var keyUpHandlers = [Shortcut: [KeyAction]]()
private static var legacyKeyDownHandlers = [Name: [KeyAction]]()
private static var legacyKeyUpHandlers = [Name: [KeyAction]]()

private static var userDefaultsKeyDownHandlers = [Name: [KeyAction]]()
private static var userDefaultsKeyUpHandlers = [Name: [KeyAction]]()
private static var streamKeyDownHandlers = [Name: [UUID: KeyAction]]()
private static var streamKeyUpHandlers = [Name: [UUID: KeyAction]]()

private static var shortcutsForLegacyHandlers: Set<Shortcut> {
let shortcuts = [legacyKeyDownHandlers.keys, legacyKeyUpHandlers.keys]
.flatMap { $0 }
.compactMap(\.shortcut)

return Set(shortcuts)
}

private static var shortcutsForStreamHandlers: Set<Shortcut> {
let shortcuts = [streamKeyDownHandlers.keys, streamKeyUpHandlers.keys]
.flatMap { $0 }
.compactMap(\.shortcut)

return Set(shortcuts)
}

private static var shortcutsForHandlers: Set<Shortcut> {
shortcutsForLegacyHandlers.union(shortcutsForStreamHandlers)
}

/**
When `true`, event handlers will not be called for registered keyboard shortcuts.
Expand All @@ -35,13 +54,45 @@ public enum KeyboardShortcuts {
registeredShortcuts.insert(shortcut)
}

/**
Register the shortcut for the given name if it has a shortcut.
*/
private static func registerShortcutIfNeeded(for name: Name) {
guard let shortcut = getShortcut(for: name) else {
return
}

register(shortcut)
}

private static func unregister(_ shortcut: Shortcut) {
CarbonKeyboardShortcuts.unregister(shortcut)
registeredShortcuts.remove(shortcut)
}

// TODO: Doc comment and make this public.
static func unregisterAll() {
/**
Unregister the given shortcut if it has no handlers.
*/
private static func unregisterIfNeeded(_ shortcut: Shortcut) {
guard !shortcutsForHandlers.contains(shortcut) else {
return
}

unregister(shortcut)
}

/**
Unregister the shortcut for the given name if it has no handlers.
*/
private static func unregisterShortcutIfNeeded(for name: Name) {
guard let shortcut = name.shortcut else {
return
}

unregisterIfNeeded(shortcut)
}

private static func unregisterAll() {
CarbonKeyboardShortcuts.unregisterAll()
registeredShortcuts.removeAll()

Expand All @@ -52,13 +103,18 @@ public enum KeyboardShortcuts {
Remove all handlers receiving keyboard shortcuts events.
This can be used to reset the handlers before re-creating them to avoid having multiple handlers for the same shortcut.
- Note: This method does not affect listeners using `.on()`.
*/
public static func removeAllHandlers() {
CarbonKeyboardShortcuts.unregisterAll()
keyDownHandlers = [:]
keyUpHandlers = [:]
userDefaultsKeyDownHandlers = [:]
userDefaultsKeyUpHandlers = [:]
let shortcutsToUnregister = shortcutsForLegacyHandlers.subtracting(shortcutsForStreamHandlers)

for shortcut in shortcutsToUnregister {
unregister(shortcut)
}

legacyKeyDownHandlers = [:]
legacyKeyUpHandlers = [:]
}

// TODO: Also add `.isEnabled(_ name: Name)`.
Expand Down Expand Up @@ -179,18 +235,22 @@ public enum KeyboardShortcuts {
return
}

if let handlers = keyDownHandlers[shortcut] {
for (name, handlers) in legacyKeyDownHandlers {
guard getShortcut(for: name) == shortcut else {
continue
}

for handler in handlers {
handler()
}
}

for (name, handlers) in userDefaultsKeyDownHandlers {
for (name, handlers) in streamKeyDownHandlers {
guard getShortcut(for: name) == shortcut else {
continue
}

for handler in handlers {
for handler in handlers.values {
handler()
}
}
Expand All @@ -201,18 +261,22 @@ public enum KeyboardShortcuts {
return
}

if let handlers = keyUpHandlers[shortcut] {
for (name, handlers) in legacyKeyUpHandlers {
guard getShortcut(for: name) == shortcut else {
continue
}

for handler in handlers {
handler()
}
}

for (name, handlers) in userDefaultsKeyUpHandlers {
for (name, handlers) in streamKeyUpHandlers {
guard getShortcut(for: name) == shortcut else {
continue
}

for handler in handlers {
for handler in handlers.values {
handler()
}
}
Expand Down Expand Up @@ -240,16 +304,8 @@ public enum KeyboardShortcuts {
```
*/
public static func onKeyDown(for name: Name, action: @escaping KeyAction) {
if userDefaultsKeyDownHandlers[name] == nil {
userDefaultsKeyDownHandlers[name] = []
}

userDefaultsKeyDownHandlers[name]?.append(action)

// If the keyboard shortcut already exist, we register it.
if let shortcut = getShortcut(for: name) {
register(shortcut)
}
legacyKeyDownHandlers[name, default: []].append(action)
registerShortcutIfNeeded(for: name)
}

/**
Expand All @@ -274,16 +330,8 @@ public enum KeyboardShortcuts {
```
*/
public static func onKeyUp(for name: Name, action: @escaping KeyAction) {
if userDefaultsKeyUpHandlers[name] == nil {
userDefaultsKeyUpHandlers[name] = []
}

userDefaultsKeyUpHandlers[name]?.append(action)

// If the keyboard shortcut already exist, we register it.
if let shortcut = getShortcut(for: name) {
register(shortcut)
}
legacyKeyUpHandlers[name, default: []].append(action)
registerShortcutIfNeeded(for: name)
}

private static let userDefaultsPrefix = "KeyboardShortcuts_"
Expand Down Expand Up @@ -325,6 +373,74 @@ public enum KeyboardShortcuts {
}
}

extension KeyboardShortcuts {
@available(macOS 10.15, *)
public enum EventType {
case keyDown
case keyUp
}

/**
Listen to the keyboard shortcut with the given name being pressed.
You can register multiple listeners.
You can safely call this even if the user has not yet set a keyboard shortcut. It will just be inactive until they do.
Ending the async sequence will stop the listener. For example, in the below example, the listener will stop when the view disappears.
```
import SwiftUI
import KeyboardShortcuts
struct ContentView: View {
@State private var isUnicornMode = false
var body: some View {
Text(isUnicornMode ? "🦄" : "🐴")
.task {
for await _ in KeyboardShortcuts.on(.keyUp, for: .toggleUnicornMode) {
isUnicornMode.toggle()
}
}
}
}
```
- Note: This method is not affected by `.removeAllHandlers()`.
*/
@available(macOS 10.15, *)
public static func on(_ type: EventType, for name: Name) -> AsyncStream<Void> {
AsyncStream { continuation in
let id = UUID()

switch type {
case .keyDown:
streamKeyDownHandlers[name, default: [:]][id] = {
continuation.yield()
}
case .keyUp:
streamKeyUpHandlers[name, default: [:]][id] = {
continuation.yield()
}
}

registerShortcutIfNeeded(for: name)

continuation.onTermination = { _ in
switch type {
case .keyDown:
streamKeyDownHandlers[name]?[id] = nil
case .keyUp:
streamKeyUpHandlers[name]?[id] = nil
}

unregisterShortcutIfNeeded(for: name)
}
}
}
}

extension Notification.Name {
static let shortcutByNameDidChange = Self("KeyboardShortcuts_shortcutByNameDidChange")
}
2 changes: 1 addition & 1 deletion Sources/KeyboardShortcuts/NSMenuItem++.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension NSMenuItem {

set()

// TODO: Use Combine when targeting macOS 10.15.
// TODO: Use AsyncStream when targeting macOS 10.15.
AssociatedKeys.observer[self] = NotificationCenter.default.addObserver(forName: .shortcutByNameDidChange, object: nil, queue: nil) { notification in
guard
let nameInNotification = notification.userInfo?["name"] as? KeyboardShortcuts.Name,
Expand Down

0 comments on commit 0dcedd5

Please sign in to comment.