Switched to previous version of Double-press lock recording with brief delay consideration
This commit is contained in:
parent
357e09b173
commit
9003e93b5d
@ -7,11 +7,11 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
E129E77B2D943393009322D9 /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E129E77A2D943393009322D9 /* whisper.xcframework */; };
|
||||
E129E77C2D943393009322D9 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E129E77A2D943393009322D9 /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = E1A261112CC143AC00B233D1 /* KeyboardShortcuts */; };
|
||||
E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; };
|
||||
E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD45E2CC544F100303ECB /* Sparkle */; };
|
||||
E1B1FDBE2D8C403100ADD08E /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */; };
|
||||
E1B1FDBF2D8C403100ADD08E /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -32,13 +32,13 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
E1B1FDC02D8C403100ADD08E /* Embed Frameworks */ = {
|
||||
E129E77D2D943393009322D9 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
E1B1FDBF2D8C403100ADD08E /* whisper.xcframework in Embed Frameworks */,
|
||||
E129E77C2D943393009322D9 /* whisper.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -49,6 +49,7 @@
|
||||
E11473B02CBE0F0A00318EE4 /* VoiceInk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoiceInk.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E11473C32CBE0F0B00318EE4 /* VoiceInkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E11473CD2CBE0F0B00318EE4 /* VoiceInkUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E129E77A2D943393009322D9 /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = "<group>"; };
|
||||
E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@ -75,7 +76,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1B1FDBE2D8C403100ADD08E /* whisper.xcframework in Frameworks */,
|
||||
E129E77B2D943393009322D9 /* whisper.xcframework in Frameworks */,
|
||||
E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */,
|
||||
E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */,
|
||||
E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */,
|
||||
@ -123,6 +124,7 @@
|
||||
E114741C2CBE1DE200318EE4 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E129E77A2D943393009322D9 /* whisper.xcframework */,
|
||||
E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
@ -138,7 +140,7 @@
|
||||
E11473AC2CBE0F0A00318EE4 /* Sources */,
|
||||
E11473AD2CBE0F0A00318EE4 /* Frameworks */,
|
||||
E11473AE2CBE0F0A00318EE4 /* Resources */,
|
||||
E1B1FDC02D8C403100ADD08E /* Embed Frameworks */,
|
||||
E129E77D2D943393009322D9 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
||||
@ -19,13 +19,6 @@ extension KeyboardShortcuts.Name {
|
||||
static let selectPrompt9 = Self("selectPrompt9")
|
||||
}
|
||||
|
||||
// State machine enum for recorder state
|
||||
enum RecorderState {
|
||||
case idle // Not recording, recorder not visible
|
||||
case recording // Actively recording with key held down
|
||||
case lockedRecording // Recording in locked mode (after double press)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class HotkeyManager: ObservableObject {
|
||||
@Published var isListening = false
|
||||
@ -50,11 +43,11 @@ class HotkeyManager: ObservableObject {
|
||||
private var runLoopSource: CFRunLoopSource?
|
||||
private var visibilityTask: Task<Void, Never>?
|
||||
|
||||
// State machine properties
|
||||
private var recorderState: RecorderState = .idle
|
||||
private var lastKeyPressTime: Date?
|
||||
private var keyPressStartTime: Date? // Track when key was pressed for duration calculation
|
||||
private let doublePressThreshold = 0.3 // 300ms for double-press detection
|
||||
// New properties for advanced key handling
|
||||
private var keyPressStartTime: Date?
|
||||
private var lastKeyPressEndTime: Date?
|
||||
private var isLockedRecording = false // For toggle mode after double-press
|
||||
private let doublePressThreshold = 0.3 // 300ms for faster double-press detection
|
||||
private let briefPressThreshold = 1.0 // 1000ms threshold for brief press
|
||||
|
||||
enum PushToTalkKey: String, CaseIterable {
|
||||
@ -103,8 +96,9 @@ class HotkeyManager: ObservableObject {
|
||||
|
||||
private func resetKeyStates() {
|
||||
currentKeyState = false
|
||||
lastKeyPressTime = nil
|
||||
recorderState = .idle
|
||||
keyPressStartTime = nil
|
||||
lastKeyPressEndTime = nil
|
||||
isLockedRecording = false
|
||||
}
|
||||
|
||||
private func setupVisibilityObserver() {
|
||||
@ -118,10 +112,6 @@ class HotkeyManager: ObservableObject {
|
||||
removeEscapeShortcut()
|
||||
removeEnhancementShortcut()
|
||||
removePromptShortcuts()
|
||||
// Ensure state is reset when recorder is dismissed externally
|
||||
if recorderState != .idle {
|
||||
recorderState = .idle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,73 +176,58 @@ class HotkeyManager: ObservableObject {
|
||||
|
||||
currentKeyState = isKeyPressed
|
||||
|
||||
// Key is pressed down
|
||||
if isKeyPressed {
|
||||
await handleKeyPress()
|
||||
} else {
|
||||
await handleKeyRelease()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleKeyPress() async {
|
||||
let now = Date()
|
||||
keyPressStartTime = now // Track when the key was pressed
|
||||
|
||||
switch recorderState {
|
||||
case .idle:
|
||||
// Start recording
|
||||
recorderState = .recording
|
||||
// If we're in locked recording mode, key press should stop recording
|
||||
if isLockedRecording && whisperState.isMiniRecorderVisible {
|
||||
isLockedRecording = false
|
||||
await whisperState.handleToggleMiniRecorder()
|
||||
return
|
||||
}
|
||||
|
||||
// Start timing the key press
|
||||
keyPressStartTime = Date()
|
||||
|
||||
// Check for double press
|
||||
if let lastEndTime = lastKeyPressEndTime,
|
||||
Date().timeIntervalSince(lastEndTime) < doublePressThreshold {
|
||||
// Double press detected - set locked recording mode
|
||||
isLockedRecording = true
|
||||
}
|
||||
|
||||
// Show recorder if not already visible
|
||||
if !whisperState.isMiniRecorderVisible {
|
||||
await whisperState.handleToggleMiniRecorder()
|
||||
}
|
||||
}
|
||||
// Key is released
|
||||
else {
|
||||
let now = Date()
|
||||
lastKeyPressEndTime = now
|
||||
|
||||
case .recording:
|
||||
// This shouldn't happen in normal flow
|
||||
break
|
||||
|
||||
case .lockedRecording:
|
||||
// If in locked recording, pressing the key again should stop recording
|
||||
recorderState = .idle
|
||||
await whisperState.handleToggleMiniRecorder()
|
||||
}
|
||||
|
||||
// Check for double press
|
||||
if let lastPress = lastKeyPressTime,
|
||||
now.timeIntervalSince(lastPress) < doublePressThreshold {
|
||||
// Double press detected, transition to locked recording
|
||||
recorderState = .lockedRecording
|
||||
}
|
||||
|
||||
lastKeyPressTime = now
|
||||
}
|
||||
|
||||
private func handleKeyRelease() async {
|
||||
let now = Date()
|
||||
|
||||
switch recorderState {
|
||||
case .idle:
|
||||
// This shouldn't happen in normal flow
|
||||
break
|
||||
|
||||
case .recording:
|
||||
// Check if this was a brief press
|
||||
if let startTime = keyPressStartTime,
|
||||
now.timeIntervalSince(startTime) < briefPressThreshold {
|
||||
// Brief press - dismiss without transcribing
|
||||
recorderState = .idle
|
||||
await whisperState.dismissMiniRecorder()
|
||||
} else {
|
||||
// Normal release - stop recording and transcribe
|
||||
recorderState = .idle
|
||||
await whisperState.handleToggleMiniRecorder()
|
||||
// Calculate press duration
|
||||
if let startTime = keyPressStartTime {
|
||||
let pressDuration = now.timeIntervalSince(startTime)
|
||||
|
||||
// 1. Brief press (< 1s): Delay dismissal to check for double-press
|
||||
if pressDuration < briefPressThreshold && !isLockedRecording {
|
||||
// Wait to see if this is part of a double-press
|
||||
try? await Task.sleep(nanoseconds: 200_000_000) // 200ms delay
|
||||
|
||||
// After waiting, check if we should still dismiss
|
||||
if !isLockedRecording {
|
||||
await whisperState.dismissMiniRecorder()
|
||||
}
|
||||
}
|
||||
// 2. Normal press in non-locked mode: Use handleToggleMiniRecorder to stop and transcribe
|
||||
else if !isLockedRecording && whisperState.isMiniRecorderVisible {
|
||||
await whisperState.handleToggleMiniRecorder()
|
||||
}
|
||||
// 3. If in locked mode, we don't do anything on release
|
||||
}
|
||||
|
||||
case .lockedRecording:
|
||||
// When in locked recording, key release does nothing
|
||||
// Stay in locked recording state
|
||||
break
|
||||
keyPressStartTime = nil
|
||||
}
|
||||
|
||||
keyPressStartTime = nil // Reset press start time
|
||||
}
|
||||
|
||||
private func setupEscapeShortcut() {
|
||||
@ -262,8 +237,8 @@ class HotkeyManager: ObservableObject {
|
||||
guard let self = self,
|
||||
await self.whisperState.isMiniRecorderVisible else { return }
|
||||
|
||||
// Reset state machine when using Escape key
|
||||
self.recorderState = .idle
|
||||
// Reset locked recording state when using Escape key
|
||||
self.isLockedRecording = false
|
||||
|
||||
SoundManager.shared.playEscSound()
|
||||
await self.whisperState.dismissMiniRecorder()
|
||||
@ -285,6 +260,7 @@ class HotkeyManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPromptShortcuts() {
|
||||
// Set up Command+1 through Command+9 shortcuts with proper key definitions
|
||||
KeyboardShortcuts.setShortcut(.init(.one, modifiers: .command), for: .selectPrompt1)
|
||||
@ -359,16 +335,7 @@ class HotkeyManager: ObservableObject {
|
||||
private func setupShortcutHandler() {
|
||||
KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder) { [weak self] in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Update state when using the main shortcut
|
||||
if self.recorderState == .idle {
|
||||
self.recorderState = .recording
|
||||
} else {
|
||||
self.recorderState = .idle
|
||||
}
|
||||
|
||||
await self.whisperState.handleToggleMiniRecorder()
|
||||
await self?.whisperState.handleToggleMiniRecorder()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -381,4 +348,4 @@ class HotkeyManager: ObservableObject {
|
||||
removeEnhancementShortcut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user