GSettings

Depuis Gnome 3, le stockage de la configuration d'une application se fait par l'intermédiaire de GSettings, une API « agnostique » dont l'utilisation dans un plugin Gedit ou une extension Gnome-shell pose de petits problèmes car elle nécessite les droits administrateurs. Voici quelques pistes pour l'utiliser « en local ».

L'enregistrement de la configuration d'une application est un principe simple qui consiste à stocker une série de couples clé/valeur, soit le nom d'une propriété de configuration et sa valeur. L'utilisateur retrouve ensuite ces valeurs à chaque fois qu'il lance l'application.

Gnome 2 et GConf

Sous Gnome 2, le stockage des informations de configuration se faisait avec GConf. Son utilisation était dés plus simple :

# Importer gconf
import gconf

# Chemin de stockage de la configuration 
# d'une application 'myapp'
GCONF_DIR = '/apps/myapp/'

# Récupérer la valeur de 'foo': 'bar'
value = gconf.client_get_default().get_string(GCONF_DIR + 'foo')

# Assigner une nouvelle valeur à 'foo': 'baz'
gconf.client_get_default().set_string(GCONF_DIR + 'foo', 'baz')
Exemple d'utilisation de GConf en python.

Gnome 3 et DConf - GSettings

Avec Gnome 3, les choses se complexifient un (tout) petit peu. D'abord parce que GConf est remplacé par Dconf ; ensuite parce que les applications n'interagissent plus directement avec l'outil de stockage mais avec GSettings, une API générique qui est chargée d'aller dialoguer avec DConf(1).

L'avantage de cette technique est évident : en utilisant GSettings, on reste indépendant de la manière dont les données vont être stockées. Si le projet Gnome décide un jour d'abandonner DConf pour utiliser un autre système de stockage, le changement sera transparent.

Utiliser un schéma

Nous partirons d'une application « myapp » possédant un dossier « data » contenant lui-même un dossier « schema » dans lequel nous stockerons les schémas GSettings.

  • myapp
    • data
Organisation de l'application de test.

Création du schéma

GSettings se base sur l'utilisation d'un schéma de configuration au format xml :

<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
    <schema path="/org/myapp/" id="org.myapp">
        <key name="boolkey" type="b">
            <default>true</default>
            <summary>A boolean configuration key.</summary>
            <description>...</description>
        </key>
        <key name="strkey" type="s">
            <default>"string"</default>
            <summary>A string configuration key.</summary>
            <description>...</description>
        </key>
        <key name="intkey" type="i">
            <default>25</default>
            <range min="0" max="100" />
            <summary>A integer configuration key.</summary>
            <description>...</description>
        </key>
        <key name="choicekey" type="s">
            <choices>
                <choice value="choice 1"/>
                <choice value="choice 2"/>
            </choices>
            <default>"choice 1"</default>
            <summary>A choice configuration key.</summary>
        </key>
    </schema>
</schemalist>
Schéma de configuration GSettings de notre application.

La documentation de GSettings fournit la DTD de ce type de fichier qui est structuré autour d'une liste d'éléments key représentant des données de configuration. Chaque clé doit être définie par :

  • Un attribut type correspondant à son type.
  • Un attribut name pour l'identifier.
  • Un élément default pour spécifier sa valeur par défaut.

Chaque clé peut aussi contenir les éléments suivants :

  • sumamry : courte définition de la clé.
  • description : description de la clé.

Ces fichiers de schémas doivent avoir un nom qui se termine par « gschema.xml ». Ils ne sont utilisés que sous forme compilée, compilation qui s'effectue grâce au programme glib-compile-schemas.

  • myapp
    • data
      • schema
        • org.myapp.gschema.xml
Insertion d'un schéma dans l'application.

Compilation du schéma

Compilation au niveau du système

