Creating an Application in Kivy: Part 5

In Part 4, we took a break from developing Kivy interfaces and focused on improving the interface we already had. In this part, we’ll get back to interface development by creating a widget to display the buddy list. Here’s the plan: we’ll start with a new widget to render the information we’re getting from the roster. Then we’ll figure out how to query sleekxmpp to get additional information about the user, such as their online status and status message.

If you haven’t been following along or want to start with a clean slate, you can clone the state of the example repository as of the end of part 4:

git clone https://github.com/buchuki/orkiv.git
git checkout end_part_four

Remember to create and activate a virtualenv populated with the appropriate dependencies, as discussed in part 1.

Table Of Contents

If you’re just joining us, you might want to jump to earlier steps in this tutorial:

  1. Part 1: Introduction to Kivy
  2. Part 2: A basic KV Language interface
  3. Part 3: Handling events
  4. Part 4: Code and interface improvements
  5. Part 5: Rendering a Buddy List
  6. Part 6: ListView Interaction
  7. Part 7: Receiving messages and interface fixes
  8. Part 8: Width based layout
  9. Part 9: Deploying your Kivy application

Rendering A Basic ListView

So far, after successful login, a list of usernames is printed on the ConnectionModal popup. That’s not quite what we want to do. Rather, we want to dismiss the popup and render the buddy list in the root window instead of the login form.

The first thing we’ll have to do here is wrap the login form in a new root widget. We should have done this from the start, but to be honest, I didn’t think of it. This widget will be a sort of manager for the AccountDetailsForm, BuddyList, and ChatWindow. If we only ever wanted to show one window at a time, we could use Kivy’s ScreenManager class to great effect. However, I hope to display widgets side by side if the window is wide enough, so let’s try coding it up manually.

First we can add an OrkivRoot (leaving it empty for now) class to __main__.py. Let’s add a BuddyList class while we’re at it, since we’ll need that shortly.

(commit)

from kivy.uix.boxlayout import BoxLayout  # at top of file

class BuddyList(BoxLayout):
    pass


class OrkivRoot(BoxLayout):
    pass

Next, we update the orkiv.kv to make OrkivRoot the new root object (replacing the current AccountDetailsForm: opening line) and make any instances of OrkivRoot contain an AccountDetailsForm:

(commit)

OrkivRoot:

<OrkivRoot>:
    AccountDetailsForm:

Now let’s add a show_buddy_list method to the OrkivRoot class. This method will simply clear the contents of the widget and construct a new BuddyList object for now:

(commit)

class OrkivRoot(BoxLayout):
    def show_buddy_list(self):
        self.clear_widgets()
        self.buddy_list = BuddyList()
        self.add_widget(self.buddy_list)

And finally, we can remove the temporary code that renders buddy list jabberids on the ConnectionModal label and replace it with a call to show_buddy_list:

(commit)

def connect_to_jabber(self):
    app = Orkiv.get_running_app()
    try:
        app.connect_to_jabber(self.jabber_id, self.password)
        app.root.show_buddy_list()
        self.dismiss()

Note that we also explicitly dismiss the ConnectionModal. The new BuddyList widget, which is currently blank, will be displayed as soon as we have a valid connection.

Let’s add some styling to that BuddyList class in our orkiv.kv file. We’ll add a ListView which provides a scrollable list of widgets. Of course, we’re not actually putting anything into the ListView, so if we run it now, it won’t show anything… but it’s there!

(commit)

<BuddyList>:
    list_view: list_view
    ListView:
        id: list_view

Notice that I gave the ListView an id property and connected it to a list_view property on the root widget. If you remember part 3 at all, you’re probably expecting to add an ObjectProperty to the BuddyList class next. You’re right!

(commit)

class BuddyList(BoxLayout):
    list_view = ObjectProperty()

    def __init__(self):
        super(BuddyList, self).__init__()
        self.app = Orkiv.get_running_app()
        self.list_view.adapter.data = sorted(self.app.xmpp.client_roster.keys())

