Grid widget: add addressing options.

encoder_driver
peterhinch 2023-05-31 11:55:35 +01:00
rodzic ebfb980e94
commit e31fbe3603
4 zmienionych plików z 133 dodań i 23 usunięć

Wyświetl plik

@ -1090,7 +1090,7 @@ Screen.change(BaseScreen)
### 6.1.1 Grid widget
```python
from gui.widgets import Grid # File: grid.py
from gui.widgets import Grid # Files: grid.py, parse2d.py
```
![Image](./images/grid.JPG)
@ -1127,9 +1127,35 @@ Constructor args:
in the `Label` as defined by `lwidth`.
Method:
* `__getitem__` This enables an individual `Label`'s `value` method to be
retrieved using index notation. The args detailed above enable inividual cells
to be updated.
* `__getitem__` Return a list containing one or more `Label` instances.
* `__setitem__` Assign a value to one or more labels. If multiple labels are
specified and a single text value is passed, all labels will receive that
value. If an iterator is passed, consecutive labels will receive values from
the iterator. If the iterator runs out of data, the last value will be
repeated.
Addressing:
The `Label` instances may be addressed as a 1D array as follows
```python
grid[20] = str(42)
grid[20:25] = iter([str(n) for n in range(20, 25)])
```
or as a 2D array:
```python
grid[2, 5] = "A" # Row == 2, col == 5
grid[0:7, 3] = "b" # Populate col 3 of rows 0..6
grid[1:3, 1:3] = (str(n) for n in range(25)) # Produces
# 0 1
# 2 3
```
Columns are populated from left to right, rows from top to bottom. Unused
iterator values are ignored. If an iterator runs out of data the last value is
repeated, thus
```python
grid[1:3, 1:3] = (str(n) for n in range(2)) # Produces
# 0 1
# 1 1
```
Example uses:
```python
@ -1137,6 +1163,8 @@ colwidth = (20, 30) # Col 0 width is 20, subsequent columns 30
self.grid = Grid(wri, row, col, colwidth, rows, cols, justify=Label.CENTRE)
self.grid[20] = "" # Clear cell 20 by setting its value to ""
self.grid[2, 5] = str(42) # 2D array syntax
grid[1:6, 0] = iter("ABCDE") # Label row and col headings
grid[0, 1:cols] = (str(x + 1) for x in range(cols))
d = {} # For indiviual control of cell appearance
d["fgcolor"] = RED
d["text"] = str(99)

Wyświetl plik

@ -32,8 +32,7 @@ class BaseScreen(Screen):
self.lbl = Label(wri, row, col, text = (colwidth + 4) * cols, justify=Label.CENTRE)
row = self.lbl.mrow
self.grid = Grid(wri, row, col, colwidth, rows, cols, justify=Label.CENTRE)
for n, day in enumerate(DateCal.days):
self.grid[0, n] = day[:3]
self.grid[0, 0:7] = iter([d[:3] for d in DateCal.days]) # 3-char day names
row = self.grid.mrow + 4
ht = 30

Wyświetl plik

@ -7,6 +7,18 @@ from gui.core.ugui import Widget, display
from gui.core.writer import Writer
from gui.core.colors import *
from gui.widgets import Label
from .parse2d import do_args
# Given a slice and a maximum address return start and stop addresses (or None on error)
# Step value must be 1, hence does not support start > stop (used with step < 0)
def _do_slice(sli, nbytes):
if not (sli.step is None or sli.step == 1):
raise NotImplementedError("only slices with step=1 (or None) are supported")
start = sli.start if sli.start is not None else 0
stop = sli.stop if sli.stop is not None else nbytes
start = min(start if start >= 0 else max(nbytes + start, 0), nbytes)
stop = min(stop if stop >= 0 else max(nbytes + stop, 0), nbytes)
return (start, stop) if start < stop else None # Caller should check
# lwidth may be integer Label width in pixels or a tuple/list of widths
class Grid(Widget):
@ -34,28 +46,27 @@ class Grid(Widget):
r += self.cheight
c = col
def _idx(self, n):
if isinstance(n, tuple) or isinstance(n, list): # list allows old syntax l[[r, c]]
if n[0] >= self.nrows:
raise ValueError("Grid row index too large")
if n[1] >= self.ncols:
raise ValueError("Grid col index too large")
idx = n[1] + n[0] * self.ncols
else:
idx = n
if idx >= self.ncells:
raise ValueError("Grid cell index too large")
return idx
def __getitem__(self, *args): # Return the Label instance
return self.cells[self._idx(args[0])]
indices = do_args(args, self.nrows, self.ncols)
res = []
for i in indices:
res.append(self.cells[i])
return res
# allow grid[[r, c]] = "foo" or kwargs for Label:
# grid[[r, c]] = {"text": str(n), "fgcolor" : RED}
def __setitem__(self, *args):
v = self.cells[self._idx(args[0])].value
x = args[1]
_ = v(**x) if isinstance(x, dict) else v(x)
x = args[1] # Value
indices = do_args(args[: -1], self.nrows, self.ncols)
for i in indices:
try:
z = next(x) # May be a generator
except StopIteration:
pass # Repeat last value
except TypeError:
z = x
v = self.cells[i].value # method of Label
_ = v(**z) if isinstance(x, dict) else v(z)
def show(self):
super().show() # Draw border

Wyświetl plik

@ -0,0 +1,72 @@
# parse2d.py Parse args for item access dunder methods for a 2D array.
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
# Called from __getitem__ or __setitem__ args is a 1-tuple. The single item may be an int or a
# slice for 1D access. Or it may be a 2-tuple for 2D access. Items in the 2-tuple may be ints
# or slices in any combination.
# As a generator it returns offsets into the underlying 1D array or list.
def do_args(args, nrows, ncols):
# Given a slice and a maximum address return start and stop addresses (or None on error)
# Step value must be 1, hence does not support start > stop (used with step < 0)
def do_slice(sli, nbytes):
step = sli.step if sli.step is not None else 1
start = sli.start if sli.start is not None else 0
stop = sli.stop if sli.stop is not None else nbytes
start = min(start if start >= 0 else max(nbytes + start, 0), nbytes)
stop = min(stop if stop >= 0 else max(nbytes + stop, 0), nbytes)
ok = (start < stop and step > 0) or (start > stop and step < 0)
return (start, stop, step) if ok else None # Caller should check
def ivalid(n, nmax): # Validate an integer arg, handle -ve args
n = n if n >= 0 else nmax + n
if n < 0 or n > nmax - 1:
raise IndexError("Index out of range")
return n
def fail(n):
raise IndexError("Invalid index", n)
ncells = nrows * ncols
n = args[0]
if isinstance(n, int): # Index into 1D array
yield ivalid(n, ncells)
elif isinstance(n, slice): # Slice of 1D array
cells = do_slice(n, ncells)
if cells is not None:
for cell in range(*cells):
yield cell
elif isinstance(n, tuple) or isinstance(n, list): # list allows for old [[]] syntax
if len(n) != 2:
fail(n)
row = n[0] # May be slice
if isinstance(row, int):
row = ivalid(row, nrows)
col = n[1]
if isinstance(col, int):
col = ivalid(col, ncols)
if isinstance(row, int) and isinstance(col, int):
yield row * ncols + col
elif isinstance(row, slice) and isinstance(col, int):
rows = do_slice(row, nrows)
if rows is not None:
for row in range(*rows):
yield row * ncols + col
elif isinstance(row, int) and isinstance(col, slice):
cols = do_slice(col, ncols)
if cols is not None:
for col in range(*cols):
yield row * ncols + col
elif isinstance(row, slice) and isinstance(col, slice):
rows = do_slice(row, nrows)
cols = do_slice(col, ncols)
if cols is not None and rows is not None:
for row in range(*rows):
for col in range(*cols):
yield row * ncols + col
else:
fail(n)
else:
fail(n)