fix: keep initial playback focus off the titlebar pin

Restore the playback window as first responder after initial setup and after file/YouTube dialogs return. Keep native control focus indicators and Tab navigation unchanged.

Verified hidden native window focus, both dialog cancellations, explicit control focus, Tab navigation, P shortcut, app build, and compact-window checks.
This commit is contained in:
2026-10-10 23:15:09 +02:00
parent 8fb08db6f3
commit 5bc0d81bbf
2 changed files with 5 additions and 0 deletions
+2
View File
@@ -53,6 +53,8 @@ Drag the compact video to move the window; on release, each axis snaps independe
Single-key shortcuts apply only to the playback window and do not intercept text editing, file-picker input, or Command/Control/Option combinations. Held toggle keys do not repeatedly toggle; held `J`/`L` continue seeking and held `,`/`.` continue stepping. `C` captions remains unbound. Single-key shortcuts apply only to the playback window and do not intercept text editing, file-picker input, or Command/Control/Option combinations. Held toggle keys do not repeatedly toggle; held `J`/`L` continue seeking and held `,`/`.` continue stepping. `C` captions remains unbound.
Initial window setup and returning from the file/YouTube dialogs leave keyboard focus on playback rather than automatically highlighting the titlebar pin. Tab navigation and native focus indicators remain available when choosing controls explicitly.
Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. Consecutive same-direction repeats share one counted queue entry without skipping frames or reordering seeks/direction changes. The last presented image remains visible during decoder preroll and queued steps; the progress thumb follows each authoritative stepped position. Opening/closing media still clears the old image. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame. Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. Consecutive same-direction repeats share one counted queue entry without skipping frames or reordering seeks/direction changes. The last presented image remains visible during decoder preroll and queued steps; the progress thumb follows each authoritative stepped position. Opening/closing media still clears the old image. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame.
- Existing shortcuts remain: **⌘O** opens files, **←/→** seeks five seconds, **↑/↓** adjusts volume by five percent, **⌃⌘F** toggles fullscreen, and **Esc** leaves fullscreen. Double-clicking normal-mode video also toggles fullscreen. - Existing shortcuts remain: **⌘O** opens files, **←/→** seeks five seconds, **↑/↓** adjusts volume by five percent, **⌃⌘F** toggles fullscreen, and **Esc** leaves fullscreen. Double-clicking normal-mode video also toggles fullscreen.
+3
View File
@@ -85,6 +85,7 @@ final class PlayerModel: ObservableObject {
func openPanel() { func openPanel() {
guard canOpen else { return } guard canOpen else { return }
defer { window?.makeFirstResponder(nil) }
revealControls() revealControls()
let panel = NSOpenPanel() let panel = NSOpenPanel()
panel.canChooseFiles = true panel.canChooseFiles = true
@@ -97,6 +98,7 @@ final class PlayerModel: ObservableObject {
func openYouTubePanel() { func openYouTubePanel() {
guard canOpen else { return } guard canOpen else { return }
defer { window?.makeFirstResponder(nil) }
revealControls() revealControls()
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Open YouTube Video" alert.messageText = "Open YouTube Video"
@@ -1030,6 +1032,7 @@ private struct WindowChrome: NSViewRepresentable {
guard let self, let window else { return } guard let self, let window else { return }
self.model.attachWindow(window) self.model.attachWindow(window)
self.updateButtons() self.updateButtons()
window.makeFirstResponder(nil)
} }
} }