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:
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:
[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.
public Dictionary<string,string> GetMenu(string text)
{
Dictionary<string,string> retDic = new Dictionary<string, string>();
LogMessage(string.Format("GetMenu is called from {0}",text));
retDic.Add("MenuItem1","ID1");
retDic.Add("MenuItem2","ID2");
retDic.Add("MenuItem3","ID3");
return retDic;
}
The method takes a string
argument and returns a Dictionary
of string
s. 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.
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.
public delegate void StatusChangedHandler(string param);
[Interface("com.cadexis.centerdocbus")]
public class NotificationObject : MarshalByRefObject
{
public event StatusChangedHandler StatusChanged;
public void ChangeStatus(string status)
{
if (StatusChanged!=null)
{
SendMessage(string.Format("Changing status to {0} ",status));
StatusChanged(status);
}
else
{
SendMessage("No delegate");
}
}
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.
using NDesk.DBus;
using org.freedesktop.DBus;
public delegate void Logger(string msg);
public class DBusMonitor
{
Bus bus;
NotificationObject notify;
Logger log =null;
ObjectPath path = new ObjectPath ("/com/cadexis/dbusmenusample");
string busName = "com.cadexis.dbusmenusample";
public DBusMonitor (Logger log_)
{
log = log_;
BusG.Init();
bus = Bus.Session;
}
public void InitObjects()
{
if (notify==null)
{
if (bus.RequestName (busName) == RequestNameReply.PrimaryOwner)
{
notify = new NotificationObject (log);
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.
public void SendTest()
{
try
{
NotificationObject not = bus.GetObject<notificationobject /> (busName, path);
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
class MainClass
{
static DBusMonitor dbmon;
public static void Main (string[] args)
{
Application.Init ();
dbmon = new DBusMonitor(LogMsg);
...
}
static void OnButtonListenClicked(object obj, EventArgs args)
{
LogMsg("Start Listening");
dbmon.InitializeBus();
}
static void OnButtonSendClicked (object sender, System.EventArgs e)
{
LogMsg("Send Message");
dbmon.SendTest();
}
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:
const NotificationObjectInterface = {
name: 'com.cadexis.dbusmenusample',
methods: [
{
name: 'Status',
inSignature: 's',
outSignature: 's'
},
{
name: 'SendMessage',
inSignature: 's',
outSignature: ''
},
{
name: 'GetMenu',
inSignature: 's',
outSignature: 'a{ss}'
},
],
signals: [
{
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 string
s as key and string
s 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:
function init()
{
let NotificationProxy = DBus.makeProxyClass(NotificationObjectInterface);
dbusNotify = new NotificationProxy(DBus.session,
'com.cadexis.dbusmenusample',
'/com/cadexis/dbusmenusample');
dbusNotify.connect("StatusChanged",_statusChanged);
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 StatusChanged
event 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
.
AppMenu.prototype = {
__proto__: PanelMenu.SystemStatusButton.prototype,
_init: function(notify) {
this._notify = dbusNotify;
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.
function enable() {
Main.panel._rightBox.insert_actor(dbusMenuButton.actor, 1);
Main.panel._rightBox.child_set(dbusMenuButton.actor, { y_fill : true } );
Main.panel._menus.addMenu(dbusMenuButton.menu);
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.
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) {
if ("AppMenuRefresh"==this._idTxt)
{
this._appMenu.handleEvent(this._idTxt);
}
else
{
this._notifyApp(this._idTxt);
}
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