diff --git a/README.md b/README.md index 3c4b1b3..198d5a5 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,30 @@ killall legacyScreenSaver then reopen System Settings. +### 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 the bundle, the sheet reports +`isVisible == true` with a live `sheetParent`, while `occlusionState` never gains +`.visible`: attached to a ghost, and never drawn. + +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. + ## Development `make preview` builds and runs `NecoSaverPreview`, a small host app that puts the diff --git a/Sources/ConfigureSheetController.swift b/Sources/ConfigureSheetController.swift index c4b91d0..bac67a1 100644 --- a/Sources/ConfigureSheetController.swift +++ b/Sources/ConfigureSheetController.swift @@ -21,7 +21,7 @@ final class ConfigureSheetController: NSObject { let window: NSWindow - private var settings = NecoSaverSettings.standard + private var settings = NecoSaverSettings.load() private let onDismiss: (_ saved: Bool) -> Void private var rows: [Row] = [] private let pictureCheckbox = NSButton(checkboxWithTitle: "Picture:", target: nil, action: nil) @@ -39,28 +39,13 @@ final class ConfigureSheetController: NSObject { super.init() window.title = "NecoSaver" // ARC owns this window through `window`; leaving the default on would let a - // host that closes the sheet release it a second time. + // host that closes the sheet rather than ordering it out release it a + // second time. window.isReleasedWhenClosed = false buildInterface() - reload() - } - - /// Re-reads the stored 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 = Self.stored() refresh() } - /// The saved settings, with the picture seeded the same way the saver seeds it, - /// so the field shows the path the cats are actually walking on. - private static func stored() -> NecoSaverSettings { - var settings = NecoSaverSettings.load() - settings.seedPicturePath(with: Wallpaper.desktopPicturePath(for: nil) ?? "") - return settings - } - // MARK: - Interface private func buildInterface() { diff --git a/Sources/NecoSaverView.swift b/Sources/NecoSaverView.swift index c514c14..e7bd530 100644 --- a/Sources/NecoSaverView.swift +++ b/Sources/NecoSaverView.swift @@ -8,8 +8,8 @@ import ScreenSaver @objc(NecoSaverView) final class NecoSaverView: ScreenSaverView { private let engine: NecoSaverEngine - /// 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) { @@ -62,26 +62,19 @@ final class NecoSaverView: 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(settings()) + loadWallpaper() + } + 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(settings()) - loadWallpaper() - } - } - /// Reads a picture only when one is going to be drawn — decoding one costs /// several megabytes that a grey backdrop has no use for. private func loadWallpaper() {