Translate

Archives

GNOME Shell/Cinnamon Extension Configuration Persistence

In a previous post, I demonstrated how you could modify the appearance and actions of a Cinnamon extension called righthotcorner, which added an overview hot corner to the right upper corner of your primary screen, by communicating with the extension via D-Bus. In this post I show you how I extended this extension to incorporate persistence of a user’s preferences for the extension’s configurable options, namely hot corner icon visibility and hot corner ripple visibility. Although I am using a Cinnamon extension for the purposes of this post, the concept is equally applicable to a GNOME Shell extension.

Most of the current GNOME Shell extensions that support persistent configuration use gsettings to store and retrieve their configuration information. For example the ubiquitous user-theme GNOME Shell extension stores the name of the current theme in a key called name in a schema named org.gnome.shell.extensions.user-theme whose schema definition lives in /usr/share/glib-2.0/schemas/org.gnome.shell.extensions.user-theme.gschema.xml. Here are the contents of that schema definition (the source form of the schema) file.

<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="gnome-shell-extensions">
  <schema path="/org/gnome/shell/extensions/user-theme/" id="org.gnome.shell.extensions.user-theme">
    <key type="s" name="name">
      <default>""</default>
      <summary>Theme name</summary>
      <description>The name of the theme, to be loaded from ~/.themes/name/gnome-shell</description>
    </key>
  </schema>
</schemalist>

Currently, there is work to support storing extension gschemas in a subdirectory under extensiondir called schemas. See convenience.js in the GNOME Shell Extensions git (git.gnome.org/gnome-shell-extensions).

/**
 * getSettings:
 * @schema: (optional): the GSettings schema id
 *
 * Builds and return a GSettings schema for @schema, using schema files
 * in extensionsdir/schemas. If @schema is not provided, it is taken from
 * metadata['settings-schema'].
 */
function getSettings(schema) {
    let extension = ExtensionUtils.getCurrentExtension();

    schema = schema || extension.metadata['settings-schema'];

    const GioSSS = Gio.SettingsSchemaSource;

    // check if this extension was built with "make zip-file", and thus
    // has the schema files in a subfolder
    // otherwise assume that extension has been installed in the
    // same prefix as gnome-shell (and therefore schemas are available
    // in the standard folders)
    let schemaDir = extension.dir.get_child('schemas');
    let schemaSource;
    if (schemaDir.query_exists(null))
        schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
                                                 GioSSS.get_default(),
                                                 false);
    else
        schemaSource = GioSSS.get_default();

    let schemaObj = schemaSource.lookup(schema, false);
    if (!schemaObj)
        throw new Error('Schema ' + schema + ' could not be found for extension '
                        + extension.metadata.uuid + '. Please check your installation.');

    return new Gio.Settings({ settings_schema: schemaObj });
}


Other people are experimenting with using .ini files to store preferences.

So why not use gsettings and DConf? Because it requires extra unnecessary work on the part of the extension developer, the extension packager and possibly the system administrator on the system where the extension is installed. This goes against the KISS principle! As an aside, the GNOME developers always seem to contravene the KISS principle where possible. I wonder what they smoke or drink at their meetups! If you want to use GSettings, you have to specify a schema that describes the keys in your settings and their types and default values, and that schema has to live in the standard location for such schemas. The source form of a schema (the schema description) is an XML document but before it can be actually used it must be converted into a compact binary form that is created by the glib-compile-schemas utility. This all seems very convoluted and unnecessary for a Cinnamon or GNOME Shell extension.

As an experiment, I decided to see how difficult it would be to store preferences in extensiondir using JSON in a file called prefs.json. Well, it turned out to be very easy to do so. Much easier than using gsettings or an .ini file.

Here is the modified extension.js with persistent preferences support:

//
//  Copyright (c) 2012  Finnbarr P. Murphy.  All rights reserved.
//
//  Version: 1.0 (02/16/2012)
//
//  License: Attribution Assurance License (see www.opensource.org/licenses)
//


const Lang = imports.lang;
const DBus = imports.dbus;
const Clutter = imports.gi.Clutter;
const Cinnamon = imports.gi.Cinnamon;
const St = imports.gi.St;
const Main = imports.ui.main;
const Tweener = imports.ui.tweener;

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Config = imports.misc.config;
const FileUtils = imports.misc.fileUtils;

const HOT_CORNER_ACTIVATION_TIMEOUT = 0.5;
const ICON_NAME    = 'overview-corner';  // 24x24 icon PNG under /usr/share/icons/... 
const HOTSPOT_SIZE = 1;                  // change if you need/want a bigger hotspot

// Preferences support
const EXTENSION_PREFS = '{ "iconvisible": true, "ripplevisible": true }';

// D-Bus support
const EXTENSION_VERSION = '1.0';
const EXTENSION_PATH = '/org/Cinnamon/extensions/righthandcorner';
const EXTENSION_IFACE = 'org.Cinnamon.extensions.righthandcorner';
const RightHotCornerIface = {
    name: EXTENSION_IFACE,
    properties: [{ name: 'ExtensionVersion', signature: 's', access: 'read' },
                 { name: 'HotCornerRippleVisibility', signature: 'b', access: 'readwrite' },
                 { name: 'HotCornerIconVisibility', signature: 'b', access: 'readwrite' }]
};


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

RightHotCorner.prototype = {
    ExtensionVersion: EXTENSION_VERSION,

    _init : function(extensionPath) {
        this._entered = false;

        // Preferences support
        this._prefspath =extensionPath;
        this._prefs = null;
        if (!this.readprefs(this._prefspath)) 
            this._prefs = JSON.parse(EXTENSION_PREFS);
        
        // D-Bus support
        DBus.session.proxifyObject(this, EXTENSION_IFACE, EXTENSION_PATH);
        DBus.session.exportObject(EXTENSION_PATH, this);
        DBus.session.acquire_name(EXTENSION_IFACE, 0, null, null);

        this.overviewCorner = new St.Button({name: ICON_NAME, 
                                             reactive: true, track_hover: true });
        Main.layoutManager._chrome.addActor(this.overviewCorner, 
                                             { visibleInFullscreen: false });

        this.overviewCorner.connect('button-release-event', 
                                     Lang.bind(this, function(b) {
            if (this.shouldToggleOverviewOnClick())
                Main.overview.toggle();
            return true;
        }));

        let primaryMonitor = global.screen.get_primary_monitor();
        let monitor =  global.screen.get_monitor_geometry(primaryMonitor)
        let cornerX = monitor.x + monitor.width;
        let cornerY = monitor.y;

        this.actor = new Clutter.Group({ name: 'right-hot-corner-environs',
                                         width: HOTSPOT_SIZE + 2,
                                         height: HOTSPOT_SIZE + 2,
                                         reactive: true });

        this._corner = new Clutter.Rectangle({ name: 'right-hot-corner',
                                               width: HOTSPOT_SIZE + 2,
                                               height: HOTSPOT_SIZE + 2,
                                               opacity: 0,
                                               reactive: true });
        this._corner._delegate = this;
        this.actor.add_actor(this._corner);
        this._corner.set_position(this.actor.width - this._corner.width, 0);
        this.actor.set_anchor_point_from_gravity(Clutter.Gravity.NORTH_EAST);
        this.actor.set_position(cornerX, cornerY);

        this.overviewCorner.set_position(monitor.x + monitor.width - 40, monitor.y + 1);
        this.overviewCorner.set_size(32, 32);

        this.actor.connect('leave-event',
                           Lang.bind(this, this._onEnvironsRight));
        this.actor.connect('button-release-event',
                           Lang.bind(this, this._onCornerClicked));

        this._corner.connect('enter-event',
                             Lang.bind(this, this._onCornerEntered));
        this._corner.connect('button-release-event',
                             Lang.bind(this, this._onCornerClicked));
        this._corner.connect('leave-event',
                             Lang.bind(this, this._onCornerRight));

        this._rhripple1 = new St.BoxLayout({ style_class: 'rhc-ripple-box', opacity: 0 });
        this._rhripple2 = new St.BoxLayout({ style_class: 'rhc-ripple-box', opacity: 0 });
        this._rhripple3 = new St.BoxLayout({ style_class: 'rhc-ripple-box', opacity: 0 });
       
        Main.uiGroup.add_actor(this._rhripple1);
        Main.uiGroup.add_actor(this._rhripple2);
        Main.uiGroup.add_actor(this._rhripple3);

        Main.layoutManager._chrome.addActor(this.actor);
    },

    destroy: function() {
        this.actor.destroy();
    },

    get HotCornerIconVisibility() {
       return this._prefs.iconvisible;
    },

    set HotCornerIconVisibility(visible) {
        if (this._prefs.iconvisible != visible) {
            this._prefs.iconvisible = visible;
            this.writeprefs(this._prefspath);
            if (visible)
                this.overviewCorner.show();
            else
                this.overviewCorner.hide();
        }
    },

    get HotCornerRippleVisibility() {
        return this._prefs.ripplevisible;
    },

    set HotCornerRippleVisibility(visible) {
        if (this._prefs.ripplevisible != visible) {
            this._prefs.ripplevisible = visible;
            this.writeprefs(this._prefspath);
        }
    },

    _animateRipple : function(ripple, delay, time, startScale, startOpacity, finalScale ) {
        ripple._opacity =  startOpacity;
        ripple.set_anchor_point_from_gravity(Clutter.Gravity.NORTH_EAST);

        ripple.visible = true;
        ripple.opacity = 255 * Math.sqrt(startOpacity);
        ripple.scale_x = ripple.scale_y = startScale;

        let [x, y] = this._corner.get_transformed_position();
        ripple.x = x + HOTSPOT_SIZE;
        ripple.y = y;

        Tweener.addTween(ripple, { _opacity: 0,
                                   scale_x: finalScale,
                                   scale_y: finalScale,
                                   delay: delay,
                                   time: time,
                                   transition: 'linear',
                                   onUpdate: function() { ripple.opacity = 255 * Math.sqrt(ripple._opacity); },
                                   onComplete: function() { ripple.visible = false; } });

    },

    _rippleAnimation: function() {
        if (this._prefs.ripplevisible) {
            this._animateRipple(this._rhripple1, 0.0,   0.83,  0.25,  1.0,     1.5);
            this._animateRipple(this._rhripple2, 0.05,  1.0,   0.0,   0.7,     1.25);
            this._animateRipple(this._rhripple3, 0.35,  1.0,   0.0,   0.3,     1);
        }
    },

    _onCornerEntered : function() {
        if (!this._entered) {
            this._entered = true;
            if (!Main.overview.animationInProgress) {
                this._activationTime = Date.now() / 1000;
                this._rippleAnimation();
                Main.overview.toggle();
            }
        }
        return false;
    },

    _onCornerClicked : function() {
        if (this.shouldToggleOverviewOnClick())
             Main.overview.toggle();
        return true;
    },

    _onCornerRight : function(actor, event) {
        if (event.get_related() != this.actor)
            this._entered = false;
        return true;
    },

    _onEnvironsRight : function(actor, event) {
        if (event.get_related() != this._corner)
            this._entered = false;
        return false;
    },

    shouldToggleOverviewOnClick: function() {
        if (Main.overview.animationInProgress)
            return false;
        if (this._activationTime == 0 || 
            Date.now() / 1000 - this._activationTime > HOT_CORNER_ACTIVATION_TIMEOUT)
            return true;
        return false;
    },

    disable: function() {
        this.overviewCorner.hide();
        this.actor.hide();
    },

    enable: function() {
        this.readprefs(this._prefspath);
        this.overviewCorner.show();
        this.actor.show();
    },

    readprefs: function(path) {
        let dir = Gio.file_new_for_path(path);
        let prefsFile = dir.get_child('prefs.json');
        if (!prefsFile.query_exists(null)) {
            global.log('No prefs.json found');
            return false;
        }
        let prefsContent;
        try {
            prefsContent = Cinnamon.get_file_contents_utf8_sync(prefsFile.get_path());
        } catch (e) {
            global.log('Failed to load prefs.json: ' + e);
            return false;
        }
        this._prefs =  JSON.parse(prefsContent);
    },

    writeprefs: function(path) {
        let f = Gio.file_new_for_path(path + '/prefs.json');
        let raw = f.replace(null, false,
                            Gio.FileCreateFlags.NONE,
                            null);
        let out = Gio.BufferedOutputStream.new_sized (raw, 4096);
        Cinnamon.write_string_to_stream(out, JSON.stringify(this._prefs));
        out.close(null);
    }
};


function init(extensionMeta) {
    return new RightHotCorner(extensionMeta.path);
}

DBus.conformExport(RightHotCorner.prototype, RightHotCornerIface);


While it is common to store configuration data in XML files, the reason I decided to use JSON is because Cinnamon and the GNOME Shell already have good JSON support. The general idea is that each extension with configurable options would have a single configuration file, prefs.json, in the same base subdirectory (extensiondir) as the extension itself, and that the extension would be able to read its configuration information from this file and update this file if the configuration changed.

This is the read prefs.json function:

    readprefs: function(path) {
        // path obtained from extension metadata
        let dir = Gio.file_new_for_path(path);
        let prefsFile = dir.get_child('prefs.json');
        // check that a prefs.json file exists
        if (!prefsFile.query_exists(null)) {
            global.log('No prefs.json found');
            return false;
        }
        // try to read file as string into variable
        let prefsContent;
        try {
            prefsContent = Cinnamon.get_file_contents_utf8_sync(prefsFile.get_path());
        } catch (e) {
            global.log('Failed to load prefs.json: ' + e);
            return false;
        }
        // parse the string and create the new object _prefs
        this._prefs =  JSON.parse(prefsContent);
    },


and this is the write new prefs.json function.

    writeprefs: function(path) {
        // path obtained from extension metadata
        // create GLocalFile object f
        let f = Gio.file_new_for_path(path + '/prefs.json');
        // set up 
        let raw = f.replace(null, false,
                            Gio.FileCreateFlags.NONE,
                            null);
        // create buffeted output stream
        let out = Gio.BufferedOutputStream.new_sized (raw, 4096);
        // write the _prefs object as a JSON string
        Cinnamon.write_string_to_stream(out, JSON.stringify(this._prefs));
        // close file handle
        out.close(null);
    }


