Refactor: Unify and improve hotkey settings UI

This commit is contained in:
Beingpax 2025-07-01 22:52:09 +05:45
parent 888cc5125b
commit 3e609d1e3b
10 changed files with 519 additions and 734 deletions

View File

@ -5,57 +5,62 @@ import AppKit
extension KeyboardShortcuts.Name {
static let toggleMiniRecorder = Self("toggleMiniRecorder")
static let toggleMiniRecorder2 = Self("toggleMiniRecorder2")
}
@MainActor
class HotkeyManager: ObservableObject {
@Published var isListening = false
@Published var isShortcutConfigured = false
@Published var isPushToTalkEnabled: Bool {
@Published var selectedHotkey1: HotkeyOption {
didSet {
UserDefaults.standard.set(isPushToTalkEnabled, forKey: "isPushToTalkEnabled")
resetKeyStates()
setupKeyMonitor()
UserDefaults.standard.set(selectedHotkey1.rawValue, forKey: "selectedHotkey1")
setupHotkeyMonitoring()
}
}
@Published var pushToTalkKey: PushToTalkKey {
@Published var selectedHotkey2: HotkeyOption {
didSet {
UserDefaults.standard.set(pushToTalkKey.rawValue, forKey: "pushToTalkKey")
resetKeyStates()
UserDefaults.standard.set(selectedHotkey2.rawValue, forKey: "selectedHotkey2")
setupHotkeyMonitoring()
}
}
private var whisperState: WhisperState
private var currentKeyState = false
private var miniRecorderShortcutManager: MiniRecorderShortcutManager
// Change from single monitor to separate local and global monitors
// NSEvent monitoring for modifier keys
private var globalEventMonitor: Any?
private var localEventMonitor: Any?
// Key handling properties
// Key state tracking
private var currentKeyState = false
private var keyPressStartTime: Date?
private let briefPressThreshold = 1.0 // 1 second threshold for brief press
private var isHandsFreeMode = false // Track if we're in hands-free recording mode
// Add cooldown management
private var lastShortcutTriggerTime: Date?
private let shortcutCooldownInterval: TimeInterval = 0.5 // 500ms cooldown
private var isHandsFreeMode = false
// Debounce for Fn key
private var fnDebounceTask: Task<Void, Never>?
private var pendingFnKeyState: Bool? = nil
enum PushToTalkKey: String, CaseIterable {
// Keyboard shortcut state tracking
private var shortcutKeyPressStartTime: Date?
private var isShortcutHandsFreeMode = false
private var shortcutCurrentKeyState = false
private var lastShortcutTriggerTime: Date?
private let shortcutCooldownInterval: TimeInterval = 0.5
enum HotkeyOption: String, CaseIterable {
case none = "none"
case rightOption = "rightOption"
case leftOption = "leftOption"
case leftControl = "leftControl"
case leftControl = "leftControl"
case rightControl = "rightControl"
case fn = "fn"
case rightCommand = "rightCommand"
case rightShift = "rightShift"
case custom = "custom"
var displayName: String {
switch self {
case .none: return "None"
case .rightOption: return "Right Option (⌥)"
case .leftOption: return "Left Option (⌥)"
case .leftControl: return "Left Control (⌃)"
@ -63,10 +68,11 @@ class HotkeyManager: ObservableObject {
case .fn: return "Fn"
case .rightCommand: return "Right Command (⌘)"
case .rightShift: return "Right Shift (⇧)"
case .custom: return "Custom"
}
}
var keyCode: CGKeyCode {
var keyCode: CGKeyCode? {
switch self {
case .rightOption: return 0x3D
case .leftOption: return 0x3A
@ -75,52 +81,91 @@ class HotkeyManager: ObservableObject {
case .fn: return 0x3F
case .rightCommand: return 0x36
case .rightShift: return 0x3C
case .custom, .none: return nil
}
}
var isModifierKey: Bool {
return self != .custom && self != .none
}
}
init(whisperState: WhisperState) {
self.isPushToTalkEnabled = UserDefaults.standard.bool(forKey: "isPushToTalkEnabled")
self.pushToTalkKey = PushToTalkKey(rawValue: UserDefaults.standard.string(forKey: "pushToTalkKey") ?? "") ?? .rightCommand
// One-time migration from legacy single-hotkey settings
if UserDefaults.standard.object(forKey: "didMigrateHotkeys_v2") == nil {
// If legacy push-to-talk modifier key was enabled, carry it over
if UserDefaults.standard.bool(forKey: "isPushToTalkEnabled"),
let legacyRaw = UserDefaults.standard.string(forKey: "pushToTalkKey"),
let legacyKey = HotkeyOption(rawValue: legacyRaw) {
UserDefaults.standard.set(legacyKey.rawValue, forKey: "selectedHotkey1")
}
// If a custom shortcut existed, mark hotkey-1 as custom (shortcut itself already persisted)
if KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil {
UserDefaults.standard.set(HotkeyOption.custom.rawValue, forKey: "selectedHotkey1")
}
// Leave second hotkey as .none
UserDefaults.standard.set(true, forKey: "didMigrateHotkeys_v2")
}
// ---- normal initialisation ----
self.selectedHotkey1 = HotkeyOption(rawValue: UserDefaults.standard.string(forKey: "selectedHotkey1") ?? "") ?? .rightCommand
self.selectedHotkey2 = HotkeyOption(rawValue: UserDefaults.standard.string(forKey: "selectedHotkey2") ?? "") ?? .none
self.whisperState = whisperState
self.miniRecorderShortcutManager = MiniRecorderShortcutManager(whisperState: whisperState)
updateShortcutStatus()
}
private func resetKeyStates() {
currentKeyState = false
keyPressStartTime = nil
isHandsFreeMode = false
func startHotkeyMonitoring() {
setupHotkeyMonitoring()
}
private func setupKeyMonitor() {
removeKeyMonitor()
private func setupHotkeyMonitoring() {
removeAllMonitoring()
guard isPushToTalkEnabled else { return }
// Global monitor for capturing flags when app is in background
setupModifierKeyMonitoring()
setupCustomShortcutMonitoring()
}
private func setupModifierKeyMonitoring() {
// Only set up if at least one hotkey is a modifier key
guard (selectedHotkey1.isModifierKey && selectedHotkey1 != .none) || (selectedHotkey2.isModifierKey && selectedHotkey2 != .none) else { return }
globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
guard let self = self else { return }
Task { @MainActor in
await self.handleNSKeyEvent(event)
await self.handleModifierKeyEvent(event)
}
}
// Local monitor for capturing flags when app has focus
localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
guard let self = self else { return event }
Task { @MainActor in
await self.handleNSKeyEvent(event)
await self.handleModifierKeyEvent(event)
}
return event // Return the event to allow normal processing
return event
}
}
private func removeKeyMonitor() {
private func setupCustomShortcutMonitoring() {
// Hotkey 1
if selectedHotkey1 == .custom {
KeyboardShortcuts.onKeyDown(for: .toggleMiniRecorder) { [weak self] in
Task { @MainActor in await self?.handleCustomShortcutKeyDown() }
}
KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder) { [weak self] in
Task { @MainActor in await self?.handleCustomShortcutKeyUp() }
}
}
// Hotkey 2
if selectedHotkey2 == .custom {
KeyboardShortcuts.onKeyDown(for: .toggleMiniRecorder2) { [weak self] in
Task { @MainActor in await self?.handleCustomShortcutKeyDown() }
}
KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder2) { [weak self] in
Task { @MainActor in await self?.handleCustomShortcutKeyUp() }
}
}
}
private func removeAllMonitoring() {
if let monitor = globalEventMonitor {
NSEvent.removeMonitor(monitor)
globalEventMonitor = nil
@ -130,87 +175,92 @@ class HotkeyManager: ObservableObject {
NSEvent.removeMonitor(monitor)
localEventMonitor = nil
}
resetKeyStates()
}
private func handleNSKeyEvent(_ event: NSEvent) async {
private func resetKeyStates() {
currentKeyState = false
keyPressStartTime = nil
isHandsFreeMode = false
shortcutCurrentKeyState = false
shortcutKeyPressStartTime = nil
isShortcutHandsFreeMode = false
}
private func handleModifierKeyEvent(_ event: NSEvent) async {
let keycode = event.keyCode
let flags = event.modifierFlags
// Check if the target key is pressed based on the modifier flags
var isKeyPressed = false
var isTargetKey = false
switch pushToTalkKey {
case .rightOption, .leftOption:
isKeyPressed = flags.contains(.option)
isTargetKey = keycode == pushToTalkKey.keyCode
case .leftControl, .rightControl:
isKeyPressed = flags.contains(.control)
isTargetKey = keycode == pushToTalkKey.keyCode
case .fn:
isKeyPressed = flags.contains(.function)
isTargetKey = keycode == pushToTalkKey.keyCode
// Debounce only for Fn key
if isTargetKey {
pendingFnKeyState = isKeyPressed
fnDebounceTask?.cancel()
fnDebounceTask = Task { [pendingState = isKeyPressed] in
try? await Task.sleep(nanoseconds: 75_000_000) // 75ms
// Only act if the state hasn't changed during debounce
if pendingFnKeyState == pendingState {
await MainActor.run {
self.processPushToTalkKey(isKeyPressed: pendingState)
}
}
}
return
}
case .rightCommand:
isKeyPressed = flags.contains(.command)
isTargetKey = keycode == pushToTalkKey.keyCode
case .rightShift:
isKeyPressed = flags.contains(.shift)
isTargetKey = keycode == pushToTalkKey.keyCode
// Determine which hotkey (if any) is being triggered
let activeHotkey: HotkeyOption?
if selectedHotkey1.isModifierKey && selectedHotkey1.keyCode == keycode {
activeHotkey = selectedHotkey1
} else if selectedHotkey2.isModifierKey && selectedHotkey2.keyCode == keycode {
activeHotkey = selectedHotkey2
} else {
activeHotkey = nil
}
guard isTargetKey else { return }
processPushToTalkKey(isKeyPressed: isKeyPressed)
guard let hotkey = activeHotkey else { return }
var isKeyPressed = false
switch hotkey {
case .rightOption, .leftOption:
isKeyPressed = flags.contains(.option)
case .leftControl, .rightControl:
isKeyPressed = flags.contains(.control)
case .fn:
isKeyPressed = flags.contains(.function)
// Debounce Fn key
pendingFnKeyState = isKeyPressed
fnDebounceTask?.cancel()
fnDebounceTask = Task { [pendingState = isKeyPressed] in
try? await Task.sleep(nanoseconds: 75_000_000) // 75ms
if pendingFnKeyState == pendingState {
await MainActor.run {
self.processKeyPress(isKeyPressed: pendingState)
}
}
}
return
case .rightCommand:
isKeyPressed = flags.contains(.command)
case .rightShift:
isKeyPressed = flags.contains(.shift)
case .custom, .none:
return // Should not reach here
}
processKeyPress(isKeyPressed: isKeyPressed)
}
private func processPushToTalkKey(isKeyPressed: Bool) {
private func processKeyPress(isKeyPressed: Bool) {
guard isKeyPressed != currentKeyState else { return }
currentKeyState = isKeyPressed
// Key is pressed down
if isKeyPressed {
keyPressStartTime = Date()
// If we're in hands-free mode, stop recording
if isHandsFreeMode {
isHandsFreeMode = false
Task { @MainActor in await whisperState.handleToggleMiniRecorder() }
return
}
// Show recorder if not already visible
if !whisperState.isMiniRecorderVisible {
Task { @MainActor in await whisperState.handleToggleMiniRecorder() }
}
}
// Key is released
else {
} else {
let now = Date()
// Calculate press duration
if let startTime = keyPressStartTime {
let pressDuration = now.timeIntervalSince(startTime)
if pressDuration < briefPressThreshold {
// For brief presses, enter hands-free mode
isHandsFreeMode = true
// Continue recording - do nothing on release
} else {
// For longer presses, stop and transcribe
Task { @MainActor in await whisperState.handleToggleMiniRecorder() }
}
}
@ -219,41 +269,66 @@ class HotkeyManager: ObservableObject {
}
}
func updateShortcutStatus() {
isShortcutConfigured = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil
if isShortcutConfigured {
setupShortcutHandler()
setupKeyMonitor()
} else {
removeKeyMonitor()
}
}
private func setupShortcutHandler() {
KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder) { [weak self] in
Task { @MainActor in
await self?.handleShortcutTriggered()
}
}
}
private func handleShortcutTriggered() async {
// Check cooldown
private func handleCustomShortcutKeyDown() async {
if let lastTrigger = lastShortcutTriggerTime,
Date().timeIntervalSince(lastTrigger) < shortcutCooldownInterval {
return // Still in cooldown period
return
}
// Update last trigger time
guard !shortcutCurrentKeyState else { return }
shortcutCurrentKeyState = true
lastShortcutTriggerTime = Date()
shortcutKeyPressStartTime = Date()
// Handle the shortcut
await whisperState.handleToggleMiniRecorder()
if isShortcutHandsFreeMode {
isShortcutHandsFreeMode = false
await whisperState.handleToggleMiniRecorder()
return
}
if !whisperState.isMiniRecorderVisible {
await whisperState.handleToggleMiniRecorder()
}
}
private func handleCustomShortcutKeyUp() async {
guard shortcutCurrentKeyState else { return }
shortcutCurrentKeyState = false
let now = Date()
if let startTime = shortcutKeyPressStartTime {
let pressDuration = now.timeIntervalSince(startTime)
if pressDuration < briefPressThreshold {
isShortcutHandsFreeMode = true
} else {
await whisperState.handleToggleMiniRecorder()
}
}
shortcutKeyPressStartTime = nil
}
// Computed property for backward compatibility with UI
var isShortcutConfigured: Bool {
let isHotkey1Configured = (selectedHotkey1 == .custom) ? (KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil) : true
let isHotkey2Configured = (selectedHotkey2 == .custom) ? (KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder2) != nil) : true
return isHotkey1Configured && isHotkey2Configured
}
func updateShortcutStatus() {
// Called when a custom shortcut changes
if selectedHotkey1 == .custom || selectedHotkey2 == .custom {
setupHotkeyMonitoring()
}
}
deinit {
Task { @MainActor in
removeKeyMonitor()
removeAllMonitoring()
}
}
}

View File

@ -6,8 +6,9 @@ import LaunchAtLogin
struct GeneralSettings: Codable {
let toggleMiniRecorderShortcut: KeyboardShortcuts.Shortcut?
let isPushToTalkEnabled: Bool?
let pushToTalkKeyRawValue: String?
let toggleMiniRecorderShortcut2: KeyboardShortcuts.Shortcut?
let selectedHotkey1RawValue: String?
let selectedHotkey2RawValue: String?
let launchAtLoginEnabled: Bool?
let isMenuBarOnly: Bool?
let useAppleScriptPaste: Bool?
@ -37,8 +38,7 @@ class ImportExportService {
private let dictionaryItemsKey = "CustomDictionaryItems"
private let wordReplacementsKey = "wordReplacements"
private let keyIsPushToTalkEnabled = "isPushToTalkEnabled"
private let keyPushToTalkKey = "pushToTalkKey"
private let keyIsMenuBarOnly = "IsMenuBarOnly"
private let keyUseAppleScriptPaste = "UseAppleScriptPaste"
private let keyRecorderType = "RecorderType"
@ -79,8 +79,9 @@ class ImportExportService {
let generalSettingsToExport = GeneralSettings(
toggleMiniRecorderShortcut: KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder),
isPushToTalkEnabled: hotkeyManager.isPushToTalkEnabled,
pushToTalkKeyRawValue: hotkeyManager.pushToTalkKey.rawValue,
toggleMiniRecorderShortcut2: KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder2),
selectedHotkey1RawValue: hotkeyManager.selectedHotkey1.rawValue,
selectedHotkey2RawValue: hotkeyManager.selectedHotkey2.rawValue,
launchAtLoginEnabled: LaunchAtLogin.isEnabled,
isMenuBarOnly: menuBarManager.isMenuBarOnly,
useAppleScriptPaste: UserDefaults.standard.bool(forKey: keyUseAppleScriptPaste),
@ -206,12 +207,16 @@ class ImportExportService {
if let shortcut = general.toggleMiniRecorderShortcut {
KeyboardShortcuts.setShortcut(shortcut, for: .toggleMiniRecorder)
}
if let pttEnabled = general.isPushToTalkEnabled {
hotkeyManager.isPushToTalkEnabled = pttEnabled
if let shortcut2 = general.toggleMiniRecorderShortcut2 {
KeyboardShortcuts.setShortcut(shortcut2, for: .toggleMiniRecorder2)
}
if let pttKeyRaw = general.pushToTalkKeyRawValue,
let pttKey = HotkeyManager.PushToTalkKey(rawValue: pttKeyRaw) {
hotkeyManager.pushToTalkKey = pttKey
if let hotkeyRaw = general.selectedHotkey1RawValue,
let hotkey = HotkeyManager.HotkeyOption(rawValue: hotkeyRaw) {
hotkeyManager.selectedHotkey1 = hotkey
}
if let hotkeyRaw2 = general.selectedHotkey2RawValue,
let hotkey2 = HotkeyManager.HotkeyOption(rawValue: hotkeyRaw2) {
hotkeyManager.selectedHotkey2 = hotkey2
}
if let launch = general.launchAtLoginEnabled {
LaunchAtLogin.isEnabled = launch

View File

@ -5,7 +5,6 @@ import KeyboardShortcuts
// ViewType enum with all cases
enum ViewType: String, CaseIterable {
case metrics = "Dashboard"
case record = "Record Audio"
case transcribeAudio = "Transcribe Audio"
case history = "History"
case models = "AI Models"
@ -20,7 +19,6 @@ enum ViewType: String, CaseIterable {
var icon: String {
switch self {
case .metrics: return "gauge.medium"
case .record: return "mic.circle.fill"
case .transcribeAudio: return "waveform.circle.fill"
case .history: return "doc.text.fill"
case .models: return "brain.head.profile"
@ -193,6 +191,8 @@ struct ContentView: View {
.frame(minWidth: 940, minHeight: 730)
.onAppear {
hasLoadedData = true
// Initialize hotkey monitoring after the app is ready
hotkeyManager.startHotkeyMonitoring()
}
.onReceive(NotificationCenter.default.publisher(for: .navigateToDestination)) { notification in
print("ContentView: Received navigation notification")
@ -235,13 +235,12 @@ struct ContentView: View {
MetricsView(skipSetupCheck: true)
} else {
MetricsSetupView()
.environmentObject(hotkeyManager)
}
case .models:
ModelManagementView(whisperState: whisperState)
case .enhancement:
EnhancementSettingsView()
case .record:
RecordView()
case .transcribeAudio:
AudioTranscribeView()
case .history:

View File

@ -3,6 +3,7 @@ import KeyboardShortcuts
struct MetricsSetupView: View {
@EnvironmentObject private var whisperState: WhisperState
@EnvironmentObject private var hotkeyManager: HotkeyManager
@State private var isAccessibilityEnabled = AXIsProcessTrusted()
@State private var isScreenRecordingEnabled = CGPreflightScreenCaptureAccess()
@ -67,7 +68,7 @@ struct MetricsSetupView: View {
switch index {
case 0:
stepInfo = (
isCompleted: KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil,
isCompleted: hotkeyManager.selectedHotkey1 != .none,
icon: "command",
title: "Set Keyboard Shortcut",
description: "Use VoiceInk anywhere with a shortcut."
@ -149,7 +150,7 @@ struct MetricsSetupView: View {
openModelManagement()
} else {
// Handle different permission requests based on which one is missing
if KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) == nil {
if hotkeyManager.selectedHotkey1 == .none {
openSettings()
} else if !AXIsProcessTrusted() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
@ -166,7 +167,7 @@ struct MetricsSetupView: View {
}
private func getActionButtonTitle() -> String {
if KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) == nil {
if hotkeyManager.selectedHotkey1 == .none {
return "Configure Shortcut"
} else if !AXIsProcessTrusted() {
return "Enable Accessibility"
@ -185,7 +186,7 @@ struct MetricsSetupView: View {
}
private var isShortcutAndAccessibilityGranted: Bool {
KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil &&
hotkeyManager.selectedHotkey1 != .none &&
AXIsProcessTrusted() &&
CGPreflightScreenCaptureAccess()
}

View File

@ -203,32 +203,13 @@ struct OnboardingPermissionsView: View {
// Keyboard shortcut recorder (only shown for keyboard shortcut step)
if permissions[currentPermissionIndex].type == .keyboardShortcut {
VStack(spacing: 16) {
if hotkeyManager.isShortcutConfigured {
if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) {
KeyboardShortcutView(shortcut: shortcut)
.scaleEffect(1.2)
}
}
VStack(spacing: 16) {
KeyboardShortcuts.Recorder("Set Shortcut:", name: .toggleMiniRecorder) { newShortcut in
withAnimation {
if newShortcut != nil {
permissionStates[currentPermissionIndex] = true
showAnimation = true
} else {
permissionStates[currentPermissionIndex] = false
showAnimation = false
}
hotkeyManager.updateShortcutStatus()
}
}
.controlSize(.large)
SkipButton(text: "Skip for now") {
moveToNext()
}
hotkeyView(
binding: $hotkeyManager.selectedHotkey1,
shortcutName: .toggleMiniRecorder
) { isConfigured in
withAnimation {
permissionStates[currentPermissionIndex] = isConfigured
showAnimation = isConfigured
}
}
.scaleEffect(scale)
@ -424,4 +405,73 @@ struct OnboardingPermissionsView: View {
return permissionStates[currentPermissionIndex] ? "Continue" : "Enable Access"
}
}
@ViewBuilder
private func hotkeyView(
binding: Binding<HotkeyManager.HotkeyOption>,
shortcutName: KeyboardShortcuts.Name,
onConfigured: @escaping (Bool) -> Void
) -> some View {
VStack(spacing: 16) {
HStack(spacing: 12) {
Spacer()
Text("Shortcut:")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white.opacity(0.8))
Menu {
ForEach(HotkeyManager.HotkeyOption.allCases, id: \.self) { option in
if option != .none && option != .custom { // Exclude 'None' and 'Custom' from the list
Button(action: {
binding.wrappedValue = option
onConfigured(option.isModifierKey)
}) {
HStack {
Text(option.displayName)
if binding.wrappedValue == option {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
} label: {
HStack(spacing: 8) {
Text(binding.wrappedValue.displayName)
.foregroundColor(.white)
.font(.system(size: 16, weight: .medium))
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
}
.menuStyle(.borderlessButton)
Spacer()
}
if binding.wrappedValue == .custom {
KeyboardShortcuts.Recorder(for: shortcutName) { newShortcut in
onConfigured(newShortcut != nil)
}
.controlSize(.large)
}
}
.padding()
.background(Color.white.opacity(0.05))
.cornerRadius(12)
.onChange(of: binding.wrappedValue) { newValue in
onConfigured(newValue != .none)
}
}
}

View File

@ -35,13 +35,26 @@ struct OnboardingTutorialView: View {
// Keyboard shortcut display
VStack(alignment: .leading, spacing: 20) {
Text("Your Shortcut")
.font(.system(size: 28, weight: .semibold, design: .rounded))
.foregroundColor(.white)
HStack {
Text("Your Shortcut")
.font(.system(size: 28, weight: .semibold, design: .rounded))
.foregroundColor(.white)
}
if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) {
if hotkeyManager.selectedHotkey1 == .custom,
let shortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) {
KeyboardShortcutView(shortcut: shortcut)
.scaleEffect(1.2)
} else if hotkeyManager.selectedHotkey1 != .none && hotkeyManager.selectedHotkey1 != .custom {
Text(hotkeyManager.selectedHotkey1.displayName)
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundColor(.accentColor)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
}
}
@ -148,6 +161,7 @@ struct OnboardingTutorialView: View {
}
}
.onAppear {
hotkeyManager.startHotkeyMonitoring()
animateIn()
isFocused = true
}
@ -156,9 +170,9 @@ struct OnboardingTutorialView: View {
private func getInstructionText(for step: Int) -> String {
switch step {
case 1: return "Click the text area on the right"
case 2: return "Press your keyboard shortcut"
case 2: return "Press your shortcut key"
case 3: return "Speak something"
case 4: return "Press your keyboard shortcut again"
case 4: return "Press your shortcut key again"
default: return ""
}
}

View File

@ -1,299 +0,0 @@
import SwiftUI
import KeyboardShortcuts
import AppKit
struct RecordView: View {
@EnvironmentObject var whisperState: WhisperState
@EnvironmentObject var hotkeyManager: HotkeyManager
@Environment(\.colorScheme) private var colorScheme
@ObservedObject private var mediaController = MediaController.shared
private var hasShortcutSet: Bool {
KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil
}
var body: some View {
ScrollView(showsIndicators: false) {
mainContent
}
.background(Color(NSColor.controlBackgroundColor))
}
private var mainContent: some View {
VStack(spacing: 48) {
heroSection
controlsSection
}
.padding(32)
}
private var heroSection: some View {
VStack(spacing: 20) {
AppIconView()
titleSection
}
}
private var titleSection: some View {
VStack(spacing: 8) {
Text("VOICEINK")
.font(.system(size: 42, weight: .bold))
if whisperState.currentTranscriptionModel != nil {
Text("Powered by Whisper AI")
.font(.system(size: 15))
.foregroundColor(.secondary)
}
}
}
private var controlsSection: some View {
VStack(spacing: 32) {
compactControlsCard
instructionsCard
}
}
private var compactControlsCard: some View {
HStack(spacing: 32) {
shortcutSection
if hasShortcutSet {
Divider()
.frame(height: 40)
pushToTalkSection
Divider()
.frame(height: 40)
// Settings section
VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: $whisperState.isAutoCopyEnabled) {
HStack {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.secondary)
Text("Auto-copy to clipboard")
.font(.subheadline.weight(.medium))
}
}
.toggleStyle(.switch)
Toggle(isOn: .init(
get: { SoundManager.shared.isEnabled },
set: { SoundManager.shared.isEnabled = $0 }
)) {
HStack {
Image(systemName: "speaker.wave.2")
.foregroundColor(.secondary)
Text("Sound feedback")
.font(.subheadline.weight(.medium))
}
}
.toggleStyle(.switch)
Toggle(isOn: $mediaController.isSystemMuteEnabled) {
HStack {
Image(systemName: "speaker.slash")
.foregroundColor(.secondary)
Text("Mute system audio during recording")
.font(.subheadline.weight(.medium))
}
}
.toggleStyle(.switch)
.help("Automatically mute system audio when recording starts and restore when recording stops")
}
}
}
.padding(24)
.background(CardBackground(isSelected: false))
}
private var shortcutSection: some View {
VStack(spacing: 12) {
if hasShortcutSet {
if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) {
KeyboardShortcutView(shortcut: shortcut)
.scaleEffect(1.2)
}
} else {
Image(systemName: "keyboard.badge.exclamationmark")
.font(.system(size: 28))
.foregroundColor(.orange)
}
Button(action: {
NotificationCenter.default.post(
name: .navigateToDestination,
object: nil,
userInfo: ["destination": "Settings"]
)
}) {
Text(hasShortcutSet ? "Change" : "Set Shortcut")
.font(.subheadline.weight(.medium))
.foregroundColor(.accentColor)
}
.buttonStyle(.plain)
}
}
private var pushToTalkSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Push-to-Talk")
.font(.subheadline.weight(.medium))
if hotkeyManager.isPushToTalkEnabled {
SelectableKeyCapView(
text: getKeySymbol(for: hotkeyManager.pushToTalkKey),
subtext: getKeyText(for: hotkeyManager.pushToTalkKey),
isSelected: true
)
}
}
}
}
private func getKeySymbol(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return ""
case .leftOption: return ""
case .leftControl: return ""
case .rightControl: return ""
case .fn: return "Fn"
case .rightCommand: return ""
case .rightShift: return ""
}
}
private func getKeyText(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return "Right Option"
case .leftOption: return "Left Option"
case .leftControl: return "Left Control"
case .rightControl: return "Right Control"
case .fn: return "Function"
case .rightCommand: return "Right Command"
case .rightShift: return "Right Shift"
}
}
private var instructionsCard: some View {
VStack(alignment: .leading, spacing: 28) {
Text("How it works")
.font(.title3.weight(.bold))
VStack(alignment: .leading, spacing: 24) {
ForEach(getInstructions(), id: \.title) { instruction in
InstructionRow(instruction: instruction)
}
Divider()
.padding(.vertical, 4)
afterRecordingSection
}
}
.padding(28)
.background(CardBackground(isSelected: false))
}
private var afterRecordingSection: some View {
VStack(alignment: .leading, spacing: 16) {
Text("After recording")
.font(.headline)
VStack(alignment: .leading, spacing: 12) {
if whisperState.isAutoCopyEnabled {
InfoRow(icon: "doc.on.clipboard", text: "Copied to clipboard")
}
InfoRow(icon: "text.cursor", text: "Pasted at cursor position")
}
}
}
private func getInstructions() -> [(icon: String, title: String, description: String)] {
let keyName: String
switch hotkeyManager.pushToTalkKey {
case .rightOption:
keyName = "right Option (⌥)"
case .leftOption:
keyName = "left Option (⌥)"
case .leftControl:
keyName = "left Control (⌃)"
case .rightControl:
keyName = "right Control (⌃)"
case .fn:
keyName = "Fn"
case .rightCommand:
keyName = "right Command (⌘)"
case .rightShift:
keyName = "right Shift (⇧)"
}
let activateDescription = hotkeyManager.isPushToTalkEnabled ?
"Hold the \(keyName) key" :
"Press your configured shortcut"
let finishDescription = hotkeyManager.isPushToTalkEnabled ?
"Release the \(keyName) key to stop and process" :
"Press the shortcut again to stop"
return [
(
icon: "mic.circle.fill",
title: "Start Recording",
description: activateDescription
),
(
icon: "waveform",
title: "Speak Clearly",
description: "Talk into your microphone naturally"
),
(
icon: "stop.circle.fill",
title: "Finish Up",
description: finishDescription
)
]
}
}
// Simplified InstructionRow
struct InstructionRow: View {
let instruction: (icon: String, title: String, description: String)
var body: some View {
HStack(alignment: .top, spacing: 16) {
Image(systemName: instruction.icon)
.font(.system(size: 20))
.foregroundColor(.accentColor)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(instruction.title)
.font(.subheadline.weight(.medium))
Text(instruction.description)
.font(.subheadline)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
// Simplified InfoRow
struct InfoRow: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundColor(.secondary)
Text(text)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}

View File

@ -12,6 +12,7 @@ struct SettingsView: View {
@EnvironmentObject private var whisperState: WhisperState
@EnvironmentObject private var enhancementService: AIEnhancementService
@StateObject private var deviceManager = AudioDeviceManager.shared
@ObservedObject private var mediaController = MediaController.shared
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = true
@State private var showResetOnboardingAlert = false
@State private var currentShortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder)
@ -19,83 +20,143 @@ struct SettingsView: View {
var body: some View {
ScrollView {
VStack(spacing: 24) {
// Keyboard Shortcuts Section first
// Hotkey Selection Section
SettingsSection(
icon: currentShortcut != nil ? "keyboard" : "keyboard.badge.exclamationmark",
title: "Keyboard Shortcuts",
subtitle: currentShortcut != nil ? "Shortcut configured" : "Shortcut required",
showWarning: currentShortcut == nil
icon: "command.circle",
title: "VoiceInk Shortcut",
subtitle: "Choose how you want to trigger VoiceInk"
) {
VStack(alignment: .leading, spacing: 18) {
hotkeyView(
title: "Hotkey 1",
binding: $hotkeyManager.selectedHotkey1,
shortcutName: .toggleMiniRecorder
)
// Hotkey 2 Configuration (Conditional)
if hotkeyManager.selectedHotkey2 != .none {
Divider()
hotkeyView(
title: "Hotkey 2",
binding: $hotkeyManager.selectedHotkey2,
shortcutName: .toggleMiniRecorder2,
isRemovable: true,
onRemove: {
withAnimation { hotkeyManager.selectedHotkey2 = .none }
}
)
}
// "Add another hotkey" button
if hotkeyManager.selectedHotkey2 == .none {
HStack {
Spacer()
Button(action: {
withAnimation { hotkeyManager.selectedHotkey2 = .rightOption }
}) {
Label("Add another hotkey", systemImage: "plus.circle.fill")
}
.buttonStyle(.plain)
.foregroundColor(.accentColor)
}
}
Text("Quick tap to start hands-free recording (tap again to stop). Press and hold for push-to-talk (release to stop recording).")
.font(.system(size: 12))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 8)
}
}
// Recording Feedback Section
SettingsSection(
icon: "speaker.wave.2.bubble.left.fill",
title: "Recording Feedback",
subtitle: "Customize audio and system feedback"
) {
VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: $whisperState.isAutoCopyEnabled) {
Text("Auto-copy to clipboard")
}
.toggleStyle(.switch)
Toggle(isOn: .init(
get: { SoundManager.shared.isEnabled },
set: { SoundManager.shared.isEnabled = $0 }
)) {
Text("Sound feedback")
}
.toggleStyle(.switch)
Toggle(isOn: $mediaController.isSystemMuteEnabled) {
Text("Mute system audio during recording")
}
.toggleStyle(.switch)
.help("Automatically mute system audio when recording starts and restore when recording stops")
}
}
// Recorder Preference Section
SettingsSection(
icon: "rectangle.on.rectangle",
title: "Recorder Style",
subtitle: "Choose your preferred recorder interface"
) {
VStack(alignment: .leading, spacing: 8) {
if currentShortcut == nil {
Text("⚠️ Please set a keyboard shortcut to use VoiceInk")
.foregroundColor(.orange)
.font(.subheadline)
}
Text("Select how you want the recorder to appear on your screen.")
.settingsDescription()
HStack(alignment: .center, spacing: 16) {
if let shortcut = currentShortcut {
KeyboardShortcutView(shortcut: shortcut)
} else {
Text("Not Set")
.foregroundColor(.secondary)
.italic()
}
Button(action: {
KeyboardShortcuts.reset(.toggleMiniRecorder)
currentShortcut = nil
}) {
Image(systemName: "arrow.counterclockwise")
.foregroundColor(.secondary)
}
.buttonStyle(.borderless)
.help("Reset Shortcut")
}
KeyboardShortcuts.Recorder("Change Shortcut:", name: .toggleMiniRecorder) { newShortcut in
currentShortcut = newShortcut
hotkeyManager.updateShortcutStatus()
}
.controlSize(.large)
Divider()
.padding(.vertical, 4)
VStack(alignment: .leading, spacing: 6) {
Toggle("Enable Push-to-Talk", isOn: $hotkeyManager.isPushToTalkEnabled)
.toggleStyle(.switch)
if hotkeyManager.isPushToTalkEnabled {
if currentShortcut == nil {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text("Please set a keyboard shortcut first to use Push-to-Talk")
.settingsDescription()
.foregroundColor(.orange)
}
.padding(.vertical, 4)
}
VStack(alignment: .leading, spacing: 12) {
Text("Choose Push-to-Talk Key")
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
PushToTalkKeySelector(selectedKey: $hotkeyManager.pushToTalkKey)
.padding(.vertical, 4)
Text("Quick tap the key once to start hands-free recording (tap again to stop).\nPress and hold the key for push-to-talk (release to stop recording).")
.font(.system(size: 13))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.top, 4)
}
Picker("Recorder Style", selection: $whisperState.recorderType) {
Text("Notch Recorder").tag("notch")
Text("Mini Recorder").tag("mini")
}
.pickerStyle(.radioGroup)
.padding(.vertical, 4)
}
}
// Paste Method Section
SettingsSection(
icon: "doc.on.clipboard",
title: "Paste Method",
subtitle: "Choose how text is pasted"
) {
VStack(alignment: .leading, spacing: 8) {
Text("Select the method used to paste text. Use AppleScript if you have a non-standard keyboard layout.")
.settingsDescription()
Toggle("Use AppleScript Paste Method", isOn: Binding(
get: { UserDefaults.standard.bool(forKey: "UseAppleScriptPaste") },
set: { UserDefaults.standard.set($0, forKey: "UseAppleScriptPaste") }
))
.toggleStyle(.switch)
}
}
// App Appearance Section
SettingsSection(
icon: "dock.rectangle",
title: "App Appearance",
subtitle: "Dock and Menu Bar options"
) {
VStack(alignment: .leading, spacing: 8) {
Text("Choose how VoiceInk appears in your system.")
.settingsDescription()
Toggle("Hide Dock Icon (Menu Bar Only)", isOn: $menuBarManager.isMenuBarOnly)
.toggleStyle(.switch)
}
}
// Audio Cleanup Section
SettingsSection(
icon: "trash.circle",
title: "Audio Cleanup",
subtitle: "Manage recording storage"
) {
AudioCleanupSettingsView()
}
// Startup Section
SettingsSection(
@ -130,68 +191,25 @@ struct SettingsView: View {
.disabled(!updaterViewModel.canCheckForUpdates)
}
}
// App Appearance Section
// Reset Onboarding Section
SettingsSection(
icon: "dock.rectangle",
title: "App Appearance",
subtitle: "Dock and Menu Bar options"
icon: "arrow.counterclockwise",
title: "Reset Onboarding",
subtitle: "View the introduction again"
) {
VStack(alignment: .leading, spacing: 8) {
Text("Choose how VoiceInk appears in your system.")
Text("Reset the onboarding process to view the app introduction again.")
.settingsDescription()
Toggle("Hide Dock Icon (Menu Bar Only)", isOn: $menuBarManager.isMenuBarOnly)
.toggleStyle(.switch)
}
}
// Paste Method Section
SettingsSection(
icon: "doc.on.clipboard",
title: "Paste Method",
subtitle: "Choose how text is pasted"
) {
VStack(alignment: .leading, spacing: 8) {
Text("Select the method used to paste text. Use AppleScript if you have a non-standard keyboard layout.")
.settingsDescription()
Toggle("Use AppleScript Paste Method", isOn: Binding(
get: { UserDefaults.standard.bool(forKey: "UseAppleScriptPaste") },
set: { UserDefaults.standard.set($0, forKey: "UseAppleScriptPaste") }
))
.toggleStyle(.switch)
}
}
// Recorder Preference Section
SettingsSection(
icon: "rectangle.on.rectangle",
title: "Recorder Style",
subtitle: "Choose your preferred recorder interface"
) {
VStack(alignment: .leading, spacing: 8) {
Text("Select how you want the recorder to appear on your screen.")
.settingsDescription()
Picker("Recorder Style", selection: $whisperState.recorderType) {
Text("Notch Recorder").tag("notch")
Text("Mini Recorder").tag("mini")
Button("Reset Onboarding") {
showResetOnboardingAlert = true
}
.pickerStyle(.radioGroup)
.padding(.vertical, 4)
.buttonStyle(.bordered)
.controlSize(.large)
}
}
// Audio Cleanup Section
SettingsSection(
icon: "trash.circle",
title: "Audio Cleanup",
subtitle: "Manage recording storage"
) {
AudioCleanupSettingsView()
}
// Data Management Section
SettingsSection(
icon: "arrow.up.arrow.down.circle",
@ -237,24 +255,6 @@ struct SettingsView: View {
}
}
}
// Reset Onboarding Section
SettingsSection(
icon: "arrow.counterclockwise",
title: "Reset Onboarding",
subtitle: "View the introduction again"
) {
VStack(alignment: .leading, spacing: 8) {
Text("Reset the onboarding process to view the app introduction again.")
.settingsDescription()
Button("Reset Onboarding") {
showResetOnboardingAlert = true
}
.buttonStyle(.bordered)
.controlSize(.large)
}
}
}
.padding(.horizontal, 20)
.padding(.vertical, 6)
@ -270,22 +270,68 @@ struct SettingsView: View {
}
}
private func getPushToTalkDescription() -> String {
switch hotkeyManager.pushToTalkKey {
case .rightOption:
return "Using Right Option (⌥) key to quickly start recording. Release to stop."
case .leftOption:
return "Using Left Option (⌥) key to quickly start recording. Release to stop."
case .leftControl:
return "Using Left Control (⌃) key to quickly start recording. Release to stop."
case .rightControl:
return "Using Right Control (⌃) key to quickly start recording. Release to stop."
case .fn:
return "Using Function (Fn) key to quickly start recording. Release to stop."
case .rightCommand:
return "Using Right Command (⌘) key to quickly start recording. Release to stop."
case .rightShift:
return "Using Right Shift (⇧) key to quickly start recording. Release to stop."
@ViewBuilder
private func hotkeyView(
title: String,
binding: Binding<HotkeyManager.HotkeyOption>,
shortcutName: KeyboardShortcuts.Name,
isRemovable: Bool = false,
onRemove: (() -> Void)? = nil
) -> some View {
HStack(spacing: 12) {
Text(title)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
Menu {
ForEach(HotkeyManager.HotkeyOption.allCases, id: \.self) { option in
Button(action: {
binding.wrappedValue = option
}) {
HStack {
Text(option.displayName)
if binding.wrappedValue == option {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
} label: {
HStack(spacing: 8) {
Text(binding.wrappedValue.displayName)
.foregroundColor(.primary)
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 10))
.foregroundColor(.secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
}
.menuStyle(.borderlessButton)
if binding.wrappedValue == .custom {
KeyboardShortcuts.Recorder(for: shortcutName)
.controlSize(.small)
}
Spacer()
if isRemovable {
Button(action: {
onRemove?()
}) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
}
.buttonStyle(.plain)
}
}
}
}
@ -354,108 +400,4 @@ extension Text {
}
}
struct PushToTalkKeySelector: View {
@Binding var selectedKey: HotkeyManager.PushToTalkKey
var body: some View {
HStack(spacing: 12) {
ForEach(HotkeyManager.PushToTalkKey.allCases, id: \.self) { key in
Button(action: {
withAnimation(.spring(response: 0.2, dampingFraction: 0.6)) {
selectedKey = key
}
}) {
SelectableKeyCapView(
text: getKeySymbol(for: key),
subtext: getKeyText(for: key),
isSelected: selectedKey == key
)
}
.buttonStyle(.plain)
}
}
}
private func getKeySymbol(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return ""
case .leftOption: return ""
case .leftControl: return ""
case .rightControl: return ""
case .fn: return "Fn"
case .rightCommand: return ""
case .rightShift: return ""
}
}
private func getKeyText(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return "Right Option"
case .leftOption: return "Left Option"
case .leftControl: return "Left Control"
case .rightControl: return "Right Control"
case .fn: return "Function"
case .rightCommand: return "Right Command"
case .rightShift: return "Right Shift"
}
}
}
struct SelectableKeyCapView: View {
let text: String
let subtext: String
let isSelected: Bool
@Environment(\.colorScheme) private var colorScheme
private var keyColor: Color {
if isSelected {
return colorScheme == .dark ? Color.accentColor.opacity(0.3) : Color.accentColor.opacity(0.2)
}
return colorScheme == .dark ? Color(white: 0.2) : .white
}
var body: some View {
VStack(spacing: 4) {
Text(text)
.font(.system(size: 20, weight: .semibold, design: .rounded))
.foregroundColor(colorScheme == .dark ? .white : .black)
.frame(width: 44, height: 44)
.background(
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(keyColor)
// Highlight overlay
if isSelected {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color.accentColor, lineWidth: 2)
}
// Key surface highlight
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color.white.opacity(colorScheme == .dark ? 0.1 : 0.4),
Color.white.opacity(0)
]),
startPoint: .top,
endPoint: .bottom
)
)
}
)
.shadow(
color: Color.black.opacity(colorScheme == .dark ? 0.5 : 0.2),
radius: 2,
x: 0,
y: 1
)
Text(subtext)
.font(.system(size: 11))
.foregroundColor(.secondary)
}
}
}

View File

@ -115,8 +115,6 @@ struct VoiceInkApp: App {
.environmentObject(aiService)
.environmentObject(enhancementService)
.frame(minWidth: 880, minHeight: 780)
.cornerRadius(16)
.clipped()
.background(WindowAccessor { window in
// Ensure this is called only once or is idempotent
if window.title != "VoiceInk Onboarding" { // Prevent re-configuration

View File

@ -22,18 +22,18 @@ class WindowManager {
}
func configureOnboardingPanel(_ window: NSWindow) {
window.styleMask = [.borderless, .fullSizeContentView, .resizable]
window.isMovableByWindowBackground = true
window.level = .normal
window.styleMask = [.titled, .fullSizeContentView, .resizable]
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.isMovableByWindowBackground = true
window.level = .normal
window.backgroundColor = .clear
window.isReleasedWhenClosed = false
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.title = "VoiceInk Onboarding"
window.isOpaque = false
window.minSize = NSSize(width: 900, height: 780)
window.orderFrontRegardless()
window.makeKeyAndOrderFront(nil)
}
func createMainWindow(contentView: NSView) -> NSWindow {