Translate

Image of Android Wireless Application Development
Image of Beginning Google Maps API 3
Image of RHCE Red Hat Certified Engineer Linux Study Guide (Exam RH302) (Certification Press)
Image of XSLT 2.0 and XPath 2.0 Programmer's Reference (Programmer to Programmer)

Controlling a GNOME Shell or Cinnamon Extension using D-Bus

In my last post, I discussed how you could use D-Bus object introspection to enumerate the methods, signals and properties of the GNOME or Cinnamon Shell, and showed how you could enable, disable or list extensions using a command line utility that used D-Bus to talk to the Shell. In this post, I demonstrate how you can add D-Bus support to a Shell extension and allow a command line utility to control the operating characteristics of the extension via a command line utility.

The Shell extension I shall use for demonstration purposes is a simple Cinnamon extension that adds a hot corner to the upper right hand corner of your primary monitor. It does not remove the existing hot corner on the upper left hand corner of your primary monitor; it simply adds another hot corner to the upper right hand side of the monitor.

Without further ado, here is the code for the Shell extension:

//
//  Copyright (c) 2012  Finnbarr P. Murphy.  All rights reserved.
//
//  Version: 1.0 (02/16/2012)
//
//  License for new code: 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 HOT_CORNER_ACTIVATION_TIMEOUT = 0.5;
const SHOW_RIPPLE  = true;               // change to false if you do not want the ripple
const SHOW_ICON    = true;               // change to false if you do not want icon  
const ICON_NAME    = 'overview-corner';  // 24x24 icon PNG under /usr/share/icons/... 
const HOTSPOT_SIZE = 1;                  // change if you need/want a bigger hotspot

// 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(this, arguments);
}

RightHotCorner.prototype = {
    ExtensionVersion: EXTENSION_VERSION,

    _init : function() {
        this._entered = false;
        this._show_ripple = SHOW_RIPPLE;
        this._show_icon = SHOW_ICON;

	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 });
        if (this._show_icon) { 
             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._show_icon;
    },

    set HotCornerIconVisibility(visible) {
        this._show_icon = visible;
        if (visible)
            this.overviewCorner.show();
        else
            this.overviewCorner.hide();
    },

    get HotCornerRippleVisibility() {
        return this._show_ripple;
    },

    set HotCornerRippleVisibility(visible) {
        this._show_ripple = visible;
    },

    _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._show_ripple) {
            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.overviewCorner.show();
        this.actor.show();
    }
};


function init() {
    return new RightHotCorner();
}

DBus.conformExport(RightHotCorner.prototype, RightHotCornerIface);


The above code is based on earlier versions of a right hot corner extension which I wrote for the GNOME Shell. Obviously much of this code came from the original GNOME Shell source code way back when – no point in reinventing the wheel! The main differences between this code and my latest GNOME Shell righthotcorner extension are due to the fact that Cinnamon does not tie a hot corner to the Panel and the additional support for the D-Bus interface, three properties, setters and getters.

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' }]
};


The interface name for the Shell extension is defined to be org.Cinnamon.extensions.righthandcorner and the object instance path is defined to be /org/Cinnamon/extensions/righthandcorner. The interface description is defined in RightHotCornerIface. No methods or signals are exposed via this D-Bus interface, only three properties – one readonly property (access value of read) and two readwrite properties (access value of readwrite):

  • HotCornerIconVisibility: Enable or disable the hot corner icon or read current value
  • HotCornerRippleVisibility: Enable or disable the hot corner ripple effect or read current value

When the Shell extension is initialized the D-Bus interface is set up by these three lines of code:

    DBus.session.proxifyObject(this, EXTENSION_IFACE, EXTENSION_PATH);
    DBus.session.exportObject(EXTENSION_PATH, this);
    DBus.session.acquire_name(EXTENSION_IFACE, 0, null, null);


A proxy object is dynamically constructed in the first line of the above code (each method, property and signal is added to the proxy), exported in the second line, and the interface name is acquired by the D-Bus session daemon in the third line. This is a simplistic explanation but sufficient for this post.

