Skip to content
Open
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
245 changes: 173 additions & 72 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {Extension, InjectionManager} from 'resource:///org/gnome/shell/extension
import St from 'gi://St';
import Shell from 'gi://Shell';
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';

import { Keys } from './enums.js';
import { PlayerProcess } from './core/player_process.js';
Expand All @@ -17,14 +18,9 @@ import { sendErrorNotification } from './utils/notifications.js';

export default class LockscreenExtension extends Extension {
enable() {
this._backgroundCreated = false;
this._wrapperActors = {}; // connector -> actor
this._windowActors = {}; // connector -> actor
this._lockPositionSignals = [];

this._promptShown = false;
this._injectionManager = null;
this._player = null;
this._resetLockState();
this._lockActive = false;
this._screenShieldId = 0;

if (!isGtk4PaintableSinkAvailable()) {
sendErrorNotification(
Expand All @@ -36,10 +32,56 @@ export default class LockscreenExtension extends Extension {

this._settings = this.getSettings();

// Defer the actual player setup until the screen shield
// reports it is active. Hooking `Main.screenShield._dialog`
// directly at enable() time is unreliable: the dialog object
// can be null (never locked yet), or become a dead reference
// if GNOME recreates it between enable() and the next lock
// cycle. Listening to `active-changed` guarantees we inject
// into the current, live dialog on every lock.
this._screenShieldId = Main.screenShield.connect(
'active-changed',
this._onActiveChanged.bind(this)
);

// If the shield is already active at enable() time (e.g. the
// user just toggled the extension on from the lock screen),
// run setup immediately.
if (Main.screenShield.active)
this._onActiveChanged();
}

_resetLockState() {
this._backgroundCreated = false;
this._wrapperActors = {}; // connector -> actor
this._windowActors = {}; // connector -> actor
this._lockPositionSignals = [];

this._promptShown = false;
this._injectionManager = null;
this._player = null;

this._injectRetryId = 0;
this._injectAttempts = 0;
}

_onActiveChanged() {
const active = Main.screenShield.active;

if (active && !this._lockActive) {
this._lockActive = true;
this._setupForLock();
} else if (!active && this._lockActive) {
this._lockActive = false;
this._teardownForUnlock();
}
}

_setupForLock() {
const disableOnBatter = this._settings.get_boolean(Keys.DISABLE_ON_BATTERY);
if (disableOnBatter && isOnBattery()) {
console.warn('Skipping on battery');
return;
return;
}

const videoPath = this._settings.get_string(Keys.VIDEO_PATH);
Expand Down Expand Up @@ -88,7 +130,7 @@ export default class LockscreenExtension extends Extension {
framerate,
colorAccurate: colorAccurate
});

try {
this._player.run();
} catch (e) {
Expand Down Expand Up @@ -119,48 +161,79 @@ export default class LockscreenExtension extends Extension {
this._windowActors[connector] = win.get_compositor_private();
}

this._injectCreateBackground();

this._injectionManager.overrideMethod(
Main.screenShield._dialog, '_showPrompt',
(original) => {
const self = this;
return function(...args) {
original.call(this, ...args);
self._onPromptShow();
};
}
);

this._injectionManager.overrideMethod(
Main.screenShield._dialog, '_showClock',
(original) => {
const self = this;
return function(...args) {
original.call(this, ...args);
self._onPromptHide();
};
}
);
this._injectIntoDialog();
}, (err) => {
console.error(`Unable to intercept all windows: ${err}`);
})
}

//NOTE: Replacing TapAction with a fresh one if exists (for gnome 48 and older)
const actions = Main.screenShield._dialog.get_actions();
const tapAction = actions.find(a => {
//HACK: Maybe not the most beautiful solution, but works
return a.constructor.name.includes('TapAction')
_injectIntoDialog() {
// `active-changed` can fire a hair before `_dialog` is
// populated in some GNOME versions. Poll briefly if so.
//
// We cap the retry loop at ~2s worth of 100ms ticks. The
// original single-shot 100ms retry was too short on at
// least some GNOME 49 setups, where `_dialog` appears to
// be recreated on each lock and isn't ready yet on second
// and subsequent locks. The retry id is tracked so
// `_teardownForUnlock()` can cancel it and avoid firing
// injection work into a half-torn-down state.
const dialog = Main.screenShield._dialog;
if (!dialog) {
if (this._injectAttempts >= 20) {
console.warn('LiveLockScreen: _dialog never appeared after 20 retries, giving up');
this._injectAttempts = 0;
return;
}
this._injectAttempts++;
this._injectRetryId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
this._injectRetryId = 0;
if (this._lockActive) this._injectIntoDialog();
return GLib.SOURCE_REMOVE;
});
if (tapAction) {
Main.screenShield._dialog.remove_action(tapAction);

let newAction = new Clutter.TapAction();
newAction.connect('tap', Main.screenShield._dialog._showPrompt.bind(Main.screenShield._dialog));
Main.screenShield._dialog.add_action(newAction);
return;
}
this._injectAttempts = 0;

this._injectCreateBackground();

this._injectionManager.overrideMethod(
dialog, '_showPrompt',
(original) => {
const self = this;
return function(...args) {
original.call(this, ...args);
self._onPromptShow();
};
}
);

Main.screenShield._dialog._updateBackgrounds();
}, (err) => {
console.error(`Unable to intercept all windows: ${err}`);
})
this._injectionManager.overrideMethod(
dialog, '_showClock',
(original) => {
const self = this;
return function(...args) {
original.call(this, ...args);
self._onPromptHide();
};
}
);

//NOTE: Replacing TapAction with a fresh one if exists (for gnome 48 and older)
const actions = dialog.get_actions();
const tapAction = actions.find(a => {
//HACK: Maybe not the most beautiful solution, but works
return a.constructor.name.includes('TapAction')
});
if (tapAction) {
dialog.remove_action(tapAction);

let newAction = new Clutter.TapAction();
newAction.connect('tap', dialog._showPrompt.bind(dialog));
dialog.add_action(newAction);
}

dialog._updateBackgrounds();
}

_injectCreateBackground() {
Expand Down Expand Up @@ -204,7 +277,7 @@ export default class LockscreenExtension extends Extension {
});
})
}

