From 1b26fc5eb00ea5b37e2eb83619591bae9a0c4483 Mon Sep 17 00:00:00 2001 From: winebarrel Date: Mon, 10 Aug 2026 14:02:28 +0900 Subject: [PATCH] Put the configure sheet back, and document why Options dies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts #12. Its reasoning does not survive contact with the machine: hosts read configureSheet once per click, traced from inside an installed bundle, so the "second read drops the first window" hazard it was built around was never firing. The reuse it replaced that with was no better and no worse. What is left is the shape every screen saver uses, and the one Apple's own guidance shows: build a controller, hand over its window. isReleasedWhenClosed stays off. ARC owns the window through the controller, and the default would let a host that closes the sheet rather than ordering it out release it a second time. Real, if latent, and unrelated to any of the above. The Options button dying is a host bug. System Settings on macOS 26 spawns duplicate legacyScreenSaver instances and loses track of them, so the sheet is handed over, attached and ordered in — against a remote view window that is no longer on screen. Traced at the moment of the click, the sheet reports isVisible == true with a live sheetParent while occlusionState never gains .visible: attached to a ghost, never drawn. Switching the screen saver away and back reproduces it every time; a freshly spawned host never shows it. Apple has it as FB19201567, with FB19204084 for the ScreenSaverView instances that pile up alongside. Neither is fixed as of 26.1b3. Nothing in configureSheet chooses the parent window, so there is nothing here to fix — the README now says so, and says to restart the host. --- README.md | 25 +++++++++++++++++++++++++ Sources/ConfigureSheetController.swift | 13 +++---------- Sources/MacstifyView.swift | 26 +++++++++----------------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b7918fb..653f910 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,28 @@ cycle through the spectrum. | Color speed | 1.00× | 0.0–3.0 | Hue cycling rate. `0.00×` freezes each shape on its starting color. | Settings are stored per user through `ScreenSaverDefaults` under `jp.winebarrel.Macstify`. + +## Known issue: the Options button does nothing + +On macOS 26 the Options button stops responding, reliably if you switch the screen +saver away and back. Nothing appears, and nothing is logged. + +This is a bug in the host, not in the saver. System Settings spawns duplicate +`legacyScreenSaver` instances and loses track of them, so the configure sheet is +handed over, attached and ordered in — against a remote view window that is no +longer on screen. Traced from inside a screen saver bundle, the sheet reports +`isVisible == true` with a live `sheetParent`, while `occlusionState` never gains +`.visible`: attached to a ghost, and never drawn. Which window the host attaches +the sheet to is not something `configureSheet` can influence. + +Apple has it as FB19201567, along with FB19204084 for the `ScreenSaverView` +instances that pile up alongside; neither is fixed as of 26.1b3. See +[macOS 26 Tahoe Screen Saver issues](https://developer.apple.com/forums/thread/787444). + +Until it is fixed, restart the host: + +```sh +killall legacyScreenSaver +``` + +then quit System Settings and open it again. diff --git a/Sources/ConfigureSheetController.swift b/Sources/ConfigureSheetController.swift index a505735..2c41000 100644 --- a/Sources/ConfigureSheetController.swift +++ b/Sources/ConfigureSheetController.swift @@ -18,7 +18,7 @@ final class ConfigureSheetController: NSObject { let window: NSWindow - private var settings = MacstifySettings.standard + private var settings = MacstifySettings.load() private let onDismiss: (_ saved: Bool) -> Void private var rows: [Row] = [] @@ -33,17 +33,10 @@ final class ConfigureSheetController: NSObject { super.init() window.title = "Macstify" // ARC owns this window through `window`; leaving the default on would - // let a host that closes the sheet release it a second time. + // let a host that closes the sheet rather than ordering it out release + // it a second time. window.isReleasedWhenClosed = false buildInterface() - reload() - } - - /// Re-reads the saved settings into the controls. The same sheet is handed - /// to the host every time it asks, so this is what makes the second opening - /// show what is stored rather than where the first one was left. - func reload() { - settings = .load() refresh() } diff --git a/Sources/MacstifyView.swift b/Sources/MacstifyView.swift index b4027b8..e0db4ac 100644 --- a/Sources/MacstifyView.swift +++ b/Sources/MacstifyView.swift @@ -8,9 +8,8 @@ import ScreenSaver @objc(MacstifyView) final class MacstifyView: ScreenSaverView { private let engine: MacstifyEngine - /// Built once and kept: the host releases its reference once the sheet is - /// up, and a deallocated controller takes the sheet's window and targets - /// with it. + /// Held strongly: the host releases its reference once the sheet is up, + /// and a deallocated controller takes the sheet's targets with it. private var sheetController: ConfigureSheetController? override init?(frame: NSRect, isPreview: Bool) { @@ -55,25 +54,18 @@ final class MacstifyView: ScreenSaverView { true } - /// Hosts read this more than once, and can replace the view between reading - /// it and presenting it. Building a controller per read would hand back a - /// window whose only owner was dropped by the very next read — and the sheet - /// then simply never appears. One controller, kept, re-reading the settings - /// each time it is handed over. override var configureSheet: NSWindow? { - let controller = sheetController ?? makeSheetController() + let controller = ConfigureSheetController { [weak self] saved in + guard let self else { return } + if saved { + engine.apply(Self.settings(isPreview: isPreview)) + } + sheetController = nil + } sheetController = controller - controller.reload() return controller.window } - private func makeSheetController() -> ConfigureSheetController { - ConfigureSheetController { [weak self] saved in - guard let self, saved else { return } - engine.apply(Self.settings(isPreview: isPreview)) - } - } - private static func settings(isPreview: Bool) -> MacstifySettings { let settings = MacstifySettings.load() return isPreview ? settings.previewAdjusted : settings