Dusty Phillips: Creating an Application in Kivy: Part 6

24 Jul

Welcome back, or if you’ve just stumbled across this tutorial, welcome to the middle of something! You might want to jump back to the beginning if that’s the case. Don’t worry, we’ll wait for you to catch up.

We’re learning Kivy by developing a simple chat application together on the XMPP protocol. So far we’re able to display a Buddy List that doesn’t look too shabby, but we haven’t even tried chatting with anyone yet. Given the complexities of the sleekxmpp library, I’m only hoping that it won’t be too difficult!

If you want to start this part with a clean slate, in exactly the state that I’m starting it, you can clone my public repository:


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

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

Table Of Contents

Here’s some lovely navigation for you:

  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

About The Current Kivy ListView API

I’m afraid this article’s going to be going up a little late because I’ve spent a great deal of time trying to understand the ListView API for interacting with the user. I’ve read through all the documentation, I’ve read through all the examples included with Kivy, and I’ve read through the source code for all the views, adapters, and widgets. I’ve come to the conclusion that, unlike the elegance in most of Kivy, the ListView API simply sucks. I think my favorite aspect of the documentation was the statement, “Warning: This code is still experimental, and its API is subject to change in a future version.”

I did discover that Kivy’s ListView API provides all the tools to do what we want, but it’s mixed in with a whole bunch of tools that don’t work at all, don’t work the way you would expect, or are far more complicated than necessary. Figuring out which of those tools to use and how to use them was a perfect chance to practice skills for dealing with frustration.

The good news is that when I complained about the ListView API on the #kivy IRC channel I was told that the developers are already actively working on improving it!

I have tried, in these tutorials, to explain not only how to do things, but also how to figure out how to do things. I’m not going to do that for this part for a few reasons. First, there is no clear explanation on the web for “how to effectively use Kivy’s ListView API”, so I’d like this tutorial to fill that niche. Second, with the API redesign in the works, I don’t want to spend a lot of time writing outdated content. Third, the approach I used to understand the API was basic brute force, and not educational in any way. Read all the documentation, read all the sources, and write tons of test code. By trial and error and a bit of good judgment, I think you’ll come to the same conclusions I did. Finally, I spent so much time doing that brute force work that I can’t easily summarize the steps I followed or all the paths I explored.

How to Ignore Most of The Bad Bits

I’m sure you’re aching to see some code, but let’s discuss exactly what we want the interface to look like, first. While our BuddyListItem widget contains several other widgets, we want it to highlight the entire widget with a different background color when it is touched. Essentially, even though multiple items are being displayed on the widget, we want the whole widget to behave like a button. Later, we’ll want to pop up a chat window when the selection event happens.

That “behave like a button” line is the key. The Button class inherits from label and adds some things for button state and touch events. Then Kivy extends that with a ListItemButton class that manages the state of multiple buttons in a ListView. As the Kivy examples show, using this class as the cls argument to a ListView works flawlessly. You touch a button, it gets highlighted. Unfortunately, adding widgets to a button doesn’t work so well because Button is not a layout. If only we could make our BuddyListItem a button AND a layout. Hmmm:

(commit)


from kivy.uix.listview import ListItemButton # At top of file

class BuddyListItem(BoxLayout, ListItemButton):
jabberid = StringProperty()
full_name = StringProperty()
status_message = StringProperty()
online_status = StringProperty()
background = ObjectProperty()

This is multiple inheritance; BuddyListItem is inheriting from both the BoxLayout and ListItemButton superclasses. I honestly did not expect this to work with widgets, since the inheritance graph is a weird diamond. However, the Kivy devs have been careful to support multiple inheritance (by always passing **kwargs to super calls). When I ran this code and saw selectable buttons in my ListView, I said to myself, “Holy shit! It works!” Then I giggled softly and promptly forgave the Kivy team for the confusing API faux pas.

Now we get to do one of my favorite activities: Deleting code. One of the nice things we inherit from the Button class is a background_color property. This means we can take the entire canvas.before section out of orkiv.kv:

(commit)


<BuddyListItem>:
size_hint_y: None
height: "75dp"

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
Image:
source: "orkiv/icons/" + root.online_status + ".png"

Nothing new here at all, it’s just shorter! You can also remove the background property from __main__.py. Now, just change the arg_converter to set background_color instead of background and the button will look less buttony and more listitemy:

(commit)


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

Finally, I’m not too keen on the default red color for the selected item, so let’s change the selected_color (this is a property available on ListItemButton) in the Kivy file:

(commit)


<BuddyListItem>:
size_hint_y: None
height: "75dp"
selected_color: (0.2, 0.1, 0.2, 1.0)

