[Urwid] New Tutorial Chapter: Zen of ListBox

Ian Ward ian at excess.org
Sat Nov 19 18:29:26 EST 2005

Here is a peek at one of the new chapters I will be adding to the Urwid
Tutorial.  The Urwid documentation has been lacking a good explanation
of how the ListBox class works, so I hope this is a good start.


3. Zen of ListBox

3.1. ListBox Focus and Scrolling

The ListBox is a box widget that contains flow widgets. Its contents are
displayed stacked vertically, and the ListBox allows the user to scroll
through its content. One of the flow widgets displayed in the ListBox is
the focus widget. The ListBox passes key presses to the focus widget to
allow the user to interact with it. If the focus widget does not handle
a keypress then the ListBox may handle the keypress by scrolling and/or
selecting another widget to become the focus widget.

The ListBox tries to do the most sensible thing when scrolling and
changing focus. When the widgets displayed are all Text widgets or other
unselectable widgets then the ListBox will behave like a web browser
does when the user presses UP, DOWN, PAGE UP and PAGE DOWN: new text is
immediately scrolled in from the top or bottom. Urwid chooses one of the
visible widgets as its focus widegt when scrolling. When scrolling up
Urwid chooses the topmost widget as the focus, and when scrolling down
Urwid chooses the bottommost widget as the focus.

The ListBox remembers the location of the widget in focus as either an
"offset" or an "inset". An offset is the number of rows between the top
of the ListBox and the beginning of the focus widget. An offset of zero
corresponds to a widget with its top aligned with the top of the
ListBox. An inset is the fraction of rows of the focus widget that are
"above" the top of the ListBox and not visible. The ListBox uses this
method of remembering the focus widget location so that when the ListBox
is resized the text displayed will stay roughly aligned with the top of
the ListBox.

When there are selectable widgets in the ListBox the focus will move
between the selectable widgets, skipping the unselectable widgets. The
ListBox will try to scroll all the rows of a selectable widget into view
so that the user can see the new focus widget in its entirety. This
behavior can be used to bring more than a single widget into view by
using composite widgets to combine a selectable widget with other
widgets that should be displayed at the same time.

3.2. Dynamic ListBox with List Walker

While the ListBox stores the location of its focus widget, it does not
directly store the actual focus widget or other contents of the ListBox.
The storage of a ListBox's content is delegated to a "List Walker"
object. If a list of widgets is passed to the ListBox constructor then
it creates a SimpleListWalker object to manage the list.

When the ListBox is rendering a canvas or handling input it will:

   1. Call the get_focus method of its list walker object. This method
will return the focus widget and a position object.
   2. Optionally call the get_prev method of its List Walker object one
or more times, initially passing the focus position and then passing the
new position returned on each successive call. This method will return
the widget and position object "above" the position passed.
   3. Optionally call the get_next method of its List Walker object one
or more times, similarly, to collect widgets and position objects
"below" the focus position.
   4. Optionally call the set_focus method passing one of the position
objects returned in the previous steps.

This is the only way the ListBox accesses its contents, and it will not
store copies of any of the widgets or position objects beyond the
current rendering or input handling operation.

The SimpleListWalker stores a list of widgets, and uses integer indexes
into this list as its position objects. It stores the focus position as
an integer, so if you insert a widget into the list above the focus
position then you need to remember to increment the focus position in
the SimpleListWalker object or the contents of the ListBox will shift.

A custom List Walker object may be passed to the ListBox constructor
instead of a plain list of widgets. List Walker objects must implement
the List Walker interface.

The fib.py example program demonstrates a custom list walker that
doesn't store any widgets. It uses a tuple of two successive fibonacci
numbers as its position objects and it generates Text widgets to display
the numbers on the fly. The result is a ListBox that can scroll through
an unending list of widgets.

The edit.py example program demonstrates a custom list walker that loads
lines from a text file only as the user scrolls them into view. This
allows even huge files to be opened almost instantly.

The browse.py example program demonstrates a custom list walker that
uses a tuple of strings as position objects, one for the parent
directory and one for the file selected. The widgets are cached in a
separate class that is accessed using a dictionary indexed by parent
directory names. This allows the directories to be read only as
required. The custom list walker also allows directories to be hidden
from view when they are "collapsed".

3.3. Setting the Focus

The easiest way to change the current ListBox focus is to call the
set_focus method. This method doesn't require that you know the
ListBox's current dimensions (maxcol, maxrow). It will wait until the
next call to either keypress or render to complete setting the offset
and inset values using the dimensions passed to that method.

The position object passed to set_focus must be compatible with the List
Walker object that the ListBox is using. For SimpleListWalker the
position is the integer index of the widget within the list.

The coming_from parameter should be set if you know that the old
position is "above" or "below" the previous position. When the ListBox
completes setting the offset and inset values it tries to find the old
widget among the visible widgets. If the old widget is still visible, if
will try to avoid causing the ListBox contents to scroll up or down from
its previous position. If the widget is not visible, then the ListBox will:

    * Display the new focus at the bottom of the ListBox if coming_from
is "above".
    * Display the new focus at the top of the ListBox if coming_from is
    * Display the new focus in the middle of the ListBox if coming_from
is None.

More information about the Urwid mailing list