Here is some commented code which should help you in understanding the syntax used with the getters and setters. Note the coding difference between a readwrite property and a readonly D-Bus interface property.

    // getter for the ExtensionVersion property (access:read)
    ExtensionVersion: EXTENSION_VERSION,

    // getter for the HotCornerIconVisibility property (access:readwrite)
    get HotCornerIconVisibility() {
        return this._show_icon;
    },

    // setter for the HotCornerIconVisibility property (access:readwrite)
    set HotCornerIconVisibility(visible) {
        this._show_icon = visible;
        if (visible)
            this.overviewCorner.show();       // show the hot corner icon
        else
            this.overviewCorner.hide();       // hide the hot corner icon
    },

    // getter for the HotCornerRippleVisibility property (access:readwrite)
    get HotCornerRippleVisibility() {
        return this._show_ripple;
    },

    // setter for the HotCornerRippleVisibility property (access:readwrite)
    set HotCornerRippleVisibility(visible) {
        this._show_ripple = visible;        
    },

To date, Shell extensions, if configurable at all, have been typically configured by directly editing extension.js and restarting the Start, or by using gsettings, dconf-editor or some similar tool. Because I have added a D-Bus interface to the Shell extension, I can instead use D-Bus to enable or disable the hot corner icon or ripple effect, or retrieve the current values of the three D-Bus interface properties.

Here is the source code for a command line tool, which I call extension-tool, that can be used to retrieve the status of the extension configurable options, version of the extension, and to enable or disable the two configurable options. It is written in Python and uses gobject introspection (GI) to interface with GLib and GIO. I am not going to explain the code in any detail as I assume that if you are still reading this post you have a modicum of Python programming knowledge.

#!/usr/bin/python
#
#   Copyright (c) Finnbarr P. Murphy 2012.  All rights reserved.
#
#      Name: righthotcorner-tool
#
#   Version: 1.0 (02/16/2012)
#
#   License: Attribution Assurance License (see www.opensource.org/licenses)
#

try:
    import os
    import sys
    import argparse
except ImportError, detail:
    print detail 
    sys.exit(1)

from gi.repository import Gio, GLib

EXTENSION_IFACE = 'org.Cinnamon.extensions.righthandcorner'
EXTENSION_PATH  = '/org/Cinnamon/extensions/righthandcorner'

class ExtensionTool:
    def __init__(self):
        try:
            self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
            self.proxy = Gio.DBusProxy.new_sync( self.bus, Gio.DBusProxyFlags.NONE, None, 
                 EXTENSION_IFACE, EXTENSION_PATH, 'org.freedesktop.DBus.Properties', None)
        except:
            print "Exception: %s" % sys.exec_info()[1]

    def get_extension_version(self):
        output = self.proxy.Get('(ss)', EXTENSION_IFACE, 'ExtensionVersion')
        return output

    def get_icon_visibility(self):
        output = self.proxy.Get('(ss)', EXTENSION_IFACE, 'HotCornerIconVisibility')
        return output

    def get_ripple_visibility(self):
        output = self.proxy.Get('(ss)', EXTENSION_IFACE, 'HotCornerRippleVisibility')
        return output

    def set_icon_visibility(self, visible):
        args = GLib.Variant('b', visible)
        self.proxy.Set('(ssv)', EXTENSION_IFACE, 'HotCornerIconVisibility', args)

    def set_ripple_visibility(self, visible):
        args = GLib.Variant('b', visible)
        self.proxy.Set('(ssv)', EXTENSION_IFACE, 'HotCornerRippleVisibility', args)

def set_icon_visibility(visible):
    s = ExtensionTool()
    s.set_icon_visibility(visible)

def set_ripple_visibility(visible):
    s = ExtensionTool()
    s.set_ripple_visibility(visible)

def list_properties():
    s = ExtensionTool()
    version = s.get_extension_version()
    print 'Extension Version: %s' % (version)
    visible = s.get_icon_visibility()
    print 'Hot Corner Icon Visible: %s' % (visible)
    visible = s.get_ripple_visibility()
    print 'Hot Corner Ripple Visible: %s' % (visible)

