Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 3 additions & 18 deletions Sources/ConfigureSheetController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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() {
Expand Down
27 changes: 10 additions & 17 deletions Sources/NecoSaverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand Down
Loading