kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
Dynamic lists: Listbox and Dropdown widgets.
rodzic
f756f00f33
commit
1ac1fd0976
20
README.md
20
README.md
|
@ -65,6 +65,7 @@ target and a C device driver (unless you can acquire a suitable binary).
|
|||
|
||||
# Project status
|
||||
|
||||
Sept 2024: Dropdown and Listbox support dynamically variable lists of elements.
|
||||
April 2024: Add screen replace feature for non-tree navigation.
|
||||
Sept 2023: Add "encoder only" mode suggested by @eudoxos.
|
||||
April 2023: Add limited ePaper support, grid widget, calendar and epaper demos.
|
||||
|
@ -128,7 +129,9 @@ under development so check for updates.
|
|||
6.5 [ButtonList object](./README.md#65-buttonlist-object) Pushbuttons with multiple states.
|
||||
6.6 [RadioButtons object](./README.md#66-radiobuttons-object) One-of-N pushbuttons.
|
||||
6.7 [Listbox widget](./README.md#67-listbox-widget)
|
||||
6.7.1 [Dynamic changes](./README.md#671-dynamic-changes) Alter listbox contents at runtime.
|
||||
6.8 [Dropdown widget](./README.md#68-dropdown-widget) Dropdown lists.
|
||||
6.8.1 [Dynamic changes](./README.md#681-dynamic-changes) Alter dropdown contents at runtime.
|
||||
6.9 [DialogBox class](./README.md#69-dialogbox-class) Pop-up modal dialog boxes.
|
||||
6.10 [Textbox widget](./README.md#610-textbox-widget) Scrolling text display.
|
||||
6.11 [Meter widget](./README.md#611-meter-widget) Display floats on an analog meter, with data driven callbacks.
|
||||
|
@ -684,6 +687,8 @@ Some of these require larger screens. Required sizes are specified as
|
|||
(240x320).
|
||||
* `vtest.py` Clock and compass styles of vector display (240x320).
|
||||
* `calendar.py` Demo of grid control (240x320 - but could be reduced).
|
||||
* `listbox_var.py` Listbox with dynamically variable elements.
|
||||
* `dropdown_var.py` Dropdown with dynamically variable elements.
|
||||
|
||||
###### [Contents](./README.md#0-contents)
|
||||
|
||||
|
@ -1628,6 +1633,7 @@ Methods:
|
|||
the control's list, that item becomes current. Normally returns the current
|
||||
string. If a provided arg did not match any list item, the control's state is
|
||||
not changed and `None` is returned.
|
||||
* `update` No args. See [Dynamic changes](./README.md#671-dynamic-changes).
|
||||
|
||||
The callback's first argument is the listbox instance followed by any args
|
||||
specified to the constructor. The currently selected item may be retrieved by
|
||||
|
@ -1668,6 +1674,12 @@ class BaseScreen(Screen):
|
|||
|
||||
Screen.change(BaseScreen)
|
||||
```
|
||||
### 6.7.1 Dynamic changes
|
||||
|
||||
The contents of a listbox may be changed at runtime. To achieve this, elements
|
||||
must be defined as a list rather than a tuple. After the application has
|
||||
modified the list, it should call the `.update` method to refresh the control.
|
||||
The demo script `listbox_var.py` illustrates this.
|
||||
|
||||
###### [Contents](./README.md#0-contents)
|
||||
|
||||
|
@ -1734,6 +1746,7 @@ Methods:
|
|||
the control's list, that item becomes current. Normally returns the current
|
||||
string. If a provided arg did not match any list item, the control's state is
|
||||
not changed and `None` is returned.
|
||||
* `update` No args. See [Dynamic changes](./README.md#681-dynamic-changes).
|
||||
|
||||
If `select` is pressed when the `Dropdown` has focus, the list is displayed.
|
||||
The `increase` and `decrease` buttons move the list currency. If `select` is
|
||||
|
@ -1784,6 +1797,13 @@ class BaseScreen(Screen):
|
|||
|
||||
Screen.change(BaseScreen)
|
||||
```
|
||||
### 6.8.1 Dynamic changes
|
||||
|
||||
The contents of a Dropdown may be changed at runtime. To achieve this, elements
|
||||
must be defined as a list rather than a tuple. After the application has
|
||||
modified the list, it should call the `.update` method to refresh the control.
|
||||
The demo script `dropdown_var.py` illustrates this.
|
||||
|
||||
###### [Contents](./README.md#0-contents)
|
||||
|
||||
## 6.9 DialogBox class
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# dropdown_var.py micro-gui demo of Dropdown widget with changeable elements
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2024 Peter Hinch
|
||||
|
||||
# hardware_setup must be imported before other modules because of RAM use.
|
||||
import hardware_setup # Create a display instance
|
||||
from gui.core.ugui import Screen, Window, ssd
|
||||
|
||||
from gui.widgets import Label, Button, CloseButton, Dropdown
|
||||
from gui.core.writer import CWriter
|
||||
|
||||
# Font for CWriter
|
||||
import gui.fonts.font10 as font
|
||||
from gui.core.colors import *
|
||||
|
||||
|
||||
def newtext(): # Create new listbox entries
|
||||
strings = ("Iron", "Copper", "Lead", "Zinc")
|
||||
n = 0
|
||||
while True:
|
||||
yield strings[n]
|
||||
n = (n + 1) % len(strings)
|
||||
|
||||
|
||||
ntxt = newtext() # Instantiate the generator
|
||||
|
||||
|
||||
class BaseScreen(Screen):
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
|
||||
|
||||
col = 2
|
||||
row = 2
|
||||
self.els = ["Hydrogen", "Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon"]
|
||||
self.dd = Dropdown(
|
||||
wri,
|
||||
row,
|
||||
col,
|
||||
elements=self.els,
|
||||
dlines=5, # Show 5 lines
|
||||
bdcolor=GREEN,
|
||||
callback=self.ddcb,
|
||||
)
|
||||
row += 30
|
||||
self.lbl = Label(wri, row, col, self.dd.width, bdcolor=RED)
|
||||
b = Button(wri, 2, 120, text="del", callback=self.delcb)
|
||||
b = Button(wri, b.mrow + 2, 120, text="add", callback=self.addcb)
|
||||
b = Button(wri, b.mrow + 10, 120, text="h2", callback=self.gocb, args=("Hydrogen",))
|
||||
b = Button(wri, b.mrow + 2, 120, text="fe", callback=self.gocb, args=("Iron",))
|
||||
CloseButton(wri)
|
||||
|
||||
def gocb(self, _, txt): # Go button callback: Move currency to specified entry
|
||||
self.dd.textvalue(txt)
|
||||
|
||||
def addcb(self, _): # Add button callback
|
||||
self.els.append(next(ntxt)) # Append a new entry
|
||||
self.dd.update()
|
||||
|
||||
def delcb(self, _): # Delete button callback
|
||||
del self.els[self.dd.value()] # Delete current entry
|
||||
self.dd.update()
|
||||
|
||||
def ddcb(self, dd):
|
||||
if hasattr(self, "lbl"):
|
||||
self.lbl.value(dd.textvalue())
|
||||
|
||||
def after_open(self):
|
||||
self.lbl.value(self.dd.textvalue())
|
||||
|
||||
|
||||
def test():
|
||||
print("Dropdown demo.")
|
||||
Screen.change(BaseScreen)
|
||||
|
||||
|
||||
test()
|
|
@ -0,0 +1,74 @@
|
|||
# listbox_var.py micro-gui demo of Listbox class with variable elements
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2024 Peter Hinch
|
||||
|
||||
# hardware_setup must be imported before other modules because of RAM use.
|
||||
from hardware_setup import ssd # Create a display instance
|
||||
from gui.core.ugui import Screen
|
||||
from gui.core.writer import CWriter
|
||||
from gui.core.colors import *
|
||||
|
||||
from gui.widgets import Listbox, Button, CloseButton
|
||||
import gui.fonts.freesans20 as font
|
||||
|
||||
|
||||
def newtext(): # Create new listbox entries
|
||||
strings = ("Iron", "Copper", "Lead", "Zinc")
|
||||
n = 0
|
||||
while True:
|
||||
yield strings[n]
|
||||
n = (n + 1) % len(strings)
|
||||
|
||||
|
||||
ntxt = newtext() # Instantiate the generator
|
||||
|
||||
|
||||
class BaseScreen(Screen):
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
|
||||
self.els = [
|
||||
"Hydrogen",
|
||||
"Helium",
|
||||
"Neon",
|
||||
"Xenon",
|
||||
"Radon",
|
||||
"Uranium",
|
||||
"Plutonium",
|
||||
"Actinium",
|
||||
]
|
||||
self.lb = Listbox(
|
||||
wri,
|
||||
2,
|
||||
2,
|
||||
elements=self.els,
|
||||
dlines=5,
|
||||
bdcolor=RED,
|
||||
value=1,
|
||||
callback=self.lbcb,
|
||||
also=Listbox.ON_LEAVE,
|
||||
)
|
||||
Button(wri, 2, 120, height=25, text="del", callback=self.delcb)
|
||||
Button(wri, 32, 120, height=25, text="add", callback=self.addcb)
|
||||
Button(wri, 62, 120, height=25, text="h2", callback=self.gocb, args=("Hydrogen",))
|
||||
Button(wri, 92, 120, height=25, text="fe", callback=self.gocb, args=("Iron",))
|
||||
CloseButton(wri)
|
||||
|
||||
def lbcb(self, lb): # Listbox callback
|
||||
print(lb.textvalue())
|
||||
|
||||
def gocb(self, _, txt): # Go button callback: Move currency to specified entry
|
||||
self.lb.textvalue(txt)
|
||||
|
||||
def addcb(self, _): # Add button callback
|
||||
self.els.append(next(ntxt)) # Append a new entry
|
||||
self.lb.update()
|
||||
|
||||
def delcb(self, _): # Delete button callback
|
||||
del self.els[self.lb.value()] # Delete current entry
|
||||
self.lb.update()
|
||||
|
||||
|
||||
Screen.change(BaseScreen)
|
|
@ -15,7 +15,6 @@ dolittle = lambda *_ : None
|
|||
# Next and Prev close the listbox without updating the Dropdown. This is
|
||||
# handled by Screen .move bound method
|
||||
class _ListDialog(Window):
|
||||
|
||||
def __init__(self, writer, row, col, dd): # dd is parent dropdown
|
||||
# Need to determine Window dimensions from size of Listbox, which
|
||||
# depends on number and length of elements.
|
||||
|
@ -25,13 +24,21 @@ class _ListDialog(Window):
|
|||
ap_height = lb_height + 6 # Allow for listbox border
|
||||
ap_width = lb_width + 6
|
||||
super().__init__(row, col, ap_height, ap_width, draw_border=False)
|
||||
self.listbox = Listbox(writer, row + 3, col + 3,
|
||||
self.listbox = Listbox(
|
||||
writer,
|
||||
row + 3,
|
||||
col + 3,
|
||||
elements=dd.elements,
|
||||
dlines = dlines, width = lb_width,
|
||||
fgcolor = dd.fgcolor, bgcolor = dd.bgcolor,
|
||||
bdcolor=False, fontcolor = dd.fontcolor,
|
||||
dlines=dlines,
|
||||
width=lb_width,
|
||||
fgcolor=dd.fgcolor,
|
||||
bgcolor=dd.bgcolor,
|
||||
bdcolor=False,
|
||||
fontcolor=dd.fontcolor,
|
||||
select_color=dd.select_color,
|
||||
value = dd.value(), callback = self.callback)
|
||||
value=dd.value(),
|
||||
callback=self.callback,
|
||||
)
|
||||
self.dd = dd
|
||||
|
||||
def callback(self, obj_listbox):
|
||||
|
@ -41,40 +48,58 @@ class _ListDialog(Window):
|
|||
|
||||
|
||||
class Dropdown(Widget):
|
||||
def __init__(self, writer, row, col, *,
|
||||
def __init__(
|
||||
self,
|
||||
writer,
|
||||
row,
|
||||
col,
|
||||
*,
|
||||
elements,
|
||||
dlines=None, width=None, value=0,
|
||||
fgcolor=None, bgcolor=None, bdcolor=False,
|
||||
fontcolor=None, select_color=DARKBLUE,
|
||||
callback=dolittle, args=[]):
|
||||
dlines=None,
|
||||
width=None,
|
||||
value=0,
|
||||
fgcolor=None,
|
||||
bgcolor=None,
|
||||
bdcolor=False,
|
||||
fontcolor=None,
|
||||
select_color=DARKBLUE,
|
||||
callback=dolittle,
|
||||
args=[],
|
||||
):
|
||||
|
||||
self.entry_height = writer.height + 2 # Allow a pixel above and below text
|
||||
height = self.entry_height
|
||||
e0 = elements[0]
|
||||
self.select_color = select_color
|
||||
self.dlines = dlines # Referenced by _ListDialog
|
||||
|
||||
# Check whether elements specified as (str, str,...) or ([str, callback, args], [...)
|
||||
if isinstance(e0, tuple) or isinstance(e0, list):
|
||||
te = [x[0] for x in elements] # Copy text component
|
||||
self.simple = isinstance(elements[0], str)
|
||||
self.els = elements # Retain original
|
||||
elements = te
|
||||
if callback is not dolittle:
|
||||
raise ValueError('Cannot specify callback.')
|
||||
callback = self._despatch
|
||||
# Listbox works with text component only because it has a single callback.
|
||||
self.elements = elements if self.simple else [x[0] for x in elements]
|
||||
if width is None: # Allow for square at end for arrow
|
||||
self.textwidth = max(writer.stringlen(s) for s in elements)
|
||||
width = self.textwidth + 2 + height
|
||||
else:
|
||||
self.textwidth = width
|
||||
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, True)
|
||||
super()._set_callbacks(callback, args)
|
||||
self.select_color = select_color
|
||||
self.fontcolor = self.fgcolor if fontcolor is None else fontcolor
|
||||
self.elements = elements
|
||||
self.dlines = dlines
|
||||
if not self.simple:
|
||||
if callback is not dolittle:
|
||||
raise ValueError("Cannot specify callback.")
|
||||
callback = self._despatch # Override passed CB if each element has a CB.
|
||||
super()._set_callbacks(callback, args) # Callback runs on value change
|
||||
|
||||
def update(self): # Elements list has changed. Extract text component for dropdown.
|
||||
self.elements = self.els if self.simple else [x[0] for x in self.els]
|
||||
# Ensure sensible _value if list size is reduced.
|
||||
self._value = min(self._value, len(self.els) - 1)
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
if super().show():
|
||||
x, y = self.col, self.row
|
||||
self._draw(x, y)
|
||||
if not super().show():
|
||||
return
|
||||
self._draw(x := self.col, y := self.row)
|
||||
if self._value is not None:
|
||||
display.print_left(self.writer, x, y + 1, self.elements[self._value], self.fontcolor)
|
||||
|
||||
|
@ -100,8 +125,20 @@ class Dropdown(Widget):
|
|||
length = halflength * 2
|
||||
if length > 0:
|
||||
display.hline(xcentre - halflength, ycentre - halflength, length, self.fgcolor)
|
||||
display.line(xcentre - halflength, ycentre - halflength, xcentre, ycentre + halflength, self.fgcolor)
|
||||
display.line(xcentre + halflength, ycentre - halflength, xcentre, ycentre + halflength, self.fgcolor)
|
||||
display.line(
|
||||
xcentre - halflength,
|
||||
ycentre - halflength,
|
||||
xcentre,
|
||||
ycentre + halflength,
|
||||
self.fgcolor,
|
||||
)
|
||||
display.line(
|
||||
xcentre + halflength,
|
||||
ycentre - halflength,
|
||||
xcentre,
|
||||
ycentre + halflength,
|
||||
self.fgcolor,
|
||||
)
|
||||
|
||||
def do_sel(self): # Select was pushed
|
||||
if len(self.elements) > 1:
|
||||
|
|
|
@ -29,7 +29,9 @@ class Listbox(Widget):
|
|||
dlines = len(elements) if dlines is None else dlines
|
||||
# Height of control
|
||||
height = entry_height * dlines + 2
|
||||
textwidth = max(writer.stringlen(s) for s in elements) + 4
|
||||
simple = isinstance(elements[0], str) # list or list of lists?
|
||||
q = (p for p in elements) if simple else (p[0] for p in elements)
|
||||
textwidth = max(writer.stringlen(x) for x in q) + 4
|
||||
return entry_height, height, dlines, textwidth
|
||||
|
||||
def __init__(
|
||||
|
@ -52,25 +54,23 @@ class Listbox(Widget):
|
|||
also=0
|
||||
):
|
||||
|
||||
e0 = elements[0]
|
||||
self.els = elements
|
||||
# Check whether elements specified as (str, str,...) or ([str, callback, args], [...)
|
||||
if isinstance(e0, tuple) or isinstance(e0, list):
|
||||
self.els = elements # Retain original for .despatch
|
||||
self.elements = [x[0] for x in elements] # Copy text component
|
||||
if callback is not dolittle:
|
||||
self.simple = isinstance(self.els[0], str)
|
||||
self.cb = callback if self.simple else self.despatch
|
||||
if not self.simple and callback is not dolittle:
|
||||
raise ValueError("Cannot specify callback.")
|
||||
self.cb = self.despatch
|
||||
else:
|
||||
self.cb = callback
|
||||
self.elements = elements
|
||||
if any(not isinstance(s, str) for s in self.elements):
|
||||
# Iterate text values
|
||||
q = (p for p in self.els) if self.simple else (p[0] for p in self.els)
|
||||
if not all(isinstance(x, str) for x in q):
|
||||
raise ValueError("Invalid elements arg.")
|
||||
|
||||
# Calculate dimensions
|
||||
self.entry_height, height, self.dlines, tw = self.dimensions(writer, self.elements, dlines)
|
||||
self.entry_height, height, self.dlines, tw = self.dimensions(writer, self.els, dlines)
|
||||
if width is None:
|
||||
width = tw # Text width
|
||||
|
||||
self.also = also
|
||||
self.also = also # Additioal callback events
|
||||
self.ntop = 0 # Top visible line
|
||||
if not isinstance(value, int):
|
||||
value = 0 # Or ValueError?
|
||||
|
@ -86,7 +86,7 @@ class Listbox(Widget):
|
|||
self.ev = value # Value change detection
|
||||
|
||||
def update(self): # Elements list has changed.
|
||||
l = len(self.elements)
|
||||
l = len(self.els)
|
||||
nl = self.dlines # No. of lines that can fit in window
|
||||
self.ntop = max(0, min(self.ntop, l - nl))
|
||||
self._value = min(self._value, l - 1)
|
||||
|
@ -99,11 +99,13 @@ class Listbox(Widget):
|
|||
x = self.col
|
||||
y = self.row
|
||||
eh = self.entry_height
|
||||
ntop = self.ntop
|
||||
dlines = self.dlines
|
||||
nlines = min(dlines, len(self.elements)) # Displayable lines
|
||||
self.ntop = min(self.ntop, self._value) # Ensure currency is visible
|
||||
self.ntop = max(self.ntop, self._value - dlines + 1)
|
||||
ntop = self.ntop
|
||||
nlines = min(dlines, len(self.els)) # Displayable lines
|
||||
for n in range(ntop, ntop + nlines):
|
||||
text = self.elements[n]
|
||||
text = self.els[n] if self.simple else self.els[n][0]
|
||||
if self.writer.stringlen(text) > self.width: # Clip
|
||||
font = self.writer.font
|
||||
pos = 0
|
||||
|
@ -126,18 +128,25 @@ class Listbox(Widget):
|
|||
x = self.col + self.width - 2
|
||||
if ntop:
|
||||
display.vline(x, self.row, eh - 1, self.fgcolor)
|
||||
if ntop + dlines < len(self.elements):
|
||||
if ntop + dlines < len(self.els):
|
||||
y = self.row + (dlines - 1) * eh
|
||||
display.vline(x, y, eh - 1, self.fgcolor)
|
||||
|
||||
def textvalue(self, text=None): # if no arg return current text
|
||||
if text is None:
|
||||
return self.elements[self._value]
|
||||
r = self.els[self._value]
|
||||
return r if self.simple else r[0]
|
||||
else: # set value by text
|
||||
try:
|
||||
# print(text)
|
||||
# print(self.elements.index(text))
|
||||
v = self.elements.index(text)
|
||||
# print(self.els.index(text))
|
||||
if self.simple:
|
||||
v = self.els.index(text)
|
||||
else: # More RAM-efficient than converting to list and using .index
|
||||
q = (p[0] for p in self.els)
|
||||
v = 0
|
||||
while next(q) != text:
|
||||
v += 1
|
||||
except ValueError:
|
||||
v = None
|
||||
else:
|
||||
|
@ -161,7 +170,7 @@ class Listbox(Widget):
|
|||
if v:
|
||||
self._vchange(v - 1)
|
||||
elif val < 0:
|
||||
if v < len(self.elements) - 1:
|
||||
if v < len(self.els) - 1:
|
||||
self._vchange(v + 1)
|
||||
|
||||
# Callback runs if select is pressed. Also (if ON_LEAVE) if user changes
|
||||
|
|
Ładowanie…
Reference in New Issue