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

Closed
sindresorhus opened this issue Oct 20, 2022 · 11 comments
Closed

Add as SwiftUI .keyboardShortcut() helper #101

sindresorhus opened this issue Oct 20, 2022 · 11 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
@scornflake
Copy link

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: #181

@aueangpanit
Copy link

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! ❤️

@sarensw
Copy link

sarensw commented Sep 24, 2024

Hi @sindresorhus , thank you so much for providing a fix here. Just want to double check. This is how I use the MenuBarExtra right now. But, when I change the shortcut for that it is not reflected in the Button label. It works as expected, but the label still shows the previous shortcut until I restart the app. Is there anything that I have to do in addition to make this work?

MenuBarExtra("...", image: "...") {
    Button("Show") {
        ...
    }
    .globalKeyboardShortcut(.showPanel)
}

@rampatra
Copy link

rampatra commented Sep 24, 2024

I just tested this on my app and the key shortcut changes are reflected instantly. No need for restart. My code looks the same as yours. I am using macOS 15.1.

Btw, this feature made this library perfect. Thanks, @sindresorhus.

@sarensw
Copy link

sarensw commented Sep 24, 2024

@rampatra thanks for checking. Just to double check: On which macOS are you?

@sarensw
Copy link

sarensw commented Sep 24, 2024

I have just checked on my macOS 13 and macOS 15 virtual machines. No issue there. My dev env is still on macOS 14. I guess this is some set-up related issue on my side.

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

11 participants