Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Sources/App/TranscriptedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,6 @@ class TranscriptedAppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegat
appState.contextCapture.onMeetingToggle = { [weak self] in
self?.meetingOverlayController.toggleFromHotkey()
}
appState.contextCapture.onPasteLastDictation = { [weak self] in
self?.pasteLastDictationFromSettings()
}
SpeakerNamingSheet.shared.island = notchIsland
// Only the island lists voices named on their own ("who was on
// the call"), so only then does a meeting with nobody to ask get
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/TranscriptedMenuCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// This is purely additive: every action routes through an existing app-delegate
// entry point, and none of the user's recordable dictation / meeting triggers
// (push-to-talk, hands-free, meeting, paste-last) are remapped. The commands
// (dictation, meeting) are remapped. The commands
// only fire while the app is active (a window is open), which is exactly when
// navigation, search, and in-app capture make sense; the global physical
// triggers remain the background path.
Expand Down
34 changes: 3 additions & 31 deletions Sources/Capture/ContextCaptureEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,6 @@ private final class PhysicalShortcutDetector {
onShortcut?(.dictationHandsFree, .press)
case .meeting:
onShortcut?(.meeting, .press)
case .pasteLastDictation:
onShortcut?(.pasteLastDictation, .press)
}
return nil

Expand Down Expand Up @@ -410,13 +408,6 @@ private final class PhysicalShortcutDetector {
cancelPendingModifierShortcut()
onShortcut?(.meeting, .press)
}
case .pasteLastDictation:
if hasChordUsingModifier(keyCode, in: shortcutBindings, excluding: shortcut.action) {
schedulePendingModifierShortcut(keyCode: keyCode, action: .pasteLastDictation)
} else {
cancelPendingModifierShortcut()
onShortcut?(.pasteLastDictation, .press)
}
}
return nil