La documentation Gnome précise que les schémas compilés doivent être placés dans un des répertoires système de données (XDG_DATA_DIRS)(2) pour pouvoir être lus, GSettings ne pouvant pas les lire dans le répertoire local de données propre à l'utilisateur (XDG_DATA_HOME)(3). C'est un problème si on souhaite l'utiliser pour développer un plugin Gedit ou une extension Gnome-shell par exemple : l'utilisateur devra copier le fichier xml et le compiler dans un répertoire système avec les droits administrateurs :

# Se loguer en root
su -

# Copier le schéma dans le répretoire système adéquat
cp path/to/myapp/data/schema/org.myapp.gschema.xml /usr/share/glib-2.0/schemas/

# (Re)compiler les schémas GSettings.
glib-compile-schemas /usr/share/glib-2.0/schemas/
Copie et compilation d'un schéma dans /usr/share/glib-2.0/schemas/.

Compilation embarquée

Cette manipulation est une véritable regression par rapport à GConf. On peut cependant l'éviter en compilant le schéma à l'endroit où il se trouve :

glib-compile-schemas path/to/myapp/data/schema/
Compilation du schéma en « embarqué ».

La commande glib-compile-schemas va créer un fichier de compilation nommé « gschemas.compiled ».

  • myapp
    • data
      • schema
        • gschemas.compiled
        • org.myapp.gschema.xml
Embarquement de la compilation du schéma dans l'application.

Compilation locale

Comme expliqué plus haut, GSettings n'est pas capable de lire des schémas de configuration déployés en local dans ~/.local/share/glib-2.0/schemas/. En fait, cela n'est pas tout à fait vrai, cela grâce à la variable d'environnement GSETTINGS_SCHEMA_DIR :

GSETTINGS_SCHEMA_DIR. This variable can be set to the name of a directory that is considered in addition to the glib-2.0/schemas subdirectories of the XDG system data dirs when looking for compiled schemas for GSettings.

Running GIO applications

Cependant, pour pouvoir utiliser GSETTINGS_SCHEMA_DIR, il faut que l'utilisateur la définisse lui-même car elle ne l'est pas par défaut - du moins sous Fedora 17, définition à effectuer dans le fichier .profile par exemple :

# GSettings local dir
GSETTINGS_SCHEMA_DIR=$HOME/.local/share/glib-2.0/schemas/
export GSETTINGS_SCHEMA_DIR
Définition de GSETTINGS_SCHEMA_DIR dans ~/.profile.

Il faudra ensuite copier le schéma de configuration dans ce répertoire et lancer sa compilation :

import os

def schema_to_local(schema_file):
    '''
    Copier et compiler un schéma GSettings dans le répertoire local
    @param schema_dir: chemin du schéma
    @return: True si la compilation a été effectuée, False sinon.
    '''
    local_schema_dir = os.environ.get('GSETTINGS_SCHEMA_DIR')
    if local_schema_dir is not None:
        cmd_copy = 'cp ' schema_file + ' ' + local_schema_dir
        cmd_compil = 'glib-compile-schemas ' + local_schema_dir
        os.system(cmd_copy)
        os.system(cmd_compil)
        
        return True
        
    return False
Copie et compilation du schéma dans le répertoire local (python).
# Copier et compiler un schéma Gsettings local.
# param $1: chemin du schéma
schema_to_local() {
    if [[ $1 ]]; then
        if [[ $GSETTINGS_SCHEMA_DIR ]]; then
            cp $1 $GSETTINGS_SCHEMA_DIR
            glib-compile-schemas $GSETTINGS_SCHEMA_DIR
        else
            echo "GSETTINGS_SCHEMA_DIR is not defined"
        fi
    fi
}
Copie et compilation du schéma dans le répertoire local (bash).

Obliger l'utilisateur à définir une variable d'environnement pour avoir à disposition la configuration d'un plugin Gedit ou d'une extension gnome-shell est tout de même contraignant, moins que de devoir installer des fichiers avec des droits administrateurs mais contraignant tout de même. La compilation dans le plugin reste donc la solution la plus acceptable, en attendant que GSETTINGS_SCHEMA_DIR soit définie par les systèmes d'exploitation.