There are probably better ways of reading and writing prefs.json but I simply used what I am familar with for the purpose of this experiment.

If no prefs.json file is found or its contents cannot be read, the extension defaults to creating a default _prefs object.

>
const EXTENSION_PREFS = '{ "iconvisible": true, "ripplevisible": true }';
if (!this.readprefs(this._prefspath)) 
     this._prefs = JSON.parse(EXTENSION_PREFS);


The rest of the changes made in order to support persistent preferences should be self explanatory to you if you are familar with GNOME Shell or Cinnamon extensions.

Note that a minor change is coming down the pike which will break many GNOME Shell and Cinnamon extensions including this one. In a future release of the GNOME Shell (and probably Cinnamon as it tracks the GNOME Shell to a large extent), extension metadata will no longer be passed to an instance of an extension. The extension is going to have to go out and retrieve the metadata that it requires using something like the following:

let extension = imports.misc.extensionUtils.getCurrentExtension();
let metadata = extension.metadata


This is appears to be another example of poor design judgment by Owen Taylor et al. Where is the real value in this change for endusers or developers?

The extension system in the shell really wasn’t designed for extensions that had multiple files. The many people that built the system had no idea that this clever hack would be commonplace. While I hate to break API, I’d say it’s for the better. Introducing the “extension object”. Previous to now, we would construct a meta object from your metadata.json, install some random things into it and pass it to you, the extension, through the init method. Over time, we inserted more and more on the metadata object where it was this jumbled bag of things that the extension system added, and things that were in the metadata.json file. While adding a few more keys for the prefs tool below, I finally decided it was worth a redesign.
…..
Of course, you shouldn’t do these things outside of any user interaction, but a concise, imperfect example is better than one that misses the point by building an StButton, sticking it somewhere on enable(), and then removing it on disable().

You might notice that you don’t get the extension object passed to you – you go out and grab it. If you’re curious as to how we know what extension you are, there’s some clever trickery involved.

What’s on this object? A bunch of stuff that used to be on the metadata object: state, type, dir, path, etc. When we introduce new APIs specifically designed for the ease of extension, this is that place we will put them. metadata is now a pristine and untampered representation of your metadata.json file, and we’re going to keep it that way.

This all looks like change for the sake of change rather than for any real useful purpose. Perhaps somebody can explain how this change adds any value whatsoever to the GNOME Shell extension ecosystem. Were extension developers such as myself consulted? Were end users consulted? I think not!

