diff --git a/README.md b/README.md index 3340ebe..020d59e 100644 --- a/README.md +++ b/README.md @@ -65,7 +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. +Sept 2024: Dropdown and Listbox widgets 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. @@ -689,6 +689,7 @@ Some of these require larger screens. Required sizes are specified as * `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. + * `dropdown_var_tuple.py ` Dropdown with dynamically variable tuple elements. ###### [Contents](./README.md#0-contents) diff --git a/gui/demos/dropdown_var_tuple.py b/gui/demos/dropdown_var_tuple.py new file mode 100644 index 0000000..fe53d59 --- /dev/null +++ b/gui/demos/dropdown_var_tuple.py @@ -0,0 +1,101 @@ +# dropdown_var_tuple.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 * + + +class BaseScreen(Screen): + def __init__(self): + + super().__init__() + wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) + + # Create new dropdown entries + tuples = ( + ("Iron", self.mcb, ("Fe",)), + ("Copper", self.mcb, ("Cu",)), + ("Lead", self.mcb, ("Pb",)), + ("Zinc", self.mcb, ("Zn",)), + ) + + def newtup(): + n = 0 + while True: + yield tuples[n] + n = (n + 1) % len(tuples) + + self.ntup = newtup() # Instantiate the generator + + col = 2 + row = 2 + self.els = [ + ("Hydrogen", self.cb, ("H",)), + ("Helium", self.cb, ("He",)), + ("Neon", self.cb, ("Ne",)), + ("Xenon", self.cb, ("Xe",)), + ("Radon", self.cb_radon, ("Ra",)), + ("Uranium", self.cb_radon, ("U",)), + ("Plutonium", self.cb_radon, ("Pu",)), + ("Actinium", self.cb_radon, ("Ac",)), + ] + self.dd = Dropdown( + wri, + row, + col, + elements=self.els, + dlines=5, # Show 5 lines + bdcolor=GREEN, + ) + 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(self.ntup)) # 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() + self.lbl.value(self.dd.textvalue()) + + def cb(self, dd, s): + self.lbl.value(self.dd.textvalue()) + print("Gas", s) + + def mcb(self, dd, s): + self.lbl.value(self.dd.textvalue()) + print("Metal", s) + + def cb_radon(self, dd, s): # Yeah, Radon is a gas too... + self.lbl.value(self.dd.textvalue()) + print("Radioactive", s) + + def after_open(self): + self.lbl.value(self.dd.textvalue()) + + +def test(): + print("Dropdown demo.") + Screen.change(BaseScreen) + + +test() diff --git a/gui/widgets/dropdown.py b/gui/widgets/dropdown.py index a9722d3..b26e431 100644 --- a/gui/widgets/dropdown.py +++ b/gui/widgets/dropdown.py @@ -1,8 +1,9 @@ # dropdown.py Extension to ugui providing the Dropdown class # Released under the MIT License (MIT). See LICENSE. -# Copyright (c) 2021 Peter Hinch +# Copyright (c) 2021-2024 Peter Hinch +# 13 Sep 24 Support dynamic elements list. # 12 Sep 21 Support for scrolling. from gui.core.ugui import Widget, display, Window, Screen @@ -14,11 +15,14 @@ dolittle = lambda *_: None # Next and Prev close the listbox without updating the Dropdown. This is # handled by Screen .move bound method +# Callback where elements are tuples: the listbox calls its local _ListDialog callback. +# This changes the DialogBox value, so its callback runs. This is the ._despatch method +# which runs the callback of the currently selected element. class _ListDialog(Window): - def __init__(self, writer, row, col, dd): # dd is parent dropdown + def __init__(self, writer, row, col, dd, dlines, els): # dd is parent dropdown # Need to determine Window dimensions from size of Listbox, which # depends on number and length of elements. - _, lb_height, dlines, tw = Listbox.dimensions(writer, dd.elements, dd.dlines) + _, lb_height, dlines, tw = Listbox.dimensions(writer, els, dlines) lb_width = tw + 2 # Text width + 2 # Calculate Window dimensions ap_height = lb_height + 6 # Allow for listbox border @@ -28,7 +32,7 @@ class _ListDialog(Window): writer, row + 3, col + 3, - elements=dd.elements, + elements=els, dlines=dlines, width=lb_width, fgcolor=dd.fgcolor, @@ -38,6 +42,7 @@ class _ListDialog(Window): select_color=dd.select_color, value=dd.value(), callback=self.callback, + also=Listbox.NOCB, # Force passed callback even if elements are tuples ) self.dd = dd @@ -70,15 +75,16 @@ class Dropdown(Widget): self.entry_height = writer.height + 2 # Allow a pixel above and below text height = self.entry_height self.select_color = select_color - self.dlines = dlines # Referenced by _ListDialog + self.dlines = dlines # Passed to _ListDialog # Check whether elements specified as (str, str,...) or ([str, callback, args], [...) 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) + if self.simple: + self.textwidth = max(writer.stringlen(s) for s in elements) + else: + self.textwidth = max(writer.stringlen(s[0]) for s in elements) width = self.textwidth + 2 + height else: self.textwidth = width @@ -91,7 +97,6 @@ class Dropdown(Widget): 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() @@ -101,15 +106,23 @@ class Dropdown(Widget): 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) + s = self.els[self._value] if self.simple else self.els[self._value][0] + display.print_left(self.writer, x, y + 1, s, self.fontcolor) 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: - v = self.elements.index(text) - except ValueError: + 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, StopIteration): v = None else: if v != self._value: @@ -141,8 +154,8 @@ class Dropdown(Widget): ) def do_sel(self): # Select was pushed - if len(self.elements) > 1: - args = (self.writer, self.row - 2, self.col - 2, self) + if len(self.els) > 1: + args = (self.writer, self.row - 2, self.col - 2, self, self.dlines, self.els) Screen.change(_ListDialog, args=args) display.ipdev.adj_mode(True) # If in 3-button mode, go into adjust mode diff --git a/gui/widgets/listbox.py b/gui/widgets/listbox.py index 41171a3..5a37c20 100644 --- a/gui/widgets/listbox.py +++ b/gui/widgets/listbox.py @@ -3,7 +3,7 @@ # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2021-2024 Peter Hinch -# 11 Sep 24 Support variable list contents. +# 13 Sep 24 Support dynamic elements list. # 12 Sep 21 Support for scrolling. from gui.core.ugui import Widget, display @@ -19,6 +19,7 @@ dolittle = lambda *_: None class Listbox(Widget): ON_MOVE = 1 # Also run whenever the currency moves. ON_LEAVE = 2 # Also run on exit from the control. + NOCB = 4 # When used in a dropdown, force passed callback. # This is used by dropdown.py and menu.py @staticmethod @@ -57,8 +58,8 @@ class Listbox(Widget): self.els = elements # Check whether elements specified as (str, str,...) or ([str, callback, args], [...) 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: + self.cb = callback if (self.simple or also == 4) else self.despatch + if not (self.simple or also == 4) 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) @@ -138,8 +139,6 @@ class Listbox(Widget): return r if self.simple else r[0] else: # set value by text try: - # print(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 @@ -147,7 +146,7 @@ class Listbox(Widget): v = 0 while next(q) != text: v += 1 - except ValueError: + except (ValueError, StopIteration): v = None else: if v != self._value: