From 9761fbe50c08a680a473e3570b3c22cc44a3afd0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 12 Sep 2021 13:52:57 +0100 Subject: [PATCH] Support scrolling in Listbox and Dropdown widgets. --- README.md | 54 +++++++++++++++++++----------- gui/demos/dropdown.py | 6 ++-- gui/demos/listbox.py | 42 +++++++++++++++++++++++ gui/widgets/dropdown.py | 23 +++++++++---- gui/widgets/listbox.py | 74 ++++++++++++++++++++++++++++++----------- hardware_setup.py | 2 +- 6 files changed, 152 insertions(+), 49 deletions(-) create mode 100644 gui/demos/listbox.py diff --git a/README.md b/README.md index 5387a6a..d553de9 100644 --- a/README.md +++ b/README.md @@ -504,7 +504,8 @@ minimal and aim to demonstrate a single technique. mode. * `linked_sliders.py` One `Slider` updating two others, and a coding "wrinkle" required for doing this. - * `dropdown.py` A dropdown list updates a `Label`. + * `dropdown.py` A dropdown list (with scrolling) updates a `Label`. + * `listbox.py` A listbox with scrolling. * `dialog.py` `DialogBox` demo. Illustrates the screen change mechanism. * `screen_change.py` A `Pushbutton` causing a screen change using a re-usable "forward" button. @@ -1251,10 +1252,11 @@ from gui.widgets.listbox import Listbox A `listbox` with the second item highlighted. Pressing the physical `select` button will cause the callback to run. -A `Listbox` is an active widget. Its height is determined by the number of -entries in it and the font in use. Scrolling is not supported. When the widget -has focus the currently selected element may be changed using `increase` and -`decrease` buttons. On pressing `select` a callback runs. +A `Listbox` is an active widget. By default its height is determined by the +number of entries in it and the font in use. It may be reduced by specifying +`dlines` in which case scrolling will occur. When the widget has focus the +currently selected element may be changed using `increase` and `decrease` +buttons or by turning the encoder. On pressing `select` a callback runs. Constructor mandatory positional args: 1. `writer` The `Writer` instance (defines font) to use. @@ -1267,9 +1269,16 @@ Mandatory keyword only argument: list to have a separate callback. Optional keyword only arguments: + * `dlines=None` By default the height of the control is determined by the + number of elements. If an integer < number of elements is passed the list + will show that number of lines; its height will correspond. Scrolling will + occur to ensure that the current element is always visible. To indicate when + scrolling is possible, one or two vertical bars will appear to the right of + the list. * `width=None` Control width in pixels. By default this is calculated to accommodate all elements. - * `value=0` Index of currently selected list item. + * `value=0` Index of currently selected list item. If necessary the list will + scroll to ensure the item is visible. * `fgcolor=None` Color of foreground (the control itself). If `None` the `Writer` foreground default is used. * `bgcolor=None` Background color of object. If `None` the `Writer` background @@ -1306,10 +1315,10 @@ means of the instance's `value` or `textvalue` methods. By default the `Listbox` runs a common callback regardless of the item chosen. This can be changed by specifying `elements` such that each element comprises a -3-list or 3-tuple with the following contents: - 0. String to display. - 1. Callback. - 2. Tuple of args (may be ()). +3-list or 3-tuple with the following contents: + 1. String to display. + 2. Callback. + 3. Tuple of args (may be `()`). In this case constructor args `callback` and `args` must not be supplied. Args received by the callback functions comprise the `Listbox` instance followed by @@ -1354,11 +1363,12 @@ Closed dropdown list. Open dropdown list. When closed, hidden items below are refreshed. -A dropdown list. The list, when active, is drawn below the control. The height -of the control is determined by the height of the font in use. The height of -the list is determined by the number of entries in it and the font in use. -Scrolling is not supported. The dropdown should be placed high enough on the -screen to ensure that the list can be displayed +A dropdown list. The list, when active, is drawn over the control. The height +of the control is determined by the height of the font in use. By default the +height of the list is determined by the number of entries in it and the font in +use. It may be reduced by specifying `dlines` in which case scrolling will +occur. The dropdown should be placed high enough on the screen to ensure that +the list can be displayed. Constructor mandatory positional args: 1. `writer` The `Writer` instance (defines font) to use. @@ -1371,6 +1381,12 @@ Mandatory keyword only argument: each item on the dropdown list to have a separate callback. Optional keyword only arguments: + * `dlines=None` By default the height of the dropdown list is determined by + the number of elements. If an integer < number of elements is passed the list + will show that number of lines; its height will correspond. Scrolling will + occur to ensure that the current element is always visible. To indicate when + scrolling is possible, one or two vertical bars will appear to the right of + the list. * `width=None` Control width in pixels. By default this is calculated to accommodate all elements. * `value=0` Index of currently selected list item. @@ -1414,10 +1430,10 @@ means of the instance's `value` or `textvalue` methods. By default the `Dropdown` runs a single callback regardless of the element chosen. This can be changed by specifying `elements` such that each element -comprises a 3-list or 3-tuple with the following contents: - 0. String to display. - 1. Callback. - 2. Tuple of args (may be ()). +comprises a 3-list or 3-tuple with the following contents: + 1. String to display. + 2. Callback. + 3. Tuple of args (may be `()`). In this case constructor args `callback` and `args` must not be supplied. Args received by the callback functions comprise the `Dropdown` instance followed by diff --git a/gui/demos/dropdown.py b/gui/demos/dropdown.py index c17e5ae..7ff16a3 100644 --- a/gui/demos/dropdown.py +++ b/gui/demos/dropdown.py @@ -26,9 +26,11 @@ class BaseScreen(Screen): col = 2 row = 2 + els = ('hydrogen', 'helium', 'neon', 'argon', 'krypton', 'xenon', 'radon') self.dd = Dropdown(wri, row, col, - elements = ('hydrogen', 'helium', 'neon', 'xenon', 'radon'), - bdcolor = GREEN, bgcolor = DARKGREEN, + elements = els, + dlines = 5, # Show 5 lines + bdcolor = GREEN, callback=self.ddcb) row += 30 self.lbl = Label(wri, row, col, self.dd.width, bdcolor=RED) diff --git a/gui/demos/listbox.py b/gui/demos/listbox.py new file mode 100644 index 0000000..105c3d6 --- /dev/null +++ b/gui/demos/listbox.py @@ -0,0 +1,42 @@ +# listbox.py micro-gui demo of Listbox class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 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.listbox import Listbox +from gui.widgets.buttons import CloseButton +import gui.fonts.freesans20 as font + + +class BaseScreen(Screen): + def __init__(self): + def cb(lb, s): + print('Gas', s) + + def cb_radon(lb, s): # Yeah, Radon is a gas too... + print('Radioactive', s) + + super().__init__() + wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) + els = (('Hydrogen', cb, ('H',)), + ('Helium', cb, ('He',)), + ('Neon', cb, ('Ne',)), + ('Xenon', cb, ('Xe',)), + ('Radon', cb_radon, ('Ra',)), + ('Uranium', cb_radon, ('U',)), + ('Plutonium', cb_radon, ('Pu',)), + ('Actinium', cb_radon, ('Ac',)), + ) + Listbox(wri, 2, 2, + elements = els, dlines=5, bdcolor=RED, value=1, also=Listbox.ON_LEAVE) + #bdcolor = RED, fgcolor=RED, fontcolor = YELLOW, select_color=BLUE, value=1) + CloseButton(wri) + + +Screen.change(BaseScreen) diff --git a/gui/widgets/dropdown.py b/gui/widgets/dropdown.py index 3d31805..b6c8fdb 100644 --- a/gui/widgets/dropdown.py +++ b/gui/widgets/dropdown.py @@ -3,6 +3,8 @@ # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2021 Peter Hinch +# 12 Sep 21 Support for scrolling. + from gui.core.ugui import Widget, display, Window, Screen from gui.core.colors import * @@ -17,15 +19,18 @@ 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. - entry_height, lb_height, textwidth = Listbox.dimensions(writer, dd.elements) - lb_width = textwidth + 2 + _, lb_height, dlines, tw = Listbox.dimensions(writer, dd.elements, dd.dlines) + lb_width = tw + 2 # Text width + 2 # Calculate Window dimensions ap_height = lb_height + 6 # Allow for listbox border ap_width = lb_width + 6 super().__init__(row, col, ap_height, ap_width) - self.listbox = Listbox(writer, row + 3, col + 3, elements = dd.elements, width = lb_width, - fgcolor = dd.fgcolor, bgcolor = dd.bgcolor, bdcolor=False, - fontcolor = dd.fontcolor, select_color = dd.select_color, + 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 @@ -35,8 +40,11 @@ class _ListDialog(Window): class Dropdown(Widget): - def __init__(self, writer, row, col, *, elements, width=None, value=0, - fgcolor=None, bgcolor=None, bdcolor=False, fontcolor=None, select_color=DARKBLUE, + 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 @@ -60,6 +68,7 @@ class Dropdown(Widget): self.select_color = select_color self.fontcolor = self.fgcolor if fontcolor is None else fontcolor self.elements = elements + self.dlines = dlines def show(self): if super().show(): diff --git a/gui/widgets/listbox.py b/gui/widgets/listbox.py index 5742461..92d8038 100644 --- a/gui/widgets/listbox.py +++ b/gui/widgets/listbox.py @@ -2,6 +2,9 @@ # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2021 Peter Hinch + +# 12 Sep 21 Support for scrolling. + from gui.core.ugui import Widget, display from gui.core.colors import * @@ -15,16 +18,23 @@ class Listbox(Widget): ON_MOVE = 1 # Also run whenever the currency moves. ON_LEAVE = 2 # Also run on exit from the control. + # This is used by dropdown.py @staticmethod - def dimensions(writer, elements): + def dimensions(writer, elements, dlines): + # Height of a single entry in list. entry_height = writer.height + 2 # Allow a pixel above and below text - le = len(elements) - height = entry_height * le + 2 + # Number of displayable lines + 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 - return entry_height, height, textwidth + return entry_height, height, dlines, textwidth - def __init__(self, writer, row, col, *, elements, width=None, value=0, - fgcolor=None, bgcolor=None, bdcolor=False, fontcolor=None, select_color=DARKBLUE, + 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=[], also=0): e0 = elements[0] @@ -40,33 +50,49 @@ class Listbox(Widget): self.elements = elements if any(not isinstance(s, str) for s in self.elements): raise ValueError('Invalid elements arg.') - self.entry_height, height, textwidth = self.dimensions(writer, self.elements) - self.also = also + # Calculate dimensions + self.entry_height, height, self.dlines, tw = self.dimensions( + writer, self.elements, dlines) if width is None: - width = textwidth - if not isinstance(value, int) or value >= len(elements): - value = 0 + width = tw # Text width + + self.also = also + self.ntop = 0 # Top visible line + if not isinstance(value, int): + value = 0 # Or ValueError? + elif value >= self.dlines: # Must scroll + value = min(value, len(elements) - 1) + self.ntop = value - self.dlines + 1 super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, True) self.cb_args = args self.select_color = select_color self.fontcolor = fontcolor self._value = value # No callback until user selects - self.ev = value + self.ev = value # Value change detection def show(self): if not super().show(False): # Clear to self.bgcolor return - length = len(self.elements) x = self.col y = self.row - for n in range(length): + eh = self.entry_height + ntop = self.ntop + dlines = self.dlines + for n in range(ntop, ntop + dlines): if n == self._value: - display.fill_rect(x, y + 1, self.width, self.entry_height - 1, self.select_color) + display.fill_rect(x, y + 1, self.width, eh - 1, self.select_color) display.print_left(self.writer, x + 2, y + 1, self.elements[n], self.fontcolor, self.select_color) else: display.print_left(self.writer, x + 2, y + 1, self.elements[n], self.fontcolor, self.bgcolor) - y += self.entry_height + y += eh + # Draw a vertical line to hint at scrolling + x = self.col + self.width - 2 + if ntop: + display.vline(x, self.row, eh - 1, self.fgcolor) + if ntop + dlines < len(self.elements): + 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: @@ -81,16 +107,24 @@ class Listbox(Widget): self.value(v) return v + def _vchange(self, vnew): # A value change is taking place + # Handle scrolling + if vnew >= self.ntop + self.dlines: + self.ntop = vnew - self.dlines + 1 + elif vnew < self.ntop: + self.ntop = vnew + self.value(vnew) + if (self.also & Listbox.ON_MOVE): # Treat as if select pressed + self.do_sel() + def do_adj(self, _, val): v = self._value if val > 0: if v: - self.value(v - 1) + self._vchange(v -1) elif val < 0: if v < len(self.elements) - 1: - self.value(v + 1) - if (self.also & Listbox.ON_MOVE): # Treat as if select pressed - self.do_sel() + self._vchange(v + 1) # Callback runs if select is pressed. Also (if ON_LEAVE) if user changes # list currency and then moves off the control. Otherwise if we have a diff --git a/hardware_setup.py b/hardware_setup.py index 3049d80..df02c1b 100644 --- a/hardware_setup.py +++ b/hardware_setup.py @@ -51,4 +51,4 @@ sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value -display = Display(ssd, nxt, sel, prev, increase, decrease, 5) +display = Display(ssd, nxt, sel, prev, increase, decrease)