Time for action – accessing the address book

Suppose that we want to create a simple address book program. The data could come from an address book in our local computer or remotely from Google. Follow these steps to do so:

  1. Create a new Glade .ui file called address-book.ui.
  2. Add a horizontal box containing two items.
  3. Place a TreeView widget in the left-hand side of the box. Rename it to bookView. When asked for the model, create a new ListStore model and rename it to books.
  4. Rename the TreeSelection object, which is created automatically for the TreeView widget, to selection.
  5. Put a ScrollableWindow window on the right-hand side of the box.
  6. Then, put another TreeView widget inside ScrollableWindow. Create another ListStore model for it and rename it to contacts. Rename TreeView to contactView.
  7. Edit the ListStore model named books. Add two columns inside this model, both with the type gchararray.
  8. Edit the ListStore model named contact. Again, add two columns inside the model, both with the type gchararray.
  9. Our UI design should look similar to the one shown in the following screenshot:
    Time for action – accessing the address book
  10. Create a new Seed script called address-book.js.
  11. Following is a block of code that plays a very important part in the execution of the script:
    Main = new GType({
      parent: GObject.Object.type,
      name: "Main",
      init: function(self) {
    
        var bookColumn = {
          UID: 0,
          NAME: 1,
        }
    
        var contactColumn = {
          NAME: 0,
          EMAIL: 1,
        }
    
        this.listContacts = function(e) {
          var c = {};
          var q = EBook.BookQuery.any_field_contains("");
          var r = e.get_contacts_sync(q.to_string(), c, null);
          if (r && c && c.contacts && c.contacts.length > 0) {
            var store = self.contact_view.get_model();
            c.contacts.forEach(function(contact) {
              var iter = {};
              store.append(iter);
    
              var name = contact.full_name;
              if (!name) {
                name = contact.nickname;
              }
              store.set_value(iter.iter, contactColumn.NAME, name);
              store.set_value(iter.iter, contactColumn.EMAIL, contact.email_1);
              
            });
          }
        }
    
    
        this.clients = {};
    
        var book_view = ui.get_object("bookView");
        var selection = ui.get_object("selection");
        selection.signal.changed.connect(function(s) {
          var selected = {}
          s.get_selected(selected);
          var book = selected.model.get_value(selected.iter, bookColumn.UID);
    
          var uid = book.value.get_string();
          if (uid == "") {
            return;
          }
          source = self.sources.peek_source_by_uid(uid);
          var e = null;
          if (typeof(self.clients[uid]) !== "undefined") {
            e = self.clients[uid];
            if (e) {
              self.clients[uid] = e;
              self.listContacts(e);
            }
          } else {
            var e = new EBook.BookClient.c_new(source);
            var r = e.open(false, null, function() {
    
              if (e) {
                self.clients[uid] = e;
                self.listContacts(e);
              }
            });
          }
    
        });
    
        var cell = new Gtk.CellRendererText();
        var column = new Gtk.TreeViewColumn({title:'Book'});
        column.pack_start(cell);
    
        column.add_attribute(cell, 'markup', bookColumn.NAME);
        book_view.append_column(column);
    
        var contact_view = ui.get_object("contactView");
        this.contact_view = contact_view;
        cell = new Gtk.CellRendererText();
        column = new Gtk.TreeViewColumn({title:'Name'});
        column.pack_start(cell);
        column.add_attribute(cell, 'text', contactColumn.NAME);
        contact_view.append_column(column);
    
        cell = new Gtk.CellRendererText();
        column = new Gtk.TreeViewColumn({title:'E-mail'});
        column.pack_start(cell);
        column.add_attribute(cell, 'text', contactColumn.EMAIL);
        contact_view.append_column(column);
    
    
        var s = {};
        var e = EBook.BookClient.get_sources(s);
        this.sources = s.sources;
    
        var groups = this.sources.peek_groups();
        if (groups && groups.length > 0) {
          var store = book_view.get_model();
          groups.forEach(function(item) {
            var iter = {};
            store.append(iter);
    
            store.set_value(iter.iter, bookColumn.UID, "");
            store.set_value(iter.iter, bookColumn.NAME, "<b><i>" +item.peek_name()+ "</i></b>");	
    
            var sources = item.peek_sources();
            if (sources && sources.length > 0) {
              sources.forEach(function(source) {
                store.append(iter);
                store.set_value(iter.iter, bookColumn.UID, source.peek_uid());
                store.set_value(iter.iter, bookColumn.NAME, source.peek_name());	
              });
              
            }
          });
        }
    
      }
    });
  12. Run the code. The application is executed and a window is displayed, as shown in the following screenshot:
    Time for action – accessing the address book

