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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 3 additions & 10 deletions Sources/ConfigureSheetController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []

Expand All @@ -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()
}

Expand Down
26 changes: 9 additions & 17 deletions Sources/MacstifyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Loading