Suspend rotation while a live wallpaper is on screen

The extension's own log showed rotation kept firing ("Wallpaper
changed to ...") after live wallpaper playback had already started.
Each rotation change writes org.gnome.desktop.background's
picture-uri, which makes GNOME Shell repaint its own background actor
into the same _backgroundGroup our live wallpaper actor lives in,
landing on top of it and hiding the video/GIF entirely.

LiveWallpaperManager now takes an onActiveChanged callback, invoked
only on actual start/stop transitions, which extension.js uses to
suspend/unsuspend RotationManager. This is tracked separately from
user-initiated pause()/resume() so turning live wallpaper off restores
whatever rotation state the user actually had, and next()/previous()
now no-op while suspended so manual/forced-OLED wallpaper changes
can't sneak one in either. The indicator's "Next Wallpaper" item is
greyed out to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
This commit is contained in:
Claude
2026-08-04 00:33:01 +00:00
parent 4a5ef4fe86
commit d180c90434
5 changed files with 58 additions and 8 deletions
+4 -1
View File
@@ -14,7 +14,10 @@ protection against OLED burn-in.
desktop background. Frames are decoded with GStreamer and rendered desktop background. Frames are decoded with GStreamer and rendered
directly by GNOME Shell's own Clutter stage (no external, incompatible directly by GNOME Shell's own Clutter stage (no external, incompatible
Clutter build involved). Playback can automatically pause on battery Clutter build involved). Playback can automatically pause on battery
power or while a window is fullscreen. power or while a window is fullscreen. Automatic rotation is
automatically suspended while a live wallpaper is actually on screen
(and resumes afterward), since changing the static wallpaper would
otherwise repaint over the video.
- **OLED burn-in protection** — three independent, individually toggled - **OLED burn-in protection** — three independent, individually toggled
techniques: techniques:
- *Pixel shifting*: nudges the background a few pixels on a slow drift - *Pixel shifting*: nudges the background a few pixels on a slow drift
+13 -1
View File
@@ -14,7 +14,19 @@ export default class BenthicBloomExtension extends Extension {
this._logger = new Logger(this._settings, this.metadata.name); this._logger = new Logger(this._settings, this.metadata.name);
this._rotationManager = new RotationManager(this._settings, this._logger); this._rotationManager = new RotationManager(this._settings, this._logger);
this._liveWallpaperManager = new LiveWallpaperManager(this._settings, this._logger); this._liveWallpaperManager = new LiveWallpaperManager(this._settings, this._logger, {
// Rotation changes the static picture-uri GSettings key, which
// makes the shell repaint its own background actor on top of
// the live wallpaper's. Suspend rotation while a live
// wallpaper is actually on screen, independent of any
// user-initiated pause.
onActiveChanged: active => {
if (active)
this._rotationManager.suspend();
else
this._rotationManager.unsuspend();
},
});
this._oledProtectionManager = new OledProtectionManager(this._settings, this._logger, { this._oledProtectionManager = new OledProtectionManager(this._settings, this._logger, {
forceNextWallpaper: maxSeconds => { forceNextWallpaper: maxSeconds => {
if (this._rotationManager.secondsSinceLastChange >= maxSeconds) { if (this._rotationManager.secondsSinceLastChange >= maxSeconds) {
+4 -1
View File
@@ -28,6 +28,7 @@ class BenthicBloomIndicator extends PanelMenu.Button {
nextItem.connect('activate', () => { nextItem.connect('activate', () => {
rotationManager.next().catch(e => console.error(`[BenthicBloom] ${e.message ?? e}`)); rotationManager.next().catch(e => console.error(`[BenthicBloom] ${e.message ?? e}`));
}); });
nextItem.setSensitive(!settings.get_boolean(SettingsKey.LIVE_WALLPAPER_ENABLED));
this.menu.addMenuItem(nextItem); this.menu.addMenuItem(nextItem);
const liveToggle = new PopupMenu.PopupSwitchMenuItem( const liveToggle = new PopupMenu.PopupSwitchMenuItem(
@@ -58,7 +59,9 @@ class BenthicBloomIndicator extends PanelMenu.Button {
rotationToggle.setToggleState(settings.get_boolean(SettingsKey.ROTATION_ENABLED)); rotationToggle.setToggleState(settings.get_boolean(SettingsKey.ROTATION_ENABLED));
}), }),
settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_ENABLED}`, () => { settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_ENABLED}`, () => {
liveToggle.setToggleState(settings.get_boolean(SettingsKey.LIVE_WALLPAPER_ENABLED)); const enabled = settings.get_boolean(SettingsKey.LIVE_WALLPAPER_ENABLED);
liveToggle.setToggleState(enabled);
nextItem.setSensitive(!enabled);
}), }),
settings.connect(`changed::${SettingsKey.OLED_PROTECTION_ENABLED}`, () => { settings.connect(`changed::${SettingsKey.OLED_PROTECTION_ENABLED}`, () => {
oledToggle.setToggleState(settings.get_boolean(SettingsKey.OLED_PROTECTION_ENABLED)); oledToggle.setToggleState(settings.get_boolean(SettingsKey.OLED_PROTECTION_ENABLED));
+11 -3
View File
@@ -21,9 +21,10 @@ import {loadGstreamerModules, GSTREAMER_INSTALL_HINT} from './gstreamerAvailabil
* that belongs to gnome-shell itself. * that belongs to gnome-shell itself.
*/ */
export class LiveWallpaperManager { export class LiveWallpaperManager {
constructor(settings, logger) { constructor(settings, logger, {onActiveChanged} = {}) {
this._settings = settings; this._settings = settings;
this._logger = logger; this._logger = logger;
this._onActiveChanged = onActiveChanged ?? (() => {});
this._available = false; this._available = false;
this._active = false; this._active = false;
@@ -57,6 +58,13 @@ export class LiveWallpaperManager {
return this._active; return this._active;
} }
_setActive(active) {
if (this._active === active)
return;
this._active = active;
this._onActiveChanged(active);
}
async enable() { async enable() {
await this._loadGstreamer(); await this._loadGstreamer();
@@ -149,7 +157,7 @@ export class LiveWallpaperManager {
this._applyMute(); this._applyMute();
this._playbin.set_state(Gst.State.PLAYING); this._playbin.set_state(Gst.State.PLAYING);
this._active = true; this._setActive(true);
this._paused = false; this._paused = false;
this._connectPowerWatches(); this._connectPowerWatches();
this._logger.debug(`Live wallpaper started: ${path}`); this._logger.debug(`Live wallpaper started: ${path}`);
@@ -336,7 +344,7 @@ export class LiveWallpaperManager {
this._videoWidth = 0; this._videoWidth = 0;
this._videoHeight = 0; this._videoHeight = 0;
this._active = false; this._setActive(false);
this._paused = false; this._paused = false;
} }
} }
+26 -2
View File
@@ -31,6 +31,7 @@ export class RotationManager {
this._timeoutId = 0; this._timeoutId = 0;
this._settingsSignals = []; this._settingsSignals = [];
this._paused = false; this._paused = false;
this._suspended = false;
this._lastChangeTime = GLib.get_monotonic_time(); this._lastChangeTime = GLib.get_monotonic_time();
this._currentPath = null; this._currentPath = null;
this._transitionOverlays = []; this._transitionOverlays = [];
@@ -89,7 +90,7 @@ export class RotationManager {
_restartTimer() { _restartTimer() {
this._clearTimer(); this._clearTimer();
if (this._paused || !this._settings.get_boolean(SettingsKey.ROTATION_ENABLED)) if (this._paused || this._suspended || !this._settings.get_boolean(SettingsKey.ROTATION_ENABLED))
return; return;
const interval = Math.max( const interval = Math.max(
@@ -118,7 +119,30 @@ export class RotationManager {
this._restartTimer(); this._restartTimer();
} }
/**
* Distinct from user-initiated pause(): called while a live wallpaper
* is actually covering the desktop, so rotation doesn't keep changing
* a static image nobody can see (which also churns the shell's own
* background actor on top of the live wallpaper's). Resuming restores
* whatever the user's own pause() state was, rather than forcing
* rotation back on.
*/
suspend() {
this._suspended = true;
this._clearTimer();
}
unsuspend() {
this._suspended = false;
this._restartTimer();
}
async next({forced = false} = {}) { async next({forced = false} = {}) {
if (this._suspended) {
this._logger.debug('Rotation suspended while live wallpaper is active');
return;
}
if (this._images.length === 0) if (this._images.length === 0)
await this._reloadImages(); await this._reloadImages();
@@ -144,7 +168,7 @@ export class RotationManager {
} }
async previous() { async previous() {
if (this._images.length === 0) if (this._suspended || this._images.length === 0)
return; return;
const mode = this._settings.get_string(SettingsKey.ROTATION_MODE); const mode = this._settings.get_string(SettingsKey.ROTATION_MODE);