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.
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)
|
// 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
|
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
|
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
|
function main(metadata, orientation, panel_height, instanceId)
{
let myApplet = new MyApplet(metadata, orientation, panel_height, instanceId);
return myApplet;
}
|
|
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
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
|
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)
|
<person>
<name attribut="test">Bob Smith</name>
<likes>
<os>Linux</os>
<browser>Firefox</browser>
<language>JavaScript</language>
<language>Python</language>
</likes>
</person>
|
|
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
|
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
|
const Cinnamon = imports.gi.Cinnamon;
let fileContent = Cinnamon.get_file_contents_utf8_sync("chemin vers le fichier");
|
Écrire dans un fichier texte
|
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
|
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
|
msgunfmt /usr/share/cinnamon/locale/fr/LC_MESSAGES/cinnamon.mo -o cinnamon-fr.po
|
Charger un fichier Glade
GtkWindow
|
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
|
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();
}
}
|
|
// 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
Exemple d'utilisation d'une bibliothèque écrite en Vala et importée dans du code Gjs grâce au système d'introspection.
|
# 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()
|
|
# 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
|
const AppletDirectory = imports.ui.appletManager.appletMeta["nom de l'applet"].path;
|
Éxecuter une commande bash et récupérer le résultat
|
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 |
|
// error
global.logError("Texte");
// debug
global.log("Texte");
|
|
# 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
|
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
|
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();
}
}
}
|
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
|
// 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
|
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
|
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() { ... }));
},
};
|
Code source: /usr/share/cinnamon/js/ui/popupMenu.js
|
// 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);
}
};
|
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
|
const GLib = imports.gi.GLib;
for (let directoryId = 0; directoryId < GLib.UserDirectory.N_DIRECTORIES; directoryId++) {
let directoryPath = GLib.get_user_special_dir(directoryId);
}
|
|
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
}
|
|
const Gio = imports.gi.Gio;
var gFile = Gio.file_new_for_path("chemin absolu vers un fichier");
var gFileIcon = new Gio.FileIcon({ file: gFile });
|
JavaScript GTK tutorial
|
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"))
...
|
Liste des icônes
Exemples :
- Gtk.STOCK_EDIT
- Gtk.STOCK_HELP
- Gtk.STOCK_ABOUT
|
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.
|
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.
|
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 ();
|
|
// 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
|
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);
|
- 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
Errors
JS ERROR: Exception in callback for signal: activate: TypeError: Main.Util is undefined
|
//const Main = imports.ui.main;
const Util = imports.misc.util;
// replace Main.Util.spawnCommandLine("...") by
Util.spawnCommandLine("...");
|