UnlockDialog.UnlockDialogClock.prototype is undefined

Hi,

I am trying to port one of my extension from gnome-shell v44 to v45.
I am having difficulty to figure out how to solve this undefined error.

image

Here is my extension code

import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';

export default class ControlBlurExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialogClock.prototype, '_updateClock',
        () => {
            const settings = this.getSettings();
            return function () {
                    ...
           }

Any help is much appriciated.

The UnlockDialogClock class isn’t exported, so I guess you should probably pass UnlockDialog._clock.constructor.prototype instead.

@andyholmes still same error

image

export default class ControlBlurExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog._clock.constructor.prototype, '_updateClock',
        () => {

Ah, right. I suppose you’ll have to get a live instance of the UnlockDialog to do it that way. I guess you might have to do a bit of digging to find that :wink:

Does something like the following work?

import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';

export default class ControlBlurExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialog.prototype, '_init',
        originalMethod => {
            const settings = this.getSettings();
            return function (...args) {
               originalMethod.call(this, ...args);

              // monkey-patch 'this._clock'
                    ...
           }

@fmuellner thank you for the reply. It seems to work. Sorry for my late reply, late is due to I was trying all the ways how to figure out monkey-patch. I tried online how to do monkey-patching but those posts seems bit old like 2011, 2015. I tried the code like below

import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';
import ModifiedClock from './ModifiedClock.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Shell from 'gi://Shell';

export default class CustomizeClockOnLockScreenExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialog.prototype, '_init',
            originalMethod => {
                return function (...args) {
                    originalMethod.call(this, ...args);

                    // monkey-patch 'this._clock'
                    let oldFn = this._clock._updateHint;

                    this._clock._updateHint = () => {
                        this._clock._hint.text = 'PRATAP KUMAR'
                        Main.notify(JSON.stringify(this._clock._hint.text));
                    }
                    return oldFn.apply(this, ...args);
                }
            })
    }

    disable() {
        this._injectionManager.clear();
        this._injectionManager = null;
    }
}

And another code like below

import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';
import ModifiedClock from './ModifiedClock.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Shell from 'gi://Shell';

export default class CustomizeClockOnLockScreenExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialog.prototype, '_init',
            originalMethod => {
                return function (...args) {
                    originalMethod.call(this, ...args);

                    this._clock = new ModifiedClock;
                }
            })
    }

    disable() {
        this._injectionManager.clear();
        this._injectionManager = null;
    }
}
// ModifiedClock.js
import St from 'gi://St';
import GObject from 'gi://GObject';
import GnomeDesktop from 'gi://GnomeDesktop';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';

import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { formatDateWithCFormatString } from 'resource:///org/gnome/shell/misc/dateUtils.js';

const ModifiedClock = GObject.registerClass(
    class ModifiedClock extends St.BoxLayout {
        _init() {
            super._init({ style_class: 'unlock-dialog-clock', vertical: true });

            this._time = new St.Label({
                style_class: 'unlock-dialog-clock-time',
                x_align: Clutter.ActorAlign.CENTER,
            });
            this._date = new St.Label({
                style_class: 'unlock-dialog-clock-date',
                x_align: Clutter.ActorAlign.CENTER,
            });
            this._hint = new St.Label({
                style_class: 'unlock-dialog-clock-hint',
                x_align: Clutter.ActorAlign.CENTER,
                opacity: 0,
            });

            this.add_child(this._time);
            this.add_child(this._date);
            this.add_child(this._hint);

            this._wallClock = new GnomeDesktop.WallClock({ time_only: true });
            this._wallClock.connect('notify::clock', this._updateClock.bind(this));

            this._seat = Clutter.get_default_backend().get_default_seat();
            this._seat.connectObject('notify::touch-mode',
                this._updateHint.bind(this), this);

            this._monitorManager = global.backend.get_monitor_manager();
            this._monitorManager.connectObject('power-save-mode-changed',
                () => (this._hint.opacity = 0), this);

            this._idleMonitor = global.backend.get_core_idle_monitor();
            this._idleWatchId = this._idleMonitor.add_idle_watch(4 * 1000, () => {
                this._hint.ease({
                    opacity: 255,
                    duration: 300,
                });
            });

            this._updateClock();
            this._updateHint();
        }

        _updateClock() {
            this._time.text = this._wallClock.clock;
            let date = new Date();
            let dateFormat = Shell.util_translate_time_string('%A %B %-d');
            this._date.text = formatDateWithCFormatString(date, dateFormat);
        }

        _updateHint() {
            this._hint.text = this._seat.touch_mode
                ? 'Swipe up to unlock'
                : 'Click or press a key to unlock'
        }

        _onDestroy() {
            this._wallClock.run_dispose();
            this._idleMonitor.remove_watch(this._idleWatchId);
        }
    }
)

