Cinnamon applet

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

Structure des fichiers

Dans le dossier ~/.local/share/cinnamon/applets

metadata.json

Contient les informations de l'applet.
Le champs icon correspond à l'icone qui sera visible lors de l'ajout de l'applet.
La valeur du champs icon correspond au nom de l'icone qui est dans le thème de l'utilisateur courant.

metadata.json
{
    "uuid": "MyApp@AUTHOR",
    "name": "My app",
    "description": "My app is the best.",
    "icon": "",
    "version": 1.0
}

applet.js

Contient le code de l'applet.

applet.js

Import

Les fichiers javascript correspondant se trouvent dans

  • /usr/share/cinnamon/js (UI et MISC)
  • /usr/share/gjs-1.0 (cairo, dbus, format, gettext, jsUnit, lang, promise, signals)
Javascript.svg
// gettext sert à traduire les chaine de texte
const Gettext = imports.gettext.domain('cinnamon-applets');
const _ = Gettext.gettext;
const Lang = imports.lang;
const Mainloop = imports.mainloop;

// UI
const Applet = imports.ui.applet;
const PopupMenu = imports.ui.popupMenu;

// MISC
const Util = imports.misc.util;

// Gnome Introspection
const Cinnamon = imports.gi.Cinnamon;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Soup = imports.gi.Soup;
const St = imports.gi.St;

Constructeur

Javascript.svg
function MyApplet(metadata, orientation, panel_height, instanceId) {
    this._init(metadata, orientation, panel_height, instanceId);
}

The “orientation” is given to you by Cinnamon. It tells you whether the panel the applet is located in is at the top or at the bottom (this has an impact on the orientation of menus your applet might need).

Type d'applet

  • TextApplet (which show a label in the panel)
  • IconApplet (which show an icon in the panel)
  • TextIconApplet (which show both an icon and a label in the panel)
  • Applet (for hardcore developers, which show an empty box you can fill in yourself)

Corps de la classe

Javascript.svg
MyApplet.prototype = {
    // héritage de IconApplet (affiche une icone dans le panneau)
    __proto__: Applet.IconApplet.prototype,

    _init: function(metadata, orientation, panel_height, instanceId) {
        Applet.IconApplet.prototype._init.call(this, orientation);

        try {
            // définit l'icone à partir du nom d'une icone du thème
            this.set_applet_icon_name("Nom");
            // définit l'icone à partir d'un chemin
            this.set_applet_icon_path("chemin absolu");

            this.set_applet_tooltip("Texte");
            // texte avec traduction possible en utilisabt gettext
            this.set_applet_tooltip(_("Texte"));
            
            // définit le label pour les applets de type TextApplet et TextIconApplet
            this.set_applet_label("Label");
        }
        catch (e) {
            global.logError(e);
        };
    },

    on_applet_clicked: function(event)
    {
        // action à réaliser lors du clique sur l'icone
    }
}

Fonction main

Javascript.svg
function main(metadata, orientation, panel_height, instanceId)
{
    let myApplet = new MyApplet(metadata, orientation, panel_height, instanceId);
    return myApplet;
}

Menu contextuel

Javascript.svg
const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Applet = imports.ui.applet;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;

// Méthode dans le corps de l'applet, 
// ne pas oublier d'appeler la méthode dans _init
_createContextMenu: function () {    
    // Edit
    this.edit_menu_item = new Applet.MenuItem(_('Edit'), Gtk.STOCK_EDIT, function() {
        Main.Util.spawnCommandLine("xdg-open " + 
            GLib.build_filenamev([global.userdatadir, "applets/placescustom@nikus/applet.js"]));
    });
    this._applet_context_menu.addMenuItem(this.edit_menu_item); 

    // séparateur
    this._applet_context_menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
},

Icônes :

  • Gtk.STOCK_EDIT
  • Gtk.STOCK_HELP
  • Gtk.STOCK_ABOUT

