Documentation for py_cui Developers

This page contains information on writing new widgets and popups, as well as anything else required for developers and contributors for py_cui.

Developer tools

There are several tools provided to py_cui developers to aid with debugging. Frst and formost is logging, which can be enabled with:

root.enable_logging()

To add log messages, you will need a logger object, which for most classes is passed from the root PyCUI instance. Once you have a logger, you can make it write messages with:

logger.debug()
logger.warn()
logger.error()

as is the case with the built in logging module.

In addition, there are several shell/batch scripts included for auto-generating documentation from docstrings. Developers should run an appropriate script after completing a new feature with docstrings to ensure resulting markdown looks correct:

cd docs/scripts
bash generateFromDocstrings.sh

for Linux/Mac users, and

cd docs\scripts
generateFromDocstrings.bat

for windows users.

In the event that a new module was added with the changes, please add its generated markdown file to the mkdocs.yml configuration file in the root of this directory. For example, for the grid.py module, the autogenerated docs are added as follows:

- Grid: DocstringGenerated/Grid.md

Unit Tests

py_cui unit tests are written for pytest. Make sure pytest is installed, and simply run

pytest

in the root directory to run all unit tests.

Adding a new Widget

We will walk through the steps of adding a new widget to py_cui (in this case a scroll menu) in order to demonstrate this process.

Step One - Write an implementation

To begin, we need to consider what we will need for our widget to do. For this, we will add a subclass to UIImplementation. We will add this subclass to py_cui/ui.py. In our case, for a scroll menu, we need to be able to scroll up and down, and we will need some variables to represent the current items, the selected item, and the viewport. We will also add some basic getter and setters for these variables.

Below is the MenuImplementation class. note that it takes a logger object instance as a parameter that it passes to its UIImplementation superclass.

class MenuImplementation(UIImplementation):

    def __init__(self, logger):
        super().__init__(logger)
        self._top_view         = 0
        self._selected_item    = 0
        self._view_items       = []

    def clear(self):
        self._view_items = []
        self._selected_item = 0
        self._top_view = 0
        self._logger.debug('Clearing menu')

    def get_selected_item(self):
        return self._selected_item

    def set_selected_item(self, selected_item):
        self._selected_item = selected_item

    def _scroll_up(self):
        if self._top_view > 0 and self._selected_item == self._top_view:
            self._top_view = self._top_view - 1
        if self._selected_item > 0:
            self._selected_item = self._selected_item - 1

        self._logger.debug(f'Scrolling up to item {self._selected_item}')

    def _scroll_down(self, viewport_height):
        if self._selected_item < len(self._view_items) - 1:
            self._selected_item = self._selected_item + 1
        if self._selected_item > self._top_view + viewport_height:
            self._top_view = self._top_view + 1

        self._logger.debug(f'Scrolling down to item {self._selected_item}')

    def add_item(self, item_text):
        self._logger.debug(f'Adding item {item_text} to menu')
        self._view_items.append(item_text)


    def add_item_list(self, item_list):
        self._logger.debug(f'Adding item list {str(item_list)} to menu')
        for item in item_list:
            self.add_item(item)

    def remove_selected_item(self):
        if len(self._view_items) == 0:
            return
        self._logger.debug(f'Removing {self._view_items[self._selected_item]}')
        del self._view_items[self._selected_item]
        if self._selected_item >= len(self._view_items):
            self._selected_item = self._selected_item - 1

    def get_item_list(self):
        return self._view_items

    def get(self):
        if len(self._view_items) > 0:
            return self._view_items[self._selected_item]
        return None

The reason we separate any widget ui-agnostic logic into a seperate class is because we want to reuse this logic if we wish to create other UI elements that share similar characteristics but are not widgets (ex. popups).

Step Two - Extend the Widget Class

Your next step when writing a new widget is to create a class in py_cui/widgets.py that extends the base Widget class, as well as the implementation class we just constructed. We call the superclass initializers, and we add override functions for _draw and _handle_key_press.

For our ScrollMenu example:

