This site has been retired. For up to date information, see handbook.gnome.org or gitlab.gnome.org.


[Home] [TitleIndex] [WordIndex

Creating an Applet

The best way to create an applet is to simply extend the PanelMenu.Button class. It provides you with a button and a menu. You can add it anywhere on your top panel.

Code Sample

   1 const Main = imports.ui.main;
   2 
   3 const PanelMenu = imports.ui.panelMenu;
   4 const PopupMenu = imports.ui.popupMenu;
   5 
   6 const St = imports.gi.St;
   7 
   8 const Lang = imports.lang;
   9 
  10 
  11 const HelloWorld_Indicator = new Lang.Class({
  12     Name: 'HelloWorld.indicator',
  13     Extends: PanelMenu.Button   ,
  14 
  15        _init: function(){
  16            this.parent(0.0);
  17 
  18            let label = new St.Label({text: 'Button'});
  19            this.actor.add_child(label);
  20 
  21            let menuItem = new PopupMenu.PopupMenuItem('Menu Item');
  22            menuItem.actor.connect('button-press-event', function(){ Main.notify('Example Notification', 'Hello World !') });
  23 
  24            this.menu.addMenuItem(menuItem);
  25        }
  26  });
  27 
  28 
  29 function init() {
  30     log ('Example extension initalized');
  31 };
  32 
  33 function enable() {
  34      log ('Example extension enabled');
  35 
  36      let _indicator =  new HelloWorld_Indicator();
  37      Main.panel._addToPanelBox('HelloWorld', _indicator, 1, Main.panel._rightBox);
  38 };
  39 
  40 function disable(){
  41      log ('Example extension disabled');
  42 
  43      _indicator.destroy();
  44 };

Imports

A button

Labels and Icons

this.actor is a Shell.GenericContainer() inherited from PanelMenu.ButtonBox, into wich you can add a single St element, like a St.Label(), an St.Icon() or St.Entry().

To compose a button with two or more St elements, you will have to create a St container, add to it your elements and finally add the container to this.actor like so :

        // Create a St container
          let box = new St.BoxLayout();

        // Create your UI elements
          this.Label = new St.Label({text: "Hello World"});
          let Icon = new St.Icon({ icon_name: 'system-run-symbolic',
                                  style_class: 'system-status-icon' });

       // Add your elements to the container
          box.add_actor(this.Label)
          box.add_actor(Icon)

       // Than add the container to the actor
          this.actor.add_child(box);

Events

this.actor is, as well, a clutter actor we can connect or disconnect to events and to a callback, a function being called when the event occurs.

    this.actor.connect("event-name", callback);
    this.actor.disconnect("other-event")

Most of the UI containers that we will study here have a clutter actor and by default, each clutter actors have these events:

A Menu

this.menu : the PanelMenu.Button class adds a PopupDummyMenu to Panel.ButtonBox. Essentialy, a menu constuctor to wich we can add menu items :

    this.menu.addMenuItem(menuItem);

Here are most of the menu items available :

        let baseMenuItem = new PopupMenu.PopupBaseMenuItem( +parameters )

        let menuItem = new PopupMenu.PopupMenuItem("hello world");

You can change it's text directly via :

        menuItem.label.set_label("new hello world") ;

        let menuItem = new PopupMenu.PopupSeperatorMenuItem();

        let switchItem = new PopupMenu.PopupSwitchMenuItem("hello world");
        switchItem.connect("toggled", Lang.bind(this, this._doSomething);

        let subMenuItem = new PopupMenu.PopupSubMenuItem("hello world");
        subMenuItem.menu.addMenuItem(menuItem)

        const Slider = imports.ui.Slider
        let sliderItem = new Slider()

Miscelaneous

Adding your Applet to the Top panel

Adding Functionalities

File manipulations

Import :

    const Gio = imports.gi.Gio

Read a File :

    _readFile: function() {
         // Create an object reference of your file
            let file = Gio.file_new_for_path(“path to your file”);
         // Loading the contents of the file
            file.load_contents_async(null, function(file, res) {
                    let contents;
                    try { contents = file.load_contents_finish(res)[1];
                          // Do something with the contents of the file
                             print(contents);
                    } catch (error)
                            {print(error)};
                    });
     }

Write to a File :

    _writeFile: function(){
                try {
                   // Create an object reference of your file
                        var file = Gio.file_new_for_path (“path to your file”);

                   // Create an append stream
                        let file_stream = file.append_to(Gio.FileCreateFlags.NONE, null, null);

                   // Write text data to stream
                        let text = "hello world"
                        file_stream.write(text, null);
                        } catch(error) { print(error) };
    }

!! As this is not an async method, you will block the Shell if the data is too big. !!

Updating a Label

Or anything else for that matter.

Import :

        const Mainloop = imports.mainloop;

Update method :

        _UpdateLabel : function(){
                let refreshTime = 10 // in seconds
                if (this._timeout) {
                        Mainloop.source_remove(this._timeout);
                        this._timeout = null;
                }
                this._timeout = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._UpdateLabel));

                this._doSomethingThatUpdatesTheLabelsText();
        };