Since you guessed that ObjectProperty was coming, I added an initializer as well. I have to keep you interested, after all! The initializer first sets up the superclass, then sets the list_view‘s data to the buddy list keys we’ve been using all along. There’s a couple complexities in this line of code, though. First note the call to sorted, which accepts a python list (in this case containing the ids in the buddy list) and returns a copy of the list in alphabetical order. Second, we are setting the list_view.adapter.data property. Underneath the hood, ListView has constructed a SimpleListAdapter for us. This object has a data property that contains a list of strings. As soon as that list is updated, the adapter and ListView work together to update the display.

The reason that the data is stored in an adapter is to separate the data representation from the controller that renders that data. Thus, different types of adapters can be used to represent different types of data. It’s possible to construct your own adapter as long as you implement the relevant methods, but this is typically unnecessary because the adapters that come with Kivy are quite versatile.

Try running this code. Upon successful login, the modal dialog disappears and the buddy list pops up in sorted order. Resize the window to be smaller than the list and you can even see that the ListView takes care of scrolling.

The SimpleListAdapter is not sufficient for our needs. We need to be able to display additional information such as the user’s status and availability, and we want to lay it out in a pretty way. We’ll also eventually want to allow selecting of a Buddy in the list so we can chat with them. SimpleListAdapter supports none of this. However, Kivy’s ListAdapter does. So let’s edit the orkiv.kv to use a ListAdapter for the BuddyList class:

(commit)

# at top of file:
#:import la kivy.adapters.listadapter  # at top of file
#:import lbl kivy.uix.label

<BuddyList>:
    list_view: list_view
    ListView:
        id: list_view
        adapter: la.ListAdapter(data=[], cls=lbl.Label)

First, we import the listadapter module using the name la. This has a similar effect to a from kivy.adapters import listadapter as la line in a standard Python file. This #:import syntax can be used to import any python module that you might need inside the kv language file. The syntax is always #:import <alias> <module_path>. However, you can only import modules, you can’t import specific classes inside modules. This is why I used a rather uninformative variable name (la). When we construct the ListAdapter at the end of the snippet, the shortened form happens to be more readable, since we are immediately following it by a class name.

We also import the label class as lbl. While it seems odd to have to import Label when we have seen Label used in other parts of the KVLanguage file, we have to remember that sometimes KV Language allows us to switch to normal Python. That’s what’s happening here; the code after adapter: is normal python code that doesn’t know about the magic stuff Kivy has imported. So we have to import it explicitly.

This is probably not the best way to do this; we’re probably going to have to replace this view with a custom ListView subclass later, but it suits our needs for the time being.

That code is simply constructing a new ListAdapter object. It assigns empty data (that will be replaced in BuddyList.__init__) and tells the ListAdapter that the cls to render the data should be a Label. So when we run this code, we see a scrollable list of Labelcode> objects each with their text attribute automatically set to one of the jabber ids we set up in the BuddyList initializer.

However, a Label still isn’t the right widget for rendering a buddy list item. We need to show more information in there, like whether they are available or away and what their status message is. A label won’t cover this, at least not cleanly, so let’s start writing a custom widget instead.

First, add a BuddyListItem class to the __main__.py:

(commit)

from kivy.properties import StringProperty  # At top of file

class BuddyListItem(BoxLayout):
    text = StringProperty()

By default, ListAdapter passes a text property into the widget that is used as its cls. So we hook up such a property which will be rendered by the label in the KV language file. This is a StringProperty, which behaves like any other property but is restricted to character content. Now we can use this class in the KV Language file:

(commit)

# at top of file:
#:import ok __main__

<BuddyListItem>:
    size_hint_y: None
    height: "100dp"
    Label:
        text: root.text

<BuddyList>:
    list_view: list_view
    ListView:
        id: list_view
        adapter: la.ListAdapter(data=[], cls=ok.BuddyListItem)

Don’t forget to make the adapter cls point at the new BuddyListItem class. You can also remove the Label import at the top.

We set a height property on the BuddyListItem mostly just to demonstrate that the custom class is being used when we run the app. Notice also how the label is referencing the root.text property we set up in the python file.

This property is being set, seemingly by magic, to the value from the data list we set on ListAdapter. In fact, it’s not magic; Kivy is just doing a lot of work on our behalf. For each string in that list, it’s constructing a new BuddyListItem object and adding it to that scrollable window. It then sets properties on that object using its default arg_converter. The arg_converter is responsible for taking an item from a data list and converting it to a dictionary of properties to be set on the cls object. The default is to simply take a string and set the text property to that string. Not so magic after all.

