diff --git a/README.md b/README.md index 8071a8b..b3bd1d1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This project is the completely rewritten successor of my old (and now archived) - [x] a reliable order of the menu items - [x] good documentation for all of this (maybe through examples) - [x] show an exit screen when a specific exit code is returned by a callback function -- [ ] make the menu itself exitable (to enable stuff like submenus, etc.) +- [x] make the menu itself exitable (to enable stuff like submenus, etc.) - [x] make the project a valid python package diff --git a/__init__.py b/__init__.py index 0ab760e..7069d74 100644 --- a/__init__.py +++ b/__init__.py @@ -23,9 +23,8 @@ class lcdMenu: # cycle: bool - if true, start again with the first menu entry after the last one (and show the last before the first) # hide_menu_name: bool - OPTIONAL - if true, hide the menu's name (won't work in combination with a vertical scrolling direction) # name: str - OPTIONAL - a string with the name of the menu (can be hidden under certain circumstances) - # default_selection: int - OPTIONAL - the index of the item selected by default (starting with 0) - DON'T USE NEGATIVE INDEXES # debounce_time: float - OPTIONAL - the debounce time used by the library to debounce button presses - def __init__(self, lcd, buttons: dict, menu_items: list, scroll_direction: bool, cycle: bool, hide_menu_name: bool = False, name: str = "CHOOSE", default_selection: int = 0, debounce_time: float = 0.15): + def __init__(self, lcd, buttons: dict, scroll_direction: bool, cycle: bool, hide_menu_name: bool = False, name: str = "CHOOSE", debounce_time: float = 0.15): # save the argument variables self.lcd = lcd if "prev_btn" in buttons.keys(): @@ -34,22 +33,30 @@ class lcdMenu: self.prev_btn = None self.next_btn = buttons["next_btn"] self.ok_btn = buttons["ok_btn"] - self.menu_items = menu_items self.scroll_direction = scroll_direction self.cycle = cycle self.hide_menu_name = hide_menu_name self.name = name - self.current_selection = default_selection self.debounce_time = debounce_time + self.current_selection = 0 # set a standard value (can/most of the time will be changed directly after __init__ by a call to self.setup()) + self.menu_items = [] # set a standard empty (can/most of the time will be changed directly after __init__ by a call to self.setup()) + # variables that are very unlikely the user want's to set them (but can be set via ., of course) self.start_execution_msg = "Selected..." # the string displayed when an menu item is selected self.end_execution_msg = "Closing..." # the string displayed when the callback function of a selected menu item returns self.end_execution_wait = 1 # the time (in seconds) to wait after the callback function of a selected menu item returns self.start_execution_wait = 0.5 # the time (in seconds) to wait before the callback function of a selected menu item is called self.end_execution_wait = 1 # the time (in seconds) to wait after the callback function of a selected menu item returns self.fill_char = '-' # the character used to fill up space (used only on 4x20 displays); MUST BE 1 character long - - + + + # menu_items: list - a list (-> maintains order!) containing tuples with the following format: (,) + # start_selection: int - OPTIONAL - the index of the item selected by default (starting with 0) - DON'T USE NEGATIVE INDEXES + def setup(self, menu_items: list, start_selection: int = 0): + self.menu_items = menu_items + self.current_selection = start_selection + + def show_selection(self): # some checks: # 1. if you scrolling vertically, I found no elegant way to hide the name (there just need's to be something up there!) @@ -58,7 +65,6 @@ class lcdMenu: # 2. if there are no menu items to display... if len(self.menu_items) == 0: raise TypeError("Can't show empty menus! Maybe you forgot calling self.setup() after initializing me?") - # get some often used values into local variables selection_name = self.menu_items[self.current_selection][0] @@ -101,7 +107,7 @@ class lcdMenu: # maybe the following could be done with a crazy math formula - but I want to keep it simpler! # as there aren't enough menu items after the current selection to fill the display, we have to... # ... calculate where to place the current selection, how many items there are before it and how many after it - menu_items_cut = self.menu_items[:lines_for_display:-1][::-1] # cut the menu_items list to the relevant last n ones maintaining order (n = number of lines for display) + menu_items_cut = self.menu_items[::-1][:lines_for_display][::-1] # cut the menu_items list to the relevant last n ones maintaining order (n = number of lines for display) current_pos_in_cut = -len(self.menu_items) + self.current_selection + lines_for_display # calculate the current index of the selection in the new cut # draw all the lines for i in range(lines_for_display): @@ -219,11 +225,17 @@ class lcdMenu: while self.ok_btn.value() == 1: sleep(self.debounce_time) # wait till release self.execute_selection() - + def stop(self): # here to act as a callback for a menu entry (if the user wants to ofc!) + self.running = False + return True + def run(self): # show the selection first self.show_selection() + self.running = True # then listen on button presses in a loop... - while True: + while self.running: self.loop() + + return True # to prevent a "Closing menu ..." in submenu-situations diff --git a/example_quittable.py b/example_quittable.py new file mode 100644 index 0000000..91addd2 --- /dev/null +++ b/example_quittable.py @@ -0,0 +1,36 @@ +from lcdMenu import lcdMenu +from machine import Pin, I2C +from PCF8574T import I2C_LCD + +prev_btn = Pin(13, Pin.IN, Pin.PULL_DOWN) # input of the first btn +next_btn = Pin(14, Pin.IN, Pin.PULL_DOWN) # input of the second btn +ok_btn = Pin(15, Pin.IN, Pin.PULL_DOWN) # input of switch + +LCD = I2C_LCD(I2C(0, sda=Pin(8), scl=Pin(9), freq=400000),0x27,2, 16) + +def first_callback(): + print("first_callback() called!") +def second_cb(): + print("second_cb() called") +def third_cb(): + print("third_cb() called") + return True + + +button_mappings = {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn} + +submenu = lcdMenu(LCD, button_mappings, scroll_direction=True, cycle=False, hide_menu_name=False, name="Submenu!") +# the submenu.stop callback is a special callback specifically designed for use cases where the lcdMenu ist started with run() +# submenu.stop breaks an infinite loop inside the run() method, essentially quitting the run() method and giving back flow to the caller context +# so we can utilize this for us to quit a submenu (or a "main" menu if you want to, of course!) +submenuItems = [("first item", first_callback), + ("second item", second_cb), + ("third item", third_cb), + ("back", submenu.stop)] +submenu.setup(submenuItems) + +mainmenu = lcdMenu(LCD, button_mappings, scroll_direction=False, cycle=True, hide_menu_name=False, name="Main menu!") +mainmenu.setup([("Sub menu",submenu.run)]) + +mainmenu.run() + diff --git a/example_simple.py b/example_simple.py index d1b2e32..4033778 100644 --- a/example_simple.py +++ b/example_simple.py @@ -20,6 +20,8 @@ def third_cb(): menuItems = [("first item", first_callback), ("second item", second_cb), ("third item", third_cb)] -menu = lcdMenu(LCD, {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn}, menuItems, scroll_direction=False, cycle=False, hide_menu_name=True, name="Fullscreen!") +menu = lcdMenu(LCD, {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn}, scroll_direction=False, cycle=False, hide_menu_name=True, name="Fullscreen!") +menu.setup(menuItems) menu.run() + diff --git a/example_submenu.py b/example_submenu.py index 15ebc51..31eae40 100644 --- a/example_submenu.py +++ b/example_submenu.py @@ -22,7 +22,11 @@ menuItems = [("first item", first_callback), ("third item", third_cb)] button_mappings = {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn} -submenu = lcdMenu(LCD, button_mappings, menuItems, scroll_direction=True, cycle=False, hide_menu_name=False, name="Submenu!") +submenu = lcdMenu(LCD, button_mappings, scroll_direction=True, cycle=False, hide_menu_name=False, name="Submenu!") +submenu.setup(menuItems) + +mainmenu = lcdMenu(LCD, button_mappings, scroll_direction=False, cycle=True, hide_menu_name=False, name="Main menu!") +mainmenu.setup([("Sub menu",submenu.run)]) -mainmenu = lcdMenu(LCD, button_mappings, [("Sub menu",submenu.run)], scroll_direction=False, cycle=True, hide_menu_name=False, name="Main menu!") mainmenu.run() +