Run this code and you should be able to touch any list item and see it highlighted. Now that’s what I call an easy way to display selected items on a ListView! Don’t take these as gospel, since your needs may differ, but here’s some ideas that I came up with while puzzling through the documentation and code for this API:

  • Don’t use DictAdapter. As far as I can tell, you can more easily use ListAdapter on the keys and reference the underlying dictionary directly from args_coverter, rather than trying to maintain the sorted_keys parameter.
  • As I mentioned in Part 5 don’t use the template argument to ListAdapter, as that functionality has been deprecated.
  • Pretend that CompositeListItem does not exist, never did exist, and could not possibly exist.
  • ListItemLabel is not interactive and is for display only, not selection.
  • While the documentation suggests that you can create your own item class, if you need selection, use ListItemButton. You can either extend it and add widgets as we did here, or put individual buttons inside another view, depending on your requirements.

  • If you don’t use ListItemButton and you need selection, you need to make sure your object provides a on_release event, since that is what ListView is hardcoded to trigger selection on.
  • propagate_selection_to_data and SelectableDataItem add a lot of complexity and are only worth it if you really know they are worth it. Don’t “avoid them at all costs”, but make sure you really need them before trying.

Designing a Chat window

I have an exercise for you that I don’t think you’ll have any trouble with if you’ve followed the previous parts of this series. Of course, I’ll provide examples and explanations shortly if you get stuck.

Here’s the exercise: Create an empty new ChatWindow class that extends BoxLayout. Style the class in the KV language so that it contains a vertical box layout holding a label identifying the user you are chatting with, a second label containing the text of the conversation, and a horizontal box layout containing a Button to return to the buddy list, a TextInput and a second Button to send chat text. Try to figure out which of these widgets need to have an id in the KV Language file hooked up to an ObjectProperty in the python file. Finally, ensure the widgets have appropriate size and size_hint properties to look reasonable.

Here’s how my ChatWindow code turned out:

(commit)


class ChatWindow(BoxLayout):
jabber_id = StringProperty()
chat_log_label = ObjectProperty()
send_chat_textinput = ObjectProperty()

And here’s my first attempt at styling. It’s a compliment to Kivy’s simplicity and elegance that when I actually hooked up the events to display this code, it looked exactly like I expected it to:

(commit)


<ChatWindow>:
orientation: "vertical"
chat_log_label: chat_log_label
send_chat_textinput: send_chat_textinput
Label:
text: "chatting with " + root.jabber_id
size_hint_y: None
height: "40dp"
Label:
id: chat_log_label
BoxLayout:
size_hint_y: None
height: "50dp"
Button:
size_hint_x: None
width: "70dp"
text: "Buddies"
TextInput:
id: send_chat_textinput
Button:
size_hint_x: None
width: "70dp"
text: "Send"

Now we need an event to display a ChatWindow when a user makes a selection. Kivy’s ListView API has the ability to propagate selection events to the adapter and even the model behind the adapter. However, it’s rather involved and I suggest avoiding it unless you actually have a reason to store the advanced selection stuff. Unfortunately, this means you can’t listen to the adapter’s on_selection_change event.

However, for our use case, all we really want to do is show the chat window if the user touches an item in the list. Hence, we just need to listen for a touch event on the BuddyListItem (which extends Button and therefore has all of Button‘s events) and react to that.

The reaction will be to display a new ChatWindow object on the root. This means adding a new method to OrkivRoot:

(commit)


def show_buddy_chat(self, jabber_id):
self.remove_widget(self.buddy_list)
chat_window = ChatWindow(jabber_id=jabber_id)
self.add_widget(chat_window)

When called, this code will simply clear the window and display a new, empty chat window in it’s place. One thing I may not have mentioned before is how ChatWindow accepts jabber_id as a keyword argument, even though there is no __init__ method that sets the argument. This is because Kivy automatically maps keyword arguments to Property objects on the class or any parent classes, whether we defined those properties in our code or they were defined by the Kivy widget itself.

Now all we have to do is hook up the on_release event on the BuddyListItem in the KV Language file. Note that if you are doing more complicated selection, you may want to hook up an on_state handler instead. It will be called whenever the state property changes (for selection or deselection).

While we’re at it, we can also add a similar event to render the buddy list again when the Buddies button is clicked inside the ChatWindow:

(commit)


<BuddyListItem>:
size_hint_y: None
height: "75dp"
selected_color: (0.2, 0.1, 0.2, 1.0)
on_release: app.root.show_buddy_chat(self.jabberid)

and