GUI pour les settings

settings-schema.json
{
    "nom_du_setting1": {
        "type": "header",
        "description": "Titre"
    },
    
    "nom_du_setting2" : {
        "type" : "checkbox",
        "description" : "Description affichée à droite de la checkbox",
        "default" : true
    },
    
    "nom_du_setting3" : {
        "type" : "filechooser",
        "description" : "Description affichée à droite du filechooser",
        "default" : "chemin absolu",
        "select-dir" : false, /* interdit la selection de dossiers */
        "dependency" : "nom_du_setting2" /* ce setting n'est actif que si le setting nom_du_setting2 l'est également */
    }
}

Types:

  • header : titre en gras
  • checkbox
  • filechooser
Le type spinbutton rend les settings invalides
applet.js
const Settings = imports.ui.settings;

MyApplet.prototype = {
    ...
    _init: function(metadata, orientation, panel_height, instanceId) {
        ...
        this.settings = new Settings.AppletSettings(this, appletUUID, instanceId);

        this.settings.bindProperty(Settings.BindingDirection.IN,
            "nom_du_setting2", "setting2_value", this.on_setting2_changed, null);
    },

    on_setting2_changed: function() {       
        let temp = this.setting2_value;
        ...
    }
}

Settings

Javascript.svg
const Gio = imports.gi.Gio;
const AppletDirectory = imports.ui.appletManager.appletMeta["MonApplet"].path;
const SettingsFile = AppletDirectory + "/settings.json";

// watch settings file for changes
let file = Gio.file_new_for_path(SettingsFile);
this._monitor = file.monitor(Gio.FileMonitorFlags.NONE, null);
this._monitor.connect('changed', Lang.bind(this, this._on_settingsfile_changed));

_on_settingsfile_changed: function()
{
    this._readSettings();
},

// parse the settings.json file into the "settings" variable
_readSettings: function()
{
    let jsonFileContent = Cinnamon.get_file_contents_utf8_sync(SettingsFile);        
    this.settings = JSON.parse(jsonFileContent);
},

for (var i=0; i<this.settings.length; i++)
{
    var setting = this.settings[i];
    setting.key1;
    setting.key2;
}
settings.json
[
    { "key1":"value1", "key2":"value2" },
    { "key1":"value11", "key2":"value12" }
]

XML

Gjs utilise ECMAScript for XML (E4X)

Xml.svg
<person>
  <name attribut="test">Bob Smith</name>
  <likes>
    <os>Linux</os>
    <browser>Firefox</browser>
    <language>JavaScript</language>
    <language>Python</language>
  </likes>
</person>
Javascript.svg
var parser = new XML("code XML");
parser.name // Bob Smith
parser.likes.browser // Firefox
parser.name.@attribut // test

Opérations sur les fichiers

Tester si un fichier existe

Javascript.svg
const GLib = imports.gi.GLib;

if (GLib.file_test("chemin vers le fichier ou le dossier", GLib.FileTest.EXISTS))
{ }

Lire le contenu d'un fichier texte

Javascript.svg
const Cinnamon = imports.gi.Cinnamon;

let fileContent = Cinnamon.get_file_contents_utf8_sync("chemin vers le fichier");

Écrire dans un fichier texte

Javascript.svg
const Cinnamon = imports.gi.Cinnamon;
const Gio = imports.gi.Gio;

let file = Gio.file_new_for_path("chemin vers un fichier");
let raw = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
let out = Gio.BufferedOutputStream.new_sized (raw, 4096);
Cinnamon.write_string_to_stream(out, "texte à écrire");
out.close(null);

Traduction

Javascript.svg
const Gettext = imports.gettext;

// pour le fichier /usr/share/cinnamon/locale/fr/LC_MESSAGES/cinnamon.mo
Gettext.bindtextdomain("cinnamon", "/usr/share/cinnamon/locale");
Gettext.textdomain("cinnamon");