10 comments to GNOME Shell/Cinnamon Extension Preferences Persistence

  • José X.

    “As an aside, the GNOME developers always seem to contravene the KISS principle where possible.”

    Hi, excuse me for being off topic, but another situation in GNOME that goes completely against the KISS principle is how mimetypes and icons are associated with files. To this day I still can’t get my C files to display specific C icons, and unfortunately I simply can’t find icon themes the provide icons for C files. Now, the reason for this off topic comment: can you (or one of the other readers) point me an icon theme which provides C icons ? Alternatively, would you consider looking into this situation ?

    BTW, your articles about GNOME 3 are very good, pity that I didn’t buy into gnome-shell (or even Cinnamon) as I’m a Compiz fanboy (which unfortunately does not appear to be in very good shape in the 0.9 versions).

  • R.Manley

    First off, thank you for your articles. They have been a great learning resource when it comes to Gnome and its related applications. Javascript, Clutter, GObjects, D-Bus, etc. For having been used for such a period of time, documentation is scarce on many topics.

    If I may suggest or request an article, that would discuss more on using javascript and clutter for programming key-press-events (open, close, navigation, toggle). I feel there are many parts within gnome / gnome-shell that do not allow enough keyboard interaction. Activities does not allow keyboard navigation. Menu in gnome-shell does not allow open and close by key press. It would be nice to make that doable.

    Thank you for your time and knowledge thus far. I look forward to next post.

  • greg

    hi, thanks for nice article; regards extension object/metadata as I read this it may not have any real gain yet, but this is step towards supporting new extension API so when all is put together it should make more sense and hopefully enforce some good practices; what i miss from extensions framework is inability to load extensions schemas, glad there is some work on it

  • Zsolt

    Hi! I’m using some of your Shell extensions on a Wheezy installation, but there is one I’m missing, and I’m not a coder myself. I like the activities overview, but I’ve never used more than one workspace, dynamic or not. Therefore the workspace switcher is distracting and unnecessary to be present in the overview. I’ve tried to work it out, examined the “Hide Dash” extension and the stock components, but with no avail.
    Would you be interested in creating an extension with this functionality?

  • greg

    @fpmurhpy, i dont know where to post this – i am not sure if you mind others doing changes to autohide extension and upload rework to gnome extenstions site? – changes are to stop hidding panel when tooltip? (like calendar) is open, address errors chucked by 112 line and some problems when autohide refuses to work

    off topic – apologies for it—————-
    @zsolt, i just uploaded Light Overview extension to gnome extensions site, it is panding approval – this removes dash/buttons/search/notification area from Overivew and behaves like Compiz scale – when you have only one workspace then workspace swither is hidden at right side – for some it can bother a bit but for others ability to rearrange open windows accross workspaces is of real value, please check it out, could be that it will work better than anything you tried so far

  • JMH

    regarding http://blog.fpmurphy.com/2011/03/customizing-the-gnome-3-shell.html,

    I’ve tried everything, gconf-editor, gnome-tweak-tool, gconftool-2 and I cannot add minimize, maximize and close buttons to the button_layout. Following is the output of gconftool-2 –all-entries for windows:

    # gconftool-2 –all-entries /desktop/gnome/shell/windows
    theme = Adwaita
    button_layout = :minimize,maximize,close
    attach_modal_dialogs = true
    edge_tiling = true
    workspaces_only_on_primary = true

  • Congrats Gnome3 wizard :)

    I tried commenting on this post but it seems that due to this post’s age comments are closed…
    http://blog.fpmurphy.com/2011/03/customizing-the-gnome-3-shell.html

    My question is if in a Gnome 3.4+ system it’s possible from a bash script, using some of the gnome command line tools, to change the title bar color of the CURRENT terminal session (the current window only, not all).

    I remember that in previous versions of Gnome (2.x) it was possible to change on a per-window basis, but perhaps I’m wrong.

    What I’d like to achieve is to write a caller bash script that intercepts ssh(say I call it sshv2.sh, and it in turn calls ssh with the parameters passed to it), but before calling ssh change the current terminal window (caller program) title bar color to stand out, to easily differentiate visually between terminal sessions that are local, and others (say: red titlebar) that are remote ssh sessions.

    Is this doable without hacking the gnome source code?.

    FC

  • Michael Nirza

    Hey I looked at your No a11y extension for gnome 3.2 I have gotten it to work with gnome 3.4 and was just needed an email address so i could send it to you. I had tried to create a Gnome.org account to login to gnome extenstions to upload for other users but have had no success getting into freashly created account there so i figured next best thing is to send it to you so you can upload it my email is above is not nirzamichael@gmail.com

  • Alex

    Hello!

    Sorry about this off-topic post, but anyone knows a extension to hide the gnome-shell applications categories? The categories is useless for me on gnome-shell…

    Thanks!!!

  • Veselin

    Hello Mr. Murphy,
    First of all I want to thank you for all these amazing extensions! They made my gnome 3 far more nice and clean!
    One of the extensions I’ve installed on my Fedora Verne was activitiesbuttonicon to change the text with Fedora logo but currently I’m trying new distro for me – openSUSE and my question is how can I use openSUSE logo instead Fedora logo? I search in extension.js and found the const containing icon filename but I could not managed to find out where the script is looking for the icon file? So please, could you help me to achieve my goal to use openSUSE icon instead Fedora’s one on my current distro?

    Thank you in advance!

    Best regards,
    Veselin