And now we’re going to make it less magic. Instead of a text property, let’s make a few properties that better reflect the kind of data we want to display:

(commit)

class BuddyListItem(BoxLayout):
    jabberid = StringProperty()
    full_name = StringProperty()
    status_message = StringProperty()
    online_status = StringProperty()

Don’t try running this without updating the KV file, since it’s trying to reference a text property that is no longer there:

(commit)

<BuddyListItem>:
    size_hint_y: None
    height: "40dp"
    Label:
        text: root.jabberid
    Label:
        text: root.full_name
    Label:
        text: root.status_message
    Label:
        text: root.online_status

Of course, running it in this state will bring us an empty window, since the ListAdapter is still using the default args_converter. Let’s add a new method to the BuddyList class. Make sure you’re sitting down for this one, it’s a bit complicated:

(commit)

def roster_converter(self, index, jabberid):
    result = {
        "jabberid": jabberid,
        "full_name": self.app.xmpp.client_roster[jabberid]['name']
    }

    presence = sorted(
        self.app.xmpp.client_roster.presence(jabberid).values(),
        key=lambda p: p.get("priority", 100), reverse=True)

    if presence:
        result['status_message'] = presence[0].get('status', '')
        show = presence[0].get('show')
        result['online_status'] = show if show else "available"
    else:
        result['status_message'] = ""
        result['online_status'] = "offline"

    return result

Deep breaths, we’ll explore this code one line at a time. First, the new method is named roster_converter and takes two arguments, the index of the item in the list (which we’ll ignore for now), and the jabberid coming in from the data list. These are the normal arguments for any ListAdapter args_converter in Kivy.

Next, we start constructing the result dictionary that we’ll be returning from this method. Its keys are the properties in the BuddyListItem class. It’s values, of course, will be displayed on the window.

Then we ask sleekxmpp to give us presence information about the user in question. This is not a trivial piece of code. I don’t want to spend a lot of time on it, since it involves ugly Jabber and sleekxmpp details, and our focus is on Kivy. In fact, you can skip the next paragraph if you’re not interested.

First, sleekxmpp’s client_roster.presence method returns a dictionary mapping connected resources to information about that resource. For simplicity, we’re ignoring resources here, but the basic idea is that you can be connected from two locations, say your cell phone and your laptop, and you’ll have two sets of presence information. Since we don’t care about resources, we ignore the keys in the dictionary and ask for just the values(). However, we still need to pick the “most important” resource as the one that we want to get presence information from. We do this by wrapping the list in the sorted function. Each resource has an integer priority, and the highest priority is the resource we want. So we tell the sorted function to sort by priority, passing it a key argument that gets the priority using a lambda function. The function defaults to 100 if the resource doesn’t specify a priority; thus we prefer an empty priority over any other value. The reversed keyword puts the highest priority at the front of the list.

Seriously, I hope you skipped that, because it’s a lot of information that is not too pertinent to this tutorial, even if the code it describes is. It took me a lot of trial and error to come up with this code, so just be glad you don’t have to!

So, assuming a list of resources is returned for the given jabber id, we pick the front one off the list, which is the one deemed to have highest priority. We then set the status_message directly from that item, defaulting to an empty string if they user hasn’t set one. The show key in this dict may return the empty string if the user is online (available) without an explicit status, so we check if the value is set and default to “available” if it is not.

However, there’s a chance that call returns a completely empty list which tells us what?: That the user is not online. So we have that else clause constructing a fake presence dict indicating that the user is offline.

Finally, things become simple again. We return the constructed dictionary that maps property names to values, and let Kivy populate the list for us. All we have to do for that to happen is tell Kivy to use our custom args_converter instead of the default one. Change the adapter property in the KV Language file to:

(commit)

adapter:
    la.ListAdapter(data=[], cls=ok.BuddyListItem,
    args_converter=root.roster_converter)

Notice that I split the command into two lines (indentation is important, if a bit bizarre.) in order to conform to pep 8‘s suggestion that line length always be less than 80 characters.

Now, finally, if you run this code and login, you should see all the information about your buddies pop up! However, it’s still a wee bit ugly. Let’s find some ways to spice it up.

