Dynamic lists: Listbox and Dropdown widgets.

pull/50/head
Peter Hinch 2024-09-13 12:11:58 +01:00
rodzic f756f00f33
commit 1ac1fd0976
5 zmienionych plików z 283 dodań i 64 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -10,12 +10,11 @@ from gui.core.colors import *
from gui.widgets.listbox import Listbox
dolittle = lambda *_ : None
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,63 +24,89 @@ 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,
elements = dd.elements,
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)
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,
select_color=dd.select_color,
value=dd.value(),
callback=self.callback,
)
self.dd = dd
def callback(self, obj_listbox):
display.ipdev.adj_mode(False) # If in 3-button mode, leave adjust mode
Screen.back()
self.dd.value(obj_listbox.value()) # Update it
self.dd.value(obj_listbox.value()) # Update it
class Dropdown(Widget):
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=[]):
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=[],
):
self.entry_height = writer.height + 2 # Allow a pixel above and below text
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.els = elements # Retain original
elements = te
if callback is not dolittle:
raise ValueError('Cannot specify callback.')
callback = self._despatch
self.simple = isinstance(elements[0], str)
self.els = elements # Retain original
# 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 self._value is not None:
display.print_left(self.writer, x, y + 1, self.elements[self._value], self.fontcolor)
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)
def textvalue(self, text=None): # if no arg return current text
def textvalue(self, text=None): # if no arg return current text
if text is None:
return self.elements[self._value]
else: # set value by text
else: # set value by text
try:
v = self.elements.index(text)
except ValueError:
@ -92,21 +117,33 @@ class Dropdown(Widget):
return v
def _draw(self, x, y):
#self.draw_border()
# self.draw_border()
display.vline(x + self.width - self.height, y, self.height, self.fgcolor)
xcentre = x + self.width - self.height // 2 # Centre of triangle
xcentre = x + self.width - self.height // 2 # Centre of triangle
ycentre = y + self.height // 2
halflength = (self.height - 8) // 2
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:
args = (self.writer, self.row - 2, self.col - 2, self)
Screen.change(_ListDialog, args = args)
Screen.change(_ListDialog, args=args)
display.ipdev.adj_mode(True) # If in 3-button mode, go into adjust mode
def _despatch(self, _): # Run the callback specified in elements

Wyświetl plik

@ -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:
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):
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.")
# 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