Depending on your settings, the result of our previous exercise might not look the same. (Also pardon the blurry text; they are real e-mail addresses and I want to obfuscate them for privacy reasons.) In the preceding screenshot, EDS returns four sources of address books, namely On This Computer, On LDAP Servers, WebDAV, and Google. Out of these sources, only two have the real data; these are On This Computer and Google. The names of the address books listed in the preceding screenshot are Personal and Contacts.

When we click on Contacts, a list of all the contacts is displayed on the right-hand side of the window. Here we only display two columns, the name and e-mail address.

Let's dig into the source code.

First we define the constants for the columns. Here we have two models, one for address book collection (we call it books) and one for contacts collection (we call it contacts). Each book has a unique identifier and name. Hence, we use this data for the columns.

For the contact, we only display the name and e-mail address, so we will need only two columns.

We keep a reference of the bookView variable in the .ui file and save it in the book_view variable in the following line of code:

We also get the selection object of the bookView variable and put it in the selection variable.

The behavior we want for the selection is that when we select a book, we should get the content of that book. To do that, we need to hook the changed signal to a function, as is shown here:

What we need to do in the handler first is get the selected object from the selection.

In the selected object, we get the Iter object, which is kept in the iter member. We immediately get the value of column number 0 (which is symbolized with bookColumn.UID). The value is in the type string, so we use the get_string() function. One specific behavior we set is that whenever the uid value is empty, it means that the row does not point to a specific book. It is used by the program to display the address' book source.

If uid does have some value, we ask EDS to get EBook.Source directly, which is identified by the uid value. This will return an EBook.Source type that has been saved in the e variable.

We keep the sources obtained in the client's cache, so we don't need to reopen the source every time we click on the book. When the cache has the source defined with a uid value, we just call the listContacts function; otherwise, we need to open it first. After that we will keep it in the cache. Then we list the contents using listContacts, as shown in the following code snippet:

Note that we asynchronously open the source by supplying a callback as the argument of the function. With this approach, we can give time for the opening of the source without blocking our application and making it unresponsive to the user. If our source needs to be authorized and needs our attention, a dialog window will pop up at this point.

Here, we define the columns for bookView. Visually, we display only a single column, which is the book's title or the book's source group.

We use a Pango markup as described in the next part of the code. So we don't use the text property of CellRendererText but instead use markup and map it to column 1 (which is denoted with bookColumn.NAME).

In the following code snippet, we define the columns for the contacts. We have two visual columns, and we'll assign them with the contactColumn.NAME and contactColumn.EMAIL column names respectively:

During initialization, we get the address book's sources with the get_sources function. After that, we find the available groups on the system with the peek_groups function.

For each group, we add the book ID and the book name into the table, but we won't fill the uid value because we only want to keep the uid value of the actual address book source. For the name of the group, we enclose it with a markup of bold and italic style, as shown in the following snippet of code:

Note that we used something that looks like HTML markups in the preceding code snippet. In fact, they are Pango markups, the text rendering engine used in the GNOME framework that is similar to HTML, but with comparatively fewer features. The reason we put the markups here is because we want to style the widget on the presentation layer. Hence, we shouldn't both touch the data and add the style here, but only style it when it is displayed. However, the previous approach is not correct because we modify the data before putting it into the store. If we search inside the data, we may not find the data because it is already cluttered with markups. The correct implementation would be to style it in the rendering widget. It means we will no longer be able to use Gtk.CellRendererWidget, but we will rather use a custom renderer widget that styles the data before displaying it.

We also try to get the actual address book source for each group with the peek_sources function, and put each source that is available in the table. And now we place uid in column 0 (bookColumn.UID); this is shown in the following code snippet:

Note that column 0 is not visible in the table because we did not add TreeViewColumn to it.

What the listContacts function does first is prepare the query to the address book. EDS provides the EBook.BookQuery object to convey the query to EDS. In the following code snippet, we'll ask EDS to get all the data by creating the query with the any_field_contains("") function:

The contacts member of the object that we passed to get_contacts_sync will be populated with the contacts from the address book. For each contact that we get (it is in the form of EBook.Contact), we get an interesting property (we want only the full_name, nickname, and email_1 properties) and put it into the model.