Drawers for GNOME 3 gnome-shell

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!

facebooktwittergoogle_plusredditpinterestlinkedinmailfacebooktwittergoogle_plusredditpinterestlinkedinmail
  • siwuch

    Hi.
    Could You tell me how to use it?

    Thanks :)

    • http://snippets.webaware.com.au/ rmckay

      You need to create a shell extension, and use the code above (or something better!) — see the guide here. But note that the code above was written for GNOME 3.1 shell; I believe that GNOME 3.2 is a little different; here’s guide to porting a 3.1 extension to 3.2.

      I’ve since jumped ship to XFCE so unfortunately I can’t be of help on this one any more. So if you get it going, please put it up somewhere (blog, github, …) and let me know so that I can link to it :)

      • http://butlerpc.net Michael Butler

        Wow, Gnome 3 made you go to Xfce? That is quite a jump. How do you like Xfce? Can you add drawers/pop-out-menus like Gnome 2?

  • http://snippets.webaware.com.au/ rmckay

    G’day Michael, I’ve heard that you can (there’s a panel applet for XFCE that allows you to add GNOME applets, though not in my Fedora 16 repo) but haven’t gone down that route. Mostly, I’m happy having a usable panel back again, and not playing which icon do I click on this minute? Maybe I’ll revisit drawers again one day, but pressures of work and Real Life have kept me away for now.