You will have to destroy the timeout when the extension is disabled

        function disable(){
                Mainloop.source_remove(timeout);
                Mainloop.source_remove(timeout2);
        };

HTTP requests

Import libsoup, the library to work with HTTP requests and create a session variable

        const Soup = imports.gi.Soup;
        let _httpSession;

Initialise your session in your init function.

        function init() {
                _httpSession = new Soup.SessionAsync({ssl_use_system_ca_file: true});
                Soup.Session.prototype.add_feature.call(_httpSession, new Soup.ProxyResolverDefault());
        };

Get request :

        _getRequest: function () {
                // Set the URL and parameters. If you don't need parameters, delete the variable everywhere.
                   let URL = “yourUrl”;
                   let params = “yourParameters”;

                // Create your message
                   _httpSession = new Soup.Session();
                   let message = Soup.form_request_new_from_hash('GET', URL, params);

                // Send the message and retrieve the data
                   _httpSession.queue_message(message, Lang.bind(this, function (_httpSession, message) {
                                if (message.status_code !== 200) {
                                        return;
                                };
                                let json = JSON.parse(message.response_body.data);
                                // Do something with your JSON data
                                print(json)
                   }));
        };

You want to post something ?

        _postRequest(){
                let url = "yourUrl";
                var params = “parameters” ;

                let message = Soup.Message.new('POST', url);

                message.set_request ('application/json', 2, params, params.length);
                _httpSession.queue_message(msg, function(_httpSession, message){// do something when done});
        }

Executing Shell commands and Scipts:

Import :

        const GLib = imports.gi.GLib;

Than :

        let output = GLib.spawn_command_line_async('ls', “-l”);

Easy, right ? Well hold your horses; I haven't been able to spawn commands preceded with sudo directly. To work around this is stupidly hard :

Edit your sudoer file to disable the password lock for this specific command :

        sudo visudo

Add at the end of the file :

        username ALL=(ALL) NOPASSWD: arp-scan

Create a bash script in your home directory (or where root permission is not needed) containing for example :

        #!/bin/sh sudo arp-scan --interface=wlan0 --localnet

Enable execute permissions with chmod.

Spawn the command via it's path :

        let output = GLib.spawn_command_line_async('/home/fulleco/Documents/script.sh')

Profit !

Custom Icons

A metadata is created when metadata.js is read at initialisation.

        function init(metadata) {
                // Loads the default icons
                   let theme = imports.gi.Gtk.IconTheme.get_default();
                // Append your extension's directory + /icons theme.append_search_path(extensionMeta.path + "/icons"); }

Then just add an "icons" folder inside your project root, save a 16x16 .svg icon inside it and referenced it by its file name :

        this.Icon = new St.Icon({ icon_name : “name of your icon without .svg” style_class : “system-status-icon” });

Miscelaneous

Translate an extension

First, any strings you want to translate have to use double quotes and be enclosed in _(), e.g. _("translatable string") as opposed to 'not translatable string'.

In your extension.js, include the following:

const Gettext = imports.gettext;

Gettext.textdomain("my-extension");
Gettext.bindtextdomain("my-extension", ExtensionSystem.extensionMeta["my-extension"].path + "/locale");

const _ = Gettext.gettext;

Where my-extension is the uuid in metadata.json.

Next, make sure you have gettext installed and extract the translatable string using the following command:

xgettext -k_ -kN_ -o messages.pot extension.js

Note: there is no Javascript syntax parser in gettext. You will get a warning that it will fallback to C. In certain circumstances, the default C parser is not able to properly detect the strings of your file. In theses cases, you can try to use the Perl parser which is sometimes more adapted to the Javascript syntax:

xgettext -L Perl --from-code=UTF-8 -k_ -kN_ -o messages.pot extension.js

In order to start a new translation, invoke msginit. This will create a new translation in the language of your current locale, or you can use:

LANG=sv_SE msginit

to create a new translation in e.g. swedish.

This will create a .po file. Edit it to add your translation.

Now add a directory for the translation in your extensions directory, e.g. my-extension/. In our example with a swedish translation:

mkdir -p locale/sv/LC_MESSAGES

And finally create a machine readable copy of the translation using:

msgfmt sv.po -o locale/sv/LC_MESSAGES/my-extension.mo

If you want to update an existing translation, use:

msgmerge -U sv.po messages.pot

Multi-file extensions

If your extension is not trivial, you'll want to split it in multiple .js files. To import them from your extension's source code, do this:

/* This identifier string comes from your installation directory */
const Extension = imports.misc.extensionUtils.getCurrentExtension();
const MyModule = Extension.imports.myModule;

This will import myModule.js and you can use it as MyModule.

Extension Preferences

Since version 3.3.5, Extensions may be configured in a consistent manner using the gnome-shell-extension-prefs tool. To hook into it, provide a simple javascript file called prefs.js. It must contain a function labeled buildPrefsWidget, which should return some sort of GTK+ widget. Whatever is returned from there will be inserted into the Preferences widget screen. Beyond that, a function named init may also be provided, and will be invoked after the file is loaded but before buildPrefsWidget is invoked.


2024-10-23 11:37