Compare commits
11 Commits
10562d6410
...
v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
50e9b5211b
|
|||
d5b208f3bd
|
|||
8a17a02ac2
|
|||
5c26facc0f | |||
091f61f5e7 | |||
c7b86280a1
|
|||
fdef354be9
|
|||
a45cd6e90d
|
|||
4550749c8e
|
|||
64ce12f78b
|
|||
974229f4b2
|
106
README.md
@@ -4,19 +4,107 @@ A micropython library, which supports vertical and horizontal scrolling through
|
||||
|
||||
This project is the completely rewritten successor of my old (and now archived) [ProgramChooser](/BlueFox/ProgramChooser) library.
|
||||
|
||||
It is now production-ready (tested on 2x16 displays only at the moment.)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] forward, backward and select button
|
||||
- [x] support for horizontal and vertical scrolling
|
||||
- [x] support for both 2x16 and 4x20 LCDs
|
||||
- [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 project a valid python package
|
||||
# Installation
|
||||
|
||||
To "install" the library on your Pico, clone the repository first using `git clone`. Open [Thonny](https://thonny.org). There, open a file from "This computer" (thonny asks for the location after clicking on "Open"), and select the `__init__.py` from this repository. To get this file to your Pico's storage, choose "Save as" and choose "Raspberry Pi Pico" as the location.
|
||||
|
||||
Now, create a new directory called lcdMenu in the root of the Pico's file system (you can do that in the "magic" /lib folder too, makes no difference), and after that save the file there (with `__init__.py` being the file name).
|
||||
|
||||
Now you can use the library using `import lcdMenu` in any of your scripts!
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
For basic usage, you just have to create an LCD object, use it to create a lcdMenu along with some other arguments. On this newly created object, run the `setup()` method with your menu item list. Then, start the menu by calling the (blocking) `run()` method on it.
|
||||
|
||||
To stop a running instance of lcdMenu (started by `run()`), call the method `stop()` on it!
|
||||
|
||||
For further usage, see the comments above the respective method definition in question in the [__init__.py](__init__.py) file. These try to describe the behaviour of each parameter pretty precise. Also, have a look into the examples which can be found in the [examples](examples/) folder.
|
||||
|
||||
|
||||
## Gallery
|
||||
|
||||
Here are some of examples of how a lcdMenu will look, showcasing the amount of options you have with lcdMenu. Currently, as the library is only tested with 2x16 displays, these are the only ones showing up below - but on 4x20, it should look the same except it's bigger!
|
||||
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Scroll direction</th>
|
||||
<th>Cycling</th>
|
||||
<th>Title shown</th>
|
||||
<th>Initial selection</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>horizontal</td>
|
||||
<td>yes</td>
|
||||
<td>yes</td>
|
||||
<td>first</td>
|
||||
<td><img src="images/2x16-title-horizontal.jpg" alt="With title, middle item -> forward and backward, horizontal scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>yes</td>
|
||||
<td>no</td>
|
||||
<td>first</td>
|
||||
<td><img src="images/2x16-title-vertical-up-down-cycling.jpg" alt="No title, first item & cycling on -> up & down, vertical scrolling"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>no</td>
|
||||
<td>first</td>
|
||||
<td><img src="images/2x16-no-title-vertical-only-down.jpg" alt="No title, first item & no cycling -> only down, vertical scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>no</td>
|
||||
<td>middle</td>
|
||||
<td><img src="images/2x16-no-title-vertical-up-down.jpg" alt="No title, middle item -> up & down, vertical scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>no</td>
|
||||
<td>last</td>
|
||||
<td><img src="images/2x16-no-title-vertical-only-up.jpg" alt="No title, last item & no cycling -> only up, vertical scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>yes</td>
|
||||
<td>no options (first and last)</td>
|
||||
<td><img src="images/2x16-title-vertical-no-options.jpg" alt="With title, only one option, vertical scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>yes</td>
|
||||
<td>first</td>
|
||||
<td><img src="images/2x16-title-vertical-only-down.jpg" alt="With title, first item & no cycling -> only down, vertical scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>yes</td>
|
||||
<td>middle</td>
|
||||
<td><img src="images/2x16-title-vertical-up-down.jpg" alt="With title, middle item -> up & down, vertical scrolling"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vertical</td>
|
||||
<td>no</td>
|
||||
<td>yes</td>
|
||||
<td>last</td>
|
||||
<td><img src="images/2x16-title-vertical-only-up.jpg" alt="With title, last item & no cycling -> only up, vertical scrolling"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GPL-v3-or-later. A copy can be found [here](LICENSE).
|
||||
|
||||
|
34
__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,14 +33,15 @@ 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
|
||||
@@ -50,11 +50,21 @@ class lcdMenu:
|
||||
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):
|
||||
# a check:
|
||||
# if you scrolling vertically, I found no elegant way to hide the name (there just need's to be something up there!)
|
||||
# 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!")
|
||||
raise TypeError("Hiding the menu name whilst having the scroll direction set to horizontal is unsupported!")
|
||||
# 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]
|
||||
@@ -97,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):
|
||||
@@ -215,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
examples/quittable.py
Normal 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()
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
BIN
images/2x16-no-title-vertical-only-down.jpg
Normal file
After Width: | Height: | Size: 3.2 MiB |
BIN
images/2x16-no-title-vertical-only-up.jpg
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
images/2x16-no-title-vertical-up-down.jpg
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
images/2x16-title-horizontal.jpg
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
images/2x16-title-vertical-no-options.jpg
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
images/2x16-title-vertical-only-down.jpg
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
images/2x16-title-vertical-only-up.jpg
Normal file
After Width: | Height: | Size: 2.4 MiB |
BIN
images/2x16-title-vertical-up-down-cycling.jpg
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
images/2x16-title-vertical-up-down.jpg
Normal file
After Width: | Height: | Size: 2.5 MiB |