diff --git a/lib/lcdMenu/__init__.py b/lib/lcdMenu/__init__.py deleted file mode 100644 index f03b045..0000000 --- a/lib/lcdMenu/__init__.py +++ /dev/null @@ -1,241 +0,0 @@ -""" -lcdMenu - A micropython library, which supports vertical and horizontal scrolling through menu items on both 2x16 and 4x20 LCDs -Copyright (C) 2024 Benjamin Burkhardt - -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 . -""" - -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: (,) - # 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) - # debounce_time: float - OPTIONAL - the debounce time used by the library to debounce button presses - 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(): - self.prev_btn = buttons["prev_btn"] - else: - self.prev_btn = None - self.next_btn = buttons["next_btn"] - self.ok_btn = buttons["ok_btn"] - self.scroll_direction = scroll_direction - self.cycle = cycle - self.hide_menu_name = hide_menu_name - self.name = name - 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!) - if self.scroll_direction and self.hide_menu_name: - raise TypeError("Hiding the menu name whilst having the scroll direction set to horizontal is impossible!") - # 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] - lw = self.lcd.num_columns - - # fill the custom character fields in the displays memory - self.lcd.custom_char(0, bytearray([0x04,0x0A,0x11,0x00,0x00,0x00,0x00,0x00])) # arrow up - self.lcd.custom_char(1, bytearray([0x00,0x00,0x00,0x00,0x00,0x11,0x0A,0x04])) # arrow down - self.lcd.custom_char(2, bytearray([0x04,0x0A,0x11,0x00,0x00,0x11,0x0A,0x04])) # arrow up and down - self.lcd.custom_char(3, bytearray([0x11,0x0A,0x04,0x00,0x00,0x04,0x0A,0x11])) # no options - self.lcd.custom_char(4, bytearray([0x08,0x08,0x08,0x0F,0x08,0x08,0x08,0x08])) # line with a fork (to show the current selection - v scrolling) - self.lcd.custom_char(5, bytearray([0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08])) # line without a fork (to show unselected items - v scrolling) - self.lcd.custom_char(6, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x00])) # three dots in a row - - # 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: - lines_for_display = self.lcd.num_lines - items_before_selection = self.current_selection # e.g. if current selection is the second -> 1; one item is before this - items_after_selection = len(self.menu_items) - self.current_selection - 1 - self.lcd.move_to(0,0) - if not self.hide_menu_name: - self.lcd.putstr(f"[{self.name[0:lw-2].center(lw-2)}]") # print the menu name surrounded by sqare brackets [] - lines_for_display -= 1 # decrease the amount of lines available for displaying menu items by 1 (as it's used for the menu name) - - # now we want to display the selections - # where the currently selected item should be the uppermost if possible - # only at the end of the item list, the current item has to be in a lower line... - # ... to avoid emtpy lines at the end as this would seem unprofessional/unclean to me - # but it adds an noticeable amount of extra work / a complex algorithm - if items_after_selection < (lines_for_display - 1): # if there aren't enough items to fill the display after the current selection - # 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[::-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): - if i == current_pos_in_cut: # if drawing the currently selected item - self.lcd.putstr(f"{chr(4)} {menu_items_cut[i][0][0:lw-4]}") - self.lcd.putstr(" " * ((lw-4)-len(menu_items_cut[i][0][0:lw-4]))) # fit the line - else: - self.lcd.putstr(f"{chr(5)} {menu_items_cut[i][0][0:lw-4]}") - self.lcd.putstr(" " * ((lw-4)-len(menu_items_cut[i][0][0:lw-4]))) # fit the line - # now the arrow - if i == 0: # if the first element is drawn, think about printing or not printing the up arrow - if self.current_selection == 0 and not self.cycle: # first item selected and no cycling - self.lcd.putstr(" ") # leave the line with spaces - else: - self.lcd.putstr(" " + chr(0)) - elif i == lines_for_display-1: # if the last element is drawn, print a down arrow? - if self.current_selection == (len(self.menu_items)-1) and not self.cycle: # first item selected and no cycling - self.lcd.putstr(" ") # leave the line with spaces - else: - self.lcd.putstr(" " + chr(1)) # arrow down - - else: # there are enough items to fill the display after the current selection - # the first line - self.lcd.putstr(f"{chr(4)} {selection_name[0:lw-4]}") - self.lcd.putstr(" " * ((lw-4)-len(selection_name[0:lw-4]))) # fill the line with spaces up to 2 before the end of the line - - if len(self.menu_items) <= 1: - self.lcd.putstr(" " + chr(3)) # no options icon (as there's only one menu item!) - elif lines_for_display == 1: # if there is exactly one line to display the menu entries... - if self.current_selection == 0 and not self.cycle: # ...and the first element is selected and cycling is disabled so you can't go back - self.lcd.putstr(" "+chr(1)) - elif self.current_selection == (len(self.menu_items)-1) and not self.cycle: # ...as before but with the last element -> you can't go further - self.lcd.putstr(" "+chr(0)) - else: # ...or anything else (cycling or in the middle -> you can go in both directions - self.lcd.putstr(" "+chr(2)) - elif self.current_selection == 0 and not self.cycle: # first item selected and no cycling - self.lcd.putstr(" ") # leave the line with spaces - else: - self.lcd.putstr(" " + chr(0)) - - # the other lines... (if existing!) - for i in range(lines_for_display-1): - self.lcd.putstr(f"{chr(5)} {self.menu_items[self.current_selection+i+1][0][0:lw-4]}") - self.lcd.putstr(" " * ((lw-4)-len(self.menu_items[self.current_selection+i+1][0][0:lw-4]))) # fit the line - # check if it's the last line being drawn... - if (i+1) == (lines_for_display-1): - if self.current_selection == (len(self.menu_items)-1) and not self.cycle: # last item selected and no cycling - self.lcd.putstr(" ") - else: - self.lcd.putstr(" " + chr(1)) - else: # else: if it's not the last line, leave the last two columns of the line empty - self.lcd.putstr(" ") - - - - 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 -> show a closing message - 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) - self.show_selection() - else: # -> show no message and quit directly back into the lcdMenu - self.show_selection() - - - # 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 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 self.running: - self.loop() - - return True # to prevent a "Closing menu ..." in submenu-situations