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.