Signals and Events
Signals are the mechanism by which a widget calls your code: a named event that the widget emits, a handler that you attach with connect, and - for the input events - the boolean return value that decides whether the default action still runs. The model has three parts to master: the connection (the handler, its signature, the user data, the destroy flag), the event signals (the -event signals that carry a GdkEvent and honor the return value), and the management (disconnecting, the handler id, the stop_emission that ends the signal's propagation). This chapter covers the connection's options, the event hierarchy and the return-value contract, the event masks that opt a widget into the events it needs, and the disconnect discipline that keeps a long-running app from leaking handlers.
Connecting: The Options
import gtk
button = gtk.Button(label="Go")
# the handler signature: the widget first, then the
# signal-specific arguments
def on_clicked(widget):
print("clicked:", widget.get_label())
# the full connect signature:
# connect(signal_name, callback, user_data=None,
# *flags)
button.connect("clicked", on_clicked)
# user_data: a value passed to the handler (the common case
# is data the handler needs that the signal does not carry)
def on_clicked_with_data(widget, data):
print("action:", data)
button.connect("clicked", on_clicked_with_data, "open-file")
# the return value: connect hands back a handler ID
handler_id = button.connect("clicked", on_clicked)
# the ID is what disconnect takes (below)
connect(signal_name, callback) is the binding, and its options are the working vocabulary. The callback receives the emitting widget as its first argument, then the signal-specific arguments - and any user_data passed to connect is appended after those, which is the mechanism for giving a handler the data the signal does not carry (the action name, the target, the index). The return value is the handler id - an integer the connection is registered under, and the argument disconnect takes to remove it later. The flags (CONNECT_AFTER for a handler that runs after the defaults, the CONNECT_SWAPPED that puts the user data first) are the advanced cases; the ordinary app uses the plain connect with an optional user data, and the handler id is kept when the handler will be removed. The signal name is a string - the toolkit's signal is a named, registered event, and the name is the contract between the widget and the handler.
Event Signals and the Return Value
import gtk
window = gtk.Window()
# the input events arrive as "-event" signals carrying a
# GdkEvent; the handler's return value is the contract:
# True = the event was HANDLED - stop, no default, no parent
# False = NOT handled - the default action runs, and the
# event propagates to the parent (for the propagating
# events)
def on_delete(widget, event):
# the event object: event.type names it
print("delete-event:", event.type)
gtk.main_quit()
return False # let the window close
def on_key(widget, event):
# event.keyval names the key (gdk.KEY_q, etc.)
if event.keyval == gdk.KEY_Escape:
gtk.main_quit()
return True # handled: Escape does not propagate
return False # other keys: default behavior
window.connect("delete-event", on_delete)
window.connect("key-press-event", on_key)
# the event types the app meets: key-press-event,
# button-press-event, button-release-event, motion-notify-event,
# focus-in-event, focus-out-event, expose-event
The input events are the -event signals, and they differ from the action signals (clicked) in the return-value contract: the handler receives a GdkEvent as the second argument, and its return value is a boolean that decides the event's fate. Returning True means the event was handled - the default action is suppressed and (for the propagating events) the event does not continue to the parent. Returning False means it was not handled - the default action runs (the delete-event closes the window, the key propagates) and the event may bubble up the hierarchy. The GdkEvent object carries the specifics: keyval for the key, button for the mouse button, x/y for the position, type for the event's identity. The contract is the discipline: an event handler that should swallow the event returns True, one that should let it through returns False, and getting it backwards is the "Escape does nothing" or "the window will not close" bug.
Event Masks: Opting Into the Events
import gtk
import gdk
# a widget only RECEIVES the events it has asked for - the
# event mask is the opt-in, set before the widget is shown
area = gtk.DrawingArea()
# the default mask is button-press/release; motion must be
# requested explicitly:
area.set_events(gdk.EventMask.BUTTON_PRESS_MASK |
gdk.EventMask.BUTTON_RELEASE_MASK |
gdk.EventMask.POINTER_MOTION_MASK |
gdk.EventMask.KEY_PRESS_MASK)
# without POINTER_MOTION_MASK the motion-notify-event never
# fires, no matter how many handlers are connected
# the masks: BUTTON_PRESS/RELEASE, POINTER_MOTION, KEY_PRESS,
# ENTER/LEAVE_NOTIFY, EXPOSURE, FOCUS_CHANGE, ...
The event mask is the opt-in: a widget receives only the event types it has asked for, and the mask is set with set_events (bitwise OR of the gdk.EventMask flags) before the widget is shown. The default mask covers the basic button press/release, but motion, key events on a non-focusable widget, and the enter/leave notifications must be requested - and a motion-notify-event handler that never fires is almost always a missing POINTER_MOTION_MASK. The mask is the first thing to check when an event signal is connected but silent: the handler is correct, the connection is correct, and the widget simply is not receiving that event type. The pattern is the one the drawing chapter relies on - the drawing area that tracks the pointer sets the motion mask before it is shown, and the events arrive.
Management: Disconnect and stop_emission
import gtk
button = gtk.Button(label="Go")
def on_clicked(widget):
print("clicked")
# the handler id is the handle for removal:
hid = button.connect("clicked", on_clicked)
# disconnect by the id - the handler no longer fires:
button.disconnect(hid)
# disconnect ALL handlers for a signal on a widget:
# (the idiom for teardown, when the widget is going away)
# stop_emission: a handler ends the signal for the OTHER
# handlers (the earlier handlers already ran; the later ones
# and the default do not):
def first(widget):
print("first")
widget.stop_emission("clicked")
def second(widget):
print("second") # never runs if first stops the emission
button.connect("clicked", first)
button.connect("clicked", second)
# the rule for a long-running app: every connect in a component
# that can be created and destroyed has a matching disconnect
# in its teardown - the handler id is kept for that purpose
The management is the discipline that keeps a long-running app from accumulating dead handlers. The handler id returned by connect is the handle: disconnect(hid) removes that one connection, and the idiom for a component's teardown is to keep the ids of the connections it made and disconnect them when the component is destroyed - otherwise a recreated component has two sets of handlers firing, and a destroyed one has handlers firing on a dead widget. The stop_emission is the in-signal control: a handler calls it to end the signal for the remaining handlers and the default action, which is the mechanism for a higher-priority handler claiming an event the lower ones should not also see. The rules: keep the ids when the lifetime is managed, disconnect on teardown, and use stop_emission deliberately - it changes the order-dependent behavior of a multi-handler signal, and the code that relies on all handlers running should not have a stop_emission in the chain.
The Working Rules
- The handler receives the widget first, then the signal's arguments, then the user data. The signature is the contract, and a mismatch is an immediate, visible error at the first emission.
- Event handlers return the boolean deliberately.
Trueswallows (no default, no propagation),Falselets it through - and the backwards return is the class of bug the return value exists to prevent. - Check the event mask when an event is silent. A connected
motion-notify-eventthat never fires is a missingPOINTER_MOTION_MASK, not a broken handler. - Keep the handler id and disconnect on teardown. The component that can be destroyed keeps the ids of its connections and removes them, so recreation does not double the handlers and destruction does not leak them.
- Use
stop_emissiononly when one handler should claim the signal. It ends the signal for the later handlers and the default - a deliberate priority decision, not a default.
Summary
The signal is the widget's call into the code: connect binds a handler (the widget first, then the signal arguments, then the user data) and returns the handler id that disconnect removes. The input events are the -event signals carrying a GdkEvent, with the return-value contract - True handled (no default, no propagation), False not handled (default runs, event may propagate) - and the event mask is the opt-in that must include the event type or the signal never fires. The management is the long-running discipline: keep the handler ids, disconnect on teardown, and use stop_emission deliberately to claim a signal from the later handlers. The three - the connection, the return-value contract, the disconnect - are the working model for every piece of behavior the rest of the tutorial wires.