Drawers for GNOME 3 gnome-shell

This post is more than 13 years old.

After upgrading my computer from Fedora 14 to Fedora 15 recently, I’m getting used to the new world of GNOME 3 and the GNOME Shell. I think I probably like it. Mostly. But it’s missing one big thing that I really loved in GNOME 2: drawers!

I used to have drawers on my panel that allowed me quick access to about two dozen applications and a dozen local copies of documentation, and I really missed them. But apparently, they had to go: the new extension system in GNOME 3 isn’t compatible with the way the old panel apps were written, and drawers were a stand-out example of that.

Fortunately, the new extension environment isn’t too hard to learn, especially if you already have some JavaScript under your belt. Unfortunately, I don’t have time to write a full replacement for the drawers panel app, but it wasn’t too hard to knock up something that gives me the functionality I wanted with hard-coded lists of links, especially after looking at the code in the “places menu” extension.

So, without further ado, and assuming any keen extenders will check out the links above, here’s my simplistic implementation of drawers for the GNOME Shell. I’d be very pleased if someone picked up this code and completed the project, by adding a data store and a config screen to it. If you do work this up into a fully functional extension, please let me know!

// Simple implementation of drawers for panel on Gnome 3 shell

const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Gio = imports.gi.Gio;
const Params = imports.misc.params;
const Utils = imports.misc.util;

const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Panel = imports.ui.panel;
const PlaceDisplay = imports.ui.placeDisplay;

const PLACE_ICON_SIZE = 22;

function SimplePlace(name, path, isApp) {
    this._init(name, path, isApp);
}

SimplePlace.prototype = {
    __proto__: PlaceDisplay.PlaceInfo.prototype,

    _init: function(name, path, isApp) {
        this._path = path;
        this.name = name;
        this._lowerName = name.toLowerCase();
        this._isApp = isApp;
        this.id = 'simple:' + path;
    },

    iconFactory: function(size) {
        let icon = Shell.util_get_icon_for_uri(this._path);
        return St.TextureCache.get_default().load_gicon(null, icon, size);
    },

    launch: function(params) {
        if (params) {
            params = Params.parse(params, { workspace: null, timestamp: null });

            let launchContext = global.create_app_launch_context();
            if (params.workspace != null)
                launchContext.set_desktop(params.workspace.index());
            if (params.timestamp != null)
                launchContext.set_timestamp(params.timestamp);
        }

        if (this._isApp) {
            Utils.spawnCommandLine(this._path);
        }
        else {
            Gio.app_info_launch_default_for_uri(this._path, null);
        }
    },
}

function SimpleDrawers() {
    this._init.apply(this, arguments);
}

SimpleDrawers._install = function() {
    // TODO: move this into a JSON file somewhere...
    // TODO: provide a GUI for editing the JSON file
    // TODO: discover the available symbolic icon names so can differentiate the drawers better
    SimpleDrawers._drawers = [
        {"icon": "folder-documents", "title": "Documentation", "links": [
            {"isApp": false, "name": "PHP Manual", "path": "file:///opt/doco/php_manual_en.chm"},
            {"isApp": false, "name": "MySQL 5.1 Manual", "path": "file:///opt/doco/refman-5.1-en.a4.pdf"},
            {"isApp": false, "name": "MySQL 5.5 Manual", "path": "file:///opt/doco/refman-5.5-en.a4.pdf"},
            {"isApp": false, "name": "PostgeSQL 8.4.1", "path": "file:///opt/doco/postgresql-8.4.1-A4.pdf"},
            {"isApp": false, "name": "Subversion", "path": "file:///opt/doco/svn-book.pdf"},
            {"isApp": false, "name": "GNU Make", "path": "file:///opt/doco/make.pdf"},
        ]},
        {"icon": "folder-documents", "title": "Browsers", "links": [
            {"isApp": true, "name": "Firefox 4", "path": "/usr/lib64/firefox-4/firefox"},
            {"isApp": true, "name": "Google Chrome", "path": "/usr/bin/google-chrome"},
            {"isApp": true, "name": "Opera", "path": "/usr/bin/opera"},
            {"isApp": true, "name": "RDP", "path": "/usr/bin/remmina"},
        ]}
    ];

    // iterate over list of drawers to add them to panel, giving each a unique name
    for (let i = 0; i < SimpleDrawers._drawers.length; i++) {
        let drawName = "simple-drawers-" + i;
        Panel.STANDARD_TRAY_ICON_ORDER.unshift(drawName);
        Panel.STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION[drawName] = SimpleDrawers;
    }

    // kludge: record counter of last added drawer, so that we can have multiple drawers
    // TODO: find a nicer way to reconnect multiple instances of same "class" with different panel icons
    SimpleDrawers._lastDrawerAdded = -1;
}

SimpleDrawers.prototype = {
    __proto__: PanelMenu.SystemStatusButton.prototype,

    _init: function() {
        // kludge: use counter of last added drawer to infer next draw to add
        // TODO: find a nicer way to reconnect multiple instances of same "class" with different panel icons
        if (SimpleDrawers._lastDrawerAdded < SimpleDrawers._drawers.length - 1) {
            SimpleDrawers._lastDrawerAdded += 1;
            let draw = SimpleDrawers._drawers[SimpleDrawers._lastDrawerAdded];
            PanelMenu.SystemStatusButton.prototype._init.call(this, draw.icon, draw.title);

            this._createDrawers(draw);
        }
    },

    _createDrawers : function(draw) {
        try {
            for (let i = 0; i < draw.links.length; i++) {
                let link = draw.links[i];

                let place = new SimplePlace(link.name, link.path, link.isApp);
                let icon = place.iconFactory(PLACE_ICON_SIZE);

                let menuItem = new PopupMenu.PopupMenuItem(link.name);
                menuItem.addActor(icon, { align: St.Align.END});
                menuItem.place = place;
                this.menu.addMenuItem(menuItem);

                menuItem.connect('activate', function(actor,event) {
                    actor.place.launch();
                });
            }
        }
        catch(e) {
            let menuItem = new PopupMenu.PopupMenuItem(e.message);
            this.menu.addMenuItem(menuItem);
        }
    }
};

function main() {
    SimpleDrawers._install();
}

There. Job is (half) done!