Thursday 29 November 2012

Sending notifications in GNOME shell

This is a quick tutorial on how to send notifications in gnome shell from an extension/javascript (you can also send them over DBus but that's for another day).

Recipe

For those of you who can't wait, here is the quick way to pop up a notification:

Main.notify('notification title', 'notification summary');

The above function Main.notify does a number of things. I'll explain them in the next section:

Longer recipe

To make notifications in gnome shell, you essentially create a Source (a source of notifications) and add it to the message tray, and then make a Notification and notify it from the source.

The basic steps shown below demonstrate this, although in practice one may wish to use various subclasses of the Source or Notification classes.

  1. Create a subclass of MessageTray.Source (in GNOME 3.2 or 3.4), or a MessageTray.Source in GNOME 3.6:

    // example in GNOME 3.2 and 3.4.
    // SystemNotificationSource subclasses MessageTray.Source
    let source = new MessageTray.SystemNotificationSource();
    // example in GNOME 3.6 - we can use MessageTray.Source() directly
    let source = new MessageTray.Source('source title', 'source-icon-name');
    
  2. Create a Notification, passing in the source, title, and summary:

    let notification = new MessageTray.Notification(source,
                                                    'notification title',
                                                    'notification summary');
    
  3. Add the source to the message tray. This is so that the icon appears in the message tray (and no notification will pop up unless its source is added):

    Main.messageTray.add(source);
    
  4. Either notify the notification from the source, or simply add it (without notifying):

    // adds the notification to the source and pops it up
    source.notify(notification);
    
    // OR:
    
    // just adds the notification to the source (doesn't pop it up)
    source.pushNotification(notification);
    
The example we will work through in this post

Explanation

Here I'll explain the two main classes: a Source and a Notification.

Source

Think of a Source as a source of notifications - many notifications can come from the same source (for example many email notifications coming from your one email client). In fact, all notifications require a source.

You can also think of a Source as the icon in the message tray (that you click on to show its notifications).

The basic Source class is MessageTray.Source. To be used, it needs to know what icon to show in the Message Tray.

The Source provides the icon in the message tray. 

GNOME 3.6

In GNOME 3.6, the way to do this is to feed the icon's name into MessageTray.Source's constructor (along with the title of the source):

let source = new MessageTray.Source("Source title", 'avatar-default');

Alternatively if you wanted something more fancy, you could override Source.createIcon(size) in a subclass to return an icon of the appropriate size for the source.

GNOME 3.2, 3.4

In GNOME 3.2 and 3.4, you tell the Source what icon it should show in the message tray with one of two methods:

a. when you create a notification for the source, set the icon explicitly in the notification, OR b. subclass it, implement the createNotificationIcon() function and call _setSummaryIcon with it in the constructor.

Method b. is the one most commonly used, but to do method a) you feed in an object as the fourth parameter to MessageTray.Notification with an 'icon' property (explained a bit more in the Notification section):

let source = new MessageTray.Source("source title");
let notification = new MessageTray.Notification(source,
                                                "notification title",
                                                "notification message",
                                                {icon: new St.Icon({
                                                    icon_name: 'avatar-default',
                                                    icon_size: source.ICON_SIZE})
                                                });

// add the source to the message tray and pop up the notification
Main.messageTray.add(source);
source.notify(notification);

The bare minimum implementation of a subclass of MessageTray.Source (to demonstrate method b.) is:

const MySource = new Lang.Class({
    Name: 'MySource',
    Extends: MessageTray.Source,
    _init: function (title) {
        // call the parent constructor
        this.parent(title); 
        // set our icon
        this._setSummaryIcon(this.createNotificationIcon());
    },

    // this should return an icon to show in the message tray. You can use
    // this.ICON_SIZE for a default icon size if you want.
    createNotificationIcon: function () {
        return new St.Icon({
            icon_name: 'avatar-default',
            icon_size: this.ICON_SIZE
        });
    }
}); 

