tellme/Sources/App/HotkeyRecorder.swift
Felipe M. 54c3b65d4a
Complete Phase 4: Comprehensive preferences, localization, and UX polish
- Rename application from MenuWhisper to Tell me with new domain com.fmartingr.tellme
- Implement comprehensive preferences window with 6 tabs (General, Models, Text Insertion, Interface, Advanced, Permissions)
- Add full English/Spanish localization for all UI elements
- Create functional onboarding flow with model download capability
- Implement preview dialog for transcription editing
- Add settings export/import functionality
- Fix HUD content display issues and add comprehensive permission checking
- Enhance build scripts and app bundle creation for proper localization support
2025-09-19 13:55:46 +02:00

258 lines
No EOL
7.3 KiB
Swift

import SwiftUI
import Carbon
import CoreSettings
struct HotkeyRecorder: View {
@Binding var hotkey: HotkeyConfig
@State private var isRecording = false
@State private var recordedKeyCode: UInt32?
@State private var recordedModifiers: UInt32?
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Button(action: {
if isRecording {
stopRecording()
} else {
startRecording()
}
}) {
HStack {
if isRecording {
Text(NSLocalizedString("hotkey.press_keys", comment: "Press keys..."))
.foregroundColor(.primary)
} else {
Text(hotkeyDisplayString)
.foregroundColor(.primary)
}
}
.frame(minWidth: 150, minHeight: 30)
.background(isRecording ? Color.blue.opacity(0.2) : Color(NSColor.controlBackgroundColor))
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(isRecording ? Color.blue : Color.gray.opacity(0.3), lineWidth: 2)
)
.cornerRadius(6)
}
.buttonStyle(.plain)
.onKeyDown { event in
guard isRecording else { return false }
handleKeyEvent(event)
return true
}
if hotkey.keyCode != HotkeyConfig.default.keyCode || hotkey.modifiers != HotkeyConfig.default.modifiers {
Button(NSLocalizedString("hotkey.reset_default", comment: "Reset to Default")) {
hotkey = HotkeyConfig.default
}
.buttonStyle(.borderless)
.controlSize(.small)
.foregroundColor(.secondary)
}
}
Text(NSLocalizedString("hotkey.record_description", comment: "Click to record a new hotkey combination"))
.font(.caption)
.foregroundColor(.secondary)
}
.focusable()
}
private var hotkeyDisplayString: String {
return hotkeyToString(keyCode: hotkey.keyCode, modifiers: hotkey.modifiers)
}
private func startRecording() {
isRecording = true
recordedKeyCode = nil
recordedModifiers = nil
}
private func stopRecording() {
if let keyCode = recordedKeyCode, let modifiers = recordedModifiers {
hotkey = HotkeyConfig(keyCode: keyCode, modifiers: modifiers)
}
isRecording = false
recordedKeyCode = nil
recordedModifiers = nil
}
private func handleKeyEvent(_ event: NSEvent) {
let keyCode = event.keyCode
let modifierFlags = event.modifierFlags
// Convert NSEvent modifier flags to Carbon modifier flags
var carbonModifiers: UInt32 = 0
if modifierFlags.contains(.command) {
carbonModifiers |= UInt32(cmdKey)
}
if modifierFlags.contains(.shift) {
carbonModifiers |= UInt32(shiftKey)
}
if modifierFlags.contains(.option) {
carbonModifiers |= UInt32(optionKey)
}
if modifierFlags.contains(.control) {
carbonModifiers |= UInt32(controlKey)
}
// Only accept combinations with at least one modifier
guard carbonModifiers != 0 else { return }
recordedKeyCode = UInt32(keyCode)
recordedModifiers = carbonModifiers
// Auto-stop recording after a brief delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if isRecording {
stopRecording()
}
}
}
}
// MARK: - Helper Functions
private func hotkeyToString(keyCode: UInt32, modifiers: UInt32) -> String {
var result = ""
// Add modifier symbols
if modifiers & UInt32(controlKey) != 0 {
result += ""
}
if modifiers & UInt32(optionKey) != 0 {
result += ""
}
if modifiers & UInt32(shiftKey) != 0 {
result += ""
}
if modifiers & UInt32(cmdKey) != 0 {
result += ""
}
// Add key name
result += keyCodeToString(keyCode)
return result
}
private func keyCodeToString(_ keyCode: UInt32) -> String {
// Map common key codes to their string representations
switch keyCode {
case 0: return "A"
case 1: return "S"
case 2: return "D"
case 3: return "F"
case 4: return "H"
case 5: return "G"
case 6: return "Z"
case 7: return "X"
case 8: return "C"
case 9: return "V"
case 10: return "§"
case 11: return "B"
case 12: return "Q"
case 13: return "W"
case 14: return "E"
case 15: return "R"
case 16: return "Y"
case 17: return "T"
case 18: return "1"
case 19: return "2"
case 20: return "3"
case 21: return "4"
case 22: return "6"
case 23: return "5"
case 24: return "="
case 25: return "9"
case 26: return "7"
case 27: return "-"
case 28: return "8"
case 29: return "0"
case 30: return "]"
case 31: return "O"
case 32: return "U"
case 33: return "["
case 34: return "I"
case 35: return "P"
case 36: return ""
case 37: return "L"
case 38: return "J"
case 39: return "'"
case 40: return "K"
case 41: return ";"
case 42: return "\\"
case 43: return ","
case 44: return "/"
case 45: return "N"
case 46: return "M"
case 47: return "."
case 48: return ""
case 49: return "Space"
case 50: return "`"
case 51: return ""
case 53: return ""
case 96: return "F5"
case 97: return "F6"
case 98: return "F7"
case 99: return "F3"
case 100: return "F8"
case 101: return "F9"
case 103: return "F11"
case 105: return "F13"
case 107: return "F14"
case 109: return "F10"
case 111: return "F12"
case 113: return "F15"
case 114: return "Help"
case 115: return "Home"
case 116: return ""
case 117: return ""
case 118: return "F4"
case 119: return "End"
case 120: return "F2"
case 121: return ""
case 122: return "F1"
case 123: return ""
case 124: return ""
case 125: return ""
case 126: return ""
default:
return "Key \(keyCode)"
}
}
// MARK: - Extensions
extension View {
func onKeyDown(perform action: @escaping (NSEvent) -> Bool) -> some View {
self.background(KeyEventHandlingView(onKeyDown: action))
}
}
struct KeyEventHandlingView: NSViewRepresentable {
let onKeyDown: (NSEvent) -> Bool
func makeNSView(context: Context) -> NSView {
let view = KeyHandlingNSView()
view.onKeyDown = onKeyDown
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
class KeyHandlingNSView: NSView {
var onKeyDown: ((NSEvent) -> Bool)?
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
if let handler = onKeyDown, handler(event) {
return
}
super.keyDown(with: event)
}
}