Mar 12 10

Quick UI update

by mandel

I’ve realized that I have not communicate much lately (I blame my job, having to do a 9 am – 1 am shift is crazy). So I have decided to give a very quick update. All my current work has been focused on the design of the UI, which is quite appropriate right now ;)

I have re-written the UI to match the design better and make it more pleasant to the eye. To do so I have had to fully write from scratch all the different widgets which would have been a pain in the ass if it was not for the fact that I have writen everything using goocanvas, with a surprisingly good looking result (I needed to write a small hack, but is just tiny). Enough said, this are the current looks of the main widget with no menu etc…

And with the search box the main widget looks like this:

As you can see the avatars (besides the lovely image of myself) look much better now that the ones provided by the Gtk IconView:

New look

Old look

I have also spent a fare about of my free time on implementing a search bar similar to that one found in chrome which I think looks great:
Of course I will be posting more details about how I wrote the UI but that will have to wait until I get back from my well deserved holidays in NY (next week!). As a side note, my idea is to get the UI as finished as possible to be able to show it at UDS which this time is very close to my current residence :D

Mar 2 10

Desktopcouch Tips and Tricks II

by mandel

Well, it looks like a lot of people (I blame @sil for it) have read my tips and tricks to work with Desktopccouch so I have decided to do a second post about it with two “topics”.

Put record revisited

In my last post I mentioned that the put_record returned the id of the record that was just stored in the db. Intially I filled that as a bug since the method will force you to do another round to the db when is not needed. After some discussion with the always wise Chad a decition was taken not to brake backwards compatibility. That does not mean that you will have to be doing that extra round to the database since the put_record method has the side effect of modifying the parameter you passed to the method. (sometimes I miss ADAs syntax when dealing with parameters).

Also, Chad made a merging sprint and merged a number of branches of desktopcouch to trunk and now we have some nice new features:

  • Pop and Remove have been implemented in the MergeableList.
  • A batch update has been added to allow you to perform the smaller amount of requests to the db. This method actually started a very nice discussion.

Pop and remove have been added because it was a pain to have to delete data for lists coming from desktopcouch while I think there is no need to explain why the batch_update was added.

Objects recipes

Because this week is Ubuntus’ Opportunistic Programmer week I have decided to provide two different recipes to help people using Desktopcouch records. There are good and bad points for both of the recepies and I will not give my opinion about which one I prefer but remember that:

“Favor ‘object composition’ over ‘class inheritance’.” (Gang of Four 1995:20)

Both recipes solve a personal problem that I have with records. I have always found the need to specify the use of the application annotations a major pain in the ass and therefore I have tried to avoid having to explicitly “typing” the code to take care of that inconvenience. The later of the recipes not only allows you to forget about the need of using “application_annotations” but also allows you to use a more object orientated technique which I personally prefer (I forgot I was not going to say anything).

StrictRecord: Extending Record

In this recipe we are going to create a strict record that will ensure that if we use a property that was no defined in the record, the data will be placed in the “application_annotations” property.

class StrictRecord(Record):
    """
    A strict record that make sure that you store data correctly.
    """
 
    def __init__(self, properties, data=None, record_type=None, record_id=None):
        if properties:
            super(StrictRecord, self).__init__(data=data, 
                record_type=record_type, record_id = record_id)
            self._properties = properties
        else:
            raise ValueError("Properties of the record should be provided.")
 
    def __getitem__(self, key):
        if is_internal(key):
            raise KeyError(key)
        if key in self._properties:
            return self.application_annotations[key]
        return super(Record, self).__getitem__(key)
 
    def __setitem__(self, key, item):
        if is_internal(key):
            raise IllegalKeyException(
                "Can't set '%s'. This is an internal key." % key)
        if key in self._properties:
            self.application_annotations[key] = item
        else:
            super(Record, self).__setitem__(key, item)
 
    def __contains__(self, key):
        if is_internal(key):
            return False
        if key in self._properties:
            return key in self.application_annotations
        return super(Record, self).__contains__(key)
 
    def __delitem__(self, key):
        if is_internal(key):
            raise KeyError(key)
        if key in self._properties:
            del self.application_annotations[key]
        else:
            super(Record, self).__delitem__(key)
 
    def get(self, key, default=None):
        """Get a value from the record by key."""
        if is_internal(key):
            return default
        if key in self._properties:
            return self.application_annotations.get(key, default)
        return super(Record, self).get(key, default)