One would use it like so:

let source = new MySource("source title");
let notification = new MessageTray.Notification(source, "title", "message");

// add the source to the message tray and pop up the notification
Main.messageTray.add(source);
source.notify(notification);

Notification

Recall that every notification must belong to a source. Once a notification is added to its source, it can also be "notified".

When a notification is "notified", it pops up from the bottom of the screen showing the user the notification's title and the first line of its details (if they fit).

A notification upon being notified (above); the same notification being opened from its source (below). 

Hovering the mouse over the notification will expand it, showing the full summary of the notification.

Actually clicking on the notification will usually dismiss it (unless you override this behaviour).

After a while the notification will time out, dropping back down off the screen.

After this happens there are a couple of options:

  • if the notification is set as "transient", it will disappear forever. Make a notification transient with notification.setTransient(true).
  • otherwise (the default), it will now reside in the message tray. Clicking on the notification's icon in the message tray will show the notification, from which it may be dismissed (by clicking on it).
  • if you want a notification to never be able to be dismissed (by clicking on it after clicking on its source in the message tray), you can set the notification to be "resident". Do this with notification.setResident(true).

The base notification class is MessageTray.Notification, and it allows the construction of very versatile notifications. At its most basic, the notification has a text title and a text summary:

let notification = new MessageTray.Notification(source,
                                                'notification title',
                                                'notification message');

One can also feed in an (optional) parameters object giving the notification's icon. One typically uses an icon size of source.ICON_SIZE (24 pixels) to create the icon:

// source is whatever Source this notification belongs to
let notification = new MessageTray.Notification(
    source,
    'notification title',
    'notification message', {
    icon: new St.Icon({
        icon_name: 'avatar-default',
        icon_size: source.ICON_SIZE
    })
});

However, it is also possible to construct much more complex notifications. One can add buttons to the notification with notification.addButton, add extra text labels to it with notification.addBody, or extra actors (entry boxes, pictures, ...) to it with notification.addActor.

For an example of a notification with buttons on it, see the Telepathy Client AudieoVideoNotification (in telepathyClient.js), which has two buttons for the user to accept or reject a call.

AudioVideoNotification has had buttons added

For an example of a very complex notification, see the TelepathyClient.ChatNotification in telepathyClient.js. This adds chat texts to a scrollable area, and adds an entry box for typing chat messages.

ChatNotifiation is a very complex custom notification

Bringing it all together

Code snippet - notifying a notification with the 'avatar-default' icon. This will create the notification shown below.

GNOME 3.2 and 3.4

Where we give the source icon via the notification:

// 1. Make a source
let source = new MessageTray.Source("source title");
// 2. Make a notification
let notification = new MessageTray.Notification(source,
                                                "notification title",
                                                "notification message",
                                                {icon: new St.Icon({
                                                    icon_name: 'avatar-default',
                                                    icon_size: source.ICON_SIZE
                                                })});
// 3. Add the source to the message tray
Main.messageTray.add(source);
// 4. notify!
source.notify(notification);

Where we create our own source subclass:

const MySource = new Lang.Class({
    Name: 'MySource',
    Extends: MessageTray.Source,
    _init: function (title) {
        // call the parent constructor
        this.parent(title); 
        // set our icon
        this._setSummaryIcon(this.createNotificationIcon());
    },

    // this should return an icon to show in the message tray. You can use
    // this.ICON_SIZE for a default icon size if you want.
    createNotificationIcon: function () {
        return new St.Icon({
            icon_name: 'avatar-default',
            icon_size: this.ICON_SIZE
        });
    }
}); 

// 1. Make a source
let source = new MySource("source title");
// 2. Make a notification
let notification = new MessageTray.Notification(source,
                                                "notification title",
                                                "notification message");
// 3. Add the source to the message tray
Main.messageTray.add(source);
// 4. notify!
source.notify(notification);

GNOME 3.6

