javascriptlinuxappletgnomecinnamon

How to display multiple objects in a Cinnamon Applet


I am trying to write a Cinnamon Applet for my panels. I have figured out how to create a simple icon but I cannot figure out how can I get a second one to appear.

Here is my current simple code:

const Applet = imports.ui.applet;

class wholeModule extends Applet.Applet {
    constructor(orientation, panel_height, instance_id) {
        let icon = new someIcon(orientation, panel_height, instance_id);
        icon.updateIconName('google-chrome-symbolic');

        let icon2 = new someIcon(orientation, panel_height, instance_id);
        icon2.updateIconName('google-chrome');

        return icon;
    }
}

class someIcon extends Applet.IconApplet {
    constructor(orientation, panel_height, instance_id) {
        super(orientation, panel_height, instance_id);

        this.set_applet_icon_name('user-desktop-symbolic');
        this.set_applet_tooltip(_('test text'));
    }

    updateIconName(name) {
        this.set_applet_icon_name(name);
    }

    setToolTip(text) {
        this.set_applet_tooltip(_(text));
    }
}

function main(metadata, orientation, panel_height, instance_id) {
    return new wholeModule(orientation, panel_height, instance_id);
}

I am trying to create a workspace-switcher that has icons instead of text labels on my side panel and this part is what I cannot wrap my brain around.

I get that the main function calls for an object that ends up displayed on the Applet. How can I spawn another icon from someIcon class?


Solution

  • General approach

    The class IconApplet is only able to handle one icon. I would use the code of that class as a template to write a class MultiIconApplet that is able to handle multiples.

    You can find the original code in /usr/share/cinnamon/js/ui/applet.js and search for class IconApplet. Copy it out and modify. Where one icon container is built, you can have an array of them:

    var MultiIconApplet = class MultiIconApplet extends Applet {
    
        // provide the number of icons you need as an extra parameter
        _init(orientation, panel_height, icon_count, instance_id) {
            super._init(orientation, panel_height, instance_id);
    
            this.orientation = orientation; //orientation makes a difference
            this._applet_icon_boxes = [];   //array of containers
            this._applet_icons = [];        //array of icons
    
            for (var i = 0; i < icon_count; i++) {
                 var box = new St.Bin();
                 this._applet_icon_boxes.push(box);
    
                 box.set_fill(true,true);
                 box.set_alignment(St.Align.MIDDLE,St.Align.MIDDLE);
                 this.actor.add(box);
        }
    
        // this method constructs the actual icons
        _ensureIcon(index) {
            if (!this._applet_icons[index] ||
                !(this._applet_icons[index] instanceof St.Icon))
                this._applet_icons[index] = new St.Icon({
                     reactive: true, track_hover: true,
                     style_class: 'applet-icon'
                });
    
            this._applet_icon_boxes[index].set_child(this._applet_icons[index]);
        }
    

    Then, implement each of the IconApplet methods, but state the index of the icon you want to target. For example:

        set_applet_icon_name (index, icon_name) {
            this._ensureIcon(index);
    
            this._applet_icon[index].set_icon_name(icon_name);
            this._applet_icon[index].set_icon_type(St.IconType.FULLCOLOR);
            this._setStyle();
        }
    

    Repeat for each of the methods, exchanging this._applet_icon with this._applet_icons[index].

    There is one method that overwrites the parent class method, so you need to implement that without parameter and loop through the icons:

        on_panel_height_changed_internal() {
            this.applet_icons.forEach((icon, index) => {
                if (icon) this._setStyle(index);
            )};
            this.on_panel_height_changed();
        }
    

    Finally, you need to implement a reaction to orientation change, which is only an abstract method in the parent class:

        on_orientation_changed(neworientation) {
            this.orientation = neworientation;
    
            if (this.orientation == St.Side.TOP || this.orientation == St.Side.BOTTOM)
                this.actor.set_vertical(false);
            else
                this.actor.set_vertical(true);
        }
    }
    

    Specialized solution for a workspace switcher

    Since what you want to achieve is a workspace switcher, you can also start out with the standard applet at /usr/share/cinnamon/applets/workspace-switcher@cinnamon.org and modify its code.

    All you need to do is look for the class SimpleButton in that applet, and where it adds a St.Label to represent the workspace, instead add a St.Icon:

    class SimpleButton extends WorkspaceButton {
        constructor(index, applet) {
            super(index, applet);
    
            this.actor = new St.Button({ name: 'workspaceButton',
                                         style_class: 'workspace-button',
                                         reactive: applet._draggable.inhibit });
    
            if (applet.orientation == St.Side.TOP || applet.orientation == St.Side.BOTTOM) {
                this.actor.set_height(applet._panelHeight);
            } else {
                this.actor.set_width(applet._panelHeight);
                this.actor.add_style_class_name('vertical');
            }
    
            let icon = new St.Icon({
                 icon_name: ..., // choose one based on index
                 icon_size: applet.getPanelIconSize()
                 style_class: 'applet-icon'
            });
            this.actor.set_child(icon);
            this.update();
        }