diff --git a/lib/liveWallpaper.js b/lib/liveWallpaper.js index 6487590..bab484e 100644 --- a/lib/liveWallpaper.js +++ b/lib/liveWallpaper.js @@ -4,6 +4,8 @@ import Clutter from 'gi://Clutter'; import Graphene from 'gi://Graphene'; import St from 'gi://St'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as WorkspaceAnimation from 'resource:///org/gnome/shell/ui/workspaceAnimation.js'; +import * as UnlockDialogModule from 'resource:///org/gnome/shell/ui/unlockDialog.js'; import {SettingsKey} from './settingsKeys.js'; import {loadGstreamerModules, GSTREAMER_INSTALL_HINT} from './gstreamerAvailability.js'; @@ -96,6 +98,9 @@ export class LiveWallpaperManager { this._transitionOverlay = null; this._fadeInPending = false; + + this._patchedMethods = []; + this._lockScreenActors = []; } get isAvailable() { @@ -128,6 +133,7 @@ export class LiveWallpaperManager { this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', () => this._layoutActor()); + this._patchShellIntegration(); this._sync(); } @@ -141,10 +147,96 @@ export class LiveWallpaperManager { this._monitorsChangedId = 0; } + this._unpatchShellIntegration(); this._stop(); this._clearTransition(); } + /** + * Two of gnome-shell's own animations paint a *second*, independent + * background on top of ours, hiding the live wallpaper behind a frozen + * static image: the workspace-switch swipe (which clones the desktop + * picture per-workspace so it can slide, see WorkspaceBackground in + * workspaceAnimation.js) and the lock screen (which blurs its own + * separate copy of the desktop picture, see UnlockDialog in + * unlockDialog.js). Neither exposes a public hook for this, so both are + * patched at the prototype level — wrapped in try/catch per-patch since + * this reaches into shell-private methods that could rename across + * versions, and one patch failing shouldn't take down the other. + */ + _patchShellIntegration() { + const manager = this; + + try { + const {WorkspaceBackground} = WorkspaceAnimation; + this._patchMethod(WorkspaceBackground.prototype, '_createBackground', original => function (...args) { + const result = original.call(this, ...args); + // Only the freshly-added static background clone is hidden + // (not the whole WorkspaceBackground group), so any real + // desktop-icon windows it also hosts stay visible. + const staticBackground = this.get_last_child(); + if (manager.isActive && staticBackground) + staticBackground.visible = false; + return result; + }); + } catch (e) { + this._logger.debug(`Workspace-switch live wallpaper integration unavailable (${e.message ?? e})`); + } + + try { + const {UnlockDialog} = UnlockDialogModule; + this._patchMethod(UnlockDialog.prototype, '_createBackground', original => function (monitorIndex) { + const result = original.call(this, monitorIndex); + manager._attachLockScreenActor(this._backgroundGroup.get_last_child()); + return result; + }); + } catch (e) { + this._logger.debug(`Lock screen live wallpaper integration unavailable (${e.message ?? e})`); + } + } + + _patchMethod(obj, name, wrap) { + const original = obj[name]; + obj[name] = wrap(original); + this._patchedMethods.push({obj, name, original}); + } + + _unpatchShellIntegration() { + for (const {obj, name, original} of this._patchedMethods) + obj[name] = original; + this._patchedMethods = []; + this._lockScreenActors = []; + } + + /** + * Adds an actor sharing our live frame's Content onto the lock screen's + * already-blurred per-monitor background widget (native Shell.BlurEffect + * radius 90, see UnlockDialog._createBackground) — reusing gnome-shell's + * own blur rather than applying a second one. Content is a step behind + * a frame: _updateFrame() re-points every tracked actor at `_image` + * each time a new one is decoded, so this needs no per-frame decode of + * its own and needs no update here if playback hasn't started yet + * (nothing to show is just transparent, revealing the native static + * background beneath). + */ + _attachLockScreenActor(container) { + if (!container || !this._available || !this._settings.get_boolean(SettingsKey.APPLY_TO_LOCK_SCREEN)) + return; + + const actor = new Clutter.Actor({ + x: 0, y: 0, + width: container.width, + height: container.height, + content: this._image, + content_gravity: Clutter.ContentGravity.RESIZE_FILL, + }); + container.add_child(actor); + this._lockScreenActors.push(actor); + actor.connect('destroy', () => { + this._lockScreenActors = this._lockScreenActors.filter(a => a !== actor); + }); + } + async _loadGstreamer() { try { const {Gst, Cogl} = await loadGstreamerModules(); @@ -397,6 +489,10 @@ export class LiveWallpaperManager { } strategy.upload(this._image, this._Cogl, data, width, height); this._actor.set_content(this._image); + for (const lockActor of this._lockScreenActors) { + lockActor.set_content(this._image); + lockActor.visible = true; + } return; } catch (e) { this._logger.warn( @@ -715,6 +811,13 @@ export class LiveWallpaperManager { this._videoHeight = 0; this._fadeInPending = false; + // Leaves the frozen last frame's Content in place rather than + // clearing it — a stopped/paused live wallpaper just hides its lock + // screen actor, revealing the native static blurred background + // beneath, without losing the tracked actor if playback resumes. + for (const lockActor of this._lockScreenActors) + lockActor.visible = false; + this._setActive(false); this._paused = false; }