summaryrefslogtreecommitdiff
path: root/docs/tutorial/index.rst
blob: b4a655384d0529c0dc9f9c1d6caaa838f5b1cd33 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
.. _urwid-tutorial:

******************
  Urwid Tutorial
******************

.. currentmodule:: urwid


Minimal Application
-------------------

.. image:: minimal1.png

This program displays the string ``Hello World`` in the top left corner of the
screen and will run until interrupted with *CTRL+C* (*^C*).

.. literalinclude:: minimal.py
   :linenos:

* The *txt* :class:`Text` widget handles formatting blocks of text,
  wrapping to the next line when necessary. Widgets like this are called "flow
  widgets" because their sizing can have a number of columns given, in this
  case the full screen width, then they will flow to fill as many rows as
  necessary.

* The *fill* :class:`Filler` widget fills in blank lines above or
  below flow widgets so that they can be displayed in a fixed number of rows.
  This Filler will align our Text to the top of the screen, filling all the
  rows below with blank lines. Widgets which are given both the number of
  columns and number of rows they must be displayed in are called "box
  widgets".

* The :class:`MainLoop` class handles displaying our widgets as
  well as accepting input from the user.
  The widget passed to :class:`MainLoop` is called the "topmost" widget.
  The topmost widget is used to render the whole screen and so it must be a box widget.
  In this case our widgets can't handle any user input so we need to interrupt
  the program to exit with *^C*.


Global Input
------------

.. image:: input1.png
.. image:: input2.png
.. image:: input3.png
.. image:: input4.png
.. image:: input5.png

This program initially displays the string ``Hello World``, then it displays
each key pressed, exiting when the user presses *Q*.

.. literalinclude:: input.py
   :linenos:

* The :class:`MainLoop` class has an optional function
  parameter *unhandled_input*. This function will be called once for each
  keypress that is not handled by the widgets being displayed.
  Since none of the widgets being displayed here handle input, every key the user
  presses will be passed to the *show_or_exit* function.

* The :exc:`ExitMainLoop` exception is used to exit
  cleanly from the :meth:`MainLoop.run` function when the user
  presses *Q*. All other input is displayed by replacing the current Text
  widget's content.


Display Attributes
------------------

.. image:: attr1.png
.. image:: attr2.png
.. image:: attr3.png
.. image:: attr4.png

This program displays the string ``Hello World`` in the center of the screen.
It uses different attributes for the text, the space on either side of the text
and the space above and below the text. It waits for a keypress before exiting.

The screenshots above show how these widgets react to being resized.


.. literalinclude:: attr.py
   :linenos:

* Display attributes are defined as part of a palette. Valid foreground,
  background and
  setting values are documented in :ref:`foreground-background`
  A palette is a list of tuples containing:

    1. Name of the display attribute, typically a string
    2. Foreground color and settings for 16-color (normal) mode
    3. Background color for normal mode
    4. Settings for monochrome mode (optional)
    5. Foreground color and settings for 88 and 256-color modes (optional, see
       next example)
    6. Background color for 88 and 256-color modes (optional)

* A :class:`Text` widget is created containing the string ``" Hello World "``
  with display attribute ``'banner'``. The attributes of text in a Text widget is
  set by using a (*attribute*, *text*) tuple instead of a simple text string.
  Display attributes will flow with the text, and multiple display attributes may be
  specified by combining tuples into a list. This format is called :ref:`text-markup`.

* An :class:`AttrMap` widget is created to wrap the text
  widget with display attribute ``'streak'``. :class:`AttrMap` widgets
  allow you to map any display attribute to any other display attribute, but by default they
  will set the display attribute of everything that does not already have a display attribute.
  In this case the text has an attribute, so only the areas around the text
  used for alignment will be have the new attribute.

* A second :class:`AttrMap` widget is created to wrap the
  :class:`Filler` widget with attribute ``'bg'``.

When this program is run you can now clearly see the separation of the text,
the alignment around the text, and the filler above and below the text.

.. seealso:: :ref:`using-display-attributes`


High Color Modes
----------------

.. image:: highcolors1.png

This program displays the string ``Hello World`` in the center of the screen.
It uses a number of 256-color-mode colors to decorate the text,
and will work in any terminal that supports 256-color mode. It will exit when
*Q* is pressed.

.. literalinclude:: highcolors.py
   :linenos:

This palette only defines values for the high color foregroundand
backgrounds, because only the high colors will be used. A real application
should define values for all the modes in their palette. Valid foreground,
background and setting values are documented in :ref:`foreground-background`.

* Behind the scenes our :class:`MainLoop` class has created a
  :class:`raw_display.Screen` object for drawing the screen. The program
  is put into 256-color mode by using the screen object's
  :meth:`set_terminal_properties() <raw_display.Screen.set_terminal_properties>` method.

This example also demonstrates how you can build the widgets to display
in a top-down order instead of the usual bottom-up order. In some
places we need to use a *placeholder* widget because we must provide
a widget before the correct one has been created.