Expand Down Expand Up @@ -606,8 +597,6 @@ class ContextCaptureEngine: ObservableObject {
/// the meeting subsystem is unavailable — the hotkey simply does nothing.
var onMeetingToggle: (() -> Void)?

var onPasteLastDictation: (() -> Void)?

func registerHotkey() {
guard hotkeyChangeObserver == nil else {
EventReporter.shared.capture(level: .warning, engine: "capture", event: "hotkey_already_registered",
Expand Down Expand Up @@ -767,15 +756,13 @@ class ContextCaptureEngine: ObservableObject {
handlePhysicalDictationHandsFreeComboInterrupted()
case (.meeting, .press):
handlePhysicalMeetingPress()
case (.pasteLastDictation, .press):
handlePhysicalPasteLastDictationPress()
case (.dictationHandsFree, .release), (.meeting, .release), (.pasteLastDictation, .release):
case (.dictationHandsFree, .release), (.meeting, .release):
break
case (.dictationHandsFree, .tapRelease), (.meeting, .tapRelease), (.pasteLastDictation, .tapRelease):
case (.dictationHandsFree, .tapRelease), (.meeting, .tapRelease):
break
case (.dictationPushToTalk, .comboInterrupted):
handlePhysicalDictationPushToTalkComboInterrupted()
case (.meeting, .comboInterrupted), (.pasteLastDictation, .comboInterrupted):
case (.meeting, .comboInterrupted):
break
}
}
Expand Down Expand Up @@ -807,21 +794,6 @@ class ContextCaptureEngine: ObservableObject {
onMeetingToggle?()
}

private func handlePhysicalPasteLastDictationPress() {
guard shouldAcceptHotkeyAction(.pasteLastDictation) else {
EventReporter.shared.capture(
level: .info,
engine: "capture",
event: "hotkey_repeat_ignored",
message: "Ignored rapid repeat paste-last-dictation trigger",
context: ["hotkey_id": "paste_last_dictation_physical_trigger"]
)
return
}

onPasteLastDictation?()
}

deinit {
if let observer = hotkeyChangeObserver {
NotificationCenter.default.removeObserver(observer)
Expand Down
2 changes: 0 additions & 2 deletions Sources/Capture/PhysicalShortcutMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ enum PhysicalShortcutAction: Equatable {
case dictationPushToTalk
case dictationHandsFree
case meeting
case pasteLastDictation
}

struct PhysicalShortcutBinding {
Expand Down Expand Up @@ -241,7 +240,6 @@ extension PhysicalShortcutAction {
case .dictationPushToTalk: return nil
case .dictationHandsFree: return "dictation_hands_free"
case .meeting: return "meeting_physical_trigger"
case .pasteLastDictation: return "paste_last_dictation_physical_trigger"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Speech/DictationRecordingStartOverlayPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ struct DictationEarlyReleasePresentationPolicy {
struct DictationEarlyReleaseCancelReport: Equatable {
static let engine = "dictation"
static let event = "dictation_cancelled_before_microphone_ready"
// Deliberately not "push-to-talk release": hands-free is the default
// mode, and its stop press reaches here too.
// Deliberately not "push-to-talk release": Tap to toggle (and a
// tap-kept take) stops on a press, and that press reaches here too.
static let message = "Dictation hotkey ended the session before the microphone finished opening"
/// `.error`, not `.info`: only `.error` events reach the reliability
/// counter (PostHog) and Sentry. The user asked to dictate, saw an error,
Expand Down
3 changes: 1 addition & 2 deletions Sources/Speech/DictationStartReadiness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ enum DictationStartReadinessPolicy {
///
/// Only `physical_key` is listed because it is the only trigger anything
/// emits for a global hotkey: `ContextCaptureEngine` uses it for both the
/// push-to-talk press/release pair AND the hands-free toggle (the default
/// mode). `DictationTrigger.keyboardShortcut` and `.rightOptionTap` are
/// push-to-talk press/release pair AND the Tap to toggle press. `DictationTrigger.keyboardShortcut` and `.rightOptionTap` are
/// declared but never constructed anywhere in the tree — #1744's guard
/// listed them, which read as coverage it did not have. Wire them up here
/// if something ever emits them.
Expand Down
39 changes: 9 additions & 30 deletions Sources/Support/HotkeyPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,12 @@ struct HotkeyBinding: Equatable {
let modifiers: UInt32 // Carbon modifier mask
}

enum DictationShortcutMode: String, CaseIterable, Identifiable, Hashable {
/// How one dictation take is being driven. `.pushToTalk` ends on key release;
/// `.handsFree` (raw value kept for telemetry) is a take that stays listening
/// until the next press: Tap to toggle, or a Hold or tap take kept by a tap.
enum DictationShortcutMode: String, Hashable {
case pushToTalk = "push_to_talk"
case handsFree = "hands_free"

var id: String { rawValue }

var title: String {
switch self {
case .pushToTalk:
return "Push to Talk"
case .handsFree:
return "Hands-Free"
}
}

var summary: String {
switch self {
case .pushToTalk:
return "Hold the dictation shortcut, speak, then release to paste."
case .handsFree:
return "Press once to start dictation, then press again to paste."
}
}
}

/// What the one dictation key does, like Handy's Shortcut Behavior.
Expand Down Expand Up @@ -67,7 +50,6 @@ enum HotkeyPreferences {

static let defaultDictation = HotkeyBinding(keyCode: UInt32(kVK_Space), modifiers: UInt32(optionKey))
static let defaultMeeting = HotkeyBinding(keyCode: UInt32(kVK_ANSI_M), modifiers: UInt32(optionKey))
static let defaultDictationShortcutMode: DictationShortcutMode = .handsFree

// MARK: - Right Option Tap-to-Dictate

Expand Down Expand Up @@ -121,14 +103,16 @@ enum HotkeyPreferences {
userDefaults.object(forKey: meetingKeyCodeKey) != nil
}

static func dictationShortcutMode(userDefaults: UserDefaults = .standard) -> DictationShortcutMode {
/// Pre-one-key builds stored a Push to Talk or Hands-Free mode. Only the
/// legacy binding migration reads it. A missing or unknown value was
/// treated as Hands-Free, so it still is.
static func legacyDictationShortcutMode(userDefaults: UserDefaults = .standard) -> DictationShortcutMode {
guard
let rawValue = userDefaults.string(forKey: dictationShortcutModeKey),
let mode = DictationShortcutMode(rawValue: rawValue)
else {
return defaultDictationShortcutMode
return .handsFree
}

return mode
}

Expand Down Expand Up @@ -167,11 +151,6 @@ enum HotkeyPreferences {
NotificationCenter.default.post(name: .hotkeysDidChange, object: nil)
}

static func setDictationShortcutMode(_ mode: DictationShortcutMode, userDefaults: UserDefaults = .standard) {
userDefaults.set(mode.rawValue, forKey: dictationShortcutModeKey)
NotificationCenter.default.post(name: .hotkeysDidChange, object: nil)
}

static func setDictationShortcutsEnabled(_ enabled: Bool, userDefaults: UserDefaults = .standard) {
let ud = userDefaults
ud.set(enabled, forKey: dictationShortcutsEnabledKey)
Expand Down
46 changes: 5 additions & 41 deletions Sources/Support/PhysicalDictationTriggerPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,12 @@ enum PhysicalDictationTriggerPreferences {
/// The one dictation key. Right Option, not Fn: Fn also opens emoji or
/// switches input unless macOS is set to Do Nothing.
static let defaultPushToTalkBinding = PhysicalDictationTriggerBinding(keyCode: UInt32(kVK_RightOption))
static let defaultHandsFreeBinding = PhysicalDictationTriggerBinding(keyCode: UInt32(kVK_RightOption))
/// Migration only: the old separate Hands-Free key.
private static let defaultHandsFreeBinding = PhysicalDictationTriggerBinding(keyCode: UInt32(kVK_RightOption))
static let defaultMeetingBinding = PhysicalDictationTriggerBinding(
keyCode: UInt32(kVK_ANSI_M),
modifiers: PhysicalDictationTriggerModifiers.option
)
static let defaultPasteLastDictationBinding = PhysicalDictationTriggerBinding(
keyCode: UInt32(kVK_ANSI_V),
modifiers: PhysicalDictationTriggerModifiers.option | PhysicalDictationTriggerModifiers.shift
)
static let defaultBinding = defaultPushToTalkBinding

private static let keyCodeKey = "dictationTrigger-keyCode"
Expand All @@ -84,8 +81,6 @@ enum PhysicalDictationTriggerPreferences {
private static let handsFreeModifiersKey = "dictationHandsFreeTrigger-modifiers"
private static let meetingKeyCodeKey = "meetingTrigger-keyCode"
private static let meetingModifiersKey = "meetingTrigger-modifiers"
private static let pasteLastDictationKeyCodeKey = "pasteLastDictationTrigger-keyCode"
private static let pasteLastDictationModifiersKey = "pasteLastDictationTrigger-modifiers"
private static let oneDictationKeyMigratedKey = "dictationOneKeyMigrated"
private static let functionKeyUsageDomain = "com.apple.HIToolbox" as CFString
private static let functionKeyUsageKey = "AppleFnUsageType" as CFString
Expand All @@ -105,7 +100,7 @@ enum PhysicalDictationTriggerPreferences {
)
}

static func handsFreeBinding(userDefaults: UserDefaults = .standard) -> PhysicalDictationTriggerBinding {
private static func handsFreeBinding(userDefaults: UserDefaults = .standard) -> PhysicalDictationTriggerBinding {
safeBinding(
storedBinding(
keyCodeKey: handsFreeKeyCodeKey,
Expand All @@ -127,17 +122,6 @@ enum PhysicalDictationTriggerPreferences {
)
}

static func pasteLastDictationBinding(userDefaults: UserDefaults = .standard) -> PhysicalDictationTriggerBinding {
safeBinding(
storedBinding(
keyCodeKey: pasteLastDictationKeyCodeKey,
modifiersKey: pasteLastDictationModifiersKey,
userDefaults: userDefaults
) ?? defaultPasteLastDictationBinding,
fallback: defaultPasteLastDictationBinding
)
}

static func save(_ binding: PhysicalDictationTriggerBinding, userDefaults: UserDefaults = .standard) {
savePushToTalk(binding, userDefaults: userDefaults)
}
Expand All @@ -151,15 +135,6 @@ enum PhysicalDictationTriggerPreferences {
)
}

static func saveHandsFree(_ binding: PhysicalDictationTriggerBinding, userDefaults: UserDefaults = .standard) {
saveBinding(
binding,
keyCodeKey: handsFreeKeyCodeKey,
modifiersKey: handsFreeModifiersKey,
userDefaults: userDefaults
)
}

static func saveMeeting(_ binding: PhysicalDictationTriggerBinding, userDefaults: UserDefaults = .standard) {
saveBinding(
binding,
Expand All @@ -169,15 +144,6 @@ enum PhysicalDictationTriggerPreferences {
)
}

static func savePasteLastDictation(_ binding: PhysicalDictationTriggerBinding, userDefaults: UserDefaults = .standard) {
saveBinding(
binding,
keyCodeKey: pasteLastDictationKeyCodeKey,
modifiersKey: pasteLastDictationModifiersKey,
userDefaults: userDefaults
)
}

private static func saveBinding(
_ binding: PhysicalDictationTriggerBinding,
keyCodeKey: String,
Expand Down Expand Up @@ -210,9 +176,7 @@ enum PhysicalDictationTriggerPreferences {

static func resetToDefaults(userDefaults: UserDefaults = .standard) {
savePushToTalk(defaultPushToTalkBinding, userDefaults: userDefaults)
saveHandsFree(defaultHandsFreeBinding, userDefaults: userDefaults)
saveMeeting(defaultMeetingBinding, userDefaults: userDefaults)
savePasteLastDictation(defaultPasteLastDictationBinding, userDefaults: userDefaults)
}

static func functionKeySystemAction() -> FunctionKeySystemAction {
Expand Down Expand Up @@ -592,7 +556,7 @@ enum PhysicalDictationTriggerPreferences {
}

private static func migratedPushToTalkBinding(userDefaults: UserDefaults) -> PhysicalDictationTriggerBinding {
guard HotkeyPreferences.dictationShortcutMode(userDefaults: userDefaults) == .pushToTalk else {
guard HotkeyPreferences.legacyDictationShortcutMode(userDefaults: userDefaults) == .pushToTalk else {
return defaultPushToTalkBinding
}

Expand All @@ -606,7 +570,7 @@ enum PhysicalDictationTriggerPreferences {
}

private static func migratedHandsFreeBinding(userDefaults: UserDefaults) -> PhysicalDictationTriggerBinding {
guard HotkeyPreferences.dictationShortcutMode(userDefaults: userDefaults) == .handsFree else {
guard HotkeyPreferences.legacyDictationShortcutMode(userDefaults: userDefaults) == .handsFree else {
return defaultHandsFreeBinding
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/UI/MenuBar/MenuBarShortcutLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import Foundation

/// A button shows its shortcut beside the title when there's room. Dictation
/// can have two triggers ("Fn / Right ⌥": push-to-talk, then hands-free), and
/// the pair doesn't fit a half-width button, so the button tries the full
/// A button shows its shortcut beside the title when there's room. A shortcut
/// can be a pair ("A / B"), and the pair
/// doesn't fit a half-width button, so the button tries the full
/// text first and then just the first key.
enum MenuBarShortcutLabel {
static func candidates(for shortcut: String) -> [String] {
Expand Down
2 changes: 1 addition & 1 deletion Sources/UI/Overlay/DictationSessionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class DictationSessionController: ObservableObject {
/// press or after recording starts (see `DictationStartCuePolicy`).
var didPlayStartCue = false
/// The shortcut that started this session, when a shortcut did. Read from
/// the press itself, never from `HotkeyPreferences.dictationShortcutMode()`.
/// the press itself, never from a stored setting.
var currentDictationShortcutMode: DictationShortcutMode?
var stoppedAudioRecovery: DictationStoppedAudioRecovery?
var stoppedAudioRecoveryPreservationSessionID: UUID?
Expand Down
Loading
Loading