From e1384a8a104049d565846c64997f6b21eb7ac745 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 31 Aug 2026 14:48:48 -0400 Subject: [PATCH] Fix hidden Menu Band work and instrument order --- .../Sources/JukeWizard/JukeController.swift | 55 +++++++++++++++++-- .../Sources/MenuBand/InstrumentMapView.swift | 2 +- .../InstrumentMapLayoutTests.swift | 42 ++++++++++++++ 3 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 slab/menuband/Tests/MenuBandTests/InstrumentMapLayoutTests.swift diff --git a/juke-wizard/Sources/JukeWizard/JukeController.swift b/juke-wizard/Sources/JukeWizard/JukeController.swift index 8d0190f719..addc52e8ed 100644 --- a/juke-wizard/Sources/JukeWizard/JukeController.swift +++ b/juke-wizard/Sources/JukeWizard/JukeController.swift @@ -270,15 +270,29 @@ final class JukeController: NSWindowController, NSWindowDelegate, installKeyMonitor() } required init?(coder: NSCoder) { fatalError() } - deinit { if let m = keyMonitor { NSEvent.removeMonitor(m) } } + deinit { + watchTimer?.invalidate() + activityTimer?.invalidate() + djMixer?.stopDisplay() + if let m = keyMonitor { NSEvent.removeMonitor(m) } + } // Keep the single player window alive: the menu-bar CD and Dock icon can // restore it instantly, and playback/queue state cannot be lost on close. func windowShouldClose(_ sender: NSWindow) -> Bool { + suspendPresentationActivity() sender.orderOut(nil) return false } + func windowDidMiniaturize(_ notification: Notification) { + suspendPresentationActivity() + } + + func windowDidDeminiaturize(_ notification: Notification) { + resumePresentationActivity() + } + // ── keyboard control (yields to text editing) ──────────────────────── private func installKeyMonitor() { keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] e in @@ -595,7 +609,7 @@ final class JukeController: NSWindowController, NSWindowDelegate, setPlayerChromeHidden(true) listScroll.isHidden = false activityLabel.isHidden = false - djMixer.startDisplay() + if isPresentationActive { djMixer.startDisplay() } roomAudio.useSource(.aesthetic) window?.isMovableByWindowBackground = false } else { @@ -1432,6 +1446,7 @@ final class JukeController: NSWindowController, NSWindowDelegate, guard let w = window else { return } if w.isMiniaturized { w.deminiaturize(nil) } w.makeKeyAndOrderFront(nil) + resumePresentationActivity() NSApp.activate(ignoringOtherApps: true) } @@ -1439,6 +1454,7 @@ final class JukeController: NSWindowController, NSWindowDelegate, guard let w = window else { return } if w.isVisible, !w.isMiniaturized { miniPopover?.close() + suspendPresentationActivity() w.orderOut(nil) } else { quickOpenFull() @@ -1927,15 +1943,41 @@ final class JukeController: NSWindowController, NSWindowDelegate, // ── live work awareness ──────────────────────────────────────────────── // Slab's ledger tells us which agents are active; local process inspection - // catches the narrower render/bake window. Polling is read-only and cheap. + // catches the narrower render/bake window. Neither this poll nor the DJ + // display clock should run while the retained Juke window is hidden. + private var isPresentationActive: Bool { + guard let window else { return false } + return window.isVisible && !window.isMiniaturized + } + + private func resumePresentationActivity() { + guard isPresentationActive else { return } + if djMode { djMixer.startDisplay() } + armActivityStatus() + } + + private func suspendPresentationActivity() { + djMixer.stopDisplay() + activityTimer?.invalidate() + activityTimer = nil + } + private func armActivityStatus() { + guard isPresentationActive, activityTimer == nil else { return } pollActivityStatus() activityTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in - self?.pollActivityStatus() + guard let self else { return } + guard self.isPresentationActive else { + self.activityTimer?.invalidate() + self.activityTimer = nil + return + } + self.pollActivityStatus() } } private func pollActivityStatus() { - guard activeSource == .local, !activityPollInFlight else { return } + guard isPresentationActive, activeSource == .local, + !activityPollInFlight else { return } activityPollInFlight = true let tracks = library.tracks DispatchQueue.global(qos: .utility).async { [weak self] in @@ -1943,7 +1985,8 @@ final class JukeController: NSWindowController, NSWindowDelegate, DispatchQueue.main.async { guard let self else { return } self.activityPollInFlight = false - guard self.activeSource == .local else { return } + guard self.isPresentationActive, + self.activeSource == .local else { return } self.renderActivityStatus(activities) } } diff --git a/slab/menuband/Sources/MenuBand/InstrumentMapView.swift b/slab/menuband/Sources/MenuBand/InstrumentMapView.swift index ba4837a9af..cae22ad1be 100644 --- a/slab/menuband/Sources/MenuBand/InstrumentMapView.swift +++ b/slab/menuband/Sources/MenuBand/InstrumentMapView.swift @@ -47,7 +47,7 @@ final class InstrumentListView: NSView { static var gridLayout: GridLayout { get { let raw = UserDefaults.standard.string(forKey: layoutDefaultsKey) ?? "" - return GridLayout(rawValue: raw) ?? .timbre + return GridLayout(rawValue: raw) ?? .catalogue } set { UserDefaults.standard.set(newValue.rawValue, forKey: layoutDefaultsKey) } } diff --git a/slab/menuband/Tests/MenuBandTests/InstrumentMapLayoutTests.swift b/slab/menuband/Tests/MenuBandTests/InstrumentMapLayoutTests.swift new file mode 100644 index 0000000000..b28df34ec1 --- /dev/null +++ b/slab/menuband/Tests/MenuBandTests/InstrumentMapLayoutTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import MenuBand + +final class InstrumentMapLayoutTests: XCTestCase { + func testCatalogueOrderIsTheDefault() { + let defaults = UserDefaults.standard + let key = "instrumentGridLayout" + let previous = defaults.object(forKey: key) + defer { + if let previous { + defaults.set(previous, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + + defaults.removeObject(forKey: key) + + XCTAssertEqual(InstrumentListView.gridLayout, .catalogue) + XCTAssertEqual(InstrumentListView.program(inSlot: 0), 0) + XCTAssertEqual(InstrumentListView.program(inSlot: 127), 127) + } + + func testExplicitTimbreOrderStillWorks() { + let defaults = UserDefaults.standard + let key = "instrumentGridLayout" + let previous = defaults.object(forKey: key) + defer { + if let previous { + defaults.set(previous, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + + defaults.set("timbre", forKey: key) + + XCTAssertEqual(InstrumentListView.gridLayout, .timbre) + XCTAssertEqual(InstrumentListView.program(inSlot: 0), + GMTimbreLayout.programAtSlot[0]) + } +} -- 2.54.0