Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Diving Into Events and Actions

Resource mentioned in this lesson: Textual’s Actions Guide

00:00 In the previous lesson, I showed you how to use the grid container to do widget layouts. This lesson dives deeper into events and their shortcut helpers called actions.

00:10 You’ve already seen how widgets emit events like when you click a button or press a key. You’ve also seen how you use methods with an on_ prefix to declare a handler for these events.

00:21 Textual also has a shortcut mechanism for some of the more common events to help simplify your code. They’re called actions. And in a similar fashion to event handlers, they are methods with an action_ prefix.

00:34 Once you’ve declared an action method, you can call it directly through the app’s run_action() method, but there is another way as well.

00:42 If you’re going to support a lot of key presses in your app, your on_key() method can get rather bulky. Instead, you can declare an action for each key press, then register it using the BINDINGS class property.

00:54 This takes a list of tuples with each tuple being a triple containing the key to bind, a string representing the action, and a label for help information.

01:04 The string representing the action looks just like a function call, and it’s done this way instead of using actual function references so that you can pass in arguments as well.

01:13 In a moment, I’ll show you how these work, but first, a quick tangent. In the code so far, if you’ve wanted to do something with a widget, you’ve stored it away in your object for later use.

01:24 There’s another way of getting at it though. You can search for it. As you often need to associate an ID with a widget anyways for CSS styling purposes, instead of keeping a long list of all your widgets, you can search by that ID instead.

01:38 Textual supports searching using CSS selectors. If you’ve ever done any jQuery or similar JavaScript libraries, this is the same idea. To look for a widget with a given ID, you would search using # and that ID name just like the rule declaration part of your CSS style.

01:56 Textual has several query methods, all of which take this kind of CSS selector. The most general one is .query(), which returns an iterable of widgets that match the query, but often when you’re searching, you’re only looking for a single thing like a widget with a specific ID.

02:13 If you use .query_one(), you get only one thing back saving you from having to dereference the first thing in the iterable returned by the more general .query() method.

02:23 If more than one widget matches your selector, .query_one() returns the first thing. If there isn’t supposed to be more than one match, you might consider using .query_exactly_one() instead.

02:35 It does the same thing as .query_one(), but if there’s more than one match, it raises an exception. As you’re supposed to keep ID values unique, this makes it more explicit when you’re searching.

02:46 All of these methods are available both on your app as well as on the widgets. The next two methods do relative searching. .query_ancestor() looks above your widget in the hierarchy for a match, while .query_children() looks within the widget.

03:00 Let’s go build a new app with an action, and in it I’ll query my widgets instead of storing them.

03:07 Okay, here is my action program. It includes a button and two widgets you haven’t seen before: a Digits display and a Footer. When you click the button, it increments the value shown in the Digits display and then on top of that, the app will support a hotkey for changing the border of the display as well.

03:27 The hotkey is b, and when you press it, the border style of the digits display changes. As I want to flip back and forth, I’m going to use the cycle() function from itertools.

03:37 If you’ve never seen this before, it returns an unending iterable always giving you the next value, looping back to the first value when it runs out. In addition to using the on_ prefix for handlers, you can also use the @on decorator. With it, you declare a method and then use the decorator to register that against the event.

03:58 And here are those three widgets I was talking about. The Digits display is a text display that looks a little bit like a calculator window, and the Footer is a widget for the bottom of your app.

04:09 The Footer has a neat feature in that it automatically shows the hotkeys that you’ve defined, which is why I’m using it here.

04:16 This is the BINDINGS property I spoke about. In it, I’m registering two hotkey actions. The first binds the q key to the quit action. The quit action is built-in, so you know that on_key() and match thing that I’ve been doing so far?

04:33 Well, all of that can be replaced with this single line. The first part of the tuple is the letter q, which is what we’re binding it to. The second part is the action, the built-in quit, and then the third part is a help message.

04:47 This is my second hotkey. When you press the b key, the toggle_border action will get called. Remember, that means a method named action_ toggle_border is what gets executed. In order to count button clicks, I need a counter.

