I don’t know about you, but I was pretty excited at the end of Part 6 when I was able to send a message to myself on another account. It’s taken several weeks to get here, but by the end of this part, we’ll have a [mostly|barely] working jabber client that can send and receive messages and isn’t too annoying to interact with.
If you haven’t been following along or want to start this example at the same point at which I’m starting, you can clone the github repository with my examples:
git clone https://github.com/buchuki/orkiv.git git checkout end_part_six
Remember to create and activate a virtualenv populated with the appropriate dependencies, as discussed in part 1.
Table Of Contents
If you’ve never thought about it before, let me tell you: maintaing a table of contents on WordPress is a royal pain in the ass.
- Part 1: Introduction to Kivy
- Part 2: A basic KV Language interface
- Part 3: Handling events
- Part 4: Code and interface improvements
- Part 5: Rendering a buddy list
- Part 6: ListView Interaction
- Part 7: Receiving messages and interface fixes
- Part 8: Width based layout
- Part 9: Deploying your Kivy application
Receiving messages with SleekXMPP isn’t quite as simple as sending them, but it’s pretty damn simple! SleekXMPP has an event handler, not unlike the one Kivy provides, but don’t confuse them! SleekXMPP is running in a separate thread and doesn’t know or care that Kivy is running it’s own event loop. Further, they use different APIs, which basically means that they use different method names to accomplish similar things, like issuing an event or listening to receive one.
Thus, in order to not be confused with Kivy Events, we don’t call our handler
on_message or something in that. Instead, we add a
handle_xmpp_message method to the
def handle_xmpp_message(self, message): if message['type'] not in ['normal', 'chat']: return jabber_id = message['from'].bare if jabber_id not in self.chat_windows: self.chat_windows[jabber_id] = ChatWindow(jabber_id=jabber_id) self.chat_windows[jabber_id].chat_log_label.text += "(%s) %s: %s\n" % ( datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), jabber_id, message['body'])
When called by SleekXMPP, this method is passed a message object, which is some kind of stanza object. I didn’t bother to fully understand how this object is constructed; instead I stole some code from the SleekXMPP quickstart and then introspected it by printing stuff to the terminal when the handler is called. It can often be useful to
print(dir(message)) to see what kind of attributes and methods an unknown object has.
print(type(message)) can also be useful in that knowing the name of a class can help figure out where to look in the documentation or source code.
So after a wee bit of trial and error, I came up with the above method. It first checks the type, and gets the
bare attribute off the
jabber_id (the documentation helped here). Then, if the chat window does not exist, we create it, the exact same way we did when the user created the object. Even if the user is not currently looking at the chat, the message will show up in it. We then append the new message to the label using the same format we used when sending messages.
Now, hook up the event handler at the moment the xmpp object is constructed in the
connect_to_jabber method of the
Orkiv. It just takes one new line of code at the end of the method:
We’ll make the label look a bit cleaner shortly, but first: test it! Start chatting with your jabber friends. Tell them how you’ve written your own jabber client using Python and Kivy. If they aren’t sufficiently impressed, find new friends!
Cleaning up duplicate code
There’s a couple pieces of code in our
ChatWindow classes that look very similar to each other. First, the code for creating a new chat window only if one doesn’t exist is identical. And the code for formatting a new chat message and appending it to the chat log is also congruent.
It is a good idea to remove pieces of duplicate code as soon as you notice them. Duplicate code is a “code smell”; it indicates that your design is not good. From a practical point of view, it means maintenance can be difficult if you decide to change something (say the formatting of the date on a chat message) and forget to update multiple places at once. Of course right now you remember it’s in two places, but six months from now when these two classes have been refactored into separate files, you might not remember so easily.
There are a variety of patterns to remove duplicate code, and each situation can be different. The easiest thing to do is to refactor the things that stay the same into a function and pass the things that change in as arguments. Or you might be able to use object oriented programming and inheritance. Python’s decorator syntax or context managers can also be extremely useful for reducing duplicate code. The point is, as soon as you’ve hit the copy-paste key combination, you should start thinking about the most effective API that can be designed to remove redundancy.
In this case, we can just add a couple new methods, one to
OrkivRoot and one to
ChatWindow. Let’s start by refactoring the code that creates a chat window only if it doesn’t exist:
def get_chat_window(self, jabber_id): if jabber_id not in self.chat_windows: self.chat_windows[jabber_id] = ChatWindow(jabber_id=jabber_id) return self.chat_windows[jabber_id] def show_buddy_chat(self, jabber_id): self.remove_widget(self.buddy_list) self.add_widget(self.get_chat_window(jabber_id)) def handle_xmpp_message(self, message): if message['type'] not in ['normal', 'chat']: return jabber_id = message['from'].bare chat_window = self.get_chat_window(jabber_id) chat_window.chat_log_label.text += "(%s) %s: %s\n" % ( datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), jabber_id, message['body'])
All we did was add a new method that gets the chat window from the dictionary, creating one if it doesn’t exist. Then we simplify the two methods that were previously performing this task to simply call that method. Those methods are now easier to follow. The basic principle is to abstract the complex code into it’s own function so that calling code is more readable.
Now let’s do the same thing for message formatting by adding a
append_chat_message method to the
def send_message(self): app = Orkiv.get_running_app() app.xmpp.send_message( mto=self.jabber_id, mbody=self.send_chat_textinput.text) self.append_chat_message("Me:", self.send_chat_textinput.text) self.send_chat_textinput.text = '' def append_chat_message(self, sender, message): self.chat_log_label.text += "(%s) %s: %s\n" % ( sender, datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), message)
We also make a similar change to the code in
OrkivRoot that handles incoming text events:
def handle_xmpp_message(self, message): if message['type'] not in ['normal', 'chat']: return jabber_id = message['from'].bare chat_window = self.get_chat_window(jabber_id) chat_window.append_chat_message(jabber_id, message['body'])
Let’s make some improvements to that chat window. Start by modifying the method we just added to include some color to help the reader identify who sent what. The Label class has a feature that allows you to include markup on the label. Unfortunately, it has it’s own microsyntax instead of using any standard markup languages. This is good because it allows the language to target the things that Kivy labels can do, but not so great in that you have yet another markup to learn. (My life would be much easier if I was not constantly switching between ReST, Markdown, Trac wiki syntax, Mediawiki syntax, and HTML! Except for HTML, I’ve never really been able to learn or distinguish them, so I have to look up the syntax every time I write it.)
The Kivy label syntax is reminiscent of bbcode popular on web forums, so if you’ve used that, you should be comfortable. I’m not going to go into detail of the syntax, since the documentation does a great job of that, but I will show you how to use colors and bold text to make our
ChatWindow label much more eye-pleasing:
from kivy.utils import escape_markup # 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.append_chat_message("Me", self.send_chat_textinput.text, color="aaffbb") self.send_chat_textinput.text = '' def append_chat_message(self, sender, message, color): self.chat_log_label.text += "[b](%s) [color=%s]%s[/color][/b]: %s\n" % ( datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), color, escape_markup(sender), escape_markup(message))
We added a color parameter to
append_chat_message. It needs to be a hextet like HTML colors. Then we used the markup syntax to make the usernames colored and the dates and usernames bold. We also escape the message and sender strings so that users don’t get confused if they accidentally include Kivy formatting tags.
Remember to add the color parameter when calling this function from the
handle_xmpp_message method on
chat_window.append_chat_message(jabber_id, message['body'], color="aaaaff")
Finally, this won’t actually render as markup until we tell the label that we want markup rendered. This can be done easily by setting the
markup property on the label in the KV Language file:
Label: markup: True id: chat_log_label
Now if you run the program and chat with someone, you can easily distinguish who said what! However, if your chat gets too long, it gets truncated, both horizontally and vertically. Let’s make it scroll using a Kivy
ScrollView. This can be done entirely in
orkiv.kv, though it’s not trivial to get it right:
ScrollView: Label: size_hint_y: None text_size: (root.width, None) size: self.texture_size markup: True id: chat_log_label
In addition to wrapping the label in a scroll view, we also had to set a few sizing properties on the label. The
size_hint_y is set to
None or else the label would always be exactly the same size as the
ScrollView. We want it to be bigger than the scroll view, or else there wouldn’t be anything to scroll!. In fact, we want it to be exactly the same size as the
texture_size, which gets updated whenever lines get added to the
Label. Finally, to make the lines wrap (because we don’t want to do ugly horizontal scrolling), we set the
text_size to have the same width as the parent.
This can be a bit confusing, so let me explain, hopefully correctly (I’m a bit confused, myself, to be honest). First, we have a
ScrollView which does not have an explicit size, so it’s size is set for us by the parent
ScrollView contains a
Label, which contains a
Texture. The height of the label is set to be the height of the texture, which is calculated from the number of lines being displayed, and updated dynamically. Further, the width of the text rendered on the texture is constrained to be the width of the root widget, which forces the text to wrap. When the text wraps, the texture size gets bigger, and the label gets bigger because it’s bound to the texture size. and the size of the label can be as big as it wants since it’s scrollable.
These changes have the side effect of left-aligning chat, which I was expecting to have to do manually. Go Kivy! One more thing I want to do is scroll the window to the bottom if there’s any new text. This is easily done by adding a single line to the
def append_chat_message(self, sender, message, color): self.chat_log_label.text += "[b](%s) [color=%s]%s[/color][/b]: %s\n" % ( datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), color, escape_markup(sender), escape_markup(message)) self.chat_log_label.parent.scroll_y = 0.0
scroll_y attribute on
ScrollView is basically a percentage. it returns the current scroll position if you get it, and if you set it, it will scroll the window; 0 is the bottom, 1 is the top, and anything in between is… well, anything in between.
Now I’d like to have the form send a message on an enter keypress, not just when I click the button. This has been annoying me in testing this app and annoyances should be addressed. We can use the same technique we used in part 4. However, instead of hard-coding a call when the enter key is pressed, let’s create a new reusable widget that fires an event when enter is pressed. We’ve had quite a bit of experience binding functions to events, but we haven’t yet tried creating an event of our own.
We’ll call the new widget
EnterTextInput. Binding and dispatching events is actually surprisingly simple, so our new class is pretty short:
class EnterTextInput(TextInput): def __init__(self, **kwargs): self.register_event_type("on_enter_key") super(EnterTextInput, self).__init__(**kwargs) def _keyboard_on_key_down(self, window, keycode, text, modifiers): if keycode == 13: # 13 is the keycode for <enter> self.dispatch("on_enter_key") else: super(EnterTextInput, self)._keyboard_on_key_down( window, keycode, text, modifiers) def on_enter_key(self): pass
We explicitly register the new event type with the
EventDispatcher, which is a superclass of
Widget (and therefore, a superclass of
TextInput) and provides the methods associated with dispatching events. Then we use a
super() call to do the parent initialization.
Next, we override
_keyboard_on_key_down, just as we did in the
AccountDetailsTextInput class. If the user pressed enter, we
dispatch our custom event; otherwise we just let the superclass do it’s thing.
Now we have to hook up that event. I want to do that on the
ChatWindow text entry, but first, let’s remove some more code duplication. You might not have noticed, but when I created the
_keyboard_on_key_down method above, I copy-pasted some code from the
AccountDetailsTextInput. It turns out we can make our
AccountDetailsTextInput class extend the new code to reduce code duplication. That means if we make any improvements to the
EnterTextInput class in the future, the subclass will get them for free. Here’s the new version of
class AccountDetailsTextInput(EnterTextInput): next = ObjectProperty() def _keyboard_on_key_down(self, window, keycode, text, modifiers): if keycode == 9: # 9 is the keycode for <tab> self.next.focus = True else: super(AccountDetailsTextInput, self)._keyboard_on_key_down( window, keycode, text, modifiers) def on_enter_key(self): self.parent.parent.parent.login() # this is not future friendly
Basically, we just removed the code for dealing with the enter key and add an event handler that does the same thing when the enter key is pressed. It’s a simple refactor, and arguably isn’t terribly more readable than the original version. However, it is more maintainable. For example, if in the future we discover that an enter key can generate different keycodes in different countries, we only have to fix it in one place.
Now let’s also hook up the new event on our
ChatWindow in the KV language file. Change the
TextInput to a
EnterTextInput and hook up the new event:
EnterTextInput: id: send_chat_textinput on_enter_key: root.send_message()
That’s much better! However, there’s still a bit to be desired in terms of notifications. Let’s make Kivy play a sound whenever a message comes in! I grabbed a creative commons sound (attributed to User TwistedLemon) from Free sound and saved it as
orkiv/sounds/in.wav. You can also steal a sound from your favourite IM program or record something yourself. Playing the sound is as easy as loading it in the
from kivy.core.audio import SoundLoader # At top of file self.in_sound = SoundLoader.load("orkiv/sounds/in.wav")
And playing it in the
We’re running a bit short on space for this week, but there’s one more thing I’d like to do. Right now, when we receive a notification, it’s impossible to know who sent the message, unless we currently have that chat window open. Let’s make our
BuddyList highlight those users who have new messages.
The plan I have for this is not the best plan, as it doesn’t clearly separate data from display. We’re adding a new piece of data for each jabber_id (whether or not it has new messages), but instead of creating a proper data model, I’m just going to store that data in a set. Remember, the args_converter is drawing data from the xmpp presence and other locations, and the attribute of whether something is selected is stored in the state of the
BuddyListItem, so in some ways, the args_converter IS a data model.
It’s not elegant. However, making it elegant requires both that the
ListView API be properly designed, and that we stop abusing it to take only the parts we want. So let’s go with simple for now!
Start by adding an attribute to the
BuddyList.__init__ method. It will be a set object, and will contain only jabber_ids that have new messages:
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()) self.new_messages = set()
We use a
set instead of a
list because sets are more efficient when we want to ask the question “is this object in the set”. We aren’t going to be doing that millions of times and the size of the set isn’t going to be tens of thousands of objects, so it’s not a really important distinction for this code. However, it’s good to get in the habit of using and knowing which data structures to use for which tasks. At some point in your programming career, you will want to study up on “Algorithms Analysis” and “Data Structures” to help you make these decisions. For now, just remember that any time you are keeping a collection of objects so you can repeatedly use the
in keyword on it, use a set. Any time you need to know the order of objects, use a list.
Before we start adding messages to this set, let’s create a method on
BuddyList that forces the listview to redraw itself. This method shouldn’t really be required if we were using the ListView API properly and it was working properly, but we aren’t and it’s not:
def force_list_view_update(self): self.list_view.adapter.update_for_new_data() self.list_view._trigger_reset_populate()
Now we need to add users to the set whenever we get a new message in
handle_xmpp_message, telling the
BuddyList to redraw itself as we do so:
if chat_window not in self.children: self.buddy_list.new_messages.add(jabber_id) self.buddy_list.force_list_view_update()
We only add the “new message” notification if the chat window is not the currently displayed one. If it is, then the new message shows up on the screen immediately, so it’s not unread.
Similarly, when the user selects a new buddy from the buddy list, we need to clear the new message status for that user:
def show_buddy_chat(self, jabber_id): self.remove_widget(self.buddy_list) self.add_widget(self.get_chat_window(jabber_id)) self.buddy_list.new_messages.discard(jabber_id) self.buddy_list.force_list_view_update()
discard method, unlike
remove does not raise an exception if the item is not in the set.
Of course, running this code won’t actually highlight anything. Knowing that the jabber_id has a new message doesn’t help us unless we do something with that knowledge. Let’s replace the background color if statement in
BuddyList.roster_converter with the following:
if jabberid in self.new_messages: result['background_color'] = (0.6, 0.4, 0.6, 1) else: result['background_color'] = (0, 0, 0, 1) if index % 2: result['background_color'] = (x + .3 for x in result['background_color'])
Basically, if it’s a new message, we give it a new color. But then, to maintain the alternating colors of list items, we add 0.3 to the colors in every other row. Overall, the interface is working pretty well.
Next time, we’ll see if we can get it to render the chat window beside the buddy list, if the window is wide enough! However, please note that it’s going to be a couple weeks before I can publish that article; I’m on vacation this weekend and the following weekend I’ll be at Pycon Canada (look me up if you’re there!).
While, in previous parts of this tutorial, I’ve been soliciting funds to support my continued involvement in writing and open source projects, this week, I’m going to ask you to fund Mathieu Virbel instead. Mathieu is the inventor and (utterly brilliant) lead developer of Kivy. The more time he can spend developing Kivy, the better it is for all of us (including me)!
Or just promote Kivy and my books and articles on your favourite social media platforms. Spread the word, it’s always appreciated!