One simple thing we can do is highlight different rows by giving each one a background color. We can start by adding a background attribute as an ObjectProperty to the BuddyListItem. Then we can set this value inside the roster_converter:

(commit)

if index % 2:
    result['background'] = (0, 0, 0, 1)
else:
    result['background'] = (0.05, 0.05, 0.07, 1)

Remember that index parameter that is passed into all args_converters and we were ignoring. Turns out it comes in handy. We can take the modulo (that’s the remainder after integer devision if you didn’t know) 2 of this value to determine if it’s an even or odd numbered row. If it’s odd (the remainder is not zero), then we set the background to black. Otherwise, we set it to almost black, a very deep navy. The background color is set as a tuple of 4 values, the RGBA value (Red, Green, Blue, Alpha). Each of those takes a float between 0 and 1 representing the percentage of each of those to add to the color.

Now, most Kivy widgets don’t have a background attribute, so we’re going to have to draw a full screen rectangle on the widget’s canvas. This is great news for you, because we get to cover the canvas or graphics instructions, something this tutorial has neglected so far. The canvas is a drawing surface that accepts arbitrary drawing commands. You can think of it as lower level than widget, although in reality, widgets are also drawn on the canvas, and every widget has a canvas. Here’s how we set up a rectangle on the BuddyListItem canvas:

(commit)

canvas.before:
    Color:
        rgba: self.background
    Rectangle:
        pos: self.pos
        size: self.size

I put this code above the labels in the BudyListItem. The canvas.before directive says “issue this group of commands before painting the rest of the widget”. We first issue a Color command, setting its rgba attribute to the 4-tuple we defined as the background. Then we issue a Rectangle instruction, using the currently set Color and setting the position and size to be dynamically bound to the size of the window. If you resize the window, the rectangle will be automatically redrawn at the correct size.

Another thing I want to do to improve the interface is render status messages underneath the username. They could go in a smaller font and maybe different color. This can be done entirely in KV Language:

(commit)

<BuddyListItem>:
    size_hint_y: None
    height: "75dp"
    canvas.before:
        Color:
            rgba: self.background
        Rectangle:
            pos: self.pos
            size: self.size
    
    BoxLayout:
        size_hint_x: 3
        orientation: 'vertical'
        Label:
            text: root.jabberid
            font_size: "25dp"
            size_hint_y: 0.7
        Label:
            text: root.status_message
            color: (0.5, 0.5, 0.5, 1.0)
            font_size: "15dp"
            size_hint_y: 0.3
    Label:
        text: root.full_name
    Label:
        text: root.online_status

I changed the height of the rows to be more friendly. The canvas I left alone, but I added a BoxLayout around two of the labels to stack one above the other in a vertical orientation. I made this BoxLayout 3 times as wide as the other boxes in the horizontal layout by giving it a size_hint_x. The two labels also get size hints, font size, and a color.

When we run this it looks… a little better. Actually, it still looks like ass, but this is because of my incompetence as a designer and has nothing to do with Kivy!

Now one more thing I’d like to do is render a status icon instead of the text “available”, “offline”, etc. Before we can do that, we need some icons. I wanted something in the public domain since licensing on this tutorial is a bit hazy. A web search found this library and I found some public domain icons named circle_<color> there. I grabbed four of them using wget:

(commit)

cd orkiv
mkdir icons
wget http://openiconlibrary.sourceforge.net/gallery2/open_icon_library-full/icons/png/48x48/others/circle_green.png -O icons/available.png
wget http://openiconlibrary.sourceforge.net/gallery2/open_icon_library-full/icons/png/48x48/others/circle_grey.png -O icons/offline.png
wget http://openiconlibrary.sourceforge.net/gallery2/open_icon_library-full/icons/png/48x48/others/circle_yellow.png -O icons/xa.png 
wget http://openiconlibrary.sourceforge.net/gallery2/open_icon_library-full/icons/png/48x48/others/circle_red.png -O icons/away.png

Once the files are saved, all we have to do to render them is replace the online_status label with an Image:

(commit)

    Image:
        source: "orkiv/icons/" + root.online_status + ".png"

And that’s it for part 5. I promised we would render a complete buddy list, and that’s what we’ve done! In part 6, we’ll make this list interactive. Until then, happy hacking!