05:02 This is it, and this is my endless iterator. Let’s examine it from the inside out. It starts with a list of tuples with each tuple being a border description.

05:14 The first one is a double-sized border colored yellow, and the second one is a solid white line. If you just used this list and wanted to iterate it, when you ran out of borders, you’d have to start again.

05:25 That would mean a few extra lines of code to track it all. Instead, you can use the cycle() function from the itertools module.

05:32 This returns an endless iterator. Every time you call next() on it, you get the next thing in the iterator, cycling back to the beginning when it reaches the end.

05:41 Since our list only has two things, that means it will always switch back and forth between our two border styles.

05:48 The compose() is similar to those you’ve seen before, starting with a button, which I’m giving an ID. By the way, “button” is a terrible ID name.

05:56 You should be more descriptive, but I’m lazy. This is the digits calculator-like display. It takes a string showing its starting value, which here is zero, and I’m giving it an ID so I can find it later if I want to as well.

06:11 The Digits widget has a property called border_subtitle. By default, the Digits widget–that’s just fun to say–has a border around it.

06:21 Setting this value includes some descriptive text within that border: digits widget, digits widget, fidgeting digits widget went with it. Alright, I’ll stop now.

06:33 Yielding it and then a footer for the bottom of the app.

06:40 This is the action that got registered in the bindings property against the b key. Inside, I use query_one() to search for the ID digits.

06:49 Again, bad lazy name. Since this is query_one(), it will return a single answer. I’ve only got one of them anyways, and once I have the widget, I change its border style by getting the next tuple from the endless border iterator.

07:04 This next method is our button handler. Instead of using on_button_pressed(), I’m using the @on decorator. I’m giving it two arguments.

07:12 The first is the event to associate with the handler, which is the Pressed event class, and the second is a CSS selector restricting which events to listen to. By using #button, I’m registering this handler only against the button_pressed event for the Button widget with this ID. Like with actions, this is often nicer than having a giant if else or match block inside of the more general on_button_pressed() method.

07:39 It keeps your code more contained. The name of the function is actually irrelevant as the decorator is doing all the work. You probably should name it something more descriptive though, which brings me back to lazy.

07:52 Inside, I increment our counter, find our digits display once more, then call its update() method to set a new value to display.

08:01 Alright, let’s take a look at this.

08:08 Let me click the button, and our counter is incrementing. Nice. How about the b hotkey? And it’s cycling. That’s great. Notice the footer here at the bottom.

08:23 Without having to do anything, it automatically populated the hotkey to tell the user what they can do. But what’s this ^p on the right? Well, it’s built-in. Let’s push it.

08:36 There’s tons of stuff in here, but I’m going to pick _Change theme_,

08:41 choosing _gruvbox_, and look at that. Remember a few lessons back when I said it’s better to use primary than explicitly coloring a button blue? By doing so, you can take advantage of themes and when you change the theme, all the primary buttons get recolored to the theme’s primary color. If you’d been explicit and made it blue instead, nothing would change.

09:03 And as you saw, Textual comes with some themes built-in. You can also define your own. I’ll leave that as an exercise. Again, lazy.

09:13 You’ve seen how the bindings mechanism uses actions as a shortcut for binding keys, but you can run any action using the run_action() method.

09:23 You can also embed links in your Textual markup and associate the links with an action. You do this with the @click= setting inside of the square bracket markup.

09:34 Here, I’m calling the set_background action with an argument of red. Note the app prefix there. We’ll talk more about that in the next lesson, but actions can have scope, and putting app there makes sure that this action is scoped at the app level.

09:50 Remember, this doesn’t call set_background. It’s an action, so it calls action_set_background instead. There’s a full guide on using actions in the Textual documentation if you want to learn more about how you can take advantage of this feature.

10:06 So far, all the apps have been toy problems. In the next lesson, I’ll build something a little more real.

Become a Member to join the conversation.