Skip to content

IndicativeListBox

Ezio-Sarthak edited this page Mar 21, 2021 · 2 revisions

A urwid.ListBox does not make it obvious that due to limited space only a part of the list items is displayed. Therefore I've added bars at the top and bottom to indicate hidden elements.

Demonstration of IndicativeListBox

In addition, the behavior has been changed so that the widget is more useful in combination with (vertical) containers such as urwid.Pile.

The original creator of this library is AFoeee.


Contents

  1. Implementation
    1. Constructor
      1. Mandatory parameters
      2. Optional parameters
    2. Enumerations
      1. IndicativeListBox.POSITION
    3. Methods
      1. Internal methods
    4. Variables
      1. Internal variables
  2. Controls
  3. Examples
    1. Template
    2. Minimal
    3. Display items above/below
    4. Styled
    5. Styled version in context with other widgets
  4. Design Decisions

Implementation

Constructor

Mandatory parameters

Optional parameters

  • position= 0: int / IndicativeListBox.POSITION
    Specifies the initial position. If the specified position is out of range, the next valid position is selected.

  • on_selection_change= None: callable / None
    A hook which is triggered when the selection changes.

  • initialization_is_selection_change= False: bool
    If True, the hook is executed right after the initialization and every time set_body() is called.

  • modifier_key= MODIFIER_KEY.NONE: MODIFIER_KEY
    Specifies a modifier that must be additionally pressed, so that the widget responds to input. For example, up becomes ctrl up.
    That way the widget can be used in containers, even though those do use the same commands for navigation.

  • return_unused_navigation_input= True: bool
    If the list item at the top is selected and you navigate further upwards, the input is normally not swallowed by the list box, but passed on so that other widgets can interpret it. This may result in transferring the focus.

  • topBar_align= "center": str
    Text alignment mode – typically "left", "center" or "right".

  • topBar_endCovered_prop= ("▲", None, None): indexable
    Defines the appearance of the bar at the top when the first item is not visible. There are two color schemes, depending on whether the widget has the focus or not.
    Expects an indexable like ("text", "palette_entry_when_focus", "palette_entry_when_offFocus").

    • "text" is modified via str.format(). That way "{}" is replaced by the number of hidden elements above the visible section.
      Of course, this can be ignored and characters like "▼▲", "ᐁᐃ", "↓↑", "⇩⇧" or "⬇⬆" can be used instead.
    • "palette_entry_when_*" can be None or a str that corresponds to a palette entry.
  • topBar_endExposed_prop= ("───", None, None): indexable
    Defines the appearance of the bar at the top when the first item is visible. There are two color schemes, depending on whether the widget has the focus or not.
    Expects an indexable like ("text", "palette_entry_when_focus", "palette_entry_when_offFocus").

    • "text" markup to be displayed.
    • "palette_entry_when_*" can be None or a str that corresponds to a palette entry.
  • bottomBar_align= "center": str
    See parameter topBar_align.

  • bottomBar_endCovered_prop= ("▼", None, None): indexable
    See parameter topBar_endCovered_prop (applied to the bottom).

  • bottomBar_endExposed_prop= ("───", None, None): indexable
    See parameter topBar_endExposed_prop (applied to the bottom).

  • highlight_offFocus= None: str / None
    Defines the color scheme of the selected list item when the widget does not have the focus. (Normally, the selection would only be highlighted as long as the widget has the focus.)
    Can be None or a str that corresponds to a palette entry.

Enumerations

IndicativeListBox.POSITION

These values can be passed instead of an int to methods which select list items.

Enum values:
  • LAST= 1
    Represents the last valid index.

  • MIDDLE= 2
    Represents an index in the middle of the body.

  • RANDOM= 3
    Represents a random index.

Methods

  • get_body() -> urwid.ListWalker
    Returns the body of the contained urwid.ListBox.

  • body_len() -> int
    Convenience method, that returns the number of elements in the body.
    If the body is empty, it returns 0.

  • rearmost_position() -> int
    Convenience method, that returns the last valid index.
    If the body is empty, it returns -1.

  • body_is_empty() -> bool
    Convenience method, that returns True if the body contains no elements.

  • position_is_valid(position) -> bool
    Convenience method, that returns True if the passed position is a valid index.

    • position: int
  • get_item(position) -> urwid.AttrMap / None
    Returns the list item at the specified position or None if there is no such item.

    • position: int
  • get_first_item() -> urwid.AttrMap / None
    Convenience method, that returns the first list item or None if the body is empty.

  • get_last_item() -> urwid.AttrMap / None
    Convenience method, that returns the last list item or None if the body is empty.

  • get_selected_item() -> urwid.AttrMap / None
    Returns the selected list item or None if the body is empty.

  • get_selected_position() -> int / None
    Returns the selected position or None if the body is empty.

  • first_item_is_selected() -> bool
    Convenience method, that returns True if the first list item is selected.

  • last_item_is_selected() -> bool
    Convenience method, that returns True if the last list item is selected.

  • set_body(body, *, alternative_position=None) -> None
    Swaps the old list body with a new one.
    An attempt is made to keep the currently selected position. If not possible the nearest valid position is selected.
    It's also possible to specify an alternative position. If this position is out of range, the nearest valid position is selected.
    If initialization_is_selection_change was set in the constructor, this method triggers the on_selection_change hook.

  • select_item(position) -> None
    Select the specified position. If the position is out of range, the nearest valid postion is selected.

  • select_first_item() -> None
    Convenience method, that selects the first list item.

  • select_last_item() -> None
    Convenience method, that selects the last list item.

  • delete_position(position) -> None
    Deletes the list item at the specified position.

    • position: int
  • delete_selected_position() -> None
    Deletes the selected list item if there is one.
    In the case that the body is empty nothing is done.