// pour le fichier /usr/share/locale/fr/LC_MESSAGES/gnome-applets-3.0.mo
Gettext.bindtextdomain("gnome-applets-3.0", "/usr/share/locale");
Gettext.textdomain("gnome-applets-3.0");

const _ = Gettext.gettext;

print(_("Panel")); // Tableau de bord

Pour visualiser les textes traduits : il faut décompiler le fichier mo en fichier po

Bash.svg
msgunfmt /usr/share/cinnamon/locale/fr/LC_MESSAGES/cinnamon.mo -o cinnamon-fr.po

Charger un fichier Glade

GtkWindow

Javascript.svg
const Gtk = imports.gi.Gtk;

// Initialize the gtk
Gtk.init(null, 0);

// Create builder and load interface
let builder = new Gtk.Builder();
builder.add_from_file( "interface.glade" );

// lier l’événement destroy de la fenêtre principale à la fermeture de la fenêtre
let main_window = builder.get_object("window");
main_window.connect( "destroy", function() { Gtk.main_quit(); } );

// lier l’événement appuie sur la touche entrée pendant la saisie dans la TextBox à une méthode
let entry = builder.get_object("entry");
entry.connect("activate", function() { 
    let text = entry.text; // le texte saisie dans la TextBox
});

// lier l’événement click du bouton à une méthode
let button = builder.get_object("button");
button.connect("clicked", on_button_clicked);
function on_button_clicked() {
    ...
}

Gtk.main();
window est un mot-clé.
Il ne doit pas être utilisé comme nom de variable.

GtkDialog

Javascript.svg
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

function PasswordDialog() {
    this.password = "";
    this.init();
}

PasswordDialog.prototype = {
    init: function() {
        // Initialize the gtk
        Gtk.init(null, 0);
        
        // Create builder and load interface
        this.builder = new Gtk.Builder();
        this.builder.add_from_file( "PasswordDialog.glade" );
        
        this._main_window = this.builder.get_object("dialog");
        
        this._entry = this.builder.get_object("entry");
        this._entry.connect("activate", Lang.bind(this, this.validate_on_enter));

        let button_ok = this.builder.get_object("button_ok");
        button_ok.connect("clicked", Lang.bind(this, this.validate_password));
        
        this._main_window.run();
    },
    
    validate_on_enter: function() {
        this.validate_password();
        this._main_window.response(0);
    },
    
    validate_password: function() {
        this.password = this._entry.text;
    },
    
    destroy: function() {
        this._main_window.destroy();
    }
}
Javascript.svg
// code appelant
imports.searchPath.unshift('.');
const PasswordDialog = imports.PasswordDialog;

let password_dialog = new PasswordDialog.PasswordDialog();
print("password: " + password_dialog.password);

password_dialog.destroy();

GObject Introspection

Permet d'exploiter les bibliothèques C (fichiers *.so) depuis son code préféré.

  • /usr/lib/gjs-1.0 contient les fichiers *.so
  • /usr/lib/gjs/girepository-1.0 contient les fichiers *.typelib

Vala

Exemple d'utilisation d'une bibliothèque écrite en Vala et importée dans du code Gjs grâce au système d'introspection.

Bash.svg
# génération des fichiers gir, so et vapi à partir du code VALA
valac --enable-experimental -X -fPIC -X -shared --library=vala-object --gir=ValaObject-0.1.gir \
-o vala-object.so object.vala

# génération du fichier typelib à partir des fichier so et gir
g-ir-compiler --shared-library=vala-object.so --output=ValaObject-0.1.typelib ValaObject-0.1.gir
ValaBinding.js
var ValaObject = imports.gi.ValaObject;

ValaObject.StaticMethod()

var instance = new ValaObject.Class()
instance.Method()
Bash.svg
# path to vala-object.so
export LD_LIBRARY_PATH=.
# path to ValaObject-0.1.typelib
export GI_TYPELIB_PATH=.