* We change the topmost widget used by the :class:`MainLoop` by
  assigning to its :attr:`MainLoop.widget` property.

* :ref:`decoration-widgets` like :class:`AttrMap` have an
  ``original_widget`` property that we can assign to change the widget they wrap.

* :class:`Divider` widgets are used to create blank lines,
  colored with :class:`AttrMap`.

* :ref:`container-widgets` like :class:`Pile` have a
  ``contents`` property that we can treat like a list of
  (*widget*, *options*) tuples.  :attr:`Pile.contents` supports normal list
  operations including ``append()`` to add child widgets.
  :meth:`Pile.options` is used to generate the default options
  for the new child widgets.


Question and Answer
-------------------

.. image:: qa1.png
.. image:: qa2.png
.. image:: qa3.png

This program asks for your name then responds ``Nice to meet you, (your
name).``

.. literalinclude:: qa.py
   :linenos:

The :class:`Edit` widget is based on the :class:`Text` widget but it accepts
keyboard input for entering text, making corrections and
moving the cursor around with the *HOME*, *END* and arrow keys.

Here we are customizing the :class:`Filler` decoration widget that is holding
our :class:`Edit` widget by subclassing it and defining a new ``keypress()``
method.  Customizing decoration or container widgets to handle input this way
is a common pattern in Urwid applications.  This pattern is easier to maintain
and extend than handling all special input in an *unhandled_input* function.

* In *QuestionBox.keypress()* all keypresses except *ENTER* are passed along to
  the default :meth:`Filler.keypress` which sends them to the
  child :meth:`Edit.keypress` method.
* Note that names containing *Q* can be entered into the :class:`Edit`
  widget without causing the program to exit because :meth:`Edit.keypress`
  indicates that it has handled the key by returning ``None``.
  See :meth:`Widget.keypress` for more information.
* When *ENTER* is pressed the child widget ``original_widget`` is changed
  to a :class:`Text` widget.
* :class:`Text` widgets don't handle any keyboard input so all input
  ends up in the *unhandled_input* function *exit_on_q*, allowing the
  user to exit the program.


Signal Handlers
---------------

.. image:: sig1.png
.. image:: sig2.png
.. image:: sig3.png
.. image:: sig4.png

This program asks for your name and responds ``Nice to meet you, (your name)``
*while* you type your name.  Press *DOWN* then *SPACE* or *ENTER* to exit.

.. literalinclude:: sig.py
   :linenos:

* An :class:`Edit` widget and a :class:`Text` reply
  widget are created, like in the previous example.
* The :func:`connect_signal` function is used to attach our
  *on_ask_change()* function to our :class:`Edit` widget's
  ``'change'`` signal. Now any time the content of the :class:`Edit`
  widget changes *on_ask_change()* will be called and passed the new
  content.
* Finally we attach our *on_exit_clicked()* function to our
  exit :class:`Button`'s ``'click'`` signal.
* *on_ask_change()* updates the reply text as the user enters their
  name and *on_exit_click()* exits.


Multiple Questions
------------------

.. image:: multiple1.png
.. image:: multiple2.png
.. image:: multiple3.png
.. image:: multiple4.png

This program asks for your name and responds ``Nice to meet you, (your name).``
It then asks again, and again. Old values may be changed and the responses will
be updated when you press *ENTER*. *ENTER* on a blank line exits.

.. literalinclude:: multiple.py
   :linenos:

:class:`ListBox` widgets let you scroll through a number of flow widgets
vertically.  It handles *UP*, *DOWN*, *PAGE UP* and *PAGE DOWN* keystrokes
and changing the focus for you.  :ref:`listbox-contents` are managed by
a "list walker", one of the list walkers that is easiest to use is
:class:`SimpleFocusListWalker`.

:class:`SimpleFocusListWalker` is like a normal python list of widgets, but
any time you insert or remove widgets the focus position is updated
automatically.

Here we are customizing our :class:`ListBox`'s keypress handling by overriding
it in a subclass.

* The *question()* function is used to build widgets to communicate with the user.
  Here we return a :class:`Pile` widget with a single :class:`Edit` widget
  to start.
* We retrieve the name entered with :attr:`ListBox.focus` to get the
  :class:`Pile` in focus, the standard
  :ref:`container widget <container-widgets>` method ``[0]`` to get the
  first child of the pile and :attr:`Edit.edit_text` to get the user-entered
  text.
* For the response we use the fact that we can treat
  :attr:`Pile.contents` like a list of (*widget*, *options*) tuples to create or
  replace any existing response by assigning a one-tuple list to *contents[1:]*.  We create
  the default options using :meth:`Pile.options`.
* To add another question after the current one we treat our
  :class:`SimpleFocusListWalker` stored as :attr:`ListBox.body` like a normal
  list of widgets by calling *insert()*, then update the focus position to the widget we just
  created.


Simple Menu
-----------

