more oled features and system freeze fixes
This commit is contained in:
+169
-9
@@ -1,6 +1,7 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
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';
|
||||
|
||||
@@ -89,6 +90,9 @@ export class LiveWallpaperManager {
|
||||
this._noFrameWatchdogId = 0;
|
||||
this._imageStrategyIndex = 0;
|
||||
this._frameErrorCount = 0;
|
||||
|
||||
this._transitionOverlay = null;
|
||||
this._fadeInPending = false;
|
||||
}
|
||||
|
||||
get isAvailable() {
|
||||
@@ -113,6 +117,7 @@ export class LiveWallpaperManager {
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_ENABLED}`, () => this._sync()),
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PATH}`, () => this._sync()),
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_MUTED}`, () => this._applyMute()),
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE}`, () => this._applyDisplayMode()),
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE}`, () => this._applyPlaybackRate()),
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_ON_BATTERY}`, () => this._updatePauseState()),
|
||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_WHEN_FULLSCREEN}`, () => this._updatePauseState())
|
||||
@@ -134,6 +139,7 @@ export class LiveWallpaperManager {
|
||||
}
|
||||
|
||||
this._stop();
|
||||
this._clearTransition();
|
||||
}
|
||||
|
||||
async _loadGstreamer() {
|
||||
@@ -157,12 +163,27 @@ export class LiveWallpaperManager {
|
||||
if (shouldRun)
|
||||
this._active ? this._restart() : this._start();
|
||||
else if (this._active)
|
||||
this._stop();
|
||||
this._stop({animate: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switching to a different file while already active. If transitions
|
||||
* are on, the outgoing actor (already showing the old file's last
|
||||
* frame) is detached before _stop() would destroy it, so it can be
|
||||
* kept on screen and eased away by _playTransition() after _start()
|
||||
* has the new one playing underneath — a crossfade between the two.
|
||||
*/
|
||||
_restart() {
|
||||
const transitionsOn = this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED);
|
||||
const outgoingActor = transitionsOn ? this._actor : null;
|
||||
if (outgoingActor)
|
||||
this._actor = null;
|
||||
|
||||
this._stop();
|
||||
this._start();
|
||||
|
||||
if (outgoingActor)
|
||||
this._playTransition(outgoingActor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,11 +254,15 @@ export class LiveWallpaperManager {
|
||||
|
||||
try {
|
||||
this._actor = new Clutter.Actor({
|
||||
content_gravity: Clutter.ContentGravity.RESIZE_ASPECT,
|
||||
reactive: false,
|
||||
pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
|
||||
});
|
||||
Main.layoutManager._backgroundGroup.add_child(this._actor);
|
||||
this._layoutActor();
|
||||
this._applyDisplayMode();
|
||||
|
||||
this._fadeInPending = this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED);
|
||||
if (this._fadeInPending)
|
||||
this._actor.opacity = 0;
|
||||
|
||||
const sinkBin = this._buildSinkBin();
|
||||
|
||||
@@ -312,9 +337,17 @@ export class LiveWallpaperManager {
|
||||
try {
|
||||
this._updateFrame(mapInfo.data, width, height);
|
||||
this._frameCount++;
|
||||
if (this._frameCount === 1)
|
||||
if (this._frameCount === 1) {
|
||||
this._logger.debug(`Live wallpaper: first frame received (${width}x${height})`);
|
||||
else if (this._frameCount % 120 === 0)
|
||||
if (this._fadeInPending) {
|
||||
this._fadeInPending = false;
|
||||
this._actor.ease({
|
||||
opacity: 255,
|
||||
duration: this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS),
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
});
|
||||
}
|
||||
} else if (this._frameCount % 120 === 0)
|
||||
this._logger.debug(`Live wallpaper: ${this._frameCount} frames rendered so far`);
|
||||
} finally {
|
||||
buffer.unmap(mapInfo);
|
||||
@@ -346,6 +379,7 @@ export class LiveWallpaperManager {
|
||||
this._image = strategy.create(width, height);
|
||||
this._videoWidth = width;
|
||||
this._videoHeight = height;
|
||||
this._layoutActor();
|
||||
}
|
||||
strategy.upload(this._image, this._Cogl, data, width, height);
|
||||
this._actor.set_content(this._image);
|
||||
@@ -414,11 +448,61 @@ export class LiveWallpaperManager {
|
||||
Gst.SeekType.SET, position, Gst.SeekType.NONE, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clutter's ContentGravity enum covers fit/stretch/center natively, but
|
||||
* has no "cover" (scale-and-crop) value, so "fill" mode is done by hand:
|
||||
* the actor is overscanned to the smallest size that fully covers the
|
||||
* stage while preserving the video's aspect ratio, centered, and then
|
||||
* clipped back down to the stage bounds.
|
||||
*/
|
||||
_applyDisplayMode() {
|
||||
if (!this._actor)
|
||||
return;
|
||||
|
||||
const mode = this._settings.get_string(SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE);
|
||||
switch (mode) {
|
||||
case 'stretch':
|
||||
this._actor.content_gravity = Clutter.ContentGravity.RESIZE_FILL;
|
||||
break;
|
||||
case 'center':
|
||||
this._actor.content_gravity = Clutter.ContentGravity.CENTER;
|
||||
break;
|
||||
case 'fill':
|
||||
this._actor.content_gravity = Clutter.ContentGravity.RESIZE_FILL;
|
||||
break;
|
||||
case 'fit':
|
||||
default:
|
||||
this._actor.content_gravity = Clutter.ContentGravity.RESIZE_ASPECT;
|
||||
break;
|
||||
}
|
||||
|
||||
this._layoutActor();
|
||||
}
|
||||
|
||||
_layoutActor() {
|
||||
if (!this._actor)
|
||||
return;
|
||||
|
||||
const stageWidth = global.stage.width;
|
||||
const stageHeight = global.stage.height;
|
||||
const mode = this._settings.get_string(SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE);
|
||||
|
||||
if (mode === 'fill' && this._videoWidth > 0 && this._videoHeight > 0) {
|
||||
const scale = Math.max(stageWidth / this._videoWidth, stageHeight / this._videoHeight);
|
||||
const scaledWidth = this._videoWidth * scale;
|
||||
const scaledHeight = this._videoHeight * scale;
|
||||
const offsetX = (stageWidth - scaledWidth) / 2;
|
||||
const offsetY = (stageHeight - scaledHeight) / 2;
|
||||
|
||||
this._actor.set_position(offsetX, offsetY);
|
||||
this._actor.set_size(scaledWidth, scaledHeight);
|
||||
this._actor.set_clip(-offsetX, -offsetY, stageWidth, stageHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
this._actor.remove_clip();
|
||||
this._actor.set_position(0, 0);
|
||||
this._actor.set_size(global.stage.width, global.stage.height);
|
||||
this._actor.set_size(stageWidth, stageHeight);
|
||||
}
|
||||
|
||||
_connectPowerWatches() {
|
||||
@@ -502,7 +586,15 @@ export class LiveWallpaperManager {
|
||||
this._playbin.set_state(paused ? this._Gst.State.PAUSED : this._Gst.State.PLAYING);
|
||||
}
|
||||
|
||||
_stop() {
|
||||
/**
|
||||
* `animate: true` is for a user-visible stop (toggling the setting off,
|
||||
* or the path being cleared) — the last frame stays on screen and eases
|
||||
* away per the shared Transitions setting, revealing the static
|
||||
* wallpaper beneath. Left off for restarts (which hand the actor to
|
||||
* _playTransition() themselves, see _restart()), errors, and
|
||||
* disable(), where an immediate teardown is what's wanted.
|
||||
*/
|
||||
_stop({animate = false} = {}) {
|
||||
this._disconnectPowerWatches();
|
||||
|
||||
if (this._noFrameWatchdogId) {
|
||||
@@ -536,13 +628,81 @@ export class LiveWallpaperManager {
|
||||
this._appsink = null;
|
||||
}
|
||||
|
||||
this._actor?.destroy();
|
||||
this._actor = null;
|
||||
if (animate && this._actor && this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED)) {
|
||||
this._playTransition(this._actor);
|
||||
this._actor = null;
|
||||
} else {
|
||||
this._actor?.destroy();
|
||||
this._actor = null;
|
||||
}
|
||||
this._image = null;
|
||||
this._videoWidth = 0;
|
||||
this._videoHeight = 0;
|
||||
this._fadeInPending = false;
|
||||
|
||||
this._setActive(false);
|
||||
this._paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eases `overlay` — an actor that was just detached from active duty,
|
||||
* still showing its last frame — off screen per the Transitions
|
||||
* setting (fade/slide/zoom/random, same styles and duration as static
|
||||
* rotation), then destroys it. Raising it above everything else first
|
||||
* means this works whether it's revealing a freshly started live
|
||||
* wallpaper actor underneath (switch) or the static desktop background
|
||||
* (stop) — both are already in place by the time this runs.
|
||||
*/
|
||||
_playTransition(overlay) {
|
||||
this._transitionOverlay?.destroy();
|
||||
this._transitionOverlay = overlay;
|
||||
|
||||
Main.layoutManager._backgroundGroup.set_child_above_sibling(overlay, null);
|
||||
|
||||
const durationMs = this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS);
|
||||
let style = this._settings.get_string(SettingsKey.TRANSITION_STYLE);
|
||||
if (style === 'random')
|
||||
style = ['fade', 'slide', 'zoom'][Math.floor(Math.random() * 3)];
|
||||
|
||||
const onComplete = () => {
|
||||
if (this._transitionOverlay === overlay)
|
||||
this._transitionOverlay = null;
|
||||
overlay.destroy();
|
||||
};
|
||||
|
||||
switch (style) {
|
||||
case 'slide':
|
||||
overlay.ease({
|
||||
x: overlay.x - overlay.width,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
|
||||
onComplete,
|
||||
});
|
||||
break;
|
||||
case 'zoom':
|
||||
overlay.ease({
|
||||
scale_x: 1.15,
|
||||
scale_y: 1.15,
|
||||
opacity: 0,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_IN_QUAD,
|
||||
onComplete,
|
||||
});
|
||||
break;
|
||||
case 'fade':
|
||||
default:
|
||||
overlay.ease({
|
||||
opacity: 0,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
onComplete,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_clearTransition() {
|
||||
this._transitionOverlay?.destroy();
|
||||
this._transitionOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import Meta from 'gi://Meta';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Graphene from 'gi://Graphene';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import {SettingsKey} from './settingsKeys.js';
|
||||
@@ -13,6 +14,11 @@ const SHIFT_PATTERN = [
|
||||
|
||||
const FORCE_ROTATION_CHECK_SECONDS = 60;
|
||||
|
||||
// Extra margin, in pixels, added on top of the configured shift amount when
|
||||
// overscanning background actors — rounding/timing slack so a step never
|
||||
// exposes a sliver of empty stage at the trailing edge.
|
||||
const OVERSCAN_SAFETY_PX = 2;
|
||||
|
||||
/**
|
||||
* Reduces OLED burn-in risk through three independent techniques:
|
||||
* - pixel shifting: nudges the background actors a few px on a slow cycle
|
||||
@@ -105,6 +111,7 @@ export class OledProtectionManager {
|
||||
const [dx, dy] = SHIFT_PATTERN[this._shiftStep];
|
||||
|
||||
for (const actor of this._backgroundActors()) {
|
||||
this._ensureOverscan(actor, amount);
|
||||
actor.ease({
|
||||
translation_x: dx * amount,
|
||||
translation_y: dy * amount,
|
||||
@@ -114,12 +121,36 @@ export class OledProtectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Background actors (the real desktop background, and our own live
|
||||
* wallpaper actor in most display modes) are sized to exactly cover
|
||||
* the stage with no slack, so nudging one by a few pixels for burn-in
|
||||
* protection would drag its edge inward and expose bare stage on the
|
||||
* side it moved away from. Scaling the actor up slightly around its
|
||||
* center — by just enough margin to cover the configured shift amount
|
||||
* — keeps every edge overscanned past the stage bounds no matter which
|
||||
* step of the shift pattern is active.
|
||||
*/
|
||||
_ensureOverscan(actor, amount) {
|
||||
const width = actor.width;
|
||||
const height = actor.height;
|
||||
if (width <= 0 || height <= 0)
|
||||
return;
|
||||
|
||||
const margin = amount + OVERSCAN_SAFETY_PX;
|
||||
actor.pivot_point = new Graphene.Point({x: 0.5, y: 0.5});
|
||||
actor.scale_x = 1 + (2 * margin) / width;
|
||||
actor.scale_y = 1 + (2 * margin) / height;
|
||||
}
|
||||
|
||||
_resetShift() {
|
||||
this._shiftStep = 0;
|
||||
for (const actor of this._backgroundActors()) {
|
||||
actor.ease({
|
||||
translation_x: 0,
|
||||
translation_y: 0,
|
||||
scale_x: 1,
|
||||
scale_y: 1,
|
||||
duration: 500,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
});
|
||||
|
||||
+48
-16
@@ -2,6 +2,7 @@ import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import St from 'gi://St';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Graphene from 'gi://Graphene';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import {SettingsKey} from './settingsKeys.js';
|
||||
@@ -40,6 +41,7 @@ export class RotationManager {
|
||||
enable() {
|
||||
this._settingsSignals.push(
|
||||
this._settings.connect(`changed::${SettingsKey.WALLPAPER_FOLDERS}`, () => this._reloadImages()),
|
||||
this._settings.connect(`changed::${SettingsKey.WALLPAPER_RESCAN_REQUEST}`, () => this._reloadImages()),
|
||||
this._settings.connect(`changed::${SettingsKey.ROTATION_ENABLED}`, () => this._restartTimer()),
|
||||
this._settings.connect(`changed::${SettingsKey.ROTATION_INTERVAL_SECONDS}`, () => this._restartTimer()),
|
||||
this._settings.connect(`changed::${SettingsKey.ROTATION_MODE}`, () => this._onModeChanged())
|
||||
@@ -185,7 +187,7 @@ export class RotationManager {
|
||||
const uri = GLib.filename_to_uri(path, null);
|
||||
|
||||
if (this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED) && previousPath)
|
||||
this._playCrossfade(previousPath);
|
||||
this._playTransition(previousPath);
|
||||
|
||||
this._backgroundSettings.set_string('picture-uri', uri);
|
||||
if (this._backgroundSettings.settings_schema.has_key('picture-uri-dark'))
|
||||
@@ -200,15 +202,19 @@ export class RotationManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* The real background actor doesn't crossfade on its own, so we paint the
|
||||
* *old* image full-screen on a throwaway overlay right as the new image
|
||||
* is set underneath, then fade the overlay out to reveal it.
|
||||
* The real background actor doesn't animate changes on its own, so we
|
||||
* paint the *old* image full-screen on a throwaway overlay right as the
|
||||
* new image is set underneath, then animate the overlay away to reveal
|
||||
* it. Which animation runs is picked per monitor by style.
|
||||
*/
|
||||
_playCrossfade(previousPath) {
|
||||
_playTransition(previousPath) {
|
||||
this._clearTransitionOverlays();
|
||||
|
||||
const durationMs = this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS);
|
||||
const uri = GLib.filename_to_uri(previousPath, null).replace(/"/g, '%22');
|
||||
let style = this._settings.get_string(SettingsKey.TRANSITION_STYLE);
|
||||
if (style === 'random')
|
||||
style = ['fade', 'slide', 'zoom'][Math.floor(Math.random() * 3)];
|
||||
|
||||
for (const monitor of Main.layoutManager.monitors) {
|
||||
const overlay = new St.Widget({
|
||||
@@ -217,23 +223,49 @@ export class RotationManager {
|
||||
y: monitor.y,
|
||||
width: monitor.width,
|
||||
height: monitor.height,
|
||||
pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
|
||||
style: `background-image: url("${uri}"); background-size: cover; background-position: center;`,
|
||||
opacity: 255,
|
||||
});
|
||||
Main.layoutManager._backgroundGroup.add_child(overlay);
|
||||
this._transitionOverlays.push(overlay);
|
||||
|
||||
overlay.ease({
|
||||
opacity: 0,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
onComplete: () => {
|
||||
overlay.destroy();
|
||||
const idx = this._transitionOverlays.indexOf(overlay);
|
||||
if (idx >= 0)
|
||||
this._transitionOverlays.splice(idx, 1);
|
||||
},
|
||||
});
|
||||
const onComplete = () => {
|
||||
overlay.destroy();
|
||||
const idx = this._transitionOverlays.indexOf(overlay);
|
||||
if (idx >= 0)
|
||||
this._transitionOverlays.splice(idx, 1);
|
||||
};
|
||||
|
||||
switch (style) {
|
||||
case 'slide':
|
||||
overlay.ease({
|
||||
x: monitor.x - monitor.width,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
|
||||
onComplete,
|
||||
});
|
||||
break;
|
||||
case 'zoom':
|
||||
overlay.ease({
|
||||
scale_x: 1.15,
|
||||
scale_y: 1.15,
|
||||
opacity: 0,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_IN_QUAD,
|
||||
onComplete,
|
||||
});
|
||||
break;
|
||||
case 'fade':
|
||||
default:
|
||||
overlay.ease({
|
||||
opacity: 0,
|
||||
duration: durationMs,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
onComplete,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ export const SettingsKey = Object.freeze({
|
||||
SHOW_INDICATOR: 'show-indicator',
|
||||
DEBUG_LOGGING: 'debug-logging',
|
||||
WALLPAPER_FOLDERS: 'wallpaper-folders',
|
||||
WALLPAPER_RESCAN_REQUEST: 'wallpaper-rescan-request',
|
||||
APPLY_TO_LOCK_SCREEN: 'apply-to-lock-screen',
|
||||
|
||||
ROTATION_ENABLED: 'rotation-enabled',
|
||||
@@ -10,10 +11,12 @@ export const SettingsKey = Object.freeze({
|
||||
CURRENT_INDEX: 'current-index',
|
||||
TRANSITION_ENABLED: 'transition-enabled',
|
||||
TRANSITION_DURATION_MS: 'transition-duration-ms',
|
||||
TRANSITION_STYLE: 'transition-style',
|
||||
|
||||
LIVE_WALLPAPER_ENABLED: 'live-wallpaper-enabled',
|
||||
LIVE_WALLPAPER_PATH: 'live-wallpaper-path',
|
||||
LIVE_WALLPAPER_FOLDERS: 'live-wallpaper-folders',
|
||||
LIVE_WALLPAPER_DISPLAY_MODE: 'live-wallpaper-display-mode',
|
||||
LIVE_WALLPAPER_MUTED: 'live-wallpaper-muted',
|
||||
LIVE_WALLPAPER_PLAYBACK_RATE: 'live-wallpaper-playback-rate',
|
||||
LIVE_WALLPAPER_PAUSE_ON_BATTERY: 'live-wallpaper-pause-on-battery',
|
||||
|
||||
Reference in New Issue
Block a user