grid widget: Add addressing options.

pull/56/head
peterhinch 2023-05-30 16:14:30 +01:00
rodzic f6365fc76d
commit 417716dcc8
4 zmienionych plików z 120 dodań i 29 usunięć

Wyświetl plik

@ -67,10 +67,35 @@ Constructor args:
Methods:
* `show` Draw the grid lines to the framebuffer.
* `__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
```
Sample usage (complete example):
```python
from color_setup import ssd
@ -91,12 +116,8 @@ grid = Grid(wri, row, col, colwidth, rows, cols, align=ALIGN_CENTER)
grid.show() # Draw grid lines
# Populate grid
col = 0
for y, txt in enumerate("ABCDE"):
grid[y + 1, col] = txt
row = 0
for col in range(1, cols):
grid[row, col] = str(col)
grid[1:6, 0] = iter("ABCDE") # Label row and col headings
grid[0, 1:cols] = (str(x) for x in range(cols))
grid[20] = "" # Clear cell 20 by setting its value to ""
grid[2, 5] = str(42) # 2d array syntax
# Dynamic formatting

72
extras/parse2d.py 100644
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)

Wyświetl plik

@ -7,7 +7,6 @@ from extras.widgets.grid import Grid
from gui.widgets.label import Label, ALIGN_CENTER
from extras.date import DateCal
class Calendar:
def __init__(
self, wri, row, col, colwidth, fgcolor, bgcolor, today_c, cur_c, sun_c, today_inv=False, cur_inv=False
@ -29,8 +28,7 @@ class Calendar:
row += self.lbl.height + 3 # Two border widths
self.grid = Grid(wri, row, col, colwidth, rows, cols, **kwargs)
self.grid.show() # Draw grid lines
for n, day in enumerate(DateCal.days): # Populate day names
self.grid[0, n] = day[:3]
self.grid[0, 0:7] = iter([d[:3] for d in DateCal.days]) # 3-char day names
self.show()
def show(self):

Wyświetl plik

@ -6,6 +6,7 @@
from gui.core.nanogui import DObject, Writer
from gui.core.colors import *
from gui.widgets.label import Label
from extras.parse2d import do_args
# lwidth may be integer Label width in pixels or a tuple/list of widths
class Grid(DObject):
@ -33,28 +34,27 @@ class Grid(DObject):
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