feature: user-selectable capture hotkey with recorder in Settings
This commit is contained in:
@@ -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?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user