Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
If you plan to customize the GNOME Shell in any meaningful way, you need to understand the
technologies underlying the GNOME Shell and understand how to write a GNOME Shell extension
to provide the customization that you require. In this post I delve deeper into the technologies
behind the new GNOME Shell and provide sample code for a number of simple extensions which
demonstrate how to customize and extend various components of the GNOME Shell user interface.
ly
Essentially, the GNOME Shell is an integrated window manager, compositor, and application
launcher. It acts as a composting window manager for the desktop displaying both application
on
windows and other objects in a Clutter scene graph. Most of the UI code is written in JavaScript
which accesses Clutter and other underlying libraries via GObject Introspection.
Here is a block diagram of the underlying technologies that support the GNOME Shell as of v3.0:
se
a lu
o nn
rs
pe
r
Fo
The GNOME Shell uses OpenGL to render graphics. OpenGL uses a hardware accelerated pixel
format by default but can support software rendering. However, hardware acceleration is required
to run the GNOME Shell as it uses a number of 3D capabilities to accelerate the transforms. Most
graphics cards less than 3 years old should support hardware acceleration. If hardware
acceleration is unavailable, the GNOME Shell defaults back to a modified version of the GNOME 2
Panel. See Vincent Untz’s post for further information on this fallback mode. In addition, you can
force the GNOME Shell to use the fallback mode via a switch in the Settings, System Info panel.
Access to OpenGL is via Cogl which is a graphics API that exposes the features of 3D graphics
hardware using a more object oriented design than OpenGL.
The Clutter graphics library handles scene graphing. In Clutter, widgets are called actors, and
windows are called stages. A Clutter application contains at least one stage containing actors such
as rectangles, images, or text. A useful online resource for Clutter programming is Programming
in Clutter by Murray Cumming. By the way, the Clutter library is also used in Moblin which, along
with Maemo, is now part of Meego. Meego uses MX widgets on top of Clutter (a useful tutorial can
be found here) whereas the GNOME Shell uses a Shell Toolkit (St) which implements many
custom actors, such as containers, bins, boxes, buttons and scrollbars that are useful in
implementing GNOME Shell UI features. The Shell Toolkit was derived from the Moblin UI Toolkit.
See ../src/st in the gnome-shell GIT source code repository. The Shell Toolkit also implements CSS
ly
support (see ../src/st/st-theme*) which makes the GNOME Shell themeable. Generally if you see
any object whole name starts with St. you can assume you are dealing with the Shell Toolkit.
Accessibility is handled by Cally (Clutter Accessibility Implementation Library). By the way, the
on
GNOME Shell is implemented as a Mutter plugin.
Window management is handled by a modified version of Metacity called Mutter. Before the
introduction of Metacity in GNOME 2.2, GNOME used Enlightenment and then Sawfish as its
se
window manager. Metacity uses the GTK+ graphical widget toolkit to create its user interface
components, enabling it to be themeable and blend in with other GTK+ applications. Mutter is a
lu
newer compositing window manager based on Metacity and Clutter. Note that the GNOME Shell
fallback mode still uses Metacity and Gtk+, whereas the normal hardware accelerated mode uses
Mutter.
a
The GObject Introspection layer sits on top of Mutter and the Shell toolkit. One way to look at this
nn
layer is to consider it a glue layer between the Mutter and Shell Tookit libraries and JavaScript.
GObject Introspection is used to automatically generate the GObject bindings for JavaScript (gjs)
which is the language used to implement the GNOME Shell UI. The actual version of the
JavaScript language available using gjs is 1.8.5 as this JavaScript engine is based on Mozilla’s
o
The goal of GObject Introspection is to describe the set of APIs in a uniform, machine readable
XML format called GIR. A typelib is a compiled version of a GIR which is designed to be fast,
pe
memory efficient and complete enough so that language bindings can be written on top of it
without other sources of information. You can examine the contents of a specific typelib file using
g-ir-generate. For example, here is what is stored in the Shell Toolkit typelib file for
st_texture_cache_load_uri_sync which I use in Example 7 below.
r
Fo
# g-ir-generate /usr/lib64/gnome-shell/St-1.0.typelib
....
<method name="load_uri_sync" c:identifier="st_texture_cache_load_uri_sync" throws="1"
>
<return-value transfer-ownership="none">
<type name="Clutter.Actor"/>
</return-value>
<parameters>
<parameter name="policy" transfer-ownership="none">
<type name="TextureCachePolicy"/>
</parameter>
<parameter name="uri" transfer-ownership="none">
<type name="utf8"/>
</parameter>
<parameter name="available_width" transfer-ownership="none">
<type name="gint32"/>
</parameter>
You also need to understand the various components that make up the GNOME Shell UI if you are
going to be successful in customizing the GNOME Shell. Here are the various components of the
screen displayed just after you successfully log in.
ly
on
se
a lu
o nn
rs
● Top Bar (Also called Panel) – Horizontal bar at the top of the scrren. This is main access point to
the shell. (../js/ui/panel.js)
● Activities button (Also called Hotspot) – Button and Hot Corner that brings up the Overview (see
below). (../js/ui/panel.js)
r
● Application menu – Shows the name of the currently-focused application. You can click on it to
Fo
All the JavaScript code referenced above and in the next section is under /usr/share/gnome-shell.
The Overview screen is what you see when you click on the Activities button. It is mainly
implemented in ../js/ui/overview.js. It has the following UI components:
ly
on
se
lu
Here is a description of the various components in this particular screen:
● Dash – Vertical bar on the left, that shows your favourite applications. (../js/ui/dash.js)
a
● View Selector – Lets you pick between Windows and Applications. (../js/ui/viewSelector.js,
../js/ui/overview.js)
nn
● Search Entry – When you enter a string, various things (application names, documents, etc.) get
searched. (../js/ui/viewSelector.js for the entry, and ../js/ui/searchDisplay.js for the display of
search results)
o
● Workspace List – Vertical bar on the right, with thumbnails for the active workspaces. (
../js/ui/workspaceSwitcherPopup.js)
rs
In the following examples, I demonstrate how to customize various components of the GNOME
pe
Shell UI using extensions or by directly modifying the source code as in Example 7 if an extension
is not possible. I assume that you know JavaScript and the components that form a GNOME Shell
extension.
r
Example 1:
Fo
The GNOME Shell developers are pushing hard to eliminate the traditional notification area on the
top bar of GNOME desktops. However for the moment, tray icons are still displayed on the Panel
to the left of the System Status area.
For example gnote normally displays in the GNOME Shell message area as shown below:
ly
on
se
With this simple extension:
a lu
const Panel = imports.ui.panel;
const StatusIconDispatcher = imports.ui.statusIconDispatcher;
nn
function main() {
// add the notification(s) you want display on the top bar
// - one per line. Use the english text string displayed when
// hovering your mouse over the bottom right notification area
o
StatusIconDispatcher.STANDARD_TRAY_ICON_IMPLEMENTATIONS['gnote'] = 'gnote';
}
rs
ly
on
se
lu
Note how you import a module using the imports keyword. If you want to import a specific API
version of a module, you can do so by specifying the required version number, e.g.
a
const Gtk = imports.gi.versions.Gtk = '3.0';
nn
Example 2:
o
the Power Off, Suspend and Hibernate menu options can be displayed instead of just the Suspend
menu option.
pe
r
Fo
ly
on
se
lu
Instead of the brute force approach used by this extension, the code shown below simply locates
the suspend menu option and replaces it with the three required menu options and the auxiliary
support functions.
a
nn
const St = imports.gi.St;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
o
item.actor.visible = object.get_can_suspend();
}
function updateHibernate(object, pspec, item) {
item.actor.visible = object.get_can_hibernate();
}
r
function onSuspendActivate(item) {
Fo
Main.overview.hide();
this._screenSaverProxy.LockRemote(Lang.bind(this, function() {
this._upClient.suspend_sync(null);
}));
}
function onHibernateActivate(item) {
Main.overview.hide();
this._screenSaverProxy.LockRemote(Lang.bind(this, function() {
this._upClient.hibernate_sync(null);
}));
}
function changeUserMenu()
{
let children = this.menu._getMenuItems();
for (let i = 0; i < children.length; i++) {
let item = children[i];
if (item.label) {
let _label = item.label.get_text();
// global.log("menu label: " + _label);
if (_label == _("Suspend"))
item.destroy();
}
}
let item = new PopupMenu.PopupMenuItem(_("Suspend"));
item.connect('activate', Lang.bind(this, onSuspendActivate));
this._upClient.connect('notify::can-suspend', Lang.bind(this, updateSuspend, item));
updateSuspend(this._upClient, null, item);
this.menu.addMenuItem(item);
item = new PopupMenu.PopupMenuItem(_("Hibernate"));
item.connect('activate', Lang.bind(this, onHibernateActivate));
this._upClient.connect('notify::can-hibernate', Lang.bind(this, updateHibernate, item)
);
updateHibernate(this._upClient, null, item);
this.menu.addMenuItem(item);
item = new PopupMenu.PopupMenuItem(_("Power Off..."));
ly
item.connect('activate', Lang.bind(this, function() {
this._session.ShutdownRemote();
}));
on
this.menu.addMenuItem(item);
}
function main(metadata) {
// Post 3.0 let statusMenu = Main.panel._userMenu;
let statusMenu = Main.panel._statusmenu;
se
changeUserMenu.call(statusMenu);
}
lu
There are different types of power hibernation but the above example just uses the default method.
Some people might find it useful to have a sleep menu option also.
a
Note that I have commented out a line of code in the main function. The commented out line is
what you should use in post 3.0 versions of the GNOME Shell. How you access Panel objects is
nn
changing. See GNOME Bugzilla 646915 for full details. Essentially, a number of Panel objects
have been renamed and a _statusArea object now points to status area PanelButton objects. The
idea is that you will be able to address each Panel object consistently as follows:
o
rs
Main.panel._activities
Main.panel._appMenu
pe
Main.panel._dateMenu
Main.panel._statusArea.a11y
Main.panel._statusArea.volume
Main.panel._userMenu
r
Example 3:
Fo
In this example. I show you how to add a menu to the middle of the Panel as shown below:
ly
on
se
lu
Here is the source code for the extension used to create that menu:
a
const St = imports.gi.St;
const Main = imports.ui.main;
nn
function PlacesButton() {
this._init();
rs
}
PlacesButton.prototype = {
__proto__: PanelMenu.Button.prototype,
pe
_init: function() {
PanelMenu.Button.prototype._init.call(this, 0.0);
this._label = new St.Label({ text: _("MyPlaces") });
this.actor.set_child(this._label);
Main.panel._centerBox.add(this.actor, { y_fill: true });
r
let placeid;
this.placeItems = [];
Fo
this.defaultPlaces = Main.placesManager.getDefaultPlaces();
this.bookmarks = Main.placesManager.getBookmarks();
this.mounts = Main.placesManager.getMounts();
// Display default places
for ( placeid = 0; placeid < this.defaultPlaces.length; placeid++) {
this.placeItems[placeid] = new PopupMenu.PopupMenuItem(_(this.defaultPlaces[pl
aceid].name));
this.placeItems[placeid].place = this.defaultPlaces[placeid];
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// Display default bookmarks
for ( let bookmarkid = 0; bookmarkid < this.bookmarks.length; bookmarkid++, placei
d++) {
this.placeItems[placeid] = new PopupMenu.PopupMenuItem(_(this.bookmarks[bookma
rkid].name));
this.placeItems[placeid].place = this.bookmarks[bookmarkid];
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
if (this.mounts.length > 0) {
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
}
// Display default mounts
for ( let mountid = 0; mountid < this.mounts.length; placeid++, mountid++ ) {
this.placeItems[placeid] = new PopupMenu.PopupMenuItem(_(this.mounts[mountid].
name));
this.placeItems[placeid].place = this.mounts[mountid];
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
ly
actor.place.launch();
});
}
on
Main.panel._centerBox.add(this.actor, { y_fill: true });
Main.panel._menus.addMenu(this.menu);
}
};
function main(extensionMeta) {
se
let userExtensionLocalePath = extensionMeta.path + '/locale';
Gettext.bindtextdomain("places_button", userExtensionLocalePath);
Gettext.textdomain("places_button");
new PlacesButton();
}
lu
Notice how you can retrieve details of all places, bookmarks and mounts from
a
Main.placesManager:
nn
places = Main.placesManager.getDefaultPlaces();
bookmarks = Main.placesManager.getBookmarks();
o
mounts = Main.placesManager.getMounts();
rs
Example 4:
pe
In this example, I show you how to extend the previous example to display icons on each menu
option as shown below:
r
Fo
ly
on
se
Here is the modified source code:
a lu
const St = imports.gi.St;
const Main = imports.ui.main;
nn
this._init();
}
PlacesButton.prototype = {
pe
__proto__: PanelMenu.Button.prototype,
_init: function() {
PanelMenu.Button.prototype._init.call(this, 0.0);
this._label = new St.Label({ text: _("MyPlaces") });
this.actor.set_child(this._label);
r
this.placeItems = [];
this.defaultPlaces = Main.placesManager.getDefaultPlaces();
this.bookmarks = Main.placesManager.getBookmarks();
this.mounts = Main.placesManager.getMounts();
// Display default places
for ( placeid = 0; placeid < this.defaultPlaces.length; placeid++) {
this.placeItems[placeid] = new PopupMenu.PopupMenuItem(_(this.defaultPlaces[pl
aceid].name));
let icon = this.defaultPlaces[placeid].iconFactory(MYPLACES_ICON_SIZE);
this.placeItems[placeid].addActor(icon, { align: St.Align.END});
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// Display default bookmarks
for ( let bookmarkid = 0; bookmarkid < this.bookmarks.length; bookmarkid++, placei
d++) {
this.placeItems[placeid] = new PopupMenu.PopupMenuItem(_(this.bookmarks[bookma
rkid].name));
let icon = this.bookmarks[bookmarkid].iconFactory(MYPLACES_ICON_SIZE);
this.placeItems[placeid].addActor(icon, { align: St.Align.END});
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
if (this.mounts.length > 0) {
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
}
// Display default mounts
for ( let mountid = 0; mountid < this.mounts.length; placeid++, mountid++ ) {
this.placeItems[placeid] = new PopupMenu.PopupMenuItem(_(this.mounts[mountid].
ly
name));
let icon = this.mounts[mountid].iconFactory(MYPLACES_ICON_SIZE);
this.placeItems[placeid].addActor(icon, { align: St.Align.END});
on
this.placeItems[placeid].place = this.mounts[mountid];
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
se
}
Main.panel._centerBox.add(this.actor, { y_fill: true });
Main.panel._menus.addMenu(this.menu);
}
};
lu
function main(extensionMeta) {
let userExtensionLocalePath = extensionMeta.path + '/locale';
Gettext.bindtextdomain("places_button", userExtensionLocalePath);
a
Gettext.textdomain("places_button");
new PlacesButton();
nn
The heavy lifting in creating icons is done by iconFactory which is a JavaScript callback that
o
iconFactory: function(size) {
pe
Example 5:
Fo
In this example, I show you how to modify the previous example to display icons followed by labels
on each menu option as shown below:
ly
on
se
Here is the relevant source code:
a lu
const St = imports.gi.St;
const Main = imports.ui.main;
nn
this._init.apply(this, arguments);
}
MyPopupMenuItem.prototype = {
pe
__proto__: PopupMenu.PopupBaseMenuItem.prototype,
_init: function(icon, text, params) {
PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
this.addActor(icon);
this.label = new St.Label({ text: text });
r
this.addActor(this.label);
}
Fo
};
function PlacesButton() {
this._init();
}
function PlacesButton() {
this._init();
}
PlacesButton.prototype = {
__proto__: PanelMenu.Button.prototype,
_init: function() {
PanelMenu.Button.prototype._init.call(this, 0.0);
this._icon = new St.Icon({ icon_name: 'start-here',
icon_type: St.IconType.SYMBOLIC,
style_class: 'system-status-icon' });
this.actor.set_child(this._icon);
Main.panel._centerBox.add(this.actor, { y_fill: true });
let placeid;
this.placeItems = [];
this.defaultPlaces = Main.placesManager.getDefaultPlaces();
this.bookmarks = Main.placesManager.getBookmarks();
this.mounts = Main.placesManager.getMounts();
// Display default places
for ( placeid = 0; placeid < this.defaultPlaces.length; placeid++) {
let icon = this.defaultPlaces[placeid].iconFactory(MYPLACES_ICON_SIZE);
this.placeItems[placeid] = new MyPopupMenuItem(icon, _(this.defaultPlaces[plac
eid].name));
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// Display default bookmarks
for ( let bookmarkid = 0; bookmarkid < this.bookmarks.length; bookmarkid++, placei
d++) {
ly
let icon = this.bookmarks[bookmarkid].iconFactory(MYPLACES_ICON_SIZE);
this.placeItems[placeid] = new MyPopupMenuItem(icon, _(this.bookmarks[bookmark
id].name));
on
this.menu.addMenuItem(this.placeItems[placeid]);
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
se
if (this.mounts.length > 0) {
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
}
// Display default mounts
lu
for ( let mountid = 0; mountid < this.mounts.length; placeid++, mountid++ ) {
let icon = this.mounts[mountid].iconFactory(MYPLACES_ICON_SIZE);
this.placeItems[placeid] = new MyPopupMenuItem(icon, _(this.mounts[mountid].na
me) );
a
this.placeItems[placeid].place = this.mounts[mountid];
this.menu.addMenuItem(this.placeItems[placeid]);
nn
this.placeItems[placeid].connect('activate', function(actor,event) {
actor.place.launch();
});
}
o
}
};
function main(extensionMeta) {
pe
Example 6:
In this example, I show you how to add an Applications menu next to the Activities button.
ly
on
se
Here is the source code for extension.js:
a lu
const St = imports.gi.St;
const Main = imports.ui.main;
nn
}
MyPopupMenuItem.prototype = {
__proto__: PopupMenu.PopupBaseMenuItem.prototype,
_init: function(icon, text, menu_icon_first, params) {
PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
r
ly
this.appItems[this._categories[id]] = new PopupMenu.PopupSubMenuMenuItem(this.
_categories[id]);
this.menu.addMenuItem(this.appItems[this._categories[id]]);
on
}
let appInfos = this._appSys.get_flattened_apps().filter(function(app) {
return !app.get_is_nodisplay();
});
for (let appid = appInfos.length-1; appid >= 0; appid--) {
se
let appInfo = appInfos[appid];
let icon = appInfo.create_icon_texture(APPMENU_ICON_SIZE);
let appName = new MyPopupMenuItem(icon, appInfo.get_name(), this._menuIconFirs
t);
lu
// let appName = new PopupMenu.PopupMenuItem(appInfo.get_name());
appName._appInfo = appInfo;
this.appItems[appInfo.get_section()].menu.addMenuItem(appName);
appName.connect('activate', function(actor,event) {
a
let id = actor._appInfo.get_id();
Shell.AppSystem.get_default().get_app(id).activate(-1);
nn
});
}
},
_redisplay: function() {
o
}
this._display();
}
pe
};
function main(extensionMeta) {
let userExtensionLocalePath = extensionMeta.path + '/locale';
Gettext.bindtextdomain("applications_button", userExtensionLocalePath);
Gettext.textdomain("applications_button");
r
new ApplicationsButton(false);
Fo
ly
on
se
Example 7:
lu
Not all components of the GNOME Shell can be easily modified or customized. For example,
a
suppose I would like to display a search provider’s logo as an icon on their search provider button.
The icon is already available as a base64 string in a search provider’s OpenSearch xml file but is
nn
Looking at the current source code for the GNOME Shell, we can see that in search.js the icon
_addProvider: function(fileName) {
let path = global.datadir + '/search_providers/' + fileName;
let source = Shell.get_file_contents_utf8_sync(path);
let [success, name, url, langs, icon_uri] = global.parse_search_provider(source);
ly
}
},
on
However it is not passed to where the search provider button is created in searchDisplay.js due to
a limitation in the following piece of code in search.js:
se
getProviders: function() {
let res = [];
for (let i = 0; i
lu
_createOpenSearchProviderButton: function(provider) {
let button = new St.Button({ style_class: 'dash-search-button',
reactive: true,
a
x_fill: true,
y_align: St.Align.MIDDLE });
nn
provider.actor = button;
this._searchProvidersBox.add(button);
Fo
},
With this modified code, here is how the search provider buttons now look like:
ly
on
se
lu
A GNOME Shell extension could probably be written to monkey patch the modified versions of the
two functions into the GNOME Shell. It is something I will try to do when I get some free time.
a
Well, this post is getting too big and so it is time to conclude it. But before I do, I want to mention
something about the GNOME Shell that has been on my mind recently as I experiment with its
nn
internals. For some reason the GNOME developers seem to think that the GNOME Shell should be
an integral part of the OS and that is obvious in some of the design decisions. Recently Colin
Walters stated that
o
rs
Where we want to get to is that there are really just three things:
* Apps
pe
* Extensions
* The OS
r
This is just plain wrong as far as I am concerned. A user should always have a choice of desktop
Fo
managers and shells. Sounds like the vision of the GNOME developers, at least as articulated by
Colin Walters in his post, is that the OS and the GNOME Shell should be one and the same as in
Microsoft Windows. If this is the goal, then I fear that many existing users will abandon the
GNOME desktop.
Sometimes I get the impression that the GNOME Shell was designed and put together by a group
of arrogant young software engineers who were more interested in adhering to the tenants of
so-called usability and design theory studies than in what their end users really needed in a
modern graphical shell. Frankly, I fully agree with what Bruce Byfield recently wrote in
Datamation about usability studies hurting the free desktop.
Don’t get me wrong – I like and use the GNOME Shell and really do not want to go back to
GNOME 2 or another desktop manager, but I am often frustrated by it’s design constraints which
get in the way of me doing what I want to do quickly and efficiently. Worse, when you look under
the hood of the GNOME Shell, you quickly become aware of serious shortcomings in much of the
underlying codebase. For example, why is the GNOME shell dependant on two distinct JavaScript
engines, i.e. gjs and seed (webkit)? Pick one of these two JavaScript engines and remove the
dependencies on the other. .
ly
on
se
a lu
o nn
rs
pe
r
Fo