Monetary feedback

If you are enjoying this tutorial and would like to see similar work published in the future, please support me. I plan to one day reduce the working hours at my day job to devote more time to open source development and technical writing. If you think this article was valuable enough that you would have paid for it, please consider supporting me in one or more of the following ways:

Finally, if you aren’t in the mood or financial position to help fund this work, you can always share it on your favorite social platforms.

9 Comments

  1. [...] Creating an Application in Kivy: Part 3 Creating an Application in Kivy: Part 5 [...]

  2. [...] Creating an Application in Kivy: Part 5 Creating an Application in Kivy: Part 7 [...]

  3. Pong says:

    hi, Dusty! thanks very much for you work, it’s helpful on me. but there is a error on my ubuntu. It’s take whole day from me.

    WARNING:kivy:stderr: Traceback (most recent call last):
    WARNING:kivy:stderr: File “__main__.py”, line 136, in
    WARNING:kivy:stderr: Orkiv().run()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/app.py”, line 600, in run
    WARNING:kivy:stderr: runTouchApp()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/base.py”, line 454, in runTouchApp
    WARNING:kivy:stderr: EventLoop.window.mainloop()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/core/window/window_pygame.py”, line 325, in mainloop
    WARNING:kivy:stderr: self._mainloop()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/core/window/window_pygame.py”, line 231, in _mainloop
    WARNING:kivy:stderr: EventLoop.idle()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/base.py”, line 294, in idle
    WARNING:kivy:stderr: Clock.tick()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/clock.py”, line 370, in tick
    WARNING:kivy:stderr: self._process_events()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/clock.py”, line 481, in _process_events
    WARNING:kivy:stderr: if event.tick(self._last_tick) is False:
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/clock.py”, line 280, in tick
    WARNING:kivy:stderr: ret = callback(self._dt)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/animation.py”, line 304, in _update
    WARNING:kivy:stderr: self.stop(widget)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/animation.py”, line 188, in stop
    WARNING:kivy:stderr: self.dispatch(‘on_complete’, widget)
    WARNING:kivy:stderr: File “_event.pyx”, line 281, in kivy._event.EventDispatcher.dispatch (/home/peng/code/orkiv/venv/build/kivy/kivy/_event.c:4152)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/uix/modalview.py”, line 184, in
    WARNING:kivy:stderr: a.bind(on_complete=lambda *x: self.dispatch(‘on_open’))
    WARNING:kivy:stderr: File “_event.pyx”, line 285, in kivy._event.EventDispatcher.dispatch (/home/peng/code/orkiv/venv/build/kivy/kivy/_event.c:4203)
    WARNING:kivy:stderr: File “__main__.py”, line 78, in connect_to_jabber
    WARNING:kivy:stderr: app.root.show_buddy_list()
    WARNING:kivy:stderr: File “__main__.py”, line 59, in show_buddy_list
    WARNING:kivy:stderr: self.buddy_list = BuddyList()
    WARNING:kivy:stderr: File “__main__.py”, line 30, in __init__
    WARNING:kivy:stderr: super(BuddyList,self).__init__()
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/uix/boxlayout.py”, line 102, in __init__
    WARNING:kivy:stderr: super(BoxLayout, self).__init__(**kwargs)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/uix/layout.py”, line 61, in __init__
    WARNING:kivy:stderr: super(Layout, self).__init__(**kwargs)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/uix/widget.py”, line 163, in __init__
    WARNING:kivy:stderr: Builder.apply(self)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/lang.py”, line 1429, in apply
    WARNING:kivy:stderr: self._apply_rule(widget, rule, rule)
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/lang.py”, line 1556, in _apply_rule
    WARNING:kivy:stderr: value, rule, rctx['ids'])
    WARNING:kivy:stderr: File “/home/peng/code/orkiv/venv/local/lib/python2.7/site-packages/kivy/lang.py”, line 1221, in create_handler
    WARNING:kivy:stderr: f.bind(**{k[-1]: fn})
    WARNING:kivy:stderr: TypeError: descriptor ‘bind’ of ‘kivy._event.EventDispatcher’ object needs an argument

    • Pong says:

      hey guys, I try to run this program out of the “venv” env and I got the correct result. but I don’t know why, maybe my kivy env is wrong.