gjs ValaBinding.js

Astuces

Chemin du répertoire de l'applet

Javascript.svg
const AppletDirectory = imports.ui.appletManager.appletMeta["nom de l'applet"].path;

Éxecuter une commande bash et récupérer le résultat

Javascript.svg
const GLib = imports.gi.GLib;
const Main = imports.ui.main;
const Util = imports.misc.util;

let [res, out, err, status] = GLib.spawn_command_line_sync("cat applet.cfg");

// Autres méthodes :
Main.Util.spawnCommandLine("commande");
Util.spawnCommandLine("commande");

Écrire dans le fichier de log

~/.xsession-errors remplace ~/.cinnamon/glass.log
Javascript.svg
// error
global.logError("Texte");

// debug
global.log("Texte");
Bash.svg
# afficher le contenu du log
tail -f ~/.xsession-errors
Une fois le code modifié, il faut relancer cinnamon pour qu'il soit pris en compte:
clique-droit dans la barre des tâches → Troubleshoot → Restart Cinnamon.

Ne semble plus nécessaire:
Pour activer l'écriture dans le fichier ~/.cinnamon/glass.log :
Paramètres de Cinnamon → Général → Logger ...

Parser un fichier JSON

Javascript.svg
const Cinnamon = imports.gi.Cinnamon;

let jsonFileContent = Cinnamon.get_file_contents_utf8_sync("chemin vers le fichier JSON");
let json = JSON.parse(jsonFileContent);

Applications

Les applications installées ont un fichier *.desktop dans le répertoire /usr/share/applications ou ~/.local/share/applications.
Il est possible d'exploiter ce fichier pour obtenir des informations sur l'application tels que :

  • le nom de l'application
  • le nom de l'icone associée à l'application
  • la commande qui permet de lancer l'application
Javascript.svg
const Cinnamon = imports.gi.Cinnamon;
const Gio = imports.gi.Gio;

let appSys = Cinnamon.AppSystem.get_default();
let desktopFile = "firefox.desktop";
let app = appSys.lookup_app(desktopFile);
if (!app)
{
    // pour l'application cinnamon-settings par exemple
    app = appSys.lookup_settings_app(desktopFile);
}

if (app)
{
    let appInfo = app.get_app_info();
    let command = appInfo.get_commandline();
    let appName = appInfo.get_name();
    let icon = appInfo.get_icon();
    let iconName = null;
    if (icon)
    {
        if (icon instanceof Gio.FileIcon)
        {
            iconName = icon.get_file().get_path();
        }
        else
        {
            iconName = icon.get_names().toString();
        }
    }
}

Icone dans le menu d'ajout des applets

metadata.json
    "icon": "nom de l'image du thème courant sans l'extension"

Ou si icon n'est pas défini, le fichier icon.png.

Charger d'autres fichiers Javascript

Javascript.svg
// pour un fichier se trouvant dans le même répertoire
// on ajoute le répertoire courant aux chemins de recherche pour les imports
imports.searchPath.push('.');
imports.searchPath.unshift('.');

// puis on importe le fichier javascript
const Add = imports.NomFichierJavascript;

Notification

Javascript.svg
const Util = imports.misc.util;

Util.spawnCommandLine("notify-send --icon=mail-read \"titre\" \"message\"");

Les icônes sont recherchées dans /usr/share/icons/gnome/32x32 et /usr/share/notify-osd/icons

Portée de this

this est un mot-clé et non une variable. Ainsi il ne peut être utilisé dans une méthode anonyme.
La solution est d'utiliser Lang.bind

Javascript.svg
const Lang = imports.lang;

MyPrototype = {
    ma_methode: function() {
        objet.connect('event', Lang.bind(this, this.on_event));
    },
    on_event: function() { ... },

    ma_methode: function() {
        objet.connect('event', Lang.bind(this, function() { ... }));
    },
};

PopupMenu

Code source: /usr/share/cinnamon/js/ui/popupMenu.js

