kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
193 wiersze
7.0 KiB
Python
193 wiersze
7.0 KiB
Python
# listbox.py Extension to ugui providing the Listbox class
|
|
|
|
# Released under the MIT License (MIT). See LICENSE.
|
|
# 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
|
|
from gui.core.colors import *
|
|
|
|
dolittle = lambda *_: None
|
|
|
|
# Behaviour has issues compared to touch displays because movement between
|
|
# entries is sequential. This can affect the choice in when the callback runs.
|
|
# It always runs when select is pressed. See 'also' ctor arg.
|
|
|
|
|
|
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
|
|
def dimensions(writer, elements, dlines):
|
|
# Height of a single entry in list.
|
|
entry_height = writer.height + 2 # Allow a pixel above and below text
|
|
# Number of displayable lines
|
|
dlines = len(elements) if dlines is None else dlines
|
|
# Height of control
|
|
height = entry_height * dlines + 2
|
|
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__(
|
|
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
|
|
):
|
|
|
|
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 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)
|
|
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.els, dlines)
|
|
if width is None:
|
|
width = tw # Text width
|
|
|
|
self.also = also # Additioal callback events
|
|
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.adjustable = True # Can show adjustable border
|
|
self.cb_args = args
|
|
self.select_color = select_color
|
|
self.fontcolor = fontcolor
|
|
self._value = value # No callback until user selects
|
|
self.ev = value # Value change detection
|
|
|
|
def update(self): # Elements list has changed.
|
|
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)
|
|
self.show()
|
|
|
|
def show(self):
|
|
if not super().show(False): # Clear to self.bgcolor
|
|
return
|
|
|
|
x = self.col
|
|
y = self.row
|
|
eh = self.entry_height
|
|
dlines = self.dlines
|
|
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.els[n] if self.simple else self.els[n][0]
|
|
if self.writer.stringlen(text) > self.width: # Clip
|
|
font = self.writer.font
|
|
pos = 0
|
|
nch = 0
|
|
for ch in text:
|
|
pos += font.get_ch(ch)[2] # width of current char
|
|
if pos > self.width:
|
|
break
|
|
nch += 1
|
|
text = text[:nch]
|
|
if n == self._value:
|
|
display.fill_rect(x, y + 1, self.width, eh - 1, self.select_color)
|
|
display.print_left(
|
|
self.writer, x + 2, y + 1, text, self.fontcolor, self.select_color
|
|
)
|
|
else:
|
|
display.print_left(self.writer, x + 2, y + 1, text, self.fontcolor, self.bgcolor)
|
|
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.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:
|
|
r = self.els[self._value]
|
|
return r if self.simple else r[0]
|
|
else: # set value by text
|
|
try:
|
|
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:
|
|
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._vchange(v - 1)
|
|
elif val < 0:
|
|
if v < len(self.els) - 1:
|
|
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
|
|
# callback that refreshes another control, that second control does not
|
|
# track currency.
|
|
def do_sel(self): # Select was pushed
|
|
self.ev = self._value
|
|
self.cb(self, *self.cb_args)
|
|
|
|
def enter(self):
|
|
self.ev = self._value # Value change detection
|
|
|
|
def leave(self):
|
|
if (self.also & Listbox.ON_LEAVE) and self._value != self.ev:
|
|
self.do_sel()
|
|
|
|
def despatch(self, _): # Run the callback specified in elements
|
|
x = self.els[self()]
|
|
x[1](self, *x[2])
|