1User Interfaces
2===============
3
4Introduction
5------------
6
7Asciimatics provides a `widgets` sub-package that allows you to create interactive text user
8interfaces.  At its heart, the logic is quite simple, reusing concepts from standard web and
9desktop GUI frameworks.
10
111. The basic building block for your text UI is a `Widget`.  There is a set of standard ones
12   provided by asciimatics, but you can create a custom set if needed.  The basic set has strong
13   parallels with simple web input forms - e.g. buttons, check boxes, etc.
142. The `Widgets` need to be arranged on the `Screen` and rearranged whenever it is resized.  The
15   `Layout` class handles this for you.  You just need to add your `Widgets` to one.
163. You then need to display the `Layouts`.  To do this, you must add them to a `Frame`.  This class
17   is an `Effect` and so can be used in any `Scene` alongside any other `Effect`. The `Frame` will
18   draw any parts of the `Layouts` it contains that are visible within its boundaries.  The net
19   result is that it begins to look a bit like a window in GUI frameworks.
20
21And that's it!  You can set various callbacks to get triggered when key events occur - e.g. changes
22to values, buttons get clicked, etc. - and use these to trigger your application processing.  For
23an example, see the contact_list.py sample provided - which will look a bit like this:
24
25.. image:: contacts.png
26    :alt: Screen shot of the contacts list sample
27
28Common keys
29~~~~~~~~~~~
30When navigating around a Frame, you can use the following keys.
31
32===================  ==============================================================================
33Key                  Action
34===================  ==============================================================================
35Tab                  Move to the next Widget in the Frame
36Backtab (shift+tab)  Move to the previous Widget in the Frame
37Up arrow             Move to the Widget above the current focus in the same column.
38Down arrow           Move to the Widget below the current focus in the same column.
39Left arrow           Move to the last Widget in the column to the left of the column with the
40                     current input focus.
41Right arrow          Move to the first Widget in the column to the right of the column with the
42                     current input focus.
43Space or Return      Select the current Widget - e.g. click a Button, or pop-up a list of options.
44===================  ==============================================================================
45
46Note that the cursor keys will not traverse between Layouts.  In addition, asciimatics will not
47allow you to navigate to a disabled widget.
48
49Inside the standard text edit Widgets, the cursor key actions are overridden and instead they will
50allow you to for navigate around the editable text (or lists) as you would expect.  In addition you
51can also use the following extra keys.
52
53===================  ==========================================================
54Key                  Action
55===================  ==========================================================
56Home/End             Move to the start/end of the current line.
57Delete               Delete the character under the cursor.
58Backspace            Delete the character before the cursor.
59===================  ==========================================================
60
61Tab/backtab will still navigate out of text edit Widgets, but the rest of the keys (beyond those
62described above) will simply add to the text in the current line.
63
64Model/View Design
65-----------------
66Before we jump into exactly what all the objects are and what they do for you, it is important to
67understand how you must put them together to make the best use of them.
68
69The underlying Screen/Scene/Effect design of asciimatics means that objects regularly get thrown
70away and recreated - especially when the Screen is resized.  It is therefore vital to separate your
71data model from your code to display it on the screen.
72
73This split is often (wrongly) termed the `MVC
74<https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller>`__ model, but a more accurate
75description is `Separated Presentation
76<http://martinfowler.com/eaaDev/SeparatedPresentation.html>`__.  No matter what term you use, the
77concept is easy: use a separate class to handle your persistent data storage.
78
79In more concrete terms, let's have a closer look at the contact_list sample.  This consists of 3
80basic classes:
81
821. `ContactModel`: This is the model.  It stores simple contact details in a sqlite in-memory
83   database and provides a simple create/read/update/delete interface to manipulate any contact.
84   Note that you don't have to be this heavy-weight with the data storage; a simple class to wrap a
85   list of dictionaries would also suffice - but doesn't look as professional for a demo!
86
87.. container:: toggle
88
89    .. container:: header
90
91        **Show/hide code**
92
93    .. code-block:: python
94
95        class ContactModel(object):
96            def __init__(self):
97                # Create a database in RAM
98                self._db = sqlite3.connect(':memory:')
99                self._db.row_factory = sqlite3.Row
100
101                # Create the basic contact table.
102                self._db.cursor().execute('''
103                    CREATE TABLE contacts(
104                        id INTEGER PRIMARY KEY,
105                        name TEXT,
106                        phone TEXT,
107                        address TEXT,
108                        email TEXT,
109                        notes TEXT)
110                ''')
111                self._db.commit()
112
113                # Current contact when editing.
114                self.current_id = None
115
116            def add(self, contact):
117                self._db.cursor().execute('''
118                    INSERT INTO contacts(name, phone, address, email, notes)
119                    VALUES(:name, :phone, :address, :email, :notes)''',
120                                          contact)
121                self._db.commit()
122
123            def get_summary(self):
124                return self._db.cursor().execute(
125                    "SELECT name, id from contacts").fetchall()
126
127            def get_contact(self, contact_id):
128                return self._db.cursor().execute(
129                    "SELECT * from contacts where id=?", str(contact_id)).fetchone()
130
131            def get_current_contact(self):
132                if self.current_id is None:
133                    return {"name": "", "address": "", "phone": "", "email": "", "notes": ""}
134                else:
135                    return self.get_contact(self.current_id)
136
137            def update_current_contact(self, details):
138                if self.current_id is None:
139                    self.add(details)
140                else:
141                    self._db.cursor().execute('''
142                        UPDATE contacts SET name=:name, phone=:phone, address=:address,
143                        email=:email, notes=:notes WHERE id=:id''',
144                                              details)
145                    self._db.commit()
146
147            def delete_contact(self, contact_id):
148                self._db.cursor().execute('''
149                    DELETE FROM contacts WHERE id=:id''', {"id": contact_id})
150                self._db.commit()
151
1522. `ListView`: This is the main view.  It queries the `ContactModel` for the list of known contacts
153   and displays them in a list, complete with some extra buttons to add/edit/delete contacts.
154
155.. container:: toggle
156
157    .. container:: header
158
159        **Show/hide code**
160
161    ..  code-block:: python
162
163        class ListView(Frame):
164            def __init__(self, screen, model):
165                super(ListView, self).__init__(screen,
166                                               screen.height * 2 // 3,
167                                               screen.width * 2 // 3,
168                                               on_load=self._reload_list,
169                                               hover_focus=True,
170                                               title="Contact List")
171                # Save off the model that accesses the contacts database.
172                self._model = model
173
174                # Create the form for displaying the list of contacts.
175                self._list_view = ListBox(
176                    Widget.FILL_FRAME,
177                    model.get_summary(), name="contacts", on_select=self._on_pick)
178                self._edit_button = Button("Edit", self._edit)
179                self._delete_button = Button("Delete", self._delete)
180                layout = Layout([100], fill_frame=True)
181                self.add_layout(layout)
182                layout.add_widget(self._list_view)
183                layout.add_widget(Divider())
184                layout2 = Layout([1, 1, 1, 1])
185                self.add_layout(layout2)
186                layout2.add_widget(Button("Add", self._add), 0)
187                layout2.add_widget(self._edit_button, 1)
188                layout2.add_widget(self._delete_button, 2)
189                layout2.add_widget(Button("Quit", self._quit), 3)
190                self.fix()
191
192            def _on_pick(self):
193                self._edit_button.disabled = self._list_view.value is None
194                self._delete_button.disabled = self._list_view.value is None
195
196            def _reload_list(self):
197                self._list_view.options = self._model.get_summary()
198                self._model.current_id = None
199
200            def _add(self):
201                self._model.current_id = None
202                raise NextScene("Edit Contact")
203
204            def _edit(self):
205                self.save()
206                self._model.current_id = self.data["contacts"]
207                raise NextScene("Edit Contact")
208
209            def _delete(self):
210                self.save()
211                self._model.delete_contact(self.data["contacts"])
212                self._reload_list()
213
214            @staticmethod
215            def _quit():
216                raise StopApplication("User pressed quit")
217
2183. `ContactView`: This is the detailed view.  It queries the `ContactModel` for the current contact
219   to be displayed when it is reset (note: there may be no contact if the user is adding a contact) and writes
220   any changes back to the model when the user clicks OK.
221
222.. container:: toggle
223
224    .. container:: header
225
226        **Show/hide code**
227
228    .. code-block:: python
229
230        class ContactView(Frame):
231            def __init__(self, screen, model):
232                super(ContactView, self).__init__(screen,
233                                                  screen.height * 2 // 3,
234                                                  screen.width * 2 // 3,
235                                                  hover_focus=True,
236                                                  title="Contact Details")
237                # Save off the model that accesses the contacts database.
238                self._model = model
239
240                # Create the form for displaying the list of contacts.
241                layout = Layout([100], fill_frame=True)
242                self.add_layout(layout)
243                layout.add_widget(Text("Name:", "name"))
244                layout.add_widget(Text("Address:", "address"))
245                layout.add_widget(Text("Phone number:", "phone"))
246                layout.add_widget(Text("Email address:", "email"))
247                layout.add_widget(TextBox(5, "Notes:", "notes", as_string=True))
248                layout2 = Layout([1, 1, 1, 1])
249                self.add_layout(layout2)
250                layout2.add_widget(Button("OK", self._ok), 0)
251                layout2.add_widget(Button("Cancel", self._cancel), 3)
252                self.fix()
253
254            def reset(self):
255                # Do standard reset to clear out form, then populate with new data.
256                super(ContactView, self).reset()
257                self.data = self._model.get_current_contact()
258
259            def _ok(self):
260                self.save()
261                self._model.update_current_contact(self.data)
262                raise NextScene("Main")
263
264            @staticmethod
265            def _cancel():
266                raise NextScene("Main")
267
268Displaying your UI
269------------------
270OK, so you want to do something a little more interactive with your user.  The first thing you need
271to decide is what information you want to get from them and how you're going to achieve that.  In
272short:
273
2741. What data you want them to be able to enter - e.g. their name.
2752. How you want to break that down into fields - e.g. first name, last name.
2763. What the natural representation of those fields would be - e.g. text strings.
277
278At this point, you can now decide which Widgets you want to use.  The standard selection is as
279follows.
280
281============================= =====================================================================
282Widget type                   Description
283============================= =====================================================================
284:py:obj:`.Button`             Action buttons - e.g. ok/cancel/etc.
285:py:obj:`.CheckBox`           Simple yes/no tick boxes.
286:py:obj:`.DatePicker`         A single-line widget for selecting a date (using a pop-up list).
287:py:obj:`.Divider`            A spacer between widgets (for aesthetics).
288:py:obj:`.DropdownList`       A single-line widget that pops up a list from which the user can
289                              select a single value.
290:py:obj:`.FileBrowser`        A multi-line widget for listing the local file system.
291:py:obj:`.Label`              A label for a group of related widgets.
292:py:obj:`.ListBox`            A list of possible options from which users can select one value.
293:py:obj:`.MultiColumnListBox` Like a ListBox, but for displaying tabular data.
294:py:obj:`.RadioButtons`       A list of radio buttons.  These allow users to select one value from
295                              a list of options.
296:py:obj:`.Text`               A single line of editable text.
297:py:obj:`.TextBox`            A multi-line box of editable text.
298:py:obj:`.TimePicker`         A single-line widget for selecting a time (using a pop-up list).
299:py:obj:`.VerticalDivider`    A vertical line divider - useful for providing a visual marker
300                              between columns in a Layout.
301============================= =====================================================================
302
303.. note:: You can use the `hide_char` option on Text widgets to hide sensitive data - e.g. for
304          passwords.
305
306Asciimatics will automatically arrange these for you with just a little extra help.  All you need
307to do is decide how many columns you want for your fields and which fields should be in which
308columns.  To tell asciimatics what to do you create a `Layout` (or more than one if you want a more
309complex structure where different parts of the screen need differing column counts) and associate
310it with the `Frame` where you will display it.
311
312For example, this will create a Frame that is 80x20 characters and define 4 columns that are each
31320 columns wide:
314
315.. code-block:: python
316
317    frame = Frame(screen, 80, 20, has_border=False)
318    layout = Layout([1, 1, 1, 1])
319    frame.add_layout(layout)
320
321Once you have a Layout, you can add Widgets to the relevant column.  For example, this will add a
322button to the first and last columns:
323
324.. code-block:: python
325
326    layout.add_widget(Button("OK", self._ok), 0)
327    layout.add_widget(Button("Cancel", self._cancel), 3)
328
329If you want to put a standard label on all your input fields, that's fine too; asciimatics will
330decide how big your label needs to be across all fields in the same column and then indent them all
331to create a more aesthetically pleasing layout.  For example, this will provide a single column
332with labels for each field, indenting all of the fields to the same depth:
333
334.. code-block:: python
335
336    layout = Layout([100])
337    frame.add_layout(layout)
338    layout.add_widget(Text("Name:", "name"))
339    layout.add_widget(Text("Address:", "address"))
340    layout.add_widget(Text("Phone number:", "phone"))
341    layout.add_widget(Text("Email address:", "email"))
342    layout.add_widget(TextBox(5, "Notes:", "notes", as_string=True))
343
344If you want more direct control of your labels, you could use the :py:obj:`.Label` widget to place
345them anywhere in the Layout as well as control the justification (left, centre or right) of the text.
346
347Or maybe you just want some static text in your UI?  The simplest thing to do there is to use
348the :py:obj:`.Label` widget.  If you need something a little more advanced - e.g. a pre-formatted
349multi-line status bar, use a :py:obj:`.TextBox` and disable it as described below.
350
351In some cases, you may want to have different alignments for various blocks of Widgets.  You can use multiple
352Layouts in one Frame to handle this case.
353
354For example, if you want a search page, which allows you to enter data at the top and a list of results at the
355bottom of the Frame, you could use code like this:
356
357.. code-block:: python
358
359    layout1 = Layout([100])
360    frame.add_layout(layout1)
361    layout1.add_widget(Text(label="Search:", name="search_string"))
362
363    layout2 = Layout([100])
364    frame.add_layout(layout2)
365    layout1.add_widget(TextBox(Widget.FILL_FRAME, name="results"))
366
367
368Disabling widgets
369~~~~~~~~~~~~~~~~~
370Any widget can be disabled by setting the ``disabled`` property.  When this is ``True``,
371asciimatics will redraw the widget using the 'disabled' colour palette entry and prevent the user
372from selecting it or editing it.
373
374It is still possible to change the widget programmatically, though.  For example, you can still
375change the ``value`` of a disabled widget.
376
377This is the recommended way of getting a piece of non-interactive data (e.g. a status bar) into
378your UI.  If the disabled colour is the incorrect choice for your UI, you can override it as
379explained in :ref:`custom-colours-ref`.  For an example of such a widget, see the top.py sample.
380
381Layouts in more detail
382~~~~~~~~~~~~~~~~~~~~~~
383If you need to do something more complex, you can use multiple Layouts.  Asciimatics uses the
384following logic to determine the location of Widgets.
385
3861.  The `Frame` owns one or more `Layouts`.  The `Layouts` stack one above each other when
387    displayed - i.e. the first `Layout` in the `Frame` is above the second, etc.
3882.  Each `Layout` defines some horizontal constraints by defining columns as a proportion of the
389    full `Frame` width.
3903.  The `Widgets` are assigned a column within the `Layout` that owns them.
3914.  The `Layout` then decides the exact size and location to make each `Widget` best fit the
392    visible space as constrained by the above.
393
394For example::
395
396    +------------------------------------------------------------------------+
397    |Screen..................................................................|
398    |........................................................................|
399    |...+----------------------------------------------------------------+...|
400    |...|Frame                                                           |...|
401    |...|+--------------------------------------------------------------+|...|
402    |...||Layout 1                                                      ||...|
403    |...|+--------------------------------------------------------------+|...|
404    |...|+------------------------------+-------------------------------+|...|
405    |...||Layout 2                      |                               ||...|
406    |...|| - Column 1                   | - Column 2                    ||...|
407    |...|+------------------------------+-------------------------------+|...|
408    |...|+-------------+---------------------------------+--------------+|...|
409    |...||Layout 3     | < Widget 1 >                    |              ||...|
410    |...||             | ...                             |              ||...|
411    |...||             | < Widget N >                    |              ||...|
412    |...|+-------------+---------------------------------+--------------+|...|
413    |...+----------------------------------------------------------------+...|
414    |........................................................................|
415    +------------------------------------------------------------------------+
416
417This consists of a single `Frame` with 3 `Layouts`.  The first is a single, full-width column, the
418second has two 50% width columns and the third consists of 3 columns of relative size 25:50:25.
419The last actually contains some Widgets in the second column (though this is just for illustration
420purposes as we'd expect most Layouts to have some Widgets in them).
421
422Filling the space
423~~~~~~~~~~~~~~~~~
424Once you've got the basic rows and columns for your UI sorted, you may want to use some strategic
425spacing.  At the simplest level, you can use the previously mentioned :py:obj:`.Divider` widget to
426create some extra vertical space or insert a visual section break.
427
428Moving up the complexity, you can pick different sizes for your Frames based on the size of your
429current Screen.  The Frame will be recreated when the screen is resized and so you will use more or
430less real estate appropriately.
431
432Finally, you could also tell asciimatics to use an object to fill any remaining space.  This
433allows for the sort of UI like you'd see in applications like top where you have a fixed header
434or footer, but then a variably sized part that contains the data to be displayed.
435
436You can achieve this in 2 ways:
437
4381. You can tell a Layout to fill any remaining space in the Frame using `fill_frame=True` on
439   construction.
4402. You can tell some Widgets to fill any remaining space in the Frame using a height of
441   `Widget.FILL_FRAME` on construction.
442
443These two methods can be combined to tell a Layout to fill the Frame and a Widget to fill this
444Layout.  See the ListView class in the contact_list demo code.
445
446.. warning::
447
448    Note that you can only have one Layout and/or Widget that fills the Frame. Trying to set more
449    than one will be rejected.
450
451Full-screen Frames
452~~~~~~~~~~~~~~~~~~
453By default, asciimatics assumes that you are putting multiple Frames into one Scene and so
454provides defaults (e.g. borders) to optimize this type of UI. However, some UIs only need a
455single full-screen Frame.  This can easily be achieved by declaring a Frame the full width and
456height of the screen and then specifying `has_border=False`.
457
458Large forms
459~~~~~~~~~~~
460If you have a very large form, you may find it is too big to fit into a standard screen.  This is
461not a problem.  You can keep adding your Widgets to your Layout and asciimatics will
462automatically clip the content to the space available and scroll the content as required.
463
464If you do this, it is recommended that you set `has_border=True` on the Frame so that the user can
465use the scroll bar provided to move around the form.
466
467Colour schemes
468~~~~~~~~~~~~~~
469The colours for any Widget are determined by the `palette` property of the Frame that contains the
470Widget.  If desired, it is possible to have a different palette for every Frame, however your
471users may prefer a more consistent approach.
472
473The palette is just a simple dictionary to map Widget components to a colour tuple.  A colour tuple
474is simply the foreground colour, attribute and background colour.  For example:
475
476.. code-block:: python
477
478    (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLUE)
479
480The following table shows the required keys for the `palette`.
481
482========================  =========================================================================
483Key                       Usage
484========================  =========================================================================
485"background"              Frame background
486"borders"                 Frame border and Divider Widget
487"button"                  Buttons
488"control"                 Checkboxes and RadioButtons
489"disabled"                Any disabled Widget
490"edit_text"               Text and TextBox
491"field"                   Value of an option for a Checkbox, RadioButton or Listbox
492"focus_button"            Buttons with input focus
493"focus_control"           Checkboxes and RadioButtons with input focus
494"focus_edit_text"         Text and TextBox with input focus
495"focus_field"             As above with input focus
496"invalid"                 The widget contains invalid data
497"label"                   Widget labels
498"scroll"                  Frame scroll bar
499"selected_control"        Checkboxes and RadioButtons when selected
500"selected_field"          As above when selected
501"selected_focus_control"  Checkboxes and RadioButtons with both
502"selected_focus_field"    As above with both
503"title"                   Frame title
504========================  =========================================================================
505
506In addition to the default colour scheme for all your widgets, asciimatics provides some
507other pre-defined colour schemes (or themes) that you can use for your widgets using
508:py:meth:`~.Frame.set_theme`.  These themes are as follows.
509
510========================  =========================================================================
511Name                      Description
512========================  =========================================================================
513"monochrome"              Simple black and white colour scheme.
514"green"                   A classic green terminal.
515"bright"                  Black background, green and yellow scheme.
516"tlj256"                  Shades of black white and red - 256 colour terminals only.
517========================  =========================================================================
518
519You can add your own theme to this list by defining a new entry in the :py:obj:`~.widgets.THEMES`
520
521.. _custom-colours-ref:
522
523Custom widget colours
524~~~~~~~~~~~~~~~~~~~~~
525In some cases, a single palette for the entire Frame is not sufficient.  If you need a more
526fine-grained approach to the colouring, you can customize the colour for any Widget by setting the
527:py:obj:`~.Widget.custom_colour` for that Widget.  The only constraint on this property is that
528it must still be the value of one of the keys within the owning Frame's palette.
529
530Changing colours inline
531~~~~~~~~~~~~~~~~~~~~~~~
532The previous options should be enough for most UIs.  However, sometimes it is useful to be able to
533change the colour of some text inside the value for some widgets, e.g. to provide syntax highlighting
534in a `TextBox`.  You can do this using a :py:obj:`.Parser` object for those widgets that support it.
535
536By passing in a parser that understands extra control codes or the need to highlight certain
537characters differently, you can control colours on a letter by letter basis.  Out of the box,
538asciimatics provides 2 parsers, which can handle the ${c,a,b} format used by its Renderers, or
539the ANSI standard terminal escape codes (used by many Linux terminals).  Simply use the relevant
540parser and pass in values containing the associated control codes to change colours where needed.
541
542Check out the latest code in forms.py and top.py for examples of how this works.
543
544Setting values
545--------------
546By this stage, you should have a basic User Interface up and running, but how do you set the values
547in each of the Widgets - e.g. to pre-populate known values in a form?  There are 2 ways to handle this:
548
5491. You can set the value directly on each `Widget` using the :py:obj:`~.Widget.value` property.
5502. You can set the value for all Widgets in a `Frame` by setting at the :py:obj:`~.Frame.data` property.
551   This is a simple key/value dictionary, using the `name` property for each `Widget` as the keys.
552
553The latter is a preferred as a symmetrical solution is provided to access all the data for each
554Widget, thus giving you a simple way to read and then replay the data back into your Frame.
555
556Getting values
557--------------
558Now that you have a `Frame` with some `Widgets` in it and the user is filling them in, how do you
559find out what they entered?  There are 2 basic ways to do this:
560
5611. You can query each Widget directly, using the `value` property.  This returns the current value
562   the user has entered at any time (even when the Frame is not active).  Note that it may be
563   `None` for those `Widgets` where there is no value - e.g. buttons.
5642. You can query the `Frame`by looking at the `data` property.  This will return the value for
565   every Widget in the former as a dictionary, using the Widget `name` properties for the keys.
566   Note that `data` is just a cache, which only gets updated when you call :py:meth:`~.Frame.save`,
567   so you need to call this method to refresh the cache before accessing it.
568
569For example:
570
571.. code-block:: python
572
573    # Form definition
574    layout = Layout([100])
575    frame.add_layout(layout)
576    layout.add_widget(Text("Name:", "name"))
577    layout.add_widget(Text("Address:", "address"))
578    layout.add_widget(TextBox(5, "Notes:", "notes", as_string=True))
579
580    # Sample frame.data after user has filled it in.
581    {
582        "name": "Peter",
583        "address": "Somewhere on earth",
584        "notes": "Some multi-line\ntext from the user."
585    }
586
587Validating text data
588~~~~~~~~~~~~~~~~~~~~
589Free-form text input sometimes needs validating to make sure that the user has entered the right
590thing - e.g. a valid email address - in a form.  Asciimatics makes this easy by adding the
591`validator` parameter to `Text` widgets.
592
593This parameter takes either a regular expression string or a function (taking a single parameter
594of the current widget value).  Asciimatics will use it to determine if the widget contains valid
595data.  It uses this information in 2 places.
596
5971. Whenever the `Frame` is redrawn, asciimatics will check the state and flag any invalid values
598   using the `invalid` colour palette selection.
599
6002. When your program calls :py:meth:`~.Frame.save` specifying `validate=True`, asciimatics will
601   check all fields and throw an :py:obj:`.InvalidFields` exception if it finds any invalid data.
602
603Input focus
604~~~~~~~~~~~
605As mentioned in the explanation of colour palettes, asciimatics has the concept of an input focus.
606This is the Widget that will take any input from the keyboard.  Assuming you are using the
607default palette, the Widget with the input focus will be highlighted.  You can move the focus
608using the cursor keys, tab/backtab or by using the mouse.
609
610The exact way that the mouse affects the focus depends on a combination of the capabilities of
611your terminal/console and the settings of your Frame.  At a minimum, clicking on the Widget will
612always work.  If you specify `hover_focus=True` and your terminal supports reporting mouse move
613events, just hovering over the Widget with the mouse pointer will move the focus.
614
615Modal Frames
616~~~~~~~~~~~~
617When constructing a Frame, you can specify whether it is modal or not using the `is_modal`
618parameter.  Modal Frames will not allow any input to filter through to other Effects in the Scene,
619so when one is on top of all other Effects, this means that only it will see the user input.
620
621This is commonly used for, but not limited to, notifications to the user that must be acknowledged
622(as implemented by :py:obj:`.PopUpDialog`).
623
624Global key handling
625~~~~~~~~~~~~~~~~~~~
626In addition to mouse control to switch focus, you can also set up a global event handler to
627navigate your forms.  This is useful for keyboard shortcuts - e.g. Ctrl+Q to quit your program.
628
629To set up this handler, you need to pass it into your screen on the `play()` Method.  For example
630
631.. code-block:: python
632
633    # Event handler for global keys
634    def global_shortcuts(event):
635        if isinstance(event, KeyboardEvent):
636            c = event.key_code
637            # Stop on ctrl+q or ctrl+x
638            if c in (17, 24):
639                raise StopApplication("User terminated app")
640
641    # Pass this to the screen...
642    screen.play(scenes, unhandled_input=global_shortcuts)
643
644.. warning::
645
646    Note that the global handler is only called if the focus does not process the event.  Some
647    widgets - e.g. TextBox - take any printable text and so the only keys that always get to this
648    handler are the control codes.  Others will sometimes get here depending on the type of Widget
649    in focus and whether the Frame is modal or not..
650
651By default, the global handler will do nothing if you are playing any Scenes containing a Frame.
652Otherwise it contains the top-level logic for skipping to the next Scene (on space or enter), or
653exiting the program (on Q or X).
654
655Dealing with Ctrl+C and Ctrl+Z
656~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
657A lot of modern UIs want to be able to use Ctrl+C/Z to do something other than kill the
658application.  The problem for Python is that this normally triggers a `KeyboardInterrupt` - which
659typically kills the application - or causes the operating system to suspend the process (on UNIX
660variants).
661
662If you want to prevent this and use Ctrl+C/Z for another purpose, you can tell asciimatics to
663catch the low-level signals to prevent these interrupts from being generated (and so return the
664keypress to your application).  This is done by specifying `catch_interrupt=True` when you create
665the `Screen` by calling :py:meth:`.wrapper`.
666
667Dealing with Ctrl+S
668~~~~~~~~~~~~~~~~~~~
669Back in the days when terminals really were separate machines connected over wires to a computer,
670it was necessary to be able to signal that the terminal needed time to catch up.  This was done
671using software flow control, using the Ctrl+S/Ctrl+Q control codes to tell the computer to
672stop/restart sending text.
673
674These days, it's not really necessary, but is still a supported feature on most terminals.  On
675some systems you can switch this off so you get access to Ctrl+S, but it is not possible on them
676all.  See :ref:`ctrl-s-issues-ref` for details
677on how to fix this.
678
679Flow of control
680---------------
681By this stage you should have a program with some Frames and can extract what your user has
682entered into any of them.  But how do you know when to act and move between Frames?  The answer
683is callbacks and exceptions.
684
685Callbacks
686~~~~~~~~~
687A callback is just a function that you pass into another function to be called when the
688associated event occurs.  In asciimatics, they can usually be identified by the fact that they
689start with `on` and correspond to a significant input action from the user, e.g. `on_click`.
690
691When writing your application, you simply need to decide which events you want to use to trigger
692some processing and create appropriate callbacks.  The most common pattern is to use a `Button` and
693define an `on_click` callback.
694
695In addition, there are other events that can be triggered when widget values change.  These can
696be used to provide dynamic effects like enabling/disabling Buttons based on the current value of
697another Widget.
698
699Exceptions
700~~~~~~~~~~
701Asciimatics uses exceptions to tell the animation engine to move to a new Scene or stop the whole
702 process.  Other exceptions are not caught and so can still be used as normal.  The details for
703 the new exceptions are as follows:
704
7051. :py:obj:`.StopApplication` - This exception will stop the animation engine and return flow to
706   the function that called into the Screen.
7072. :py:obj:`.NextScene` - This exception tells the animation engine to move to a new Scene.  The
708   precise Scene is determined by the name passed into the exception.  If none is specified, the
709   engine will simply roundi robin to the next available Scene.
710
711Note that the above logic requires each Scene to be given a unique name on construction.  For
712example:
713
714.. code-block:: python
715
716    # Given this scene list...
717    scenes = [
718        Scene([ListView(screen, contacts)], -1, name="Main"),
719        Scene([ContactView(screen, contacts)], -1, name="Edit Contact")
720    ]
721    screen.play(scenes)
722
723    # You can use this code to move back to the first scene at any time...
724    raise NextScene("Main")
725
726Data handling
727-------------
728By this stage you should have everything you need for a fully functional UI.  However, it may not be quite
729clear how to pass data around all your component parts because asciimatics doesn't provide any classes to do
730it for you.  Why?  Because we don't want to tie you down to a specific implementation.  You should be able to
731pick your own!
732
733Look back at the earlier explanation of model/view design.  The model can be any class you like!  All you
734need to do is:
735
7361. Define a model class to store any state and provide suitable APIs to access it as needed from your UI
737   (a.k.a. views).
7382. Define your own views (based on an ``Effect`` or ``Frame``) to define your UI and store a reference to the
739   model (typically as a parameter on construction).
7403. Use that saved reference to the model to handle updates as needed inside your view's callbacks or methods.
741
742For a concrete example of how to do this check out the contact list sample and look at how it defines and uses
743the ``ContactModel``.  Alternatively, the quick_model sample shows how the same forms would work witha simple
744list of dictionaries instead.
745
746Dynamic scenes
747--------------
748That done, there are just a few more final touches to consider.  These all touch on dynamically changing or
749reconstructing your Scene.
750
751At a high level, you need to decide what you want to achieve.  The basic options are as follows.
752
7531. If you just want to have some extra Frames on the same Screen - e.g. pop-up windows - that's
754   fine.  Just use the existing classes (see below)!
7552. If you want to be able to draw other content outside of your existing Frame(s), you probably
756   want to use other Effects.
7573. If you want to be able to add something inside your Frame(s), you almost certainly want to
758   create a custom Widget for that new content.
759
760The rest of this section goes through those options (and a couple more related changes) in a
761little more detail.
762
763Adding other effects
764~~~~~~~~~~~~~~~~~~~~
765Since Frames are just another Effect, they can be combined with any other Effect in a Scene.  For
766example, this will put a simple input form over the top of the animated Julia set Effect:
767
768.. code-block:: python
769
770    scenes = []
771    effects = [
772        Julia(screen),
773        InputFormFrame(screen)
774    ]
775    scenes.append(Scene(effects, -1))
776    screen.play(scenes)
777
778The ordering is important.  The effects at the bottom of the list are at the top of the screen Z
779order and so will be displayed in preference to those lower in the Z order (i.e. those earlier in
780the list).
781
782The most likely reason you will want to use this is to use the :py:obj:`.Background` Effect to
783set a background colour for the whole screen behind your Frames.  See the forms.py demo for an
784example of this use case.
785
786Pop-up dialogs
787~~~~~~~~~~~~~~
788Along a similar line, you can also add a :py:obj:`.PopUpDialog` to your Scenes at any time.  These
789consist of a single text message and a set of buttons that you can define when creating the dialog.
790
791Owing to restrictions on how objects need to be rebuilt when the screen is resized, these should be
792limited to simple are confirmation or error cases - e.g. "Are you sure you want to quit?"  For more
793details on the restrictions, see the section on restoring state.
794
795Pop-up menus
796~~~~~~~~~~~~
797You can also add a :py:obj:`.PopupMenu` to your Scenes in the same way.  These allow you to create a
798simple temporary list of options from which the user has to select just one entry (by clicking on it
799or moving the focus and pressing Enter) or dismiss the whole list (by pressing Escape or clicking
800outside of the menu).
801
802Owing to their temporary nature, they are not maintained over screen resizing.
803
804Screen resizing
805~~~~~~~~~~~~~~~
806If you follow the standard application mainline logic as found in all the sample code, your
807application will want to resize all your Effects and Widgets whenever the user resizes the
808terminal.  To do this you need to get a new Screen then rebuild a new set of objects to use that
809Screen.
810
811Sound like a bit of a drag, huh?  This is why it is recommended that you separate your
812presentation from the rest of your application logic.  If you do it right you will find that it
813actually just means you go through exactly the same initialization path as you did before to
814create your Scenes in the first place.  There are a couple of gotchas, though.
815
816First, you need to make sure that asciimatics will exit and recreate a new Screen when the
817terminal is resized.  You do that with this boilerplate code that is in most of the samples.
818
819.. code-block:: python
820
821    def main(screen, scene):
822        # Define your Scenes here
823        scenes = ...
824
825        # Run your program
826        screen.play(scenes, stop_on_resize=True, start_scene=scene)
827
828    last_scene = None
829    while True:
830        try:
831            Screen.wrapper(main, arguments=[last_scene])
832            sys.exit(0)
833        except ResizeScreenError as e:
834            last_scene = e.scene
835
836This will allow you to decide how all your UI should look whenever the screen is resized and will
837 restart at the Scene that was playing at the time of the resizing.
838
839Restoring state
840~~~~~~~~~~~~~~~
841Recreating your view is only half the story.  Now you need to ensure that you have restored any
842state inside your application - e.g. any dynamic effects are added back in, your new Scene has
843the same internal state as the old, etc. Asciimatics provides a standard interface (the `clone`
844method) to help you out here.
845
846When the running `Scene` is resized (and passed back into the Screen as the start scene), the new
847`Scene` will run through all the `Effects` in the old copy looking for any with a `clone` method.
848If it finds one, it will call it with 2 parameters: the new `Screen` and the new `Scene` to own the
849cloned `Effect`.  This allows you to take full control of how the new `Effect` is recreated.
850Asciimatics uses this interface in 2 ways by default:
851
8521.  To ensure that any :py:obj:`~.Frame.data` is restored in the new `Scene`.
8532.  To duplicate any dynamically added :py:obj:`.PopUpDialog` objects in the new `Scene`.
854
855You could override this processing to handle your own custom cloning logic.  The formal definition
856of the API is defined as follows.
857
858.. code-block:: python
859
860    def clone(self, screen, scene):
861        """
862        Create a clone of this Effect into a new Screen.
863
864        :param screen: The new Screen object to clone into.
865        :param scene: The new Scene object to clone into.
866        """
867
868Reducing CPU usage
869~~~~~~~~~~~~~~~~~~
870It is the nature of text UIs that they don't need to refresh anywhere near as often as a full-blown
871animated Scene.  Asciimatics therefore optimizes the refresh rate when only Frames are being
872displayed on the Screen.
873
874However, there are some widgets that can reduce the need for animation even further by not
875requesting animation updates (e.g. for a blinking cursor).  If this is an issue for your
876application, you can specify ``reduce_cpu=True`` when constructing your Frames.  See
877contact_list.py for an example of this.
878
879Custom widgets
880--------------
881To develop your own widget, you need to define a new class that inherits from :py:obj:`.Widget`.
882You then have to implement the following functions.
883
8841. :py:meth:`~.Widget.reset` - This is where you should reset any state for your widget.  It gets
885   called whenever the owning Frame is initialised, which can be when it is first displayed, when
886   the user moves to a new Scene or when the screen is resized.
8872. :py:meth:`~.Widget.update` - This is where you should put the logic to draw your widget.  It
888   gets called every time asciimatics needs to redraw the screen (and so should always draw the
889   entire widget).
8903. :py:meth:`~.Widget.process_event` - This is where you should put your code to handle mouse and
891   keyboard events.
8924. :py:obj:`~.Widget.value` - This must return the current value for the widget.
8935. :py:meth:`~.Widget.required_height` - This returns the minimum required height for your widget.
894   It is used by the owning Layout to determine the size and location of your widget.
895
896With these all defined, you should now be able to add your new custom widget to a Layout like any
897of the standard ones delivered in this package.
898