export default ModifiedClock;

I got stuck on how to implement a monkey-patch in originalMethod function.
can you please guide me on this by referencing some examples or documentation please.

Thanks

@fmuellner can you help me with this below small monkey-patch, It is not working. If I can make this happen then I can manage with above code.

import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';

export default class CustomizeClockOnLockScreenExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialog.prototype, '_init',
            originalMethod => {
                this._settings = this.getSettings();
                return function (...args) {
                    originalMethod.call(this, ...args);

                    const originalUpdateHint = this._clock._updateHint;
                    this._clock._updateHint = function () {
                        this._clock._hint.text = 'Hello World!!!'
                        return originalUpdateHint();
                    }
                }
            })
    }

    disable() {
        this._injectionManager.clear();
        this._injectionManager = null;
    }
}

Thanks

It doesn’t work because _updateHint is used as a callback function for connection created before your patch is applied.

Thank you for your response @GdH, after several trail and errors I managed to work out the original problem.

Cool!

For for the misleading advise, I missed that the UnlockDialog is created before the extension is enabled.

At least that should make the injection easier, as you can just use Main.screenShield._dialog._clock directly.

(For the issue @GdH pointed out, it would help if we called _updateHints() from an anonymous handler instead of binding it.)

@fmuellner Thank you for your response, I will try this way too. Rite now I managed in below way

import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';
import ModifiedClock from './ModifiedClock.js';

export default class CustomizeClockOnLockScreenExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialog.prototype, '_init',
        originalMethod => {
                const settings = this.getSettings();
                return function (...args) {
                    originalMethod.call(this, ...args);
                    this._stack.remove_child(this._clock);
                    this._clock = new ModifiedClock(settings);
                    this._stack.add_child(this._clock);
                }
            })
    }

    disable() {
        this._injectionManager.clear();
        this._injectionManager = null;
    }
}

Wait, what session modes is your extension using?

Hi @fmuellner
Here are the session mode details

  "session-modes": [
    "unlock-dialog",
    "user"
  ],

This is how the extension working with the last posted code.

Mmh, I think you end up using a loop hole.

This is what I think happens:

  1. extension gets enabled in ‘user’ mode, modifies UnlockDialog
  2. while the session is getting locked, the dialog is created
  3. session mode changes to ‘unlock-dialog’
    • extension may or may not get disabled-reenabled, but the modified object already exists anyway

Assuming that the extension is only about changing the lock screen, it would be nicer to only enabled it there, instead of relying on modifications “leaking” from one mode to the other.

Hi @fmuellner,

you mean, removing the unlock-dialog from session-modes?
with the current setup, enabling and disabling the extension is working fine.

I will try to refactor and see with your points and get back to you.

Thanks

No, I mean the opposite: Removing user.

I tried to remove the user from session-modes and the extension does not have any effect.

// metadata.json

{
  "_generated": "Generated by SweetTooth, do not edit",
  "description": "Customize Clock on Lock Screen.",
  "name": "Customize Clock on Lock Screen",
  "session-modes": [
    "unlock-dialog"
  ],
  "settings-schema": "org.gnome.shell.extensions.lockscreen",
  "shell-version": [
    "45"
  ],
  "url": "https://github.com/PRATAP-KUMAR/customize-clock-on-lock-screen",
  "uuid": "CustomizeClockOnLockScreen@pratap.fastmail.fm",
  "version": 7
}
// extension.js
import { Extension, InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js';
import ModifiedClock from './ModifiedClock.js';

export default class CustomizeClockOnLockScreenExtension extends Extension {
    enable() {
        this._injectionManager = new InjectionManager();
        this._injectionManager.overrideMethod(UnlockDialog.UnlockDialog.prototype, '_init',
            originalMethod => {
                const settings = this.getSettings();
                return function (...args) {
                    originalMethod.call(this, ...args);
                    this._stack.remove_child(this._clock);
                    this._clock = new ModifiedClock(settings);
                    this._clock.set_pivot_point(0.5, 0.5);
                    this._stack.add_child(this._clock);
                }
            })
    }

    disable() {
        this._injectionManager.clear();
        this._injectionManager = null;
    }
}

I will read your points again and again and try to refactor the code @fmuellner. Thanks for your quick support.

Hi @fmuellner,
What I noticed is, after removing user from session-modes,

even though the extension is enabled, once the screen is locked and unlocked, the extension is automatically getting disabled.

May be this is the cause, the code is not effective. Any clue how to avoid this automatic disabling?

Thanks

Yes, if session-modes only contains unlock-dialog, then the extension is only enabled on the lock screen. That’s extremely uncommon, but sounds appropriate for an extension that modifies the lock screen.

Ok noted @fmuellner,

I just tested the extension with Main.notify(). Extension is working on lock-screen.
I will try something and get back to you with another blocker.

Thanks