2.6 Controllers

In GTK+, all user events are interpreted by signals that are generated by the widgets that are manipulated. This means obvious stuff like button clicks and typing text into a gtk.Entry, but it also includes less obvious events like the initial rendering of the widget and moving the mouse over a widget. To a signal, we can attach a function that is called when it is triggered, and this function is usually called a signal handler.

Many widgets have default handlers attached to their signals (these are coded in the actual GTK+ source code); a default handler, for example, is what makes the text that you type into an Entry actually display inside its white box, and what makes the checkbutton depress and change state when you click on it. However, there are many cases when you want to do something special based on a certain signal occurring. A pushbutton (gtk.Button) is a good example: it doesn't do anything by default when clicked beyond depressing, so you practically always will want to connect to its "clicked" signal. Developing a graphical application involves selecting which signals you think are important in the interface widgets, and attaching functions to them.

In Kiwi, we suggest grouping the relevant signal handlers for an interface into a class. This means that instead of using a number of independent functions, the signal handlers are really methods of a class. This class is called the Controller. The Controller is conceptually the part of the framework that handles events that are generated in the UI; click a button, a controller method is called.

Since the View holds the interface, it makes sense to attach a controller to each view4, and vice-versa; the Controller constructor takes a View instance and ties itself to it. Since the controller needs to define special methods, it should be subclassed in your code, and the methods implemented.

The Kiwi Controller has a special feature: if you write your method names using a certain syntax, it "discovers" what widget and signal you want, and attaches the handler automatically for you. This works for any type of View, even BaseViews, which means no more messing around with signal_autoconnect() and that pesky signal dialog in Glade. The handler's method name should be written as follows:

  1. Start the name with on_ or after_. Use on_ to connect before the default handler for that widget's signal; use after_ to connect after it. It is more common to use on_5.

  2. Add the widget name: this is the name of the View's instance variable for that widget. If your view has a widget called quitbutton, you would use "quitbutton".

  3. Follow the widget name by two underscores (__).

  4. Finish the name by appending the signal name you want to capture. For instance, if you wanted to handle that quitbutton's clicked signal, you would use "clicked". The final method name would be on_quitbutton__clicked.

Note that the widget must be attached directly to the controller's corresponding View; it can't be attached using this syntax to a slave of that view, for instance (you'll have to call connect() directly in that case).

Let's see a simple example to make these concepts more concrete (included in the Kiwi tarball at Kiwi/examples/framework/faren/faren.py):

\includegraphics[scale=0.905]{images/faren.eps}


#!/usr/bin/env python
import gtk

from kiwi.controllers import BaseController
from kiwi.ui.views import BaseView
from kiwi.ui.gadgets import quit_if_last

class FarenControl(BaseController):

    def on_quitbutton__clicked(self, *args):
        self.view.hide_and_quit()

    def after_temperature__insert_text(self, entry, *args):
        try:
            temp = float(entry.get_text())
        except ValueError:
            temp = 0
        celsius = (temp - 32) * 5/9.0
        farenheit = (temp * 9/5.0) + 32
        self.view.celsius.set_text("%.2f" % celsius)
        self.view.farenheit.set_text("%.2f" % farenheit)

widgets = ["quitbutton", "temperature", "celsius", "farenheit"]
view = BaseView(gladefile="faren", delete_handler=quit_if_last,
                widgets=widgets)
ctl = FarenControl(view)
view.show()
gtk.main()

Let's have a look at the code. I define a Controller FarenControl that inherits from BaseController, and which defines two methods that are signal handlers - one for the "clicked" event for the widget quitbutton, and another for the "insert_text" signal for temperature. I attach the view to the controller, and tell the view to show itself and run the event loop. Not much else is worth noting, apart from the fact that the signal handlers receive the widget as the first parameter, and that the GTK+ text widgets (gtk.Entry, gtk.Label, gtk.Text) usually take and return strings, which makes us do conversion here and there.

Thus, the event loop now has two signal handlers that will be triggered according to the user's interaction: one called when clicking the quitbutton and one when inserting text into the entry. The user can type numbers into the entry, and through after_temperature__insert_text(), the celsius and farenheit labels are changed automatically. Clicking quit calls a special method in the view, hide_and_quit(), that hides the window and quits the event loop. Note that the widgets "celsius" and "farenheit" are empty labels that appear right next to the labels that are written "Celsius" and "Farenheit"; if you are confused look at the glade file faren.glade.



Footnotes

... view4
Ancient versions of Kiwi had a MultiView class, that held various subviews for portions of it, and required one controller for each subview. The new SlaveView approach is nicer, because it allows reusing the actual interface in a much more flexible manner.
...on_5
Hint: if you are connecting to a gtk.Entry's insert_text signal and you want to use entry.get_text(), use after_entrywidget__insert_text -- the default signal handler is responsible for inserting the text into the entry and you will need it to run before your handler.



Subsections