- 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
258 lines
No EOL
7.3 KiB
Swift
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)
|
|
}
|
|
} |