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
12 changes: 11 additions & 1 deletion speaktype/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions speaktype/Constants/Shortcuts+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
42 changes: 37 additions & 5 deletions speaktype/Controllers/MiniRecorderWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions speaktype/Views/Screens/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
Loading