import SwiftUI import AppKit /// A clickable field that records a keyboard shortcut when focused. /// Click it, press a key combination, and it saves the binding. struct HotkeyRecorderView: View { let label: String @Binding var binding: HotkeyBinding @State private var isRecording = false var body: some View { HStack { Text(label) .frame(width: 140, alignment: .leading) HotkeyRecorderField(binding: $binding, isRecording: $isRecording) .frame(width: 120, height: 24) .background( RoundedRectangle(cornerRadius: 6) .fill(isRecording ? Color.accentColor.opacity(0.15) : Color.secondary.opacity(0.1)) ) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(isRecording ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1) ) } } } struct OptionalHotkeyRecorderView: View { let label: String @Binding var binding: HotkeyBinding? @State private var isRecording = false var body: some View { HStack { Text(label) .frame(width: 140, alignment: .leading) OptionalHotkeyRecorderField(binding: $binding, isRecording: $isRecording) .frame(width: 120, height: 24) .background( RoundedRectangle(cornerRadius: 6) .fill(isRecording ? Color.accentColor.opacity(0.15) : Color.secondary.opacity(0.1)) ) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(isRecording ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1) ) Button("Clear") { binding = nil } .disabled(binding == nil) } } } /// NSViewRepresentable that captures key events when focused. struct HotkeyRecorderField: NSViewRepresentable { @Binding var binding: HotkeyBinding @Binding var isRecording: Bool func makeNSView(context: Context) -> HotkeyNSView { let view = HotkeyNSView() view.onKeyRecorded = { newBinding in binding = newBinding isRecording = false } view.onFocusChanged = { focused in isRecording = focused } return view } func updateNSView(_ nsView: HotkeyNSView, context: Context) { nsView.update(currentLabel: binding.displayString, isRecording: isRecording) } } struct OptionalHotkeyRecorderField: NSViewRepresentable { @Binding var binding: HotkeyBinding? @Binding var isRecording: Bool func makeNSView(context: Context) -> HotkeyNSView { let view = HotkeyNSView() view.onKeyRecorded = { newBinding in binding = newBinding isRecording = false } view.onFocusChanged = { focused in isRecording = focused } return view } func updateNSView(_ nsView: HotkeyNSView, context: Context) { nsView.update(currentLabel: binding?.displayString ?? "Not set", isRecording: isRecording) } } /// The actual NSView that handles key capture. class HotkeyNSView: NSView { var currentLabel: String = "" var showRecording: Bool = false var onKeyRecorded: ((HotkeyBinding) -> Void)? var onFocusChanged: ((Bool) -> Void)? private let label: NSTextField = { let field = NSTextField(labelWithString: "") field.alignment = .center field.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .medium) field.translatesAutoresizingMaskIntoConstraints = false field.backgroundColor = .clear field.isBezeled = false field.lineBreakMode = .byTruncatingTail return field }() override init(frame frameRect: NSRect) { super.init(frame: frameRect) setupLabel() } required init?(coder: NSCoder) { super.init(coder: coder) setupLabel() } override var acceptsFirstResponder: Bool { true } override func layout() { super.layout() updateLabelAppearance() } override func mouseDown(with event: NSEvent) { window?.makeFirstResponder(self) } override func becomeFirstResponder() -> Bool { onFocusChanged?(true) return true } override func resignFirstResponder() -> Bool { onFocusChanged?(false) return true } override func keyDown(with event: NSEvent) { guard showRecording else { super.keyDown(with: event) return } let relevantFlags: NSEvent.ModifierFlags = [.command, .shift, .control, .option] let masked = event.modifierFlags.intersection(.deviceIndependentFlagsMask).intersection(relevantFlags) // Require at least one modifier key guard !masked.isEmpty else { return } let binding = HotkeyBinding(modifiers: masked.rawValue, keyCode: event.keyCode) onKeyRecorded?(binding) // Resign first responder after recording window?.makeFirstResponder(nil) } private func setupLabel() { addSubview(label) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), label.centerYAnchor.constraint(equalTo: centerYAnchor) ]) updateLabelAppearance() } func update(currentLabel: String, isRecording: Bool) { self.currentLabel = currentLabel showRecording = isRecording updateLabelAppearance() } private func updateLabelAppearance() { label.stringValue = showRecording ? "Press keys..." : currentLabel label.textColor = showRecording ? .controlAccentColor : .labelColor } }