Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / shell

Migrating Panel Applets to Gnome Shell using DBus

4.75/5 (5 votes)
30 Oct 2011GPL311 min read 24.9K   1K  
This articles describes a way to reproduce the behavior of a panel applet (menus and interation with an application) on Gnome Shell for Mono applications on Linux, with the help of DBus
Menu populated from the application

Introduction

Gnome Shell is a user interface technology that is becoming more prevalent on the Linux Gnome Desktop at the moment. It has a lot of good concepts but there are also some drawbacks, especially if you have developed some applications under previous Gnome versions. One of the most noticeable change is the lack of backward compatibility for Applet Panels, similar to the Status Icons in Windows. This article provides a way to migrate panel applet based application to Gnome Shell technology. The application itself is based on Mono/C# but the idea could be applied to other languages.

Background

Mono was introduced as an easy way to develop C# application on Linux. It works well and is integrated in some major Linux distributions. The integration to Gnome is done through Gtk# and provides a set of functionalities that are similar to its Windows counterparts. Status Icon is one of them and is called Panel Applet on the Gnome environment. While its usage could be argued, it is an easy way to control an application and integrate to the user's desktop.

I recently upgraded to the latest Ubuntu Oneiric Ocelot and with it came Gnome 3 and Gnome Shell. Gnome 2 is not supported officially anymore and some breaking changes have been introduced, notably the Panel Applets. Some applications that I implemented in the past rely on a Status Icon, to report information back to the user or offer menus, and are not working anymore as they should on the new distribution. I then started thinking about an alternative.

The right thing to do was probably to adhere to the new Gnome philosophy and rework the application from a shell point of view. I could also have gone back to Gnome 2 and keep things as a status quo, knowing quite well that an upgrade will ineluctably happen later on. Since I already invested a lot of time in some applications and wanted to keep the benefits of Gnome Shell, I wanted a quick solution. That is when Gnome Shell extensions come into play.

Gnome Shell extensions are a way to hook into the desktop implementation and modify its behavior. It is JavaScript based and allows to place controls or actions on the desktop, through the help of libraries. One of the integrated libraries is DBus.

