145 lines
8.3 KiB
Python
145 lines
8.3 KiB
Python
"""
|
|
lcdMenu - A micropython library, which supports vertical and horizontal scrolling through menu items on both 2x16 and 4x20 LCDs
|
|
Copyright (C) 2024 Benjamin Burkhardt <bluefox@privacynerd.de>
|
|
|
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
from time import sleep
|
|
|
|
|
|
class lcdMenu:
|
|
# lcd: I2C_LCD - an object of the I2C_LCD class (https://git.privacynerd.de/BlueFox/micropython-libraries/src/branch/main/PCF8574T)
|
|
# buttons: dict - a dictionary with machine.Pin objects as items with following keys
|
|
# - "prev_btn" - OPTIONAL - when pressed, select the previous menu item
|
|
# - "next_btn" - REQUIRED - when pressed, select the next menu item
|
|
# - "ok_btn" - REQUIRED - when pressed, call the callback function of the menu item
|
|
# menu_items: list - a list (-> maintains order!) containing tuples with the following format: (<ENTRY_NAME>,<CALLBACK FUNCTION>)
|
|
# scroll_direction: bool - if true, the scrolling direction is horizontal, if false, vertical
|
|
# 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):
|
|
# save the argument variables
|
|
self.lcd = lcd
|
|
if "prev_btn" in buttons.keys():
|
|
self.prev_btn = buttons["prev_btn"]
|
|
else:
|
|
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
|
|
|
|
# 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
|
|
|
|
|
|
def show_selection(self):
|
|
# a check:
|
|
# if you scrolling vertically, I found no elegant way to hide the name (there just need's to be something up there!)
|
|
if self.scroll_direction and self.hide_menu_name:
|
|
raise TypeError("Hiding the menu name whilst having the scroll direction set to horizontal!")
|
|
|
|
# get some often used values into local variables
|
|
selection_name = self.menu_items[self.current_selection][0]
|
|
lw = self.lcd.num_columns
|
|
|
|
# now show it off!
|
|
# Horizontal scrolling:
|
|
if self.scroll_direction:
|
|
self.lcd.move_to(0,0)
|
|
if self.lcd.num_lines == 4:
|
|
self.lcd.putstr(f"{self.fill_char*lw}{' '*lw*2}{self.fill_char*lw}") # fill the first and last line with 'fill_char's
|
|
self.lcd.move_to(0,1) # move to the second line for the starting message below (takes two lines)
|
|
self.lcd.putstr(f"[{self.name[0:lw-2].center(lw-2)}]") # the menu name (cannot be hidden in this mode)
|
|
self.lcd.putstr(f"<{selection_name[0:lw-2].center(lw-2)}>") # the current selected menu item's name
|
|
# Vertical scrolling:
|
|
else:
|
|
pass # TODO!
|
|
print(self.current_selection)
|
|
|
|
|
|
def previous_selection(self):
|
|
self.current_selection -= 1
|
|
if self.current_selection < 0: # after the last element:
|
|
if self.cycle: # if cycling is enabled, set it to the index of the last element
|
|
self.current_selection = len(self.menu_items)-1
|
|
else: # else, go to first element
|
|
self.current_selection = 0
|
|
|
|
|
|
def next_selection(self):
|
|
self.current_selection += 1
|
|
if self.current_selection >= len(self.menu_items): # after the last element:
|
|
if self.cycle: # if cycling is enabled, go to first element
|
|
self.current_selection = 0
|
|
else: # else, set it to the index of the last element
|
|
self.current_selection = len(self.menu_items)-1
|
|
|
|
|
|
def execute_selection(self):
|
|
selection = self.menu_items[self.current_selection]
|
|
lw = self.lcd.num_columns
|
|
|
|
# if the program executed had no display (so the user notices something happens!)
|
|
self.lcd.move_to(0,0)
|
|
if self.lcd.num_lines == 4:
|
|
self.lcd.putstr(f"{self.fill_char*lw}{' '*lw*2}{self.fill_char*lw}") # fill the first and last line with 'fill_char's
|
|
self.lcd.move_to(0,1) # move to the second line for the starting message below (takes two lines)
|
|
self.lcd.putstr(f"[{selection[0][0:lw-2].center(lw-2)}]{self.start_execution_msg[0:lw].center(lw)}")
|
|
sleep(self.start_execution_wait) # wait some time before execution (so that the text can be read)
|
|
|
|
# run the program
|
|
return_value = selection[1]()
|
|
|
|
# show a exit when there's no specific return value
|
|
if not return_value: # if the return value is None / nothing was returned
|
|
while self.ok_btn.value() == 1: time.sleep(self.debounce_time) # wait till ok_btn release (e.g. if the "program" is a simple send action)
|
|
self.lcd.move_to(0,0)
|
|
if self.lcd.num_lines == 4:
|
|
self.lcd.putstr(f"{self.fill_char*lw}{' '*lw*2}{self.fill_char*lw}") # fill the first and last line with 'fill_char's
|
|
self.lcd.move_to(0,1) # move to the second line for the starting message below (takes two lines)
|
|
self.lcd.putstr(f"[{selection[0][0:lw].center(lw-2)}]{self.end_execution_msg[0:lw].center(lw)}")
|
|
sleep(self.end_execution_wait)
|
|
|
|
|
|
# listen for button presses (this method should be called in an endless loop, see method run)
|
|
def loop(self):
|
|
if self.prev_btn:
|
|
if self.prev_btn.value() == 1:
|
|
self.previous_selection()
|
|
self.show_selection()
|
|
while self.prev_btn.value() == 1: sleep(self.debounce_time) # wait till release
|
|
if self.next_btn.value() == 1:
|
|
self.next_selection()
|
|
self.show_selection()
|
|
while self.next_btn.value() == 1: sleep(self.debounce_time) # wait till release
|
|
if self.ok_btn.value() == 1:
|
|
while self.ok_btn.value() == 1: sleep(self.debounce_time) # wait till release
|
|
self.execute_selection()
|
|
|
|
|
|
def run(self):
|
|
# show the selection first
|
|
self.show_selection()
|
|
|
|
# then listen on button presses in a loop...
|
|
while True:
|
|
self.loop()
|