// 1. Make a source
let source = new MessageTray.Source("source title", 'avatar-default');
// 2. Make a notification
let notification = new MessageTray.Notification(source,
                                                "notification title",
                                                "notification message");
// 3. Add the source to the message tray
Main.messageTray.add(source);
// 4. notify!
source.notify(notification);

Wednesday 14 November 2012

Solar Eclipse, Cairns 2012

On 14 Nov 2012 right in the morning, there was a total solar eclipse viewable from Cairns, QLD Australia. As I live in QLD I decided to see the eclipse - a real treat, as I've never seen one before, and a *total* (as opposed to partial or annular) solar eclipse is a fairly rare occurence in one's lifetime. I rationed off one day of leave, flying up the night before the eclipse and back the following evening.

CLOUD, Y U NO MOVE?
Wednesday morning, 5AM. I wake up and head out to Cairns esplanade, and am greeted by this dismal weather (come on QLD, we're the "Sunshine State"??!!):

It will clear up, right? There's an hour until totality....No. See that big fat cloud over the peninsula? Well, it conspired to cover the sun for the ENTIRE FIRST THREE QUARTERS of the eclipse.

ooooo, creepy. It's dark but it's 6.40am!
Still, totality was cool when it happened - the sky got dark and the birds all stopped flying/chattering.

It's a shame we didn't get to actually *see* the sun though (and still hadn't all morning), because that's one of the main points of a total solar eclipse (being able to look at the sun with your naked eye). I always thought an eclipse would be pitch-black, but turns out it's more like twilight.

Yay I got to use my eclipse glasses!
It lasted a couple of minutes before the sky got bright again. And then, 20 minutes after totality had finished, the sun *finally* decided to peek its head out from the cloud obscuring it, so we all got to see it for the first time that morning. By then the moon was only about 50% covering the sun and on the way out, but at least I got to use my eclipse glasses!

I was trying to keep a positive attitude about it all, but then I got to the airport and caught the plane back home with everyone else from my city who'd also gone to see the eclipse. 
Apparently had I been just 20mins drive south, west, or north of Cairns (or really anywhere *except* Cairns), the clouds would have cleared just in time for me to see the totality!

In the absence of a sun to look at, I could look at other things.
Like this cloud floating way too low?!?!
(It definitely isn't steam coming from the boat...)
AAARRRRGHHHHHH!! Did everyone see the eclipse except me??!! To make matters worse, had I stayed in my home city I would have at least seen an 80% eclipse whereas because of the clouds in Cairns I only saw about a 50%!

 Ahh well. I'm trying really hard to remain positive about it, although it's hard when you've gone so far only to miss by a 20minute drive. Next time...(we don't get another total eclipse in Australia til 2028).

Or...Anyone up for a holiday to Indonesia in March 2016? Anyone? :D

Thursday 8 November 2012

Upgrading your GNOME shell extensions from 3.4 to 3.6

I recently started upgrading all my extensions from 3.4 to 3.6. Here are a few things I ran into:

New useful things:

  • you can reload just a single extension rather than restarting GNOME-shell. To do it by UUID:

    Main.shellDBusService._extensionSerivce.ReloadExtension(UUID)        
    // ** NOTE ** the typo 'Serivce' --> this is a typo in 3.6.0 and 3.6.1       
    

OR, if you prefer not to have the typo:

    const ExtensionUtils = imports.misc.extensionUtils;              
    const ExtensionSystem = imports.ui.extensionSystem;                         
    ExtensionSystem.reloadExtension(ExtensionUtils.extensions[UUID]);           

Gotchas for extension developers!

Whenever you enter the lock screen all the extensions get disabled until the user logs back in, and then they get re-enabled.

This means: your extension must be able to disable and re-enable itself smoothly!

Before this wasn't so important because typically a user does not toggle their extensions on and off many times within one session; so extensions typically get enabled once (when the user logs in) and not disabled until the user logs out, after which it doesn't matter whether it disables cleanly or not.

I've noticed quite a few extensions (including most of mine, oops...) that don't disable cleanly at the lock screen and hence don't re-enable cleanly once unlocked, causing all sorts of havoc...

Changes (some)

Some changes I noticed (certainly not all of them; just ones relevant to my extensions and a few others I use):

  • ~/.xsession-errors got renamed to ~/.cache/gdm/session.log (calls to log output here, as well as in the terminal if you're running gnome-shell from the terminal).
  • panel._menus -> panel.menuManager (main panel popup menu manager is now public)
  • panel._statusArea -> panel.statusArea (status area is now public)
  • panel._dateMenu -> panel.statusArea.dateMenu (date menu etc have all been stored in the status area and now is public)
  • panel._appMenu -> panel.statusArea.appMenu (app menu moved to the status area, and now is public)
  • overview._workspacesDisplay -> overview._viewSelector._workspacesDisplay (the "windows" tab of the overview)
  • search providers: in GNOME 3.4 we had getInitialResultSet (which returns an array of results) and getInitialResultSetAsync (which calls this.searchSystem.pushResults). In GNOME 3.6 there is just getInitialResultSet, which takes an extra argument callback and should call this.searchSystem.pushResults on the results (rather than returning them) and also apply callback to the results.
  • Use Main.panel.addToStatusArea(unique_name_of_indicator, inidicator, position, box) to add a SystemStatusButton or PanelButton to the panel. This handles adding its menu to the menu manager for you (box is Main.panel._{left, right, center}Box, omitting the box argument gives right box by default, and ommitting position gives position 0). If your button is a ButtonBox only (i.e. no menu) then stick with. _{left, right, center}Box.insert_child_at_index.
  • Symbolic/Full-colour Icons: previously system status buttons would always do a symbolic icon. Now they will do either full colour or symbolic. Use 'iconname' to get the full colour and 'iconname-symbolic' to get symbolic icons (so if your extension made a SystemStatusButton in 3.4, to get it looking the same in 3.6 you have to add '-symbolic' to the icon name).
  • Overview - the "tab" system where you could click on the "Windows" or "Applications" tab has been removed (!!!). Instead the windows tab is shown by default (as before) and to get to the application tab you click on the applications button on the dash. This means you can no longer simply add your own tabs to the overview, like the Extension List extension did (which added an "Extensions" tab to manage your extensions from). This is a major regression, in my opinion!
  • Looking glass - there's no longer an "Errors" tab. global.log used to output to the Errors tab whereas log would output to the terminal. Now they all output to the terminal if you're running from the terminal (makes more sense than having two separate logging systems).
  • System status icons and panel menu buttons. The top-level actor 'indicator.actor' is NOT the top-level actor that is placed into the panel. There is a new member 'indicator.container' which is a St.Bin that contains 'indicator.actor', and 'indicator.conainer' is what gets put into the panel boxes. So if you were previously looping through *Box.get_children() and accessing child._delegate to get the associated PanelMenu.Button, now you have to access child.child._delegate.
  • StatusIconDispatcher - in GNOME 3.2 and 3.4, this listens to icons being added by application to the tray (for example Dropbox) and depending on the type, either emits 'status-icon-added' or 'message-icon-added'. The Panel listens to the former to make a button in the top panel for the icon, and the NotificationDaemon listens to the latter to make an icon in the message tray for the icon. This let some icons be "dispatched" to the top panel and others to the bottom without overlap. In 3.6, this class has been removed and the functionality implemented directly into the classes; if the notification daemon gets 'tray-icon-added' it will make an icon in the message tray for it unless it's a standard status icon, but the top panel won't make top panels for these.
  • Contact Search Provider (contactDisplay.js) was removed. Apparenty gnome-contacts is implementing one so it's no longer necessary.
  • lock screens: there is a new lock screen in GNOME3.6 that wasn't there in 3.4. It's the one where you have to swipe up (to drag the screen up).