Of course this code will let you forget about the “application_annotations” but it also gets you closer to XML schemes which is something that you probably don’t want (although I’m not sure this statement really makes sense)

Object composition

To ilustrate this recipe I’ll use the contact record as an example:

# set a list of the different fields defined in the contact record
contact_fields = ("first_name",
                  "middle_name",
                  "last_name",
                  "title",
                  "suffix",
                  "birth_date",
                  "nick_name",
                  "spouse_name",
                  "wedding_date",
                  "company",
                  "department",
                  "job_title",
                  "manager_name",
                  "assistant_name",
                  "office",
                  "categories")
 
# set a list of the different records that are considered to e special cases
special_attributes = ("_record",
                      "addresses",
                      "phone_numbers",
                      "email_addresses",
                      "urls",
                      "im_addresses",
                      "_application_annotations",)
 
class Contact(object):
    """
    Represents a contact in the database.
    """
    # collection of object that are used to perform the different filters
    # in the class
 
    application_name = None
 
    def __init__(self, first_name="", middle_name="", last_name=""):
        """
        Creates a new instance of the class.
        """
        self._record = Record(
            record_type="http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact")
        self._record.record_id = uuid.uuid4().hex
        self.first_name = first_name
        self.middle_name = middle_name
        self.last_name = last_name
        self._application_annotations = {}
        # we set the application annotations for the app
        if not Contact.application_name is None:
            logger.info("Application annotations set to be {0}".format(
                Contact.application_name))
            self._application_annotations[Contact.application_name] = {}
        # sets used to keep track of the different contact_data
        self.addresses = set()
        self.phone_numbers = set()
        self.email_addresses = set()
        self.urls = set()
        self.im_addresses = set()
 
    def __setattr__(self, attribute, value):
        """
        Allows to set the internal attribute of the class by using a record.
        """
        # if the attribute is one of those that are defined at
        #
        if attribute in contact_fields:
            self._record[attribute] = value
        elif attribute in special_attributes:
            object.__setattr__(self, attribute, value)
        elif attribute == "avatar":
            if "avatar" in self._record.list_attachments():
                self._record.detach("avatar")
            self._record.attach(value, "avatar", "image/jpg")
        elif not Contact.application_name is None:
            self._application_annotations[Contact.application_name][attribute] = value
        else:
            message = "Attribute {0} could not be set because application name was not set.".format(
                    attribute)
            logger.error(message)
            raise AttributeError(message)
 
    def __getattribute__(self, attribute):
        if attribute in contact_fields:
            if attribute in self._record:
                return self._record[attribute]
            return None
        elif attribute in special_attributes:
            return object.__getattribute__(self, attribute)
        elif attribute == "avatar":
            if "avatar" in self._record.list_attachments():
                return self._record.attachment_data("avatar")[0]
            else:
                return None
        elif not Contact.application_name is None:
            if attribute in self._application_annotations[Contact.application_name]:
                return self._application_annotations[Contact.application_name][attribute]
            else:
                return object.__getattribute__(self, attribute)
        else:
            return object.__getattribute__(self, attribute)
 
    # private methods
 
    @logged(logger)
    def _get_id(self):
        """
        @rtype: int
        @return: The id of the contact.
        """
        return self._record.record_id
 
    # end private methods
 
    # properties
 
    _id = property(_get_id)

This recipe allows you to forget about the “application_annotations” but also allows you to be use a contact as an object making it a lot nicer to work with. For example we go from this:

contact = Record(record_type="http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact")
contact["first_name"] = "Manuel"
contact["last_name"] = "de la Pena"
contact.application_annotations["my_app"]["my_data"] = "I'm lazy... too much typing here"

to this:

contact = Contact()
contact.first_name = "Manuel"
contact.last_name = "de la Pena"
contact.my_data = "I'm lazy... I prefer this"]

