Search code examples
javascriptgnome-shell-extensionsgjs

Why doesn't overriding a class function work sometimes?


I followed this https://wiki.gnome.org/Projects/GnomeShell/Extensions/StepByStepTutorial for Overwriting a function.

For example, I want to override the function _setupKeyboard() on the Keyboard class, but my override isn't invoked. The specific portion I want to change is this, to remove the if guard:

if (Meta.is_wayland_compositor()) {
    this._connectSignal(this._keyboardController, 'emoji-visible',
        this._onEmojiKeyVisible.bind(this));
}

I copied the function from the source, removed the part I didn't want, then set the replacement function like this:

const Keyboard = imports.ui.keyboard;

Keyboard.Keyboard.prototype._setupKeyboard = myOverride;

Why isn't my override being invoked and how can I achieve this?


Solution

  • There are two common reasons an override won't be invoked. If the method is invoked before your override is applied, or if the function is a callback set with Function.prototype.bind() which creates a new closure.

    In this case, the function _setupKeyboard() is called before your override is applied. When GNOME Shell starts up, it creates an instance of Keyboard.KeyboardManager here:

    // main.js, line #204
    keyboard = new Keyboard.KeyboardManager();
    

    By the time the keyboard variable has been assigned to the instance, a default Keyboard.Keyboard class has been created and the function _setupKeyboard() has already been called in Keyboard._init(), which is much sooner than your extension is loaded.

    Since there's no way to easily fix that, your best option is to just re-create the one part of the code you want to run:

    const Meta = imports.gi.Meta;
    const Main = imports.ui.main;
    const Keyboard = imports.ui.keyboard.Keyboard;
    
    
    const originalSetup = Keyboard.prototype._setupKeyboard;
    
    const modifiedSetup = function () {
        originalSetup.call(this);
    
        if (!Meta.is_wayland_compositor()) {
            this._connectSignal(this._keyboardController, 'emoji-visible',
                this._onEmojiKeyVisible.bind(this));
        }
            
        this._relayout();
    };
    
    
    function init() {
    }
    
    // Your extension's enable function (might be a class method)
    function enable() {
        let kbd = Main.keyboard.keyboardActor;
    
        if (kbd !== null) {
            if (!Meta.is_wayland_compositor()) {
                kbd.__mySignalId = kbd._connectSignal(kbd._keyboardController, 'emoji-visible',
                    kbd._onEmojiKeyVisible.bind(kbd));
            }
        }
    
        Keyboard.prototype._setupKeyboard = modifiedSetup;
    }
    
    function disable() {
        let kbd = Main.keyboard.keyboardActor;
    
        if (kbd !== null && kbd.__mySignalId) {
            kbd.disconnect(kbd.__mySignalId);
            kbd.__mySignalId = 0;
        }
    
        Keyboard.prototype._setupKeyboard = originalSetup;
    }
    

    This is not very pretty, but that is often the price of patching private code. I can also not guarantee that the code will do what you want, because I suspect the emoji key is hidden on X11 for a reason.