Javascript.svg
// Manage theme icons and image files
function CreateIcon(iconName) {
    // if the iconName is a path to an icon
    if (iconName[0] === '/') {
        var file = Gio.file_new_for_path(iconName);
        var iconFile = new Gio.FileIcon({ file: file });

        return new St.Icon({ gicon: iconFile, icon_size: 24, style_class: 'popup-menu-icon' });
    }
    else // use a themed icon
        return new St.Icon({ icon_name: iconName, icon_size: 24, icon_type: St.IconType.FULLCOLOR, style_class: 'popup-menu-icon' });
}

/**********  PopupIconSubMenuMenuItem  **********/
// Inherits from PopupSubMenuMenuItem to add a colored image to the left side
function PopupIconSubMenuMenuItem() {
    this._init.apply(this, arguments);
}
PopupIconSubMenuMenuItem.prototype = {
    __proto__: PopupMenu.PopupSubMenuMenuItem.prototype,

    _init: function(text, iconName) {
        PopupMenu.PopupSubMenuMenuItem.prototype._init.call(this, text);

        // remove previous added actor in PopupSubMenuMenuItem _init
        // because the add order is important
        this.removeActor(this.label);
        this.removeActor(this._triangleBin);

        this._icon = CreateIcon(iconName);
        this.addActor(this._icon);

        this.addActor(this.label);
        this.addActor(this._triangleBin);
    }
};

GLib

Enum GLib.UserDirectory

GLib.UserDirectory.DIRECTORY_DESKTOP : 0
GLib.UserDirectory.DIRECTORY_DOCUMENTS : 1
GLib.UserDirectory.DIRECTORY_DOWNLOAD : 2
GLib.UserDirectory.DIRECTORY_MUSIC : 3
GLib.UserDirectory.DIRECTORY_PICTURES : 4
GLib.UserDirectory.DIRECTORY_PUBLIC_SHARE : 5
GLib.UserDirectory.DIRECTORY_TEMPLATES : 6
GLib.UserDirectory.DIRECTORY_VIDEOS : 7
GLib.UserDirectory.N_DIRECTORIES : 8
Javascript.svg
const GLib = imports.gi.GLib;

for (let directoryId = 0; directoryId < GLib.UserDirectory.N_DIRECTORIES; directoryId++) {
    let directoryPath = GLib.get_user_special_dir(directoryId);
}

Gio

GFile

Javascript.svg
const Gio = imports.gi.Gio;

var gFile = Gio.file_new_for_path("chemin absolu vers un fichier");

if(gFile.query_exists(null)) {
    // test si le fichier existe
}

GFileIcon

Javascript.svg
const Gio = imports.gi.Gio;

var gFile = Gio.file_new_for_path("chemin absolu vers un fichier");
var gFileIcon = new Gio.FileIcon({ file: gFile });

Gtk

JavaScript GTK tutorial

IconTheme

Javascript.svg
const Gtk = imports.gi.Gtk;

// récupère le thème courant
let icon_theme = Gtk.IconTheme.get_default();

// test si l’icône mail-receive existe dans le thème courant
if (icon_theme.has_icon("mail-receive"))
    ...

Stock Items

Liste des icônes
Exemples :

  • Gtk.STOCK_EDIT
  • Gtk.STOCK_HELP
  • Gtk.STOCK_ABOUT

St

StIcon

Javascript.svg
const St = imports.gi.St;
const Gio = imports.gi.Gio;

// Création d'une icone à partir d'une image du thème actuel
// ici « user-home.png » (répertoire du thème par défaut: « /usr/share/icons/gnome »)
var icon = new St.Icon({ icon_name: "user-home", icon_size: 22, icon_type: St.IconType.FULLCOLOR });

// Création d'une icone à partir d'un fichier image
var file = Gio.file_new_for_path("chemin absolu vers un fichier image");
var iconFile = new Gio.FileIcon({ file: file });
var icon = new St.Icon({ gicon: iconFile, icon_size: 24 });

