Continue GNOME Shell extension article with settings

This commit is contained in:
Gergely Polonkai 2015-09-29 15:04:07 +02:00
parent 8eeb6f6351
commit 262ebd7616

View File

@ -4,10 +4,11 @@ title: "Writing a GNOME Shell extension"
--- ---
I could not find a good tutorial on how to write a GNOME Shell I could not find a good tutorial on how to write a GNOME Shell
extension. There is a so called step by step instruction list on how extension. There is a so called step by step
to do it, but it has its flaws, including grammar and clearance. As I [instruction list](https://wiki.gnome.org/Projects/GnomeShell/Extensions/StepByStepTutorial)
wanted to create an extension for my SWE GLib library to show the on how to do it, but it has its flaws, including grammar and clearance.
current position of some planets, I dug into existing (and working) As I wanted to create an extension for my SWE GLib library to display
the current position of some planets, I dug into existing (and working)
extensions source code and made up something. Comments welcome! extensions source code and made up something. Comments welcome!
--- ---
@ -31,25 +32,76 @@ either use a so called extension controller, or plain old JavaScript
functions `enable()` and `disable()`; I will go on with the former functions `enable()` and `disable()`; I will go on with the former
method for reasons discussed later. method for reasons discussed later.
If you are fine with the `enable()`/`disable()` function version, you
can ease your job with the following command:
```
gnome-shell-extension-tool --create-extension
```
This will ask you a few parameters and create the necessary files for
you. On what these parameters should look like, please come with me to
the next section.
## Placement and naming
Extensions reside under `$HOME/.local/share/gnome-shell/extensions`,
where each of them have its own directory. The directory name has to be
unique, of course; to achieve this, they are usually the same as the
UUID of the extension.
The UUID is a string of alphanumeric characters, with some extras added.
Generally, it should match this regular expression:
`^[-a-zA-Z0-9@._]+$`. The convention is to use the form
`extension-name@author-id`, e.g. `Planets@gergely.polonkai.eu`. Please
see
[this link](https://wiki.gnome.org/Projects/GnomeShell/Extensions/UUIDGuidelines)
for some more information about this.
## Anatomy of an extension ## Anatomy of an extension
The only thing you actually need is an `init()` function: Extensions consist of two main parts, `metadata.json` and
`extension.js`.
The `metadata.json` file contains compatibility information and, well,
some meta data:
```json
{
"shell-version": ["3.18"],
"uuid": "planets@gergely.polonkai.eu",
"name": "Planets",
"description": "Display current planet positions"
}
```
Here, `shell-version` must contain all versions of GNOME Shell that is
known to load and display your extension correctly. You can insert minor
versions here, like I did, or exact version numbers, like `3.18.1`.
In the `extension.js` file, which contains the actual extension code,
the only thing you actually need is an `init()` function:
```javascript
function init(extensionMeta) { function init(extensionMeta) {
// Do whatever it takes to initialize your extension, // Do whatever it takes to initialize your extension, like
// like initializing the translations. // initializing the translations. However, never do any widget
// magic here yet.
// Then return the controller object // Then return the controller object
return new ExtensionController(extensionMeta); return new ExtensionController(extensionMeta);
} }
```
## Extension controller ## Extension controller
So far so good, but what is this extension controller thing? It is an So far so good, but what is this extension controller thing? It is an
object which is capable of managing your GNOME Shell extension. Whenever object which is capable of managing your GNOME Shell extension. Whenever
the extension is loaded, its `enable()` method is called; when the the extension is loaded, its `enable()` method is called; when the
extension is unloaded, the `disable()` method gets called. extension is unloaded, you guessed it, the `disable()` method gets
called.
```javascript
function ExtensionController(extensionMeta) { function ExtensionController(extensionMeta) {
return { return {
extensionMeta: extensionMeta, extensionMeta: extensionMeta,
@ -71,6 +123,7 @@ extension is unloaded, the `disable()` method gets called.
} }
} }
} }
```
This controller will create a new instance of the `PlanetsExtension` This controller will create a new instance of the `PlanetsExtension`
class and add it to the panels right side when loaded. Upon class and add it to the panels right side when loaded. Upon
@ -84,7 +137,7 @@ extension is set to `null`.
The extension is a bit more tricky, as, for convenience reasons, it The extension is a bit more tricky, as, for convenience reasons, it
should extend an existing panel widget type. should extend an existing panel widget type.
``` ```javascript
function PlanetsExtension(extensionMeta) { function PlanetsExtension(extensionMeta) {
this._init(extensionMeta); this._init(extensionMeta);
} }
@ -119,26 +172,155 @@ The only parameter passed to the parents `_init()` function is
menu arrow. (_Note: I cannot find any documentation on this, but it menu arrow. (_Note: I cannot find any documentation on this, but it
seems that with the value `0.0`, a menu arrow is not added._) seems that with the value `0.0`, a menu arrow is not added._)
The extension class in its current form is capable of creating the
actual panel button displaying the text “Loading” in its center.
## Loading up the extension ## Loading up the extension
Now with all the necessary import lines added: Now with all the necessary import lines added:
```javascript
// The PanelMenu module that contains Button
const PanelMenu = imports.ui.panelMenu; const PanelMenu = imports.ui.panelMenu;
// The St class that contains lots of UI functions
const St = imports.gi.St; const St = imports.gi.St;
// Clutter, which is used for displaying everything
const Clutter = imports.gi.Clutter; const Clutter = imports.gi.Clutter;
```
The only thing to create now is the `metadata.json` file, which
contains compatibility information and, well, some meta data.
{
"shell-version": ["3.18"],
"uuid": "planets@gergely.polonkai.eu",
"name": "Planets",
"description": "Display current planet positions"
}
As soon as this file is ready, you can restart your Shell (press As soon as this file is ready, you can restart your Shell (press
Alt-F2 and enter the command `r`), and load the extension with Alt-F2 and enter the command `r`), and load the extension with
e.g. the GNOME Tweak Tool. You will see the Planets button on the e.g. the GNOME Tweak Tool. You will see the Planets button on the
right. This little label showing the static text “Planets”, however, right. This little label showing the static text “Planets”, however,
is pretty boring, so lets add some action. is pretty boring, so lets add some action.
## Adding some periodical change
Since the planets position continuously change, we should update our
widget every minute or so. Lets patch our `_init()` a bit:
```javascript
this.last_update = 0;
MainLoop.timeout_add(1, Lang.bind(this, function() {
this.last_update++;
this.panelLabel.set_text("Update_count: " + this.last_update);
}))
```
This, of course, needs a new import line for `MainLoop` to become available:
```javascript
const MainLoop = imports.mainloop;
const Lang = imports.lang;
```
Now if you restart your Shell, your brand new extension will increase
its counter every second. This, however, presents some problems.
SWE GLib queries can sometimes be expensive, both in CPU and disk
operations, so updating our widget every second may present problems.
Also, planets dont go **that** fast. We may update our timeout value
from `1` to `60` or something, but why dont just give our user a chance
to set it?
## Introducing settings
Getting settings from `GSettings` is barely straightforward, especially
for software installed in a non-GNOME directory (which includes
extensions). To make our lives easier, I copied over a
[convenience library](https://github.com/projecthamster/shell-extension/blob/master/convenience.js)
from the [Hamster project](https://projecthamster.wordpress.com/)s
extension, originally written by Giovanni Campagna. The relevant
function here is `getSettings()`:
```javascript
/**
* 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, true);
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 });
}
```
You can either incorporate this function into your `extension.js` file,
or just use `convenience.js` file like I (and the Hamster applet) did
and import it:
```javascript
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension;
const Convenience = Me.imports.convenience;
```
Now lets create the settings definition. GSettings schema files are XML
files. We want to add only one settings for now, the refresh interval.
```xml
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.planets" path="/org/gnome/shell/extensions/planets/">
<key name="refresh-interval" type="i">
<default>30</default>
<summary>Refresh interval of planet data</summary>
<description>Interval in seconds. Sets how often the planet positions are recalculated. Setting this too low (e.g. below 30) may raise performance issues.</description>
</key>
</schema>
</schemalist>
```
you need to compile these settings with
glib-compile-schemas --strict schemas/
Now lets utilize this new setting. In the extensions `_init()`
function, add the following line:
```javascript
this._settings = Convenience.getSettings();
```
And, for `getSettings()` to work correctly, we also need to extend our
`metadata.json` file:
```json
"settings-schema": "planets"
```
After another restart (please, GNOME guys, add an option to reload
extensions!), your brand new widget will refresh every 30 seconds.
## Displaying the planet positions
## The settings panel
## Start an application