Internal methods

  • render(size, focus=False) -> urwid.Canvas
    Has been overridden to perform the appearance update.

    • size: iterable
    • focus: bool
  • keypress(size, key) -> str / None
    Has been overridden to interpret additional keys and, if necessary, swallow unused keystrokes.

    • size: iterable
    • key: str
  • mouse_event(size, event, button, col, row, focus) -> bool
    Has been overridden to use the modified size. In addition, logic for the navigation via mouse was implemented.

    • size: iterable
    • event: str
    • button: int
    • col: int
    • row: int
    • focus: bool
  • _pass_key_to_contained_listbox(size, key) -> str / None
    Passes keystrokes to the contained urwid.ListBox and, if necessary, swallows the result.

    • size: iterable
    • key: str
  • _get_nearest_valid_position(position) -> int
    If the position is in range, it will be returned. If it is out of range, the nearest valid position will be returned.

  • _reset_highlighting() -> None
    Resets the highlighted list item to its original value and causes the highlighting to be set again the next time render() is called.

Variables

  • on_selection_change= None: callable / None
    See constructor parameter on_selection_change.

Internal variables

  • _listbox: urwid.ListBox
    The contained urwid.ListBox.

  • _top_bar: urwid.AttrMap
    A urwid.Text widget, which is wrapped in an urwid.AttrMap.

  • _bottom_bar: urwid.AttrMap
    See variable _top_bar.

  • _topBar_endCovered_markup: str
    The markup of the bar at the top when the first list item is not visible.

  • _topBar_endCovered_focus: dict
    During the initialization of urwid.AttrMap, the value can be passed as non-dict. After initializing, its value can be manipulated by passing a dict.
    This is used to change the appearance of the bar at the top when the widget does have the focus.

  • _topBar_endCovered_offFocus: dict
    This is used to change the appearance of the bar at the top when the widget does not have the focus.

  • _topBar_endExposed_markup: str
    See variable _topBar_endCovered_markup.

  • _topBar_endExposed_focus: dict
    See variable _topBar_endCovered_focus.

  • _topBar_endExposed_offFocus: dict
    See variable _topBar_endCovered_offFocus.

  • _bottomBar_endCovered_markup: str
    See variable _topBar_endCovered_markup.

  • _bottomBar_endCovered_focus: dict
    See variable _topBar_endCovered_focus.

  • _bottomBar_endCovered_offFocus: dict
    See variable _topBar_endCovered_offFocus.

  • _bottomBar_endExposed_markup: str
    See variable _topBar_endCovered_markup.

  • _bottomBar_endExposed_focus: dict
    See variable _topBar_endCovered_focus.

  • _bottomBar_endExposed_offFocus: dict
    See variable _topBar_endCovered_offFocus.

  • _highlight_offFocus: dict
    See variable _topBar_endCovered_offFocus.

  • _last_focus_state: bool / None
    Stores the last focus state to prevent that changing the terminal size deletes the original appearance of a list item when off focus highlighted.
    None represents the initialization value and indicates that an evaluation should be performed during rendering.

  • _original_item_attr_map: dict / None
    Stores the original appearance of the off focus highlighted list item.

  • _initialization_is_selection_change: bool
    See constructor parameter initialization_is_selection_change.

  • _modifier_key: MODIFIER_KEY
    See constructor parameter modifier_key.

  • _return_unused_navigation_input: bool
    See constructor parameter return_unused_navigation_input.


Controls

  • up () or down () to move one row.
    Visible rows can also be left clicked to select them.

  • page up/down or mouse wheel up/down to move one list box length.

  • home or end to jump to the corresponding end.

  • The behavior can be changed by passing a MODIFIER_KEY so that in addition to the navigation keys a corresponding modifier must be pressed. See constructor parameter modifier_key.


Examples

