diff --git a/README.md b/README.md index d409ce1..9757249 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/gui/demos/calendar.py b/gui/demos/calendar.py index 725df8d..cd5d7dd 100644 --- a/gui/demos/calendar.py +++ b/gui/demos/calendar.py @@ -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 diff --git a/gui/widgets/grid.py b/gui/widgets/grid.py index d8596db..540280a 100644 --- a/gui/widgets/grid.py +++ b/gui/widgets/grid.py @@ -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 diff --git a/gui/widgets/parse2d.py b/gui/widgets/parse2d.py new file mode 100644 index 0000000..f8983eb --- /dev/null +++ b/gui/widgets/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)