Well, I prefer the computer to do all the work :P

Feb 23 10

OpenStreetMaps + Geocoding + PyGTK

by mandel

An other few days an another UI improvement to macaco. For this past two days I have worked in yet another dialog used in macaco which was not exciting or different to those found in other applications, and on top of that it did not add too much value to the app. I’m talking about the address dialog that used to look like this:

Ok, it is clean and shows the data, but I want to add something “usefulness” for the user, something that you can easily use to have an idea of the address location. To improve the dialog and try to show more information, I have decided to add a map with its position in the UI. To do so I have used the following:

Now or dialog looks like this:

Where are the maps?

For an address book it would be a stupid thing to do to be deployed with the OpenStreetMaps, therefore the maps are downloaded by the UI when ever they are required. I can already hear people complaining about the fact that there are people with no internet connection (o_0). To solve that, the application is deployed with the initial address dialog and is the user who decides to use the map view or not. I have achieved all this through the use of Dependency Injection. Because of this posible complain, I have also refactored my code to allow different dialog definitions which will allow developers to create their own dialogs for the applications, that is, there are even more extension points to be used in the app, but that deserves its own post.

Feb 14 10

Tips and tricks for using Desktopcouch

by mandel

The main goal I had in my talk at FOSDEM was to try and point out the different problems and tricks that are useful for developers wanting to use Desktopcouch. After the talk I realized that that was probably far too much information to be absorb in a 45 min talk. In order to compensate for such a technical talk here are the different tips and tricks I mentioned:

Different ways to create records

There are three different ways to create records and it is useful to know them all:

  • Basic way: In this way you are simply creating a Record with a record_type. Remember that a record must always have a type therefore this is then the basic way to create one:
    record = Record(record_type=
        "http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact")
     
    print "Got a record of type {0} and id {1}".format(
        first_record.record_type, first_record.record_id)
  • Provide an Id: Certain applications by be interested in given their own ids to records rather than using the auto-generated one. This can be done using the record_id named parameter:
    record = Record(record_type=
        "http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact",
        record_id="second_record")
     
    print "Got a record of type {0} and id {1}".format(
        first_record.record_type, first_record.record_id)
  • Advance way: Certain application might be working with dictionaries that they want to convert to Records. The Record class can take such a dictionary as a parameter which would mean that we have all the data of the dict in the record.
    data = {
        "record_type": "http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact",
        "name":"Manuel"
    }
    record = Record(data=data)

    When using this way of constructing records you have to be careful since the records_type and the record_id parameter ca be passed to. Following the idea of “explicit is better then implicit” if the record_id or type are present in the data dictionary and are also passed through parameter the later will be used (params).

Careful with MergeableLists

All lists that are added to a record will be converted to a MergeableList. That is, if we have a list:

list = [2,3,4,5]

we will have instead:

list = {
    "_order": ["id1", "id2", "id3", "id4"],
    "id2":3,
    "id4":5,
    "id1":2,
    "id3":4
}