The module examples include an example application.
In the event that this code is a bit too complex, here are also some simpler examples.

Template

The following examples focus on the creation of IndicativeListBox. They are actually executed in the context of the code below and can be inserted at # insert example here to make them executable.

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from urwid_picker_widgets import IndicativeListBox    # installed via pip
import urwid                                              # installed via pip

# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus",              "black",            "light cyan",   "standout"),
           ("ilb_barActive_focus",       "dark cyan",        "light gray"),
           ("ilb_barActive_offFocus",    "light gray",       "dark gray"),
           ("ilb_barInactive_focus",     "light cyan",       "dark gray"),
           ("ilb_barInactive_offFocus",  "black",            "dark gray")]

# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]

# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]

# insert example here

loop = urwid.MainLoop(ilb,
                      PALETTE)
loop.run()

To quit the test program, press ctrl + c.

Minimal

ilb = IndicativeListBox(attr_body)

Demonstration of example 'Minimal'

Display items above/below

ilb = IndicativeListBox(attr_body,
                        topBar_endCovered_prop=("{} above ...", None, None),
                        bottomBar_endCovered_prop=("{} below ...", None, None))

Demonstraion of example 'Display hidden items above/below'

Styled

ilb = IndicativeListBox(attr_body,
                        topBar_endCovered_prop=("ᐃ", "ilb_barActive_focus", "ilb_barActive_offFocus"),
                        topBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"), 
                        bottomBar_endCovered_prop=("ᐁ", "ilb_barActive_focus", "ilb_barActive_offFocus"), 
                        bottomBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"))

Demonstration of example 'Styled'

Styled version in context with other widgets

This example is not executed in the context described above, but stands alone.

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from urwid_picker_widgets import IndicativeListBox, MODIFIER_KEY    # installed via pip
import urwid                                                            # installed via pip

# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus",              "black",            "light cyan",   "standout"),
           ("ilb_barActive_focus",       "dark cyan",        "light gray"),
           ("ilb_barActive_offFocus",    "light gray",       "dark gray"),
           ("ilb_barInactive_focus",     "light cyan",       "dark gray"),
           ("ilb_barInactive_offFocus",  "black",            "dark gray"),
           ("ilb_highlight_offFocus",    "black",            "dark cyan")]

# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]

# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]

ilb = ilb = IndicativeListBox(attr_body,
                              modifier_key=MODIFIER_KEY.CTRL,
                              return_unused_navigation_input=False,
                              topBar_endCovered_prop=("ᐃ", "ilb_barActive_focus", "ilb_barActive_offFocus"),
                              topBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"), 
                              bottomBar_endCovered_prop=("ᐁ", "ilb_barActive_focus", "ilb_barActive_offFocus"), 
                              bottomBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"),
                              highlight_offFocus="ilb_highlight_offFocus")

pile = urwid.Pile([urwid.Text("The listbox responds only if 'ctrl' is pressed."),
                   urwid.Divider(" "),
                   urwid.Button("a button"),
                   urwid.BoxAdapter(ilb, 6),         # Wrap flow widget in box adapter
                   urwid.Button("another button")])


loop = urwid.MainLoop(urwid.Filler(pile, "top"),
                      PALETTE)
loop.run()

Demonstraion of example 'Styled in context with other widgets'

The IndicativeListBox does only respond if you press ctrl in addition to the navigation input.
To quit the test program, press ctrl + c.


Design decisions

  • If the selection changes and a callable was passed to the constructor, a hook is executed.
    Optionally, the initialization and set_body() can also be considered as a selection change.

  • To allow the selection of values even when the surrounding widget uses the same navigation keys, a MODIFIER_KEY can be passed. If so the widget will only respond to navigation input if the appropriate modifier is additionally pressed. (up -> ctrl up)

  • return_unused_navigation_keystroke can be used to make scrolling more convenient, as the arrow keys can be held pressed down without allowing the focus to move to the next widget.

  • Every widget in body is wrapped in an urwid.AttrMap. That way off-focus highlighting is enabled.

  • The highlighting in urwid is bound to the focus. This means that the selected item is only distinguishable as long as the widget has the focus.
    Therefore, the off-focus highlighting is implemented by temporarily changing the color scheme of the selected item. This change is reversed as soon as the widget has the focus again, because then the regular highlighting takes effect.

  • For convenience, positions that are out of range do not result in exceptions when selecting. Instead, the nearest valid position is used. (Negative values result in 0. Too large values result in the last index.)

  • The bars are really just urwid.Text widgets, that are wrapped in urwid.AttrMap.

  • The change of the appearance takes place in the render() method. That way it is possible to respond dynamically to changes in the size of the terminal.

  • The procedure for determining which ends of the list are currently visible is a modification of urwid.ListBox.ends_visible(). The only real difference is that in addition the number of elements above/below the displayed section is calculated.