def main():
    parser = argparse.ArgumentParser(description="Right Hot Corner extension tool (Cinnamon)")
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-i", "--showicon", dest="showicon",
                       action="store_true", help="show top right corner icon")
    group.add_argument("-I", "--noshowicon", dest="noshowicon",
                       action="store_true", help="hide top right corner icon")
    group.add_argument("-r", "--showripple", dest="showripple",
                       action="store_true", help="show top right corner ripple")
    group.add_argument("-R", "--noshowripple", dest="noshowripple",
                       action="store_true", help="hide top right corner ripple")
    group.add_argument("-l", "--list", dest="listprop",
                       action="store_true", help="list extension properties")

    args = parser.parse_args()

    if args.showicon:
        set_icon_visibility(True)
    elif args.noshowicon:
        set_icon_visibility(False)
    elif args.showripple:
        set_ripple_visibility(True)
    elif args.noshowripple:
        set_ripple_visibility(False)
    elif args.listprop:
        list_properties()
    sys.exit(0)

if __name__ == "__main__":
    main()


The Shell extension D-Bus interface is org.Cinnamon.extensions.righthandcorner and the object path is /org/Cinnamon/extensions/righthandcorner. The extension D-Bus object instance properties are accessed via org.freedesktop.DBus.Properties interface. Here are the three methods defined by the D-Bus specification for setting or getting D-Bus properties using this interface.

org.freedesktop.DBus.Properties.Get (in STRING interface_name,
in STRING property_name,
out VARIANT value);

org.freedesktop.DBus.Properties.Set (in STRING interface_name,
in STRING property_name,
in VARIANT value);

org.freedesktop.DBus.Properties.GetAll (in STRING interface_name,
out DICT props);


If you examine the source code for the extension tool, you will notice the type signature (AKA type string) for the getters, i.e. get_icon_visibility and get_ripple_visibility, is “(ss)”, whereas the type signature for the setters, i.e. set_icon_visibility and set_ripple_visibility, is “(ssv)”. See my previous post for an explanation of the type signatures or read the D-Bus specification if you are unfamiliar with the concept. The “v” in the “(ssv)” type signature denotes a variant. Because the org.freedesktop.DBus.Properties.Set method expects a variant as its third argument, the extension tool has to wrap a boolean value, True or False in a variant using GLib.Variant in order for the method to accept the third argument.

Possible enhancements to the above Shell extension? The D-Bus proxy should probably be disconnected when the above Shell extension is disabled and re-connected when the extension is enabled. The Shell extension could easily be modified to persist its configuration settings locally across a reboot without having to rely on a GNOME schema (think Mozilla prefs.js). For the record, I have always felt that Shell extensions should persist their own configuration data in the same directory in which the Shell extension is installed rather than relying on a custom GNOME schema which lives in a separate directory. I have not given it much thought to date but it should be possible to design a D-Bus interface such that a generic Shell extension management tool could manage the various Shell extensions and their configurations without the need for a separate configuration tool for each Shell extension.

You may have noticed that the license on the source code is the Attribution Assurance License rather than the GNU Public License. The AAL has been approved by the OSI via the OSI License Review Process. Unfortunately, the GPL has become too complex for an ordinary user to understand and does not protect my rights as an author to have my copyright notice required on all copies and derivations of the software the license applies. I am not alone in having misgivings about the GPL; there is an emerging trend towards more permissive licenses.

P.S. This Shell extension and its command line tool are available for download on my Cinnamon extensions website.

2 comments to Controlling a GNOME Shell or Cinnamon Extension using D-Bus

  • Hi Finnbarr, great tutorial, not enough of these around on the net.
    I’m following your code but having some problems with Cinnamon 1.4. Have you gotten this working with the latest LinuxMint? I tried something similar not as an extension but as a Cinnamon applet, but i can’t seem to get it working.

    • I have not had time to try using DBus with Cinnamon 1.4 but a quick look at the Cinnamon 1.4 codebase did not reveal any changes that would prevent DBus working with it. Without seeing your code, it is difficult to see what is going on.