KeyboardShortcuts icon indicating copy to clipboard operation
KeyboardShortcuts copied to clipboard

Add as SwiftUI `.keyboardShortcut()` helper

Open sindresorhus opened this issue 3 years ago • 7 comments

https://github.com/sindresorhus/KeyboardShortcuts/pull/69

sindresorhus avatar Oct 20 '22 12:10 sindresorhus

For anyone interested I needed it because I wanted to display shortcuts in my menu bar items so I implemented it manually thanks to this answer on StackOverflow https://stackoverflow.com/a/35138823.

Maybe there's a cleaner way to implement the View extension and toEventModifiers, I don't really know much about Swift. Also the view doesn't get refreshed when the shortcut changes.

import KeyboardShortcuts
import SwiftUI
import Carbon


extension View {
    
    public func keyboardShortcut(_ shortcut: KeyboardShortcuts.Name) -> some View {
        if let shortcut = shortcut.shortcut {
            if let keyEquivalent = shortcut.toKeyEquivalent() {
                return AnyView(self.keyboardShortcut(keyEquivalent, modifiers: shortcut.toEventModifiers()))
            }
        }
        
        return AnyView(self)
    }
    
}

extension KeyboardShortcuts.Shortcut {
    
    func toKeyEquivalent() -> KeyEquivalent? {
        let carbonKeyCode = UInt16(self.carbonKeyCode)
        let maxNameLength = 4
        var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
        var nameLength = 0
        
        let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
        var deadKeys: UInt32 = 0
        let keyboardType = UInt32(LMGetKbdType())
        
        let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
        guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
            NSLog("Could not get keyboard layout data")
            return nil
        }
        let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
        let osStatus = layoutData.withUnsafeBytes {
            UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, carbonKeyCode, UInt16(kUCKeyActionDown),
                           modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
                           &deadKeys, maxNameLength, &nameLength, &nameBuffer)
        }
        guard osStatus == noErr else {
            NSLog("Code: 0x%04X  Status: %+i", carbonKeyCode, osStatus);
            return nil
        }
        
        return KeyEquivalent(Character(String(utf16CodeUnits: nameBuffer, count: nameLength)))
    }
    
    func toEventModifiers() -> SwiftUI.EventModifiers {
        var modifiers: SwiftUI.EventModifiers = []
        
        if self.modifiers.contains(NSEvent.ModifierFlags.command) {
            modifiers.update(with: EventModifiers.command)
        }
        
        if self.modifiers.contains(NSEvent.ModifierFlags.control) {
            modifiers.update(with: EventModifiers.control)
        }
        
        if self.modifiers.contains(NSEvent.ModifierFlags.option) {
            modifiers.update(with: EventModifiers.option)
        }
        
        if self.modifiers.contains(NSEvent.ModifierFlags.shift) {
            modifiers.update(with: EventModifiers.shift)
        }
        
        if self.modifiers.contains(NSEvent.ModifierFlags.capsLock) {
            modifiers.update(with: EventModifiers.capsLock)
        }
        
        if self.modifiers.contains(NSEvent.ModifierFlags.numericPad) {
            modifiers.update(with: EventModifiers.numericPad)
        }
        
        return modifiers
    }
    
}

Example implementation :

struct SomeView: View {
    var body: some View {
        return Button("Shortcut") {
            print("clicked")
        }.keyboardShortcut(KeyboardShortcuts.Name("..."))
    }
}

mbenoukaiss avatar Nov 23 '22 14:11 mbenoukaiss

Thanks, @mbenoukaiss! 🙏 How would you extend this so the menu bar item is updated dynamically? You currently have to restart the app for changes to take effect.

augustwester avatar May 06 '23 11:05 augustwester

This is a necessity when using this package with MenuBarExtra Button components.

othyn avatar Jul 02 '23 22:07 othyn

I agree, this is exactly where I'm at as well

castdrian avatar Jul 12 '23 19:07 castdrian

i found a short fix, you could notify the user, whenever a change in the keyboard-shortcut is made the app would restart, and you could programatically restart like this:-

func relaunch(afterDelay seconds: TimeInterval = 0.5) -> Never {
        let task = Process()
        task.launchPath = "/bin/sh"
        task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
        task.launch()
        
        NSApp.terminate(self)
        exit(0)
    }

saumsy avatar Feb 28 '24 08:02 saumsy

A little late to the party, but I've just raised a PR that solves this (same idea, different SwiftUI wrapper, which auto updates based on changes to the Shortcut state)

see: https://github.com/sindresorhus/KeyboardShortcuts/pull/181

scornflake avatar Jul 17 '24 01:07 scornflake

For anyone who needs it quickly! I've forked this project because I wanted this change this weekend 🙂 -> https://github.com/aueangpanit/KeyboardShortcuts

Example usage: Button(action: captureTextViewModel.captureText, label: { Text("Capture Text") }).keyboardShortcut(for: KeyboardShortcuts.Name("captureText"))


In case it's helpful, all of the changes for this feature in this file: https://github.com/aueangpanit/KeyboardShortcuts/blob/main/Sources/KeyboardShortcuts/View%2B%2B.swift

Hopefully, we have it in the main project soon! ❤️

aueangpanit avatar Aug 11 '24 09:08 aueangpanit