icon_type: par défaut il est à St.IconType.SYMBOLIC et va chercher les icones avec le préfixe *-symbolic.* dans le dossier scalable du thème actuel (par défaut: /usr/share/icons/gnome/scalable)
L'utilisation de St.IconType.FULLCOLOR permet d'avoir les icones classiques.

Goa

Javascript.svg
const Goa = imports.gi.Goa;

let goaClient = Goa.Client.new_sync(null);
let accounts = goaClient.get_accounts();

for (let i = 0; i < accounts.length; i++) {
    accounts[i].get_account().provider_name // GOOGLE
    accounts[i].get_account().id // account_1234567890
    accounts[i].get_account().identity // user@gmail.com
    accounts[i].get_account().presentation_identity // user@gmail.com
    accounts[i].get_mail().email_address // user@gmail.com
    accounts[i].get_oauth_based().consumer_secret // anonymous
    accounts[i].get_oauth2_based() // null
}

Clutter

Création de fenêtres.

Javascript.svg
const Clutter = imports.gi.Clutter;

Clutter.init (null);
let stage = Clutter.Stage.get_default ();
stage.title = "Test";
stage.set_color(new Clutter.Color( {red:150, blue:0, green:0, alpha:255} ));
stage.show ();
Clutter.main ();
stage.destroy ();

GnomeKeyring

Javascript.svg
// lister toutes les entrées
const GnomeKeyring = imports.gi.GnomeKeyring;

var keyrings = GnomeKeyring.list_keyring_names_sync();

for (var i = 0; i < keyrings[1].length; i++) {
    var keyring = keyrings[1][i];
    
    var ids = GnomeKeyring.list_item_ids_sync(keyring);
    for (var j = 0; j < ids[1].length; j++) {
        var id = ids[1][j];
        
        var item = GnomeKeyring.item_get_info_sync(keyring, id);
        print(item[1].get_display_name() + " = " + item[1].get_secret());
    }
}

Les méthodes list_keyring_names_sync, list_item_ids_sync et item_get_info_sync renvoient un tableau de 2 éléments :

  • le premier est un entier correspondant à l'Enum GnomeKeyring.Result (0:OK , 5:BAD_ARGUMENTS)
  • le deuxième est un objet contenant le retour de la méthode

Secret library

Javascript.svg
const Secret = imports.gi.Secret;

const EXAMPLE_SCHEMA = new Secret.Schema(
    "org.gnome.Application.Password",
    Secret.SchemaFlags.NONE,
    {
        "string": Secret.SchemaAttributeType.STRING,
        "integer": Secret.SchemaAttributeType.INTEGER,
        "bool": Secret.SchemaAttributeType.BOOLEAN,
    }
);

// Storage
var attributes = {
    "string": "test",
    "integer": 10,
    "bool": true
};

Secret.password_store_sync(
    EXAMPLE_SCHEMA, 
    attributes, 
    Secret.COLLECTION_DEFAULT,
    "Label-Description", 
    "le mot de passe", 
    null);

// Lookup
var password = Secret.password_lookup_sync(
    EXAMPLE_SCHEMA, { "string": "test", "integer": 10, "bool": true }, null);

Secret.SchemaFlags

  • NONE no flags for the schema
  • DONT_MATCH_NAME don't match the schema name when looking up or removing passwords
libgnome-keyring n'enregistre pas les noms de schema.
Il faut doc utiliser DONT_MATCH_NAME pour récupérer les mots de passes stockés par gnome-keyring.

SecretSchema

Soup

Errors

Main.Util is undefined

JS ERROR: Exception in callback for signal: activate: TypeError: Main.Util is undefined
Js.svg
//const Main = imports.ui.main;
const Util = imports.misc.util;

// replace Main.Util.spawnCommandLine("...") by
Util.spawnCommandLine("...");