Chargement du schéma

Nous pouvons maintenant charger un objet Gsettings dans notre code en partant du principe que le schéma à utiliser sera éventuellement installé localement :

from gi.repository import Gio
import os

def get_settings(schema_name, schema_dir = None):
    '''
    Charger un objet de configuration Gio.Settings.
    
    La méthode cherche d'abord si la configuration demandée est chargé globalement.
    Si ce n'est pas le cas, elle tentera de charger un schéma 'local', présent
    dans le répertoire 'schema_dir'.
    
    @param schema_name: Nom du schéma.
    @param schema_dir: répertoire du schéma.
    @return: Gio.Settings ou None
    '''
    schemas = Gio.Settings.list_schemas()
    
    if schema_name in schemas:
        return Gio.Settings.new(schema_name)
    
    if schema_dir is not None and os.path.isdir(schema_dir):
        schema_source = Gio.SettingsSchemaSource.new_from_directory(
            schema_dir, 
            Gio.SettingsSchemaSource.get_default(), 
            False
        )
        schema = schema_source.lookup(schema_name, False)
        
        if schema is not None:
            return Gio.Settings.new_full(schema, None, None)
    
    return None
Lire un schéma GSettings (python).

const Gio = imports.gi.Gio;
/**
 *  Charger un objet de configuration Gio.Settings.
 *  
 *  La méthode cherche d'abord si la configuration demandée est chargé globalement.
 *  Si ce n'est pas le cas, elle tentera de charger un schéma 'local', présent
 *  dans le répertoire 'schema_dir'.
 *  
 *  @param {String} schema_name Nom du schéma.
 *  @param {String} [schema_dir] Répertoire local du schéma.
 *  @return {Gio.Settings|null}
 */
const get_settings = function (schema_name, schema_dir) {
    let schemas = Gio.Settings.list_schemas(),
        schema_source,
        schema;
    
    if (schema_name in schemas) {
        return new Gio.Settings({ settings_schema: schemas[schema_name] });
    }
    
    if (schema_dir) {
        let dir = Gio.File.new_for_path(schema_dir);
        if (!dir.query_exists(null)) {
            return null;
        }
    }
    schema_source = Gio.SettingsSchemaSource.new_from_directory(
        schema_dir,
        Gio.SettingsSchemaSource.get_default(),
        false
    );
    schema = schema_source.lookup(schema_name, false);
    
    if (schema) {
        return new Gio.Settings({ settings_schema: schema });
    }
    
    return null;
}
Lire un schéma GSettings (javascript).

GSettings : API

Il n'est pas question ici de faire un tour complet de l'API de GSettings ; la documentation Gnome le fait très bien. Ne sont abordés ici que 2 points essentiels : la lecture / écriture des clés d'un schéma et l'écoute des changements de valeur de ces clés. Un troisième point, la liaison entre une clé et un composant graphique, est traité en détail dans le chapitre suivant.

Les exemples de code partent du principe que la méthode d'acquisition d'un objet Gsettings (get_settings) décrite précédemment.

Lecture / écriture de clé

settings = get_settings('org.myapp', '/path/to/myapp/data/schema')

# Lecture de valeurs
strvalue = settings.get_string('strkey')
boolvalue = settings.get_boolean('boolkey')
intvalue = settings.get_int('intkey')

# Nouvelles valeurs
settings.set_string('strkey', 'new string')
settings.set_boolean('boolkey', False)
settings.set_int('intkey', 10)
Exemple de lecture / écriture de clés GSettings en python.
var settings = get_settings('org.myapp', '/path/to/myapp/data/schema');

// Lecture de valeurs
var strvalue = settings.get_string('strkey');
var boolvalue = settings.get_boolean('boolkey');
var intvalue = settings.get_int('intkey');

// Nouvelles valeurs
settings.set_string('strkey', 'new string');
settings.set_boolean('boolkey', false);
settings.set_int('intkey', 10);
Exemple de lecture / écriture de clés GSettings en javascript.

