Added quitting a menu functionality (actually quitting the run()'s infinite internal loop)

This commit is contained in:
BlueFox 2024-11-12 22:15:15 +01:00
parent 974229f4b2
commit 64ce12f78b
Signed by: BlueFox
GPG Key ID: 327233DA85435270
5 changed files with 68 additions and 14 deletions

View File

@ -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

View File

@ -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 <lcdMenuObject>.<attribute_name>, 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: (<ENTRY_NAME>,<CALLBACK FUNCTION>)
# 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

36
example_quittable.py Normal file
View File

@ -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()

View File

@ -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()

View File

@ -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()