This important because you will have to think of what operations you are doing to your lists and which ones are more expensive in this representations. Currently in trunk there is not remove and pop implementations for the MergeableList and therefore those operations will throw an exception, I have submitted a patch for the lack of remove and pop methods (lp:~mandel/desktopcouch/fix_bug_519873 ⇒ lp:desktopcouch) but it has not yet been merged with trunk :(

It is also important to know that if you generate your own mergeable lists you have to ensure that you use the same uuid pattern that Ubuntu One uses, otherwhise yo will get a very nice 500 error from the web interface and not much more

record = Record(record_type="url")
# DO USE this format
record[str(uuid4())] = "test"

Storing records

Storing records in the a CouchDatabase with Desktopcouch is very simple but there are two dirty corners you have to be careful with:

  • Always check that the db exists: This might sound like a stupid advice but bugs happen because of this.
  • Retrieving the records’ rev: Every time a doc/record is put in the database you have to remember that you have to retrieve the record revision from the database, that is, the following code will give you a ConflictException:
    record = Record(record_type="my_record_type_url")
    db = CouchDatabase("my_db", create=True)
    db.put_record(record)
    # dome some more work with the record
    ...
    ...
    record["name"] = "manuel"
    ...
    ...
    record["last_name"] = "de la Pena"
    ...
    ...
    # store record again
    db.put_record(record)

    In order for the code to work you will have to do a put_record followed by a get_record which is a pain and a bug. But until the patch is merged you will have to be careful with that.

Extras

There are a couple of very nice features in the Desktopcouch API that are not documented and you might like to use:

  • Listening to changes: Because the database will be accessed by more than one app you will want to listen to the changes that other applications perform. There is a very nice API that works in the following way:
    # we are going to be listening to the changes
    def changes_cb(seq=None, id=None, changes=None):
        print seq
        print id
        print changes
     
    db = CouchDatabase("fosdem")
    # better, use glib main loop or twisted task!
    while True:
        db.report_changes(changes_cb)
        time.sleep(30)
  • Attachments: CouchDb documents can have attachments and although it is not recommended to use CouchDb to sync BLOBS there are some exceptions where that is the correct way to do it (I do in macaco to sync the avatars of the contact :P ). An example of how to attach a file would be:
    db = CouchDatabase("fosdem")
    record = Record(record_type="url")
    record.attach("/home/mandel/Desktop/avatar.jpg", "blob", "image/jpg")
    db.put_record(record)

Well, that is all for now, I hope someone finds this useful!

Feb 14 10

Having fun with PyGooCanvas

by mandel

I have been recently improving the UI of macaco-contacts to ensure that is more usable. One of the things I had in my TODO list was to provide a decent dialog to choose an avatar for a contact. In my initial attempt a simply allowed the user to choose a file from the file system which would be used as the contacts avatar. This solution, although fast, did not give nice results since the user will probably used a non squared image and the scaling process would deform it, on top of that the dialog looked like this:

lame, right?

For the new solution I’ve decided to write my own dialog from scratch and to do so I’ve used PyGooCanvas. The main reason to use GooCanvas was the fact that Ubuntu has organized an “Opportunistic Programming week” in which Rick Spencer is going to talk about PyGooCanvas, I decided to GooCanvas it before the talk so that if I had any problem I could ask him questions! :D (no need at the end).

Lets take a look at the new dialog:

and a view of it working:

The idea is very simple, the dialog allows the user to zoom in and out of the image a choose an area of the image that will be used as the avatar. For your joy here is the source code of the main Widget that is used to achieve this behavior:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
### BEGIN LICENSE
# Copyright (C) 2009 Manuel de la Pena <mandel@themacaque.com>
#This program is free software: you can redistribute it and/or modify it
#under the terms of the GNU General Public License version 3, as published
#by the Free Software Foundation.
#
#This program is distributed in the hope that it will be useful, but
#WITHOUT ANY WARRANTY; without even the implied warranties of
#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
#PURPOSE.  See the GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License along
#with this program.  If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
import gtk
import goocanvas
import os
from gtk import gdk
from macaco.macacoconfig import getdatapath
 
class ResizableImageEntry(gtk.VBox):
    """
    Represents an entry that allow the user to point to an image in the
    file system and selecte the area to crop in order to fit the size
    limits of the app.
    """
 
    def __init__(self, x=300, y=300):
        """
        Creates a new entry that can be used to select an image and correctly
        crop it.
        """
        gtk.VBox.__init__(self)
        # store the x and y
        self.x = x
        self.y = y
        # init the image
        self.pict_url = os.path.join(getdatapath(), 'media', 'contact-120.png')
        self.pict = gdk.pixbuf_new_from_file(self.pict_url)
        # set the image used in the shadows
        self.shadow_pict = gdk.pixbuf_new_from_file(
            os.path.join(getdatapath(), 'media', 'shadow.png'))
        # set init the basic vars
        self.selected_item = None
        self.selection_rectangle = None
        self.top_shadow = None
        self.bottom_shadow = None
        self.left_shadow = None
        self.right_shadow = None
        # we create the canvas that will be used to draw the image.
        self.canvas = goocanvas.Canvas()
        self.canvas.set_size_request(x,y)
        self.canvas.show()
        self.root = self.canvas.get_root_item()
        # create an event box for the canvas
        self.event = gtk.EventBox()
        self.event.show()
        self.event.add(self.canvas)
        self.pack_start(self.event, expand=True, fill=True, padding=10)
        self.pack_start(self._get_zoom_widget(), expand=True, fill=True, padding=10)
        # create a button that will allow the user to change the image
        # to crop
        self.change_button = gtk.Button("Select image")
        self.change_button.show()
        self.pack_start(self.change_button, expand=False, fill=True, padding=10)
        self.show()
        # set a flag used to decide if the image is begin moved
        self.moving = False
        # get the root of the canvas
        self.root = self.canvas.get_root_item()
        # draw the image for the first time
        # connect the different events
        self.zoom.connect("value-changed", self._on_changed)
        self.canvas.connect("button_press_event",self._mouse_down)
        self.canvas.connect("button_release_event",self._mouse_up)
        self.canvas.connect("motion_notify_event", self._move)
        self.canvas.connect("scroll-event", self._scroll)
        self.change_button.connect("clicked", self._change_image)
        self.show_all()
        # draw for the first time
        self._draw(is_init=True)
 
    def _get_zoom_widget(self):
        # create a small and bigger image to indecate what the slider is for
        small_pict_url = os.path.join(getdatapath(), 'media', 'icon-22.png')
        small_pict = gdk.pixbuf_new_from_file_at_size(small_pict_url, 22, 22)
        small_image = gtk.image_new_from_pixbuf(small_pict)
        small_image.set_alignment(0, 0.85)
        small_image.show()
        big_pict_url = os.path.join(getdatapath(), 'media', 'icon-48.png')
        big_pict = gdk.pixbuf_new_from_file_at_size(big_pict_url, 48, 48)
        big_image = gtk.image_new_from_pixbuf(big_pict)
        big_image.set_alignment(0, 1)
        big_image.show()
        self.zoom = gtk.HScale(adjustment=gtk.Adjustment(
            value=100, lower=10, upper=200, step_incr=2))
        self.zoom.show()
        box = gtk.HBox()
        box.pack_start(small_image, expand=False, fill=True)
        box.pack_start(self.zoom, expand=True, fill=True, padding=10)
        box.pack_start(big_image, expand=False, fill=True)
        return box
 
    def _scroll(self, menu, event):
        if event.direction == gtk.gdk.SCROLL_DOWN:
            self.zoom.set_value(self.zoom.get_value() - 2)
        else:
            self.zoom.set_value(self.zoom.get_value() + 2)
 
    def _change_image(self, button):
        """
        Allows to show a dialog that will be used to change the image in the
        UI.
        """
        dialog = gtk.FileChooserDialog(title="Avatar",action=gtk.FILE_CHOOSER_ACTION_OPEN,
            buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
        response = dialog.run()
        if response == gtk.RESPONSE_OK:
            # change the image url and draw making it init
            self.pict_url = dialog.get_filename()
            self.pict = gdk.pixbuf_new_from_file(self.pict_url)
            # set the value of the zoom
            self.zoom.set_value(100)
            self._draw(is_init=True)
        dialog.destroy()
 
    def _scale_widget(self):
        """
        Returns the HBox that will contain the slider used to let the user
        know he can zoom in and out of the image
        """
        box = gtk.HBox()
        # create the zoom at 100%
        self.zoom = gtk.HScale(adjustment=gtk.Adjustment(
            value=100, lower=10, upper=200, step_incr=2))
        box.pack_start(self.zoom, expand=False, fill=True, padding=10)
        return box
 
    def _draw(self, is_init=False):
        # call the different draw steps
        self._draw_image(is_init)
        self._draw_selector()
        self._draw_shadows()
 
    def _draw_image(self, is_init=False):
        """
        Takes care of drawing the image in the canvas in the current position.
        is_init is used to tell if it is the first time to draw the image, this
        is important since we want to store the position of the image the first
        time.
        """
        # remove the last image
        if self.selected_item:
            self.selected_item.remove()
        # get the size of the canvas in order to place the img
        self.canvas.set_bounds(0,0,self.x,self.x)
        cont_left, cont_top, cont_right, cont_bottom = self.canvas.get_bounds()
        img_w = self.pict.get_width()
        img_h = self.pict.get_height()
        img_left = (self.x - img_w)/2
        img_top = (self.y - img_h)/2
        if is_init:
            self.image_x = img_left
            self.image_y = img_top
        self.selected_item = goocanvas.Image(parent=self.root, pixbuf=self.pict,
            x=self.image_x,y=self.image_y)
 
    def _draw_selector(self):
        """
        Takes care of drwaing the rectangle that is used to select the
        portion of the image that will be used.
        """
        # remove the last selection square if present
        if self.selection_rectangle:
            self.selection_rectangle.remove()
        self.select_left = (self.x - 120)/2
        self.select_top = (self.y - 120)/2
        self.selection_rectangle = goocanvas.Rect( parent=self.root,
            width=120, height=120, stroke_color="black",
            line_width=1.0, x=self.select_left, y=self.select_top)
 
    def _draw_shadows(self):
        """
        Takes care of drawing the sadows that will cover those parts of the
        image that will not be taken into the final result.
        """
        # remove shadows if fresent
        if self.top_shadow:
            self.top_shadow.remove()
        if self.bottom_shadow:
            self.bottom_shadow.remove()
        if self.left_shadow:
            self.left_shadow.remove()
        if self.right_shadow:
            self.right_shadow.remove()
        # re-create the shadows
        self.top_shadow = goocanvas.Rect( parent=self.root,
            width=300, height=self.select_top, fill_pixbuf=self.shadow_pict,
            line_width=0, x=0, y=0)
        self.bottom_shadow = goocanvas.Rect( parent=self.root,
            width=300, height=300 - (self.select_top + 120), fill_pixbuf=self.shadow_pict,
            line_width=0, x=0, y=self.select_top+120)
        self.left_shadow = goocanvas.Rect( parent=self.root,
            width=self.select_left, height=120, fill_pixbuf=self.shadow_pict,
            line_width=0, x=0, y=self.select_top)
        self.right_shadow = goocanvas.Rect( parent=self.root,
            width=300 - (self.select_left + 120), height=120,
            fill_pixbuf=self.shadow_pict, line_width=0, x=self.select_left + 120,
            y=self.select_top)
 
    def _move(self, item, event):
        if self.moving:
            self.image_x -= (self.initial_x - event.x)
            self.image_y -= (self.initial_y - event.y)
            self.initial_x = event.x
            self.initial_y = event.y
            # re draw the images with the new locations
            self._draw()
 
    def _on_changed(self, widget):
        value = widget.get_value()/100
        aux_pict = gdk.pixbuf_new_from_file(self.pict_url)
        # we scale the pixbuf according to the value of the slider
        self.pict = aux_pict .scale_simple(
            int(aux_pict.get_width() * value),
            int(aux_pict.get_height() * value), gtk.gdk.INTERP_TILES)
        self._draw()
 
    def _mouse_down(self, item, event):
        """
        Callback used when the mouse in pressed.
        """
        self.moving = True
        self.initial_x = event.x
        self.initial_y = event.y
        self.canvas.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND1))
 
    def _mouse_up(self, item, event):
        """
        Callback used when the mouse is released.
        """
        self.moving = False
        self.canvas.window.set_cursor(None)
 
    @property
    def pixbuf(self):
        """
        Returns a pixbuf of the selected area.
        """
        # create a pixbuf with the original image
        pict = gtk.gdk.Pixbuf(gdk.COLORSPACE_RGB, True, 8, 120, 120)
        # scale by using the diff between the selector position and the image
        # using the value of the zoom and the scale percentage
        scale_factor = self.zoom.get_value()/100
        x_pos = self.select_left - self.image_x
        y_pos = self.select_top - self.image_y
        sub_pict = self.pict.subpixbuf(x_pos, y_pos, 120, 120)
        return sub_pict

I’m sure that someone more experience will find improvements to it, so please let me know!! The complete code of the dialog is in at lp:macaco in the ui directory :P