Ecoute des modifications

L'événement changed est émis lors de la modification d'une clé de l'objet GSettings. Pour être uniquement à l'écoute des modifications d'une clé particulière « x », il faut plutôt écouter l'événement changed::x.

def on_settings_changed(settings, key, data = None):
    print key + ' has changed'

def on_strkey_changed(settings, key, data = None):
    print 'strkey has changed'

settings = get_settings('org.myapp', '/path/to/myapp/data/schema')
settings.connect('changed', on_settings_changed)
settings.connect('changed::strkey', on_strkey_changed)
Exemple d'écoute des événements changed en python.
var on_settings_changed = function (settings, key, data) {
    print(key + ' has changed');
};

var on_strkey_changed = function (settings, key, data) {
    print('strkey has changed');
};

var settings = get_settings('org.myapp', '/path/to/myapp/data/schema');
settings.connect('changed', on_settings_changed);
settings.connect('changed::strkey', on_strkey_changed);
Exemple d'écoute des événements changed en javascript.

écran de configuration

Nous allons illustrer l'utilisation de GSettings avec l'écriture d'un écran de configuration de 2 applications écrites avec 2 languages de programmation différents : une extension gnome-shell écrite en javascript et un plugin Gedit écrit en python.

Liaison entre composant graphique et clé GSettings

La liaison entre un composant graphique et une clé GSettings se fait par l'intermédiaire de la méthode bind d'un objet GSettings :

method
void bind ( key , object , property , flag )
param
String key Nom de la clé du schéma
param
Object object Composant graphique lié à la clé.
param
String property Propriété de l'objet liée à la clé.
param
GSettingsBindingFlags flag Flag

L'objet en question sera un composant graphique GTK adpaté au type de la clé, par exemple :

  • Gtk.Entry pour une chaîne de caractères.
  • Gtk.SpinButton pour une entier.
  • Gtk.Switch pour un booléen.

On pourra aussi gérer certains cas particuliers : une clé String ayant une liste de choix pourra être liée à une Gtk.ComboBoxText.

Le module settings

J'ai créé un module settings en javascript et en python à la fois pour faciliter l'utilisation de schémas Gsettings en « embarqué » dans une application mais aussi pour automatiser la création d'une interface graphique de configuration basée sur les clés d'un schéma Gsettings.

method
Gio.Settings get ( schema_name [ , schema_dir ] )
Description
Récupérer un objet GSettings.
param
String schema_name Nom du chemin
param
String schema_dir Chemin local éventuel - optional
return
Gio.Settings

La méthode get reprend purement et simplement le code de la méthode get_settings vue dans l'article expliquant comment utiliser un schéma GSettings.

method
Object get_details ( schema_path , schema_name )
Description
Parser le fichier xml décrivant le schéma GSettings pour récupérer les informations concernant les clés
param
String schema_path Chemin du fichier GSettings.
param
String schema_name Identifiant du schéma.
return
Object

The schemas are compiled in a database, that will only contain the default values and potential constraints about the values for a key. So if an application wants to access the description of a key (which is the exception, not the usual case), it will have to parse the XML files for this.

GSettings Hackfest: Day 1

Les schémas GSettings sont utilisés sous forme compilée et cette compilation ne stocke que les valeurs des clés, pas les autres informations qui pourraient être utiles lors de la création d'un écran de configuration, comme le résumé ou la description des clés. Pour récupérer toutes les informations des clés, il faut donc lire le fichier xml du schéma, ce que la méthode get_details fait.

class
Gui ( settings , cfg )
Description
Classe de génération d'interface graphique pour un fichier de configuration.
param
Gio.Settings settings Objet
param
Object cfg Objet de configuration.
  • String ui_path Chemin du fichier Glade construisant l'interface graphique.
  • String[] ids Liste des identifiants des composants graphiques à lier.

Décrire l'utilisation de Gui.

Utilisation dans une application