class ScrollMenu(Widget, py_cui.ui.MenuImplementation):

    def __init__(self, id, title, grid, row, column, row_span, column_span, padx, pady, logger):
        Widget.__init__(self, id, title, grid, row, column, row_span, column_span, padx, pady, logger)
        py_cui.ui.MenuImplementation.__init__(self, logger)

    def _handle_key_press(self, key_pressed):

        super().handle_key_press(key_pressed)

    def _draw(self):

        super().draw()

The _handle_key_press and _draw functions must be extended for your new widget. You may leave the _handle_key_press as above, if you don't require any keybindings for the widget. The _draw function must extended, as the base class does no drawing itself, instead just setting up color rules.

Step 3 - Add Key Bindings

Next, add any default key bindings you wish to have for the widget when in focus mode. In the case of the scroll menu, we wish for the arrow keys to scroll up and down, so we extend the handle_key_press function:

def _handle_key_press(self, key_pressed):

    super().handle_key_press(key_pressed)
    if key_pressed == py_cui.keys.KEY_UP_ARROW:
        self.scroll_up()
    if key_pressed == py_cui.keys.KEY_DOWN_ARROW:
        self.scroll_down()

Note that the way default key bindings are added are simply if statements, which happen after the super() call. The _scroll_up() and _scroll_down() functions simply contain the logic for editing the viewport for the menu, and should have been implemented in the MenuImplementation superclass.

Step 4 - implement the Draw function

In the draw function, you must use the self._renderer object to render your widget to the screen. In our case, we want a border around the menu widget, and we also want to draw menu items that are within our viewport. The key renderer functions we will use are:

self._renderer.draw_border(self)

which will draw a border around the widget space, and

self._renderer.draw_text(self, text, y_position)

which will draw the text in the y_position. For our scroll menu, we would write the following:

def _draw(self):

    super()._draw()
    self._renderer.set_color_mode(self._color)
    self._renderer.draw_border(self)
    counter = self._pady + 1
    line_counter = 0
    for line in self._view_items:
        if line_counter < self._top_view:
            line_counter = line_counter + 1
        else:
            if counter >= self._height - self._pady - 1:
                break
            if line_counter == self._selected_item:
                self._renderer.draw_text(self, line, self._start_y + counter, selected=True)
            else:
                self._renderer.draw_text(self, line, self._start_y + counter)
            counter = counter + 1
            line_counter = line_counter + 1
    self._renderer.unset_color_mode(self._color)
    self._renderer.reset_cursor(self)

Note that you should call super()._draw() and self._renderer.set_color_mode(self._color) at the start of the function (to initialize color modes), and self._renderer.unset_color_mode(self._color) and self._renderer.reset_cursor(self) to remove color settings, and place the cursor in the correct location.

Step 5 - Add a function to PyCUI class to add the widget

Finally, add a function to the PyCUI class in __init__.py that will add the widget to the CUI. In our case we write the following:

def add_scroll_menu(self, title, row, column, row_span = 1, column_span = 1, padx = 1, pady = 0):

    id = f'Widget{len(self.get_widgets().keys())}'
    new_scroll_menu = widgets.ScrollMenu(   id, 
                                            title, 
                                            self._grid, 
                                            row, 
                                            column, 
                                            row_span, 
                                            column_span, 
                                            padx, 
                                            pady, 
                                            self._logger)
    self.get_widgets()[id] = new_scroll_menu
    if self._selected_widget is None:
        self.set_selected_widget(id)
    self._logger.debug(f'Adding widget {title} w/ ID {id} of type {str(type(new_scroll_menu))}'
    return new_scroll_menu

The function must:

  • Create an id titled 'Widget####' where #### is replaced with the number of widget
  • Add the widget to the PyCUI widgets dict with the ID as a key
  • If there is no selected widget, make this new widget the selected one
  • Return a reference to the widget

That's it!

Your widget is now ready to be added to the CUI! Simply call the add function with appropriate parameters on the root PyCUI window:

root.add_scroll_menu('Demo', 1, 1)

Adding a new Popup

This documentation section is incomplete. Feel free to expand me.

Working on the renderer

This documentation section is incomplete. Feel free to expand me.

Working on color rules

This documentation section is incomplete. Feel free to expand me.