Button:
size_hint_x: None
width: "70dp"
text: "Buddies"
on_release: app.root.show_buddy_list()

Now, if we run the app, we can switch between the buddy list and a chat view whenever we want. This will be useful for a phone-sized layout, and in a later tutorial we’ll make it possible to switch to a side-by side view for tablets and desktops.

However, this isn’t an optimal code design. First, it’s wasteful of resources; each time, we go back to the Buddy List view, the list is constructed afresh, from scratch. Worse, new chat windows are also constructed each time we go back to a jabberid, meaning that any previous conversation would be lost. Let’s fix both these things in the OrkivRoot class:

(commit)


class OrkivRoot(BoxLayout):
def __init__(self):
super(OrkivRoot, self).__init__()
self.buddy_list = None
self.chat_windows = {}

def show_buddy_list(self):
self.clear_widgets()
if not self.buddy_list:
self.buddy_list = BuddyList()
for buddy_list_item in self.buddy_list.list_view.adapter.selection:
buddy_list_item.deselect()
self.add_widget(self.buddy_list)

def show_buddy_chat(self, jabber_id):
self.remove_widget(self.buddy_list)
if jabber_id not in self.chat_windows:
self.chat_windows[jabber_id] = ChatWindow(jabber_id=jabber_id)
self.add_widget(self.chat_windows[jabber_id])

We added an initializer that sets a couple variables for us. In the show_buddy_list method, we now instantiate the BuddyList only if it has not been previously created. However, this means that selection is kept between calls, so we have to loop over the selected items and deselect them.

This might need some explaining. The ListView doesn’t know anything about selection; that is kept track of in the ListAdapter. The selection property is a list containing all BuddyListItem objects that are currently selected. Because BuddyListItem is a ListItemButton, which provides the SelectableView, interface, it has a deselect method that we can use to disable selection of that item.

The chat_windows dictionary maps jabber_id keys to individual ChatWindow objects, any of which can be displayed when the associated user is touched in the buddy list.

Sending a Message

Ok, now that our chat text box is looking pretty sweet and we can switch between buddies at will, let’s try actually sending a message! Add a new method, send_message to the ChatWindow widget in __main__.py:

(commit)


import datetime # At top of file

def send_message(self):
app = Orkiv.get_running_app()
app.xmpp.send_message(
mto=self.jabber_id,
mbody=self.send_chat_textinput.text)
self.chat_log_label.text += "(%s) Me: %s\n" % (
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
self.send_chat_textinput.text)
self.send_chat_textinput.text = ''

This is actually fairly simple code considering how exciting it is to send a message and have it show up on a different account when we run it. First we get the app and instruct the xmpp object to send a message. I got this code from the SleekXMPP documentation. Then, while that message is finding it’s way to our other chat client, we update the chat_log_label text so that it contains a new line with the current date and the text we sent. Finally, we update the send_chat_textinput to be empty so we can send another value right away.

Now we just need to invoke this method whenever the send button is clicked. I’m sure you can figure out how to do that by yourself. It’s just one line of code in the Kv Language file:

(commit)


Button:
size_hint_x: None
width: "70dp"
text: "Send"
on_release: root.send_message()

Now for the fun part. If you have two different jabber accounts, connect your normal instant messenger to one of them, and then log into Orkiv with the other one. Send a message to the “normal” messenger and giggle manically when it arrives. This is why we program: the success is so tangible!

And I’ll leave you with that success for today. In the next part, we’ll hook up the message receiving side of things and make several interface improvements to make the app much more usable.

Monetary feedback

In previous parts of this tutorial, I’ve asked users to support me financially if they’d like to see more of it. I’m trying to gauge whether it is feasible for me to consider switching from full time work to being supported entirely by open source development, writing, and education in the (far) future.

Instead, today, I’d like to take some time to thank those of you who have answered my call. It’s impossible to tell how many book sales come from readers if these essays, or how many Gittips were prompted by this work rather than my various other open source projects, but as both have increased in the weeks that I’ve been publishing this series, it’s clear that some of you support my work. I would like to thank you from the bottom of my heart. I truly believe that an economy based on paying what the buyer, rather than the seller, thinks something is worth, will one day lead to a happier, more peaceful world.

I’d also like to thank all of you who have tweeted, commented, blogged, or otherwise spread the word about these articles. I think they are one of the best available resources for new Kivy developers outside of the excellent Kivy documentation itself. I am exhilarated to know that so many of you agree!

via Planet Python http://archlinux.me/dusty/2013/07/24/creating-an-application-in-kivy-part-6/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 77 other followers

%d bloggers like this: