feature: user-selectable capture hotkey with recorder in Settings

This commit is contained in:
2026-09-01 22:33:13 +04:00
parent 3c55be174a
commit fa3e7b0a4a
4 changed files with 258 additions and 15 deletions
+72 -6
View File
@@ -4,6 +4,8 @@ import ShotdeckCore
struct SettingsView: View {
@Environment(AppModel.self) private var model
@State private var isRecordingHotkey = false
@State private var recorder = HotkeyRecorderBox()
private let labelWidth: CGFloat = 104
@@ -16,13 +18,19 @@ struct SettingsView: View {
.gridCellColumns(2)
}
GridRow(alignment: .firstTextBaseline) {
GridRow(alignment: .center) {
fieldLabel("Capture")
Text("⌥⇧2 — fixed in this version")
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(minHeight: 22, alignment: .leading)
HStack(spacing: 8) {
Button {
armHotkeyRecorder()
} label: {
Text(isRecordingHotkey ? "Press keys…" : model.hotkeyDisplayString)
.foregroundStyle(isRecordingHotkey ? .secondary : .primary)
.lineLimit(1)
}
Spacer(minLength: 0)
}
.frame(minHeight: 22)
}
GridRow {
@@ -57,6 +65,35 @@ struct SettingsView: View {
.padding(16)
.frame(minWidth: 320, idealWidth: 360, maxWidth: 360, alignment: .leading)
.controlSize(.small)
.onDisappear { disarmHotkeyRecorder() }
}
private func armHotkeyRecorder() {
guard !isRecordingHotkey else { return }
isRecordingHotkey = true
recorder.onKey = { keyCode, flags in
handleRecorderKey(keyCode: keyCode, flags: flags)
}
recorder.arm()
}
private func handleRecorderKey(keyCode: UInt16, flags: NSEvent.ModifierFlags) {
if keyCode == 53 { // kVK_Escape
disarmHotkeyRecorder()
return
}
guard let pref = HotkeyPreference.fromKeyEvent(keyCode: keyCode, modifierFlags: flags) else {
return
}
pref.save()
model.reRegisterHotkey()
disarmHotkeyRecorder()
}
private func disarmHotkeyRecorder() {
recorder.disarm()
recorder.onKey = nil
isRecordingHotkey = false
}
private func fieldLabel(_ title: String) -> some View {
@@ -80,6 +117,35 @@ struct SettingsView: View {
}
}
/// Local keyDown monitor for the Settings capture-hotkey recorder. Callbacks hop onto the
/// main actor the same way `RegionPickerController` does local monitors fire on the
/// main run loop during `NSApp.sendEvent`.
@MainActor
private final class HotkeyRecorderBox {
var onKey: ((UInt16, NSEvent.ModifierFlags) -> Void)?
private var monitor: Any?
func arm() {
disarm()
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
guard let self else { return event }
let keyCode = event.keyCode
let rawFlags = event.modifierFlags.rawValue
MainActor.assumeIsolated {
self.onKey?(keyCode, NSEvent.ModifierFlags(rawValue: rawFlags))
}
return nil
}
}
func disarm() {
if let monitor {
NSEvent.removeMonitor(monitor)
}
monitor = nil
}
}
extension AppModel: SettingsWindowPresenting {
private static var settingsWindowController: NSWindowController?