Commit ba5c6496 authored by Kourser's avatar Kourser
Browse files

fix(macos): keep the Touch Bar alive across focus-engaging views

Both remaining symptoms — the empty episode level after tapping an
artwork, and the playing bar vanishing when opening the queue or the
settings — share one cause: when a focus-engaging SwiftUI view appears
(selectable list, form), a responder closer than the application
provides its own (empty) touch bar, shadowing NSApp.touchBar. This also
retroactively explains the popover dismissals.

The controller now sets the current bar on the key window as well
(higher in the touch-bar responder chain) and re-asserts it whenever
the window's first responder changes (KVO), plus on key-window changes.
Entering the episodes level also defensively reloads the episode
scrubber in case its data arrived before the bar materialized.

Co-Authored-By: Claude (RCA)
parent ecc5b103
Loading
Loading
Loading
Loading
+42 −6
Original line number Diff line number Diff line
@@ -31,6 +31,9 @@ final class TouchBarController: NSObject {
    private var idleBar: NSTouchBar?
    private var playingBar: NSTouchBar?
    private var episodesBar: NSTouchBar?
    private var currentBar: NSTouchBar?
    private var keyWindowObserver: NSObjectProtocol?
    private var responderObservation: NSKeyValueObservation?

    private var playPauseItem: NSButtonTouchBarItem?
    private var titleLabel: NSTextField?
@@ -64,11 +67,41 @@ final class TouchBarController: NSObject {
        observe()
    }

    /// Installs the bar matching the current state; the application object
    /// sits at the end of the Touch Bar responder chain, so `NSApp.touchBar`
    /// is the fallback for every view.
    /// Installs the bar matching the current state. The bar is set on the
    /// application (chain fallback) **and** on the key window, and re-asserted
    /// whenever the first responder changes: focus-engaging SwiftUI views
    /// (selectable lists, forms) otherwise shadow it with an empty bar.
    func install() {
        applyLevel()
        keyWindowObserver = NotificationCenter.default.addObserver(
            forName: NSWindow.didBecomeKeyNotification, object: nil, queue: .main
        ) { [weak self] note in
            MainActor.assumeIsolated {
                guard let self, let window = note.object as? NSWindow else { return }
                self.watch(window)
            }
        }
        if let window = NSApp.keyWindow ?? NSApp.windows.first {
            watch(window)
        }
    }

    /// Keeps our bar on `window`, re-applying it each time the first
    /// responder changes (which is when SwiftUI installs its shadowing bar).
    private func watch(_ window: NSWindow) {
        assertBar(on: window)
        responderObservation = window.observe(\.firstResponder) { [weak self] window, _ in
            MainActor.assumeIsolated {
                self?.assertBar(on: window)
            }
        }
    }

    private func assertBar(on window: NSWindow?) {
        guard let currentBar else { return }
        if let window, window.touchBar !== currentBar {
            window.touchBar = currentBar
        }
    }

    // MARK: Levels
@@ -87,18 +120,21 @@ final class TouchBarController: NSObject {
        switch target {
        case .idle:
            if idleBar == nil { idleBar = makeBar([.skingomzPodcasts]) }
            NSApp.touchBar = idleBar
            currentBar = idleBar
        case .playing:
            if playingBar == nil {
                playingBar = makeBar([.skingomzSkipBack, .skingomzPlayPause, .skingomzSkipForward,
                                      .skingomzTitle, .skingomzPosition,
                                      .fixedSpaceSmall, .skingomzPodcastsCapped])
            }
            NSApp.touchBar = playingBar
            currentBar = playingBar
        case .episodes:
            if episodesBar == nil { episodesBar = makeBar([.skingomzBack, .skingomzEpisodes]) }
            NSApp.touchBar = episodesBar
            currentBar = episodesBar
            episodeScrubber?.reloadData() // defensive: content may predate the bar
        }
        NSApp.touchBar = currentBar
        assertBar(on: NSApp.keyWindow ?? NSApp.windows.first)
        // Leaving the episodes level: clear the picker selections.
        if target != .episodes {
            episodeScrubber?.selectedIndex = -1