diff --git a/extras/README.md b/extras/README.md index a3a331d..0ab1e12 100644 --- a/extras/README.md +++ b/extras/README.md @@ -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 diff --git a/extras/parse2d.py b/extras/parse2d.py new file mode 100644 index 0000000..f8983eb --- /dev/null +++ b/extras/parse2d.py @@ -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) diff --git a/extras/widgets/calendar.py b/extras/widgets/calendar.py index d44cbce..3663935 100644 --- a/extras/widgets/calendar.py +++ b/extras/widgets/calendar.py @@ -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): diff --git a/extras/widgets/grid.py b/extras/widgets/grid.py index 89228a9..5edb6a6 100644 --- a/extras/widgets/grid.py +++ b/extras/widgets/grid.py @@ -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