if (this._promptSettings[Keys.PROMPT_PAUSE])
this._player?.pause();
}
Expand All @@ -216,7 +289,7 @@ export default class LockscreenExtension extends Extension {
if (this._promptSettings[Keys.PROMPT_CHANGE_BLUR]) {
const radius = this._blurRadius;
const brightness = radius ? this._blurBrightness : 1;

Object.values(this._wrapperActors).forEach(actor => {
actor.ease_property('@effects.lockscreen-extension-blur.radius', radius, {
duration: this._promptSettings[Keys.PROMPT_BLUR_ANIM_DURATION],
Expand Down Expand Up @@ -267,26 +340,26 @@ export default class LockscreenExtension extends Extension {
const isLastMonitor = monitorIndex === Main.layoutManager.monitors.length - 1;
const monitor = Main.layoutManager.monitors[monitorIndex];
const windowActor = this._windowActors[targetConnector];

if (windowActor) {
const parent = windowActor.get_parent();
if (parent) parent.remove_child(windowActor);

const wrapper = new Clutter.Actor();

Main.screenShield._dialog._backgroundGroup.add_child(wrapper);
Main.screenShield._dialog._backgroundGroup.set_child_above_sibling(wrapper, null);

wrapper.add_effect(new Shell.BlurEffect(this._blurEffect));

// Adding color desaturation effect if needed
if (this._promptSettings[Keys.PROMPT_GRAYSCALE]) {
wrapper.add_effect_with_name(
'lockscreen-extension-desaturate',
'lockscreen-extension-desaturate',
new Clutter.DesaturateEffect({ factor: 0.0 })
);
}

if (!this._backgroundCreated)
wrapper.opacity = 0;

Expand All @@ -312,8 +385,8 @@ export default class LockscreenExtension extends Extension {
wrapper.set_size(monitor.width, monitor.height);
wrapper.set_clip_to_allocation(true);

// NOTE:
// This might look like an overkill,
// NOTE:
// This might look like an overkill,
// but you really need to aggressively position actors on any
// size/position change
const fixPositionAndScale = () => {
Expand All @@ -328,16 +401,16 @@ export default class LockscreenExtension extends Extension {
const sigW = windowActor.connect('notify::width', fixPositionAndScale);
const sigH = windowActor.connect('notify::height', fixPositionAndScale);
this._lockPositionSignals.push({ actor: windowActor, ids: [sigX, sigY, sigW, sigH] });

fixPositionAndScale();
}

this._wrapperActors[targetConnector] = wrapper;

} else {
console.warn(`No window actor for monitor ${monitorIndex}, skipping`);
}

if (!this._backgroundCreated && isLastMonitor) {
this._initLoginManager();
this._startAnimation();
Expand All @@ -363,20 +436,30 @@ export default class LockscreenExtension extends Extension {
});
}

disable() {
_teardownForUnlock() {
// Cancel any pending dialog-injection retry so it can't fire
// into a partially torn-down state or leak across lock cycles.
if (this._injectRetryId) {
try { GLib.source_remove(this._injectRetryId); } catch (_) {}
this._injectRetryId = 0;
}
this._injectAttempts = 0;

for (const { actor, ids } of this._lockPositionSignals) {
for (const id of ids) {
actor.disconnect(id);
try { actor.disconnect(id); } catch (_) { /* actor gone */ }
}
}
this._lockPositionSignals = [];

// Return all window actors to window_group before destroying
for (const windowActor of Object.values(this._windowActors)) {
const parent = windowActor.get_parent();
if (parent) parent.remove_child(windowActor);
global.window_group.add_child(windowActor);
windowActor.hide();
try {
const parent = windowActor.get_parent();
if (parent) parent.remove_child(windowActor);
global.window_group.add_child(windowActor);
windowActor.hide();
} catch (_) { /* actor gone */ }
}
this._windowActors = {};

Expand All @@ -386,17 +469,35 @@ export default class LockscreenExtension extends Extension {
this._injectionManager?.clear();
this._injectionManager = null;

if (this._sleepId) {
if (this._sleepId && this._loginManager) {
this._loginManager.disconnect(this._sleepId);
this._sleepId = null;
}
this._loginManager = null;

Object.values(this._wrapperActors).forEach(actor => {
actor.remove_effect_by_name('lockscreen-extension-blur');
actor.remove_effect_by_name('lockscreen-extension-desaturate');
actor.destroy()
})
try {
actor.remove_effect_by_name('lockscreen-extension-blur');
actor.remove_effect_by_name('lockscreen-extension-desaturate');
actor.destroy();
} catch (_) { /* already destroyed */ }
});
this._wrapperActors = {};
this._backgroundCreated = false;
this._promptShown = false;
}

disable() {
if (this._screenShieldId) {
Main.screenShield.disconnect(this._screenShieldId);
this._screenShieldId = 0;
}

if (this._lockActive) {
this._teardownForUnlock();
this._lockActive = false;
}

this._settings = null;
}
}