On peut utiliser le module settings dans une application GTK : nous allons en créé une un peu particulière

  • myapp
    • commongjs
      • settings.js
      • utils.js
    • commonpy
      • settings.py
      • utils.py
    • data
      • schema
        • gschema.compiled
        • org.myapp.gschema.xml
      • ui
        • config.glade
    • myapp.js
    • myapp.py
organisation de l'application GTK.

Capture d'écran

Ecran de l'application construit à partir d'un fichier Glade.

Capture d'écran

Ecran de l'application construit automatiquement.

Cas des extensions gnome-shell

La configuration des extensions gnome-shell se fait par l'intermédiaire d'un fichier prefs.js. Celui-ci doit contenir une méthode buildPrefsWidget qui retourne le contenu qui sera placé dans la fenêtre de configuration.

  • gnome-shell-extension
    • common
      • settings.js
    • data
      • schema
        • gschemas.compiled
        • org.gnome.shell.extensions.cfgext.gschema.xml
      • ui
        • config.glade
    • extension.js
    • metadata.json
    • prefs.js
Contenu d'une extension gnome-shell.
Contenu indisponible.
Contenu du fichier prefs.js.

Capture d'écran

Ecran de configuration d'une extension gnome-shell.

Cas des plugins Gedit

Gedit permet la définition de l'écran de configuration d'un plugin si celui-ci implémente l'interface PeasGtk.Configurable. La méthode do_create_configure_widget doit alors retourner le contenu qui sera placé dans la fenêtre de configuration.

  • gedit-plugin
    • cfgplugin
      • common
        • settings.py
      • data
        • schema
          • gschemas.compiled
          • org.gnome.gedit.plugins.cfgplugin.gschema.xml
        • ui
          • config.glade
      • __init__.py
    • cfgplugin.plugin
Contenu d'un plugin Gedit.
# -*- coding: utf-8 -*-

import os
from gi.repository import GObject, Gtk, Gedit, PeasGtk, Gio
from common.settings import Settings

cdir        = os.path.dirname(__file__)
schema_dir  = os.path.join(cdir, 'data', 'schema')
schema_id   = 'org.gnome.gedit.plugins.cfgplugin'
schema_path = os.path.join(schema_dir, schema_id + '.gschema.xml')
ui_path     = os.path.join(cdir, 'data', 'ui', 'config.glade')
ui_ids      = ['config', 'intkey_adj']
cfg_prefs   = ['strkey', 'boolkey', 'intkey', 'choicekey']

# Gio.Settings
sets = Settings.get(schema_id, schema_dir)

class CfgPlugin(GObject.Object, Gedit.WindowActivatable, PeasGtk.Configurable):
    
    __gtype_name__ = 'CfgPlugin'
    
    window = GObject.property(type=Gedit.Window)
    
    def __init__(self):
        GObject.Object.__init__(self)
    
    def do_create_configure_widget(self):
        gui = Settings.Gui(sets, {
            'uipath'    : ui_path,
            'schemapath': schema_path,
            'ids'       : ui_ids,
            'prefs'     : cfg_prefs
        })
        return gui.get_widget()
        
    def do_activate(self):
        pass

    def do_deactivate(self):
        pass

    def do_update_state(self):
        pass
Contenu du fichier prefs.js.

Capture d'écran

Ecran de configuration d'un plugin Gedit.

Ressources et références

Titre
GIO Reference Manual
chapter
GSettings
Éditeur
Gnome Dev Center
Titre
GIO Reference Manual
chapter
Running GIO applications
Éditeur
Gnome Dev Center
Titre
XDG Base Directory Specification
Auteurs
  • Waldo BASTIAN
  • Ryan LORTIE
  • Lennart POETTERING
Éditeur
freedesktop.org
Titre
GConf Reference Manual
Éditeur
Gnome Dev Center
Titre
Using GSettings with Python/PyGObject
Auteurs
  • Micah CARRICK
Éditeur
micahcarrick.com
Date
Titre
Extensions
chapter
Extension Preferences
Éditeur
Gnome Live
Date