Creating an extension with a simple toggle button

I’d like to create a minimal extension that has a simple toggle button without menu. I simply want the icon that appears in the gnome panel to directly be a toggle button.

I found plenty of examples involving toggle buttons within submenus, but I can’t figure out how to register a click handler on the icon directly.

Any ideas?

const { GObject, St } = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;

const _ = ExtensionUtils.gettext;

const Indicator = GObject.registerClass(
class Indicator extends PanelMenu.Button {
    _init() {
        super._init(0.0, _('My Shiny Indicator'));

        const enabledIcon = new St.Icon({
            icon_name: 'face-smile-symbolic',
            style_class: 'system-status-icon',
        });

        const disabledIcon = new St.Icon({
            icon_name: 'face-sad-symbolic',
            style_class: 'system-status-icon',
        });

        this.add_child(enabledIcon);


        // How can I do soemthing like this?
        let toggleStatus = true

        function onIconClick() {
            toggleStatus = !toggleStatus
            if (toggleStatus) {
                Main.notify('Toggle has been turned on!');
                this.remove_child(disabledIcon);
                this.add_child(enabledIcon);
            } else {
                Main.notify('Toggle has been turned off!');
                this.remove_child(enabledIcon);
                this.add_child(disabledIcon);
            }
            Main.notify('The new toggle status is: ' + toggleStatus);
        }

        enabledIcon.on('click', onIconClick)
        disabledIcon.on('click', onIconClick)


    }
});

class Extension {
    constructor(uuid) {
        this._uuid = uuid;
    }

    enable() {
        this._indicator = new Indicator();
        Main.panel.addToStatusArea(this._uuid, this._indicator);
    }

    disable() {
        this._indicator.destroy();
        this._indicator = null;
    }
}

function init(meta) {
    return new Extension(meta.uuid);
}

The PanelMenu.Button is a subclass of Clutter.Actor, so you can connect to the Clutter.Actor::event signal:

_init () {
    super._init(0.0, _('My Shiny Indicator'));

    this._icon = new St.Icon({
        icon_name: 'face-smile-symbolic',
        style_class: 'system-status-icon',
    });
    this.add_child(this._icon);

    // Connect to signals using GObject.Object.connect(), use
    // Function.bind() on the callback so you can use `this`
    this.connect('event', this._onClicked.bind(this));

    // Alternatively, using an arrow function will allow the same
    this.connect('event', (actor, event) => {
        // etc
    });
}

_onClicked(actor, event) {
    // Some other non-clicky event happened; bail
    if ((event.type() !== Clutter.EventType.TOUCH_BEGIN &&
         event.type() !== Clutter.EventType.BUTTON_PRESS))
        return Clutter.EVENT_PROPAGATE;

    // Might as well check the toggled state based on the icon-name
    if (this._icon.icon_name === 'face-smile-symbolic') {
        this._icon.icon_name = 'face-sad-symbolic';
        Main.notify('Toggle has been turned off!');
    } else {
        this._icon.icon_name = 'face-smile-symbolic';
        Main.notify('Toggle has been turned on!');
    }

    // Propagate the event
    return Clutter.EVENT_PROPAGATE;
}
1 Like

Thank you, that works like a charm! :slight_smile: The link to the documentation is really helpful as well. I’ve been searching through the source code on Gitlab the whole time and couldn’t make sense of the class hierarchy.

Edit:
In case anyone needs it, here is a full code example:

// Simple Toogle Button Extension

const { Clutter, GObject, St } = imports.gi;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;

const TOGGLE_ON_ICON = 'face-smile-symbolic';
const TOGGLE_OFF_ICON = 'face-sad-symbolic';

const Indicator = GObject.registerClass(
class Indicator extends PanelMenu.Button {
    _init () {
        super._init(0.0, 'Toggle Button');
    
        this._icon = new St.Icon({
            icon_name: 'face-smile-symbolic',
            style_class: 'system-status-icon',
        });

        this.add_child(this._icon);

        this.connect('event', this._onClicked.bind(this));
    }
    _onClicked(actor, event) {
        if ((event.type() !== Clutter.EventType.TOUCH_BEGIN && event.type() !== Clutter.EventType.BUTTON_PRESS)) {
            // Some other non-clicky event happened; bail
            return Clutter.EVENT_PROPAGATE;
        }
    
        if (this._icon.icon_name === TOGGLE_ON_ICON) {
            this._icon.icon_name = TOGGLE_OFF_ICON;
            Main.notify('Toggle has been turned off!');
        } else {
            this._icon.icon_name = TOGGLE_ON_ICON;
            Main.notify('Toggle has been turned on!');
        }
        
        return Clutter.EVENT_PROPAGATE;
    }
});

class Extension {
    constructor(uuid) {
        this._uuid = uuid;
    }

    enable() {
        this._indicator = new Indicator();
        
        Main.panel.addToStatusArea(this._uuid, this._indicator);
    }

    disable() {
        this._indicator.destroy();
        this._indicator = null;
    }
}

function init(meta) {
    return new Extension(meta.uuid);
}
1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.