DBus is a framework that provides interprocess communication. It supports callback and method calls, based on marshalled objects. It is usually part of major Linux distributions by default and has a wide range of language libraries (Perl, JavaScript, C, C#, python,...). This communication layer is the glue for the solution explained in this article.

The Implementation

There are two parts to the code, the shell extension and the DBus Listener.

The shell extension displays an icon on the status bar with the available menus. It also sends events to the DBus Listener when the user clicks on a menu item. Finally, it also responds to events raised by the DBus Listener and shows a message to the user. A menu item is available to refresh the menu if the DBus listener was not available when the shell extension is initialized.

The DBus Listener application will provide the necessary objects on the bus to support the extension. There is a button to initialize the bus, a button to send the time to the extension, and a text control to show the extension activity.

DBus Integration

DBus is the central piece of the article. The namespaces related to it are:

C#
using NDesk.DBus;
using org.freedesktop.DBus;

The references are not available directly in MonoDevelop so would need to be linked manually from the following location:

  • /usr/lib/mono/gac/NDesk.DBus/1.0.0.0__f6716e4f9b2ed099/NDesk.DBus.dll
  • /usr/lib/mono/gac/NDesk.DBus.GLib/1.0.0.0__f6716e4f9b2ed099/NDesk.DBus.GLib.dll

It is possible that the path needs to be adjusted for a different distribution. The libraries are also available with the demo binaries and should work with a relatively recent Mono version.

If the libraries are not in the mono GAC, they probably need to be registered from the repository with the following command:

apt-get install libndesk-dbus-glib1.0-cil libndesk-dbus1.0-cil

DBus works through marshalled objects and interfaces. So a class provided on the bus is defined like the below:

C#
[Interface("com.cadexis.dbusmenusample")]
public class NotificationObject : MarshalByRefObject
{
	...
}

The Interface attribute is to uniquely name the object on the Bus. The name can be anything but the usual convention is to use a domain name in reverse, as those should be unique enough to share objects with any application, as well as logically grouped when alphabetically sorted, to make discoverability easier.

The class also inherits MarshalByRefObject to facilitate the basic types of the class to be passed between processes.

Method to Provide the List of Menu Items to the Extension

Calling a method from the extension is not really different from calling it from the main application, and the implementation will look the same in the class.

C#
public Dictionary<string,string> GetMenu(string text)
{
	//Initialize the object that will be returned
	Dictionary<string,string> retDic = new Dictionary<string, string>();
	//Log the message on the application that some consumer used the method
	LogMessage(string.Format("GetMenu is called from {0}",text));
	//Populate the list of items (name and unique Id)
	retDic.Add("MenuItem1","ID1");
	retDic.Add("MenuItem2","ID2");
	retDic.Add("MenuItem3","ID3");
	//Return the list of items
	return retDic;
}

The method takes a string argument and returns a Dictionary of strings. This argument is used as a modifier, based on the application that calls it, to return enough information for the extension menu. The names will be displayed as menu items and the IDs will be passed back to the main application to process the data.

Method to Submit an Action When a menuitem is Clicked

This is just another method in the class, which will be called from the extension but executed on the main application.

C#
public void SendMessage(string text)
{
	Console.WriteLine(text);
}

Nothing other than logging the text is done in this case. That could be made more complex, in order to execute specific actions based on the argument.

Callback from the Application

The extension can react on events triggered from the main applicationEvent. First, a delegate needs to be defined to specify the signature of the callback. It takes a string as argument. Then a public event has to be defined on the class, which is then linked to it in the extension. Finally, the event has to be raised by some logic. This is achieved by calling the ChangeStatus method from the main application.

C#
public delegate void StatusChangedHandler(string param);

[Interface("com.cadexis.centerdocbus")]
public class NotificationObject : MarshalByRefObject
{
	//Handler to notify the extension of an event
	public event StatusChangedHandler StatusChanged;

	//Method to indicate to the application that the status has changed 
         //and raise the event for the extension to react
	public void ChangeStatus(string status)
	{
		if (StatusChanged!=null)
		{
			SendMessage(string.Format("Changing status to {0} ",status));
			StatusChanged(status);
		}
		else
		{
			SendMessage("No delegate");
		}
	}

	//Application Log
	public void SendMessage(string text)
	{
		Console.WriteLine(text);
	}

	...
}

The class is now complete and can be integrated to the extension and the main application, called DBus Listener.

DBus Listener

This is a basic desktop application that instantiates the NotificationObject class and handles the notifications from the extension.

To isolate the Bus specific information from the main application, a class DBusMonitor is created.

C#
//Imports of the DBus namespaces
using NDesk.DBus;
using org.freedesktop.DBus;

//Definition of the delegate to log information on the application
public delegate void Logger(string msg);

//Wrapper class for the NotificationObject class
public class DBusMonitor
{
	//DBus session that is used for the application
	Bus bus;
	//Marshalled object that will live on the bus for remote access by the extension
	NotificationObject notify;
	//Logger method to allow a hook from the UI application
	Logger log =null;
	//DBus path of the NotificationObject
	ObjectPath path = new ObjectPath ("/com/cadexis/dbusmenusample");
	//DBus session name
	string busName = "com.cadexis.dbusmenusample";

	//Constructor
	public DBusMonitor (Logger log_)
	{
		//Set the logger of the instance
		log = log_;
		//Initializes a DBus session
		BusG.Init();
		//Set the current session of the bus for future use
		bus = Bus.Session;
	}

	public void InitObjects()
	{
		//Check that the object is not already instantiated
		if (notify==null)
		{
			//Check that the current bus session is 
                           //owned by the current application
			if (bus.RequestName (busName) == RequestNameReply.PrimaryOwner)
			{
		                //Create a new instance of the NotificationObject, 
                                  //with the associated logger delegate
		                notify = new NotificationObject (log);
				//Register the object on the bus so that it can 
                                     //be accessible from other applications, 
                                     //including Gnome extensions.
				//The object will be available through the path value.
	                	bus.Register(path, notify);
	    	        }
		}
	}
...
}

The Logger delegate is an easy way to pass the information back to the UI. If it is not set, the output will be sent to the console. It is passed as argument of the constructor.

The busName value should be the same as the one specified on the NotificationObject interface (com.cadexis.centerdocbus).

The path of the object in the bus session should be unique. The same value as the busName has been chosen since only one object will be registered, changing the dots by slashes to make it look like a path.

BusG.Init() is what integrates the application with DBus. It instantiates some static variables that can be used afterwards. In this case, DBus.Session is the instance that will be used to register the NotificationObject. It is done through the method Register(string objectPath,object marshalByRefObject).

A test method is also added to this class to simulate a change of status of the application, event that should flow to the extension.

C#
public void SendTest()
{
	try
	{
		//Get the notification object instance that is on the bus
		NotificationObject not = bus.GetObject<notificationobject /> (busName, path);
		//Raised the StatusChanged event
		not.ChangeStatus("SERVER_EVENT");
	}
	catch (Exception ex)
	{
		Log("Cannot instantiate the remote object"+ex.Message);
	}
}

The try catch could have been avoided by using the already available notify variable of the class but this is to demonstrate how to access an object that is on the bus. This is achieved by using the method GetObject<t />(string busName, string objectPath).

On the application code itself, the integration is done with the DBusMonitor wrapper quite simply:

  • by providing a Logger method that will be passed along the instances
  • by instantiating the DBusMonitor when the application starts
  • by calling the InitializeBus method when the "Initialize DBus Object" button is clicked on
  • by calling the SendTest method when the "Send Message" button is clicked on
C#
class MainClass
{
	static DBusMonitor dbmon;

	public static void Main (string[] args)
	{
		Application.Init ();
		//Init of the DBusMonitor to send and receive the events
		dbmon = new DBusMonitor(LogMsg);

		//Init the layout (buttons and textview
		...
	}

	//Initialize the objects on the bus
	static void OnButtonListenClicked(object obj, EventArgs args)
	{
		LogMsg("Start Listening");
		dbmon.InitializeBus();
	}

	//Send a message on the bus for the clients
	static void OnButtonSendClicked (object sender, System.EventArgs e)
	{
		LogMsg("Send Message");
		dbmon.SendTest();
	}

	//Logs messages in the textview
	static void LogMsg(string msg)
	{
		textview1.Buffer.InsertAtCursor(string.Format
		("{0}: {1}{2}",DateTime.Now.ToString(),msg,Environment.NewLine));
	}
}

Shell Extension Basics

The extension is the integration part to the Gnome desktop.

Extensions are specific to the version of the shell that is installed on the machine. So the best way to create a compatible extension is to use the following command:

gnome-shell-extension-tool --create-extension
Name: DBbusMenuSample
Description: Gnome extension with Dbus integration
Uuid: dbusmenusample@cadexis.com

This command will create three files under the directory ~/.local/share/gnome-shell/extensions/dbusmenusample@cadexis.com/:

  • metadata.json: This file contains the description of the extension and the gnome shell version that the extension targets.
  • stylesheet.css: This is the style of the extension for specific customization.
  • extension.js: This is the file that contains the code logic.

extension.js has a default implementation, which is usually to show "Hello World" in the middle of the screen when clicking on the new icon shown in the top bar.

An extension requires three methods:

  • init: called the first time the extension is loaded.
  • disable: The user can disable or enable extensions on demand and this method is called when the extension is deactivated.
  • enable: called when the extension is loaded or when the extension is activated by the user.

In order to make this new extension activated, the Shell needs to be restarted. It is done by pressing alt+F2 and entering the r command (r=Restart).

If after the Shell is restarted the new icon is not shown, there is another tool that could be used to investigate any issue, the looking glass. It is available through alt+F2 and entering the lg command (lg=Looking Glass). That will show the errors encountered and other information regarding the shell.

The status bar is referred in the code with const Main = imports.ui.main;.
Main exposes the various parts of the bar through the panel property (_rightbox for the icons on the left, _centerbox for the icons in the center, _leftbox for the icons on the right, _menus for the menus attached with the panel).

Shell Extension Logic

The extension will still be the default behavior so by replacing the default extension.js by the file provided along this article, the integration will DBus with be possible.

First, the code of the extension needs to replicate the NotificationObject object, from the JavaScript point of view.

Since most of the logic is actually executed on the application side, only the signature is necessary on the extension side.

  • name: This section corresponds to the name of the interface on the C# side.
  • methods: This section lists the methods signature that will be available on the bus through this object.
  • signals: Same as methods but for events.

The full definition of the object will be the following:

C#
//Remote object definition
const NotificationObjectInterface = {
    name: 'com.cadexis.dbusmenusample',
    methods: [
	{    //method to get the current status, getting a string and passing a string
            name: 'Status',
            inSignature: 's',
            outSignature: 's'
        },
	{    //method to send a message, passing a string as argument
            name: 'SendMessage',
            inSignature: 's',
            outSignature: ''
        },
	{   //method to retrieve the menu items
	    name: 'GetMenu',
            inSignature: 's',
            outSignature: 'a{ss}'
	},
    ],
    signals: [
        {    //event raised when the status on the application is changed.
            name: 'StatusChanged',
            inSignature: 's'
        }
    ]
};

A few things to note:

  • The DBus namespace can be imported with the statement const DBus = imports.dbus;
  • The name of the object is NotificationObjectInterface instead of NotificationObject on the C# side. This is a way for DBus to properly resolve the objects and methods, JavaScript not being a strongly type language. This naming "quirk" is also used for methods where the term "Remote" is added to the name, when calling the methods from the extension.
  • Each object in method or signal is defined by a name that is mandatory and the optional inSignature and outSignature for the input and output signatures respectively.
  • The signature definition is using the DBus API signature, which defines the type of the argument by characters, like "s" for string parameters.
  • a{ss} is the signature of a dictionary of strings as key and strings as values, which will contain the menu items definition as output for the GetMenu method.

Then the integration of the object to the extension can be done on the init method:

C#
function init()
{
	//Create the remote object, based on the correct path and bus name
	let NotificationProxy = DBus.makeProxyClass(NotificationObjectInterface);
	dbusNotify = new NotificationProxy(DBus.session,
                                                    'com.cadexis.dbusmenusample',
                                                    '/com/cadexis/dbusmenusample');

	//Set the delegate to the StatusChanged Event
	dbusNotify.connect("StatusChanged",_statusChanged);

	//Create the icon button
	dbusMenuButton = new AppMenu(dbusNotify);

	this.enable();
}

The same bus name (com.cadexis.dbusmenusample) and object path (/com/cadexis/dbusmenusample) are used to retrieve the object registered on the bus by the application. The StatusChangedevent is monitored through the help of the connect method and will call the _statusChanged method when the event is triggered from the DBus Listener.

AppMenu is a class containing the UI code. It exposes an actor property, which is the icon seen in the menu bar, and a menu, which is the list of actions exposed through DBus. It is deriving from the class SystemStatusButton.

C#
AppMenu.prototype = {
    __proto__: PanelMenu.SystemStatusButton.prototype,

    _init: function(notify) {
	//Save the remote object as reference
	this._notify = dbusNotify;
	//Create the StatusButton on the Panel Menu, using the system-run icon
        PanelMenu.SystemStatusButton.prototype._init.call(this, 'system-run');
    },
	...
};

The enable() method is integrating the icon (referred as actor) to the right part of the bar and the menu.

C#
function enable() {
	//Add the appMenu object on the right box
	Main.panel._rightBox.insert_actor(dbusMenuButton.actor, 1);
	Main.panel._rightBox.child_set(dbusMenuButton.actor, { y_fill : true } );
	//Add the appMenu item container to the panel
	Main.panel._menus.addMenu(dbusMenuButton.menu);

	//Async callback to retrieve the content of the appMenu items
	dbusNotify.GetMenuRemote('INIT', _refreshMenuList);
}

GetMenuRemote is calling GetMenu on the NotificationObject and will populate the menu items through the _refreshMenuList method.

Each element in the menuitem inherits from PopupMenu.PopupBaseMenuItem, exposes an actor, which is a label with the item text, and executes _notifyApp with the item ID when the actor is activated.

C#
AppMenuItem.prototype = {
    __proto__: PopupMenu.PopupBaseMenuItem.prototype,

    _init: function (lblText,lblId,appMenu, notify, params) {
        PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
        this.label = new St.Label({ text: lblText });
        this.addActor(this.label);
	this._notify=notify;
	this._text = lblText;
	this._idTxt = lblId;
	this._appMenu = appMenu;
    },

    activate: function (event) {
	//This allows to refresh the menu item list when the AppMenuRefresh is clicked.
	if ("AppMenuRefresh"==this._idTxt)
	{
		this._appMenu.handleEvent(this._idTxt);
	}
	else
	{
		this._notifyApp(this._idTxt);
	}
	//This will close the menu after it the menu item is closed
	PopupMenu.PopupBaseMenuItem.prototype.activate.call(this, event);
    },

    _notifyApp: function (id)
    {
	this._notify.SendMessageRemote('MenuClick:'+ id);
    }
};

The disable method just calls the destroy method of the AppMenu class, completing the extension.

Points of Interest

Overall, this is just an example of a DBus usage and can be made more or less complex to accommodate different application needs.

The introduction of a two part application instead of a single language application may seem awkward and harder to make it work consistency across various system and distribution.
However, this is also an opportunity to make an application more modular and start exposing some functionalities to external processes and conversely.

Useful Links

History

  • 2011-10-29 - First release

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)