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:

  • "button-press-event": Emitted when the user presses the mouse over the actor.
  • "button-release-event": Emitted when the user releases the mouse over the actor.
  • "motion-event": Emitted when the user moves the mouse over the actor.
  • "enter-event": Emitted when the user moves the mouse in to the actor's area.
  • "leave-event": Emitted when the user moves the mouse out of the actor's area.

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 :

  • PopupBaseMenuItem: Base class for popup menu items - an empty popup menu item. Has an 'activate' signal that gets fired when the item is activated. Use .addActor to add visual elements to it (like a St.Label). All the other PopupMenuItem classes inherit from this (and respond to the 'activate' signal).

        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();
  • PopupSwitchMenuItem: A PopupBaseMenuItem containing a label/text and a Switch to the right. Example: Wifi/wired connection on/off switch in the network menu. You can access the switch directly at PopupMenu.Switch(). Use the 'toggled' event name to determine when the user toggles it.

        let switchItem = new PopupMenu.PopupSwitchMenuItem("hello world");
        switchItem.connect("toggled", Lang.bind(this, this._doSomething);
  • PopupSubMenuMenuItem: A PopupBaseMenuItem defining a collapsible submenu - click on it to expand/open the submenu and reveal the items inside. You can add any menu items to it the same way you add items to your indicator's DummyMenu.

        let subMenuItem = new PopupMenu.PopupSubMenuItem("hello world");
        subMenuItem.menu.addMenuItem(menuItem)
  • PopupImageMenuItem: A PopupBaseMenuItem containing a label/text and an icon to the right. Example: in the network menu, available wifi networks have the network name and an icon indicating connection strength.

  • PopupComboBoxMenuItem: A PopupBaseMenuItem that is a combo box. Example: the chat status combo box (available/offline) in the user menu. Use myItem.addMenuItem(item) to add a choice to the combo box, and myItem.setActiveItem to set the active item. When a selection is changed, it emits signal active-item-changed with the (0-based) index of the activated item.

  • PopupAlternatingMenuItem: A PopupBaseMenuItem that has two texts - one that shows by default, and one that appears upon pressing Clutter.ModifierType.MOD1_MASK (Alt for me). The alternating 'Suspend...'/'Power off' menu item in the user menu is one of these.

  • Slider : Pretty self explanatory, you will have to import it's class first and than get it's "value-change" signal :

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

Miscelaneous

  • PopupMenuManager: "Basic implementation of a menu manager. Call addMenu to add menus". I think you use this if you want to manage multiple menus (???). For example, all the status area menus and the date menu appear to have the same manager (Main.panel._menus). It also looks like it handles the menus grabbing focus, responding to key events, etc.

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.

Projects/GnomeShell/Extensions/EcoDoc/Applet (last edited 2017-05-06 05:14:27 by Fulleco)