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

Add as SwiftUI .keyboardShortcut() helper #101

Open
sindresorhus opened this issue Oct 20, 2022 · 5 comments
Open

Add as SwiftUI .keyboardShortcut() helper #101

sindresorhus opened this issue Oct 20, 2022 · 5 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@sindresorhus
Copy link
Owner

#69

@sindresorhus sindresorhus added enhancement New feature or request help wanted Extra attention is needed labels Oct 20, 2022
@mbenoukaiss
Copy link

mbenoukaiss commented Nov 23, 2022

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("..."))
    }
}

zddhub added a commit to zddhub/KeyboardShortcuts that referenced this issue Apr 15, 2023
Use solution from sindresorhus#101 (comment)

The old solution creates thousands associatedShortcut, it isn't good
@augustwester
Copy link

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.

@othyn
Copy link

othyn commented Jul 2, 2023

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

@castdrian
Copy link

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

@27Saumya
Copy link

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)
    }

zddhub added a commit to zddhub/KeyboardShortcuts that referenced this issue Mar 20, 2024
Use solution from sindresorhus#101 (comment)

The old solution creates thousands associatedShortcut, it isn't good
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

7 participants
@sindresorhus @augustwester @othyn @castdrian @mbenoukaiss @27Saumya and others