From 776a3a361d29a1f0522dfca71df9b8a937e706f6 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Fri, 10 Jul 2026 18:06:25 +0530 Subject: [PATCH] fix: hide the recorder pill when idle, with an opt-in toggle (#100) The recorder redesign made the pill always-present: showIdleRecorder() showed it at launch and returnToIdle() deliberately kept it on screen, so it never went away in either recording mode. Restore hide-when-idle as the default and gate always-visible behind a new "Always show recorder pill" setting (default off): - returnToIdle() now orderOut()s the panel when the toggle is off (hit after commit and after cancel/no-speech). Recording still shows the HUD normally. - showIdleRecorder() only fronts the pill at launch when the toggle is on. - Toggling the setting posts .recorderIdleVisibilityChanged; AppDelegate observes it and calls applyIdleVisibilityPreference() to show/hide live (guards against hiding mid-session via the panel's interactive state). Fixes #100. --- speaktype/App/AppDelegate.swift | 12 +++++- .../Constants/Shortcuts+Extensions.swift | 3 ++ .../MiniRecorderWindowController.swift | 42 ++++++++++++++++--- .../Views/Screens/Settings/SettingsView.swift | 24 +++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/speaktype/App/AppDelegate.swift b/speaktype/App/AppDelegate.swift index fe76309..706d82b 100644 --- a/speaktype/App/AppDelegate.swift +++ b/speaktype/App/AppDelegate.swift @@ -17,9 +17,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { miniRecorderController = MiniRecorderWindowController() - // Show the always-present resting pill so the recorder lives on screen. + // Prepare the resting pill. It stays hidden until recording unless the + // user opts into always showing it (issue #100). miniRecorderController?.showIdleRecorder() + // React to the "always show recorder pill" toggle at runtime. + NotificationCenter.default.addObserver( + forName: .recorderIdleVisibilityChanged, + object: nil, + queue: .main + ) { [weak self] _ in + self?.miniRecorderController?.applyIdleVisibilityPreference() + } + // Only show the Dock icon while the dashboard is actually open; otherwise // live quietly in the menu bar. observeWindowsForDockIcon() diff --git a/speaktype/Constants/Shortcuts+Extensions.swift b/speaktype/Constants/Shortcuts+Extensions.swift index cd2b015..704ca93 100644 --- a/speaktype/Constants/Shortcuts+Extensions.swift +++ b/speaktype/Constants/Shortcuts+Extensions.swift @@ -11,4 +11,7 @@ extension Notification.Name { static let recordingStartRequested = Notification.Name("recordingStartRequested") static let recordingStopRequested = Notification.Name("recordingStopRequested") static let recordingCancelRequested = Notification.Name("recordingCancelRequested") + /// Posted when the "always show recorder pill" preference changes so the + /// window controller can show/hide the idle pill immediately. + static let recorderIdleVisibilityChanged = Notification.Name("recorderIdleVisibilityChanged") } diff --git a/speaktype/Controllers/MiniRecorderWindowController.swift b/speaktype/Controllers/MiniRecorderWindowController.swift index 5528388..4212aba 100644 --- a/speaktype/Controllers/MiniRecorderWindowController.swift +++ b/speaktype/Controllers/MiniRecorderWindowController.swift @@ -9,8 +9,15 @@ class MiniRecorderWindowController: NSObject { UserDefaults.standard.object(forKey: "restoreClipboardAfterAutoPaste") as? Bool ?? true } - /// Show the always-present resting pill. Called once at launch; the pill then - /// lives on screen and morphs into the recording HUD on demand. + /// When on, the resting pill stays on screen even when idle. Default off: + /// the recorder appears only while dictating and hides afterward (issue #100). + private var alwaysShowIdlePill: Bool { + UserDefaults.standard.bool(forKey: "alwaysShowRecorderPill") + } + + /// Prepare the resting pill. Called once at launch. When "always show" is on + /// the pill lives on screen and morphs into the recording HUD; when off it + /// stays hidden until recording starts. func showIdleRecorder() { if panel == nil { setupPanel() @@ -22,11 +29,31 @@ class MiniRecorderWindowController: NSObject { // behind it so the transparent window never blocks the desktop or dock. panel.ignoresMouseEvents = true - if !panel.isVisible { + if alwaysShowIdlePill, !panel.isVisible { panel.orderFrontRegardless() } } + /// React to the "always show recorder pill" preference changing at runtime. + func applyIdleVisibilityPreference() { + if panel == nil { + setupPanel() + } + guard let panel = panel else { return } + + if alwaysShowIdlePill { + centerPanel() + panel.ignoresMouseEvents = true + if !panel.isVisible { + panel.orderFrontRegardless() + } + } else if panel.ignoresMouseEvents { + // Only hide when idle — during an active session the panel is + // interactive (ignoresMouseEvents == false), so leave it alone. + panel.orderOut(nil) + } + } + // Start recording - show panel and begin recording func startRecording() { // Capture previous app to restore focus later @@ -68,9 +95,14 @@ class MiniRecorderWindowController: NSObject { } } - /// Return the pill to its passive resting state without hiding it. + /// Return the pill to its passive resting state. When "always show" is off + /// (the default) this hides the recorder so it only appears while dictating. private func returnToIdle() { - panel?.ignoresMouseEvents = true + guard let panel = panel else { return } + panel.ignoresMouseEvents = true + if !alwaysShowIdlePill { + panel.orderOut(nil) + } } // Stop recording - trigger transcription and paste diff --git a/speaktype/Views/Screens/Settings/SettingsView.swift b/speaktype/Views/Screens/Settings/SettingsView.swift index 0885567..24e5825 100644 --- a/speaktype/Views/Screens/Settings/SettingsView.swift +++ b/speaktype/Views/Screens/Settings/SettingsView.swift @@ -92,6 +92,7 @@ struct GeneralSettingsTab: View { @AppStorage("restoreClipboardAfterAutoPaste") private var restoreClipboardAfterAutoPaste = true @AppStorage("showMenuBarIcon") private var showMenuBarIcon: Bool = true + @AppStorage("alwaysShowRecorderPill") private var alwaysShowRecorderPill: Bool = false @AppStorage("transcriptionLanguage") private var transcriptionLanguage: String = "auto" @AppStorage("recentTranscriptionLanguages") private var recentLanguagesString: String = "" @AppStorage("enableAutoEdit") private var enableAutoEdit: Bool = false @@ -230,6 +231,29 @@ struct GeneralSettingsTab: View { .font(Typography.captionSmall) .foregroundStyle(Color.textMuted) } + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Always show recorder pill") + .font(Typography.bodyMedium) + .foregroundStyle(Color.textPrimary) + Spacer() + Toggle("", isOn: $alwaysShowRecorderPill) + .labelsHidden() + .onChange(of: alwaysShowRecorderPill) { + NotificationCenter.default.post( + name: .recorderIdleVisibilityChanged, object: nil) + } + } + + Text( + alwaysShowRecorderPill + ? "The small floating recorder stays on screen even when you're not dictating." + : "The floating recorder appears only while you're dictating and hides when idle." + ) + .font(Typography.captionSmall) + .foregroundStyle(Color.textMuted) + } } }