.. image:: smenu1.png
.. image:: smenu2.png
.. image:: smenu3.png

We can create a very simple menu using a list of :class:`Button` widgets.
This program lets you choose an option then repeats what you chose.

.. literalinclude:: smenu.py
   :linenos:

* *menu()* builds a :class:`ListBox` with a *title* and a sequence of :class:`Button`
  widgets.  Each button has its ``'click'`` signal attached to *item_chosen*,
  with item name is passed as data.
  The buttons are decorated with an :class:`AttrMap` that applies
  a display attribute when a button is in focus.
* *item_chosen()* replaces the menu displayed with text indicating the users'
  choice.
* *exit_program()* causes the program to exit on any keystroke.
* The menu is created and decorated with an :class:`Overlay` using a
  :class:`SolidFill` as the background.  The :class:`Overlay` is given a
  miniumum width and height but is allowed to expand to 60% of the available
  space if the user's terminal window is large enough.


Cascading Menu
--------------

.. image:: cmenu1.png
.. image:: cmenu2.png
.. image:: cmenu3.png
.. image:: cmenu4.png

A nested menu effect can be created by having some buttons open new menus.  This program
lets you choose an option from a nested menu that cascades across the screen.  You may
return to previous menus by pressing *ESC*.

.. literalinclude:: cmenu.py
   :linenos:

* *menu_button()* returns an :class:`AttrMap`-decorated :class:`Button`
  and attaches a *callback* to its ``'click'`` signal.  This function is
  used for both sub-menus and final selection buttons.
* *sub_menu()* creates a menu button and a closure that will open the
  menu when that button is clicked.  Notice that
  :ref:`text markup <text-markup>` is used to add ``'...'`` to the end of
  the *caption* passed to *menu_button()*.
* *menu()* builds a :class:`ListBox` with a *title* and a sequence of widgets.
* *item_chosen()* displays the users' choice similar to the previous example.
* *menu_top* is the top level menu with all of its child menus and
  options built using the functions above.

This example introduces :class:`WidgetPlaceholder`. :class:`WidgetPlaceholder` is a
:ref:`decoration widget <decoration-widgets>` that does nothing to the widget it
decorates.  It is useful if you need a simple way to replace a widget that doesn't
involve knowing its position in a :ref:`container <container-widgets>`, or in this
case as a base class for a widget that will be replacing its own contents regularly.

* *CascadingBoxes* is a new widget that extends :class:`WidgetPlaceholder`.
  It provides an *open_box()* method that displays a box widget *box* "on top of"
  all the previous content with an :class:`Overlay` and a :class:`LineBox`.
  The position of each successive box is shifted right and down from the
  previous one.
* *CascadingBoxes.keypress()* intercepts *ESC* keys to cause the current box
  to be removed and the previous one to be shown.  This allows the user to
  return to a previous menu level.


Horizontal Menu
---------------

.. image:: hmenu1.png
.. image:: hmenu2.png
.. image:: hmenu3.png
.. image:: hmenu4.png

This example is like the previous but new menus appear on the right and push
old menus off the left side of the screen.
The look of buttons and other menu elements are heavily customized
and new widget classes are used instead of factory functions.

.. literalinclude:: hmenu.py
   :linenos:

* *MenuButton* is a customized :class:`Button` widget.  :class:`Button` uses
  :class:`WidgetWrap` to create its appearance and this class replaces the
  display widget created by :class:`Button` by the wrapped widget in
  *self._w*.
* *SubMenu* is implemented with a *MenuButton* but uses :class:`WidgetWrap`
  to hide the implementation instead of inheriting from *MenuButton*.
  The constructor builds a widget for the menu that this button will open
  and stores it in *self.menu*.
* *Choice* is like *SubMenu* but displays the item chosen instead of
  another menu.

The *palette* used in this example includes an entry with the special name
``None``.  The foreground and background specified in this entry are used
as a default when no other display attribute is specified.

* *HorizontalBoxes* arranges the menus displayed similar to the previous
  example.  There is no special handling required for going to previous
  menus here because :class:`Columns` already handles switching focus
  when *LEFT* or *RIGHT* is pressed.  :class:`AttrMap` with the *focus_map*
  dict is used to change the appearance of a number of the display attributes
  when a menu is in focus.


Adventure Game
--------------

.. image:: adventure1.png
.. image:: adventure2.png
.. image:: adventure3.png
.. image:: adventure4.png

We can use the same sort of code to build a simple adventure game.  Instead
of menus we have "places" and instead of submenus and parent menus we just
have "exits".  This example scrolls previous places off the top of the
screen, allowing you to scroll back to view but not interact with previous
places.

.. literalinclude:: adventure.py
   :linenos:

This example starts to show some separation between the application logic
and the widgets that have been created.  The *AdventureGame* class is
responsible for all the changes that happen through the game and manages
the topmost widget, but isn't a widget itself.  This is a good pattern to
follow as your application grows larger.