From cad9767688872204ab2e155532729cc80e4da71e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 12 Jul 2021 14:47:18 +0100 Subject: [PATCH] Doc Menu class. Various fixes/improvements. --- README.md | 150 +++++++++++++++++++++++++++++++------------ gui/core/colors.py | 10 ++- gui/core/ugui.py | 48 +++++++------- gui/demos/menu.py | 4 +- gui/demos/various.py | 1 - gui/widgets/menu.py | 7 +- hardware_setup.py | 2 +- 7 files changed, 149 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 7725b93..fd0b8c3 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,6 @@ Code is new and issues are likely: please report any found. The project is under development so check for updates. I also plan to upgrade the performance of some display drivers. -The encoder interface is under development and currently is rather erratic. - # 0. Contents 1. [Basic concepts](./README.md#1-basic-concepts) Including installation and test. @@ -113,18 +111,19 @@ The encoder interface is under development and currently is rather erratic. 19. [ScaleLog widget](./README.md#19-scalelog-widget) Wide dynamic range float entry and display. 20. [Dial widget](./README.md#20-dial-widget) Display multiple vectors. 21. [Knob widget](./README.md#21-knob-widget) Rotary potentiometer float entry. -22. [Graph plotting](./README.md#22-graph-plotting) Widgets for Cartesian and polar graphs. - 22.1 [Concepts](./README.md#221-concepts) -      22.1.1 [Graph classes](./README.md#2211-graph-classes) -      22.1.2 [Curve classes](./README.md#2212-curve-classes) -      22.1.3 [Coordinates](./README.md#2213-coordinates) - 22.2 [Graph classes](./README.md#221-graph-classes) -      22.2.1 [Class CartesianGraph](./README.md#2221-class-cartesiangraph) -      22.2.2 [Class PolarGraph](./README.md#2222-class-polargraph) - 22.3 [Curve classes](./README.md#223-curve-classes) -      22.3.1 [Class Curve](./README.md#2231-class-curve) -      22.3.2 [Class PolarCurve](./README.md#2232-class-polarcurve) - 22.4 [Class TSequence](./README.md#224-class-tsequence) Plotting realtime, time sequential data. +22. [Menu class](./README.md#22-menu-class) +23. [Graph plotting](./README.md#22-graph-plotting) Widgets for Cartesian and polar graphs. + 23.1 [Concepts](./README.md#231-concepts) +      23.1.1 [Graph classes](./README.md#2311-graph-classes) +      23.1.2 [Curve classes](./README.md#2312-curve-classes) +      23.1.3 [Coordinates](./README.md#2313-coordinates) + 23.2 [Graph classes](./README.md#231-graph-classes) +      23.2.1 [Class CartesianGraph](./README.md#2321-class-cartesiangraph) +      23.2.2 [Class PolarGraph](./README.md#2322-class-polargraph) + 23.3 [Curve classes](./README.md#233-curve-classes) +      23.3.1 [Class Curve](./README.md#2331-class-curve) +      23.3.2 [Class PolarCurve](./README.md#2332-class-polarcurve) + 23.4 [Class TSequence](./README.md#234-class-tsequence) Plotting realtime, time sequential data. [Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, encoder interface, use of graphics primitives # 1. Basic concepts @@ -441,7 +440,7 @@ minimal and aim to demonstrate a single technique. * `aclock.py` An analog clock using the `Dial` vector display. Also shows screen layout using widget metrics. Has a simple `uasyncio` task. * `tbox.py` Text boxes and user-controlled scrolling. - * `tstat.py` A demo of the `Tstat` class. + * `tstat.py` A demo of the `Meter` class with data sensitive regions. ### 1.11.2 Test scripts @@ -616,18 +615,33 @@ PALE_YELLOW = create_color(12, 150, 150, 0) # index, r, g, b If a 4-bit driver is in use, the color `rgb(150, 150, 0)` will be assigned to "spare" color number 12. Any color number in range `0 <= n <= 15` may be used, implying that predefined colors may be reassigned. It is recommended -that `BLACK` (0) and `WHITE` (15) are not changed. `GREY` (6) and `YELLOW` (5) -are GUI defaults for "greyed out" widgets and for "precision mode" borders, so -changing these will have obvious effects. - -If an 8-bit or larger driver is in use, the first `index` arg is ignored and -there is no practical restriction on the number of colors that may be created. +that `BLACK` (0) and `WHITE` (15) are not changed. If an 8-bit or larger driver +is in use, the color umber is ignored and there is no practical restriction on +the number of colors that may be created. In the above example, regardless of the display driver, the `PALE_YELLOW` variable may be used to refer to the color. An example of custom color definition may be found in [this nano-gui demo](https://github.com/peterhinch/micropython-nano-gui/blob/4ef0e20da27ef7c0b5c34136dcb372200f0e5e66/gui/demos/color15.py#L92). +There are five default colors which are defined by a `color_map` list. These +may be reassigned in user code. For example the following will cause the border +of any control with the focus to be red: +```python +from colors import * +color_map[FOCUS] = RED +``` +The `color_map` index constants and default colors (defined in `colors.py`) +are: + +| Index | Color | Purpose | +|:---------:|:------:|:-----------------------------------------:| +| FOCUS | WHITE | Border of control with focus | +| PRECISION | YELLOW | Border in precision mode | +| FG | WHITE | Window foreground default | +| BG | BLACK | Background default including screen clear | +| GREY_OUT | GREY | Color to render greyed-out controls | + ###### [Contents](./README.md#0-contents) ### 2.3.1 Monochrome displays @@ -678,22 +692,35 @@ along with the `Pin` instances used for input; also whether an encoder is used. Pins are arbitrary, but should be defined as inputs with pullups. Pushbuttons are connected between `Gnd` and the relevant pin. -The constructor takes the following args: +The constructor takes the following positional args: 1. `objssd` The `SSD` instance. A reference to the display driver. 2. `nxt` A `Pin` instance for the `next` button. 3. `sel` A `Pin` instance for the `select` button. 4. `prev=None` A `Pin` instance for the `previous` button (if used). - 5. `up=None` A `Pin` instance for the `increase` button (if used). - 6. `down=None` A `Pin` instance for the `decrease` button (if used). - 7. `encoder=False` If an encoder is used, an integer must be passed. This - represents the division ratio. A value of 1 provides the native rate of the - encoder. I found the Adafruit encoder overly sensitive. A value of 5 slows it - down improving usability. + 5. `incr=None` A `Pin` instance for the `increase` button (if used). + 6. `decr=None` A `Pin` instance for the `decrease` button (if used). + 7. `encoder=False` If an encoder is used, an integer must be passed. + +Class variables: + * `verbose=True` Causes a message to be printed indicating whether an encoder + was specified. + +#### Encoder usage If an encoder is used, it should be connected to the pins assigned to `increase` and `decrease`. If the direction of movement is wrong, these pins should be transposed (physically or in code). +To specify to the GUI that an encoder is in use an integer should be passed to +the `Display` constructor `encoder` arg. Its value represents the division +ratio. A value of 1 defines the native rate of the encoder; if the native rate +is 32 pulses per revolution, a value of 4 would yield a virtual device with +8 pulses per rev. I found the Adafruit encoder to be too sensitive. A value of 5 +improved usability. + +If an encoder is used but the `encoder` arg is `False`, response to the encoder +will be erratic. + ###### [Contents](./README.md#0-contents) # 4. Screen class @@ -2078,7 +2105,50 @@ value changes. This enables dynamic color change. ###### [Contents](./README.md#0-contents) -# 22. Graph Plotting +# 22 Menu class + +```python +from gui.widgets.menu import Menu +``` + +This enables the creation of single or two level menus. The top level of the +menu consists of a row of `Button` instances at the top of the screen. Each +button can either call a callback or instantiate a dropdown list comprising the +second menu level. + +Constructor mandatory positional arg: + 1. `writer` The `Writer` instance (defines font) to use. + +Keyword only args: + * `height=25` Height of top level menu buttons. + * `bgcolor=None` + * `fgcolor=None` + * `textcolor=None` + * `select_color=DARKBLUE` + * `args` This should be a tuple containing a tuple of args for each entry in + the top level menu. Each tuple should be of one of two forms: + 1. `(text, cb, (args,))` A single-level entry: the top level `Button` with + text `text` runs the callback `cb` with positional args defined by the + supplied tuple (which may be `()`). The callback receives an initial arg + being the `Button` instance. + 2. `(text, cb, (args,), (elements,))` In this instance the top level `Button` + triggers a dropdown list comprising a tuple of strings in `elements`. The + callback `cb` is triggered if the user selects an entry. The callback + receives an initial arg which is a `Listbox` instance: the chosen entry may + be retrieved by running `lb.value()` or `lb.textvalue()`. + +In this example the first two items trigger submenus, the third runs a +callback. In this example the two submenus share a callback (`cb_sm`), which +uses the passed arg to determine which menu it was called from. +```python +mnu = (('Gas', cb_sm, (0,), ('Helium','Neon','Argon','Krypton','Xenon','Radon')), + ('Metal', cb_sm, (1,), ('Lithium', 'Sodium', 'Potassium','Rubidium','Caesium')), + ('View', cb, (2,))) +``` + +###### [Contents](./README.md#0-contents) + +# 23. Graph Plotting ```python from gui.widgets.graph import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence @@ -2091,7 +2161,7 @@ from gui.widgets.graph import PolarGraph, PolarCurve, CartesianGraph, Curve, TSe For example code see `gui/demos/plot.py`. -## 22.1 Concepts +## 23.1 Concepts Data for Cartesian graphs constitutes a sequence of x, y pairs, for polar graphs it is a sequence of complex `z` values. The module supports three @@ -2101,13 +2171,13 @@ common cases: 3. One or more `y` values arrive gradually. The `X` axis represents time. This is a simplifying case of 2. -### 22.1.1 Graph classes +### 23.1.1 Graph classes A user program first instantiates a graph object (`PolarGraph` or `CartesianGraph`). This creates an empty graph image upon which one or more curves may be plotted. Graphs are passive widgets so cannot accept user input. -### 22.1.2 Curve classes +### 23.1.2 Curve classes The user program then instantiates one or more curves (`Curve` or `PolarCurve`) as appropriate to the graph. Curves may be assigned colors to @@ -2122,7 +2192,7 @@ Where it is required to plot realtime data as it arrives, this is achieved via calls to the curve's `point` method. If a prior point exists it causes a line to be drawn connecting the point to the last one drawn. -### 22.1.3 Coordinates +### 23.1.3 Coordinates `PolarGraph` and `CartesianGraph` objects are subclassed from `Widget` and are positioned accordingly by `row` and `col` with a 2-pixel outside border. The @@ -2138,9 +2208,9 @@ unit circle but will be clipped to the rectangular graph boundary. ###### [Contents](./README.md#0-contents) -## 22.2 Graph classes +## 23.2 Graph classes -### 22.2.1 Class CartesianGraph +### 23.2.1 Class CartesianGraph Constructor. Mandatory positional arguments: @@ -2167,7 +2237,7 @@ Keyword only arguments (all optional): Method: * `show` No args. Redraws the empty graph. Used when plotting time sequences. -### 22.2.2 Class PolarGraph +### 23.2.2 Class PolarGraph Constructor. Mandatory positional arguments: @@ -2192,9 +2262,9 @@ Method: ###### [Contents](./README.md#0-contents) -## 22.3 Curve classes +## 23.3 Curve classes -### 22.3.1 Class Curve +### 23.3.1 Class Curve The Cartesian curve constructor takes the following positional arguments: @@ -2234,7 +2304,7 @@ To plot x values from 1000 to 4000 we would set the `origin` x value to 1000 and the `excursion` x value to 3000. The `excursion` values scale the plotted values to fit the corresponding axis. -### 22.3.2 Class PolarCurve +### 23.3.2 Class PolarCurve The constructor takes the following positional arguments: @@ -2268,7 +2338,7 @@ Complex points should lie within the unit circle to be drawn within the grid. ###### [Contents](./README.md#0-contents) -## 22.4 Class TSequence +## 23.4 Class TSequence A common task is the acquisition and plotting of real time data against time, such as hourly temperature and air pressure readings. This class facilitates diff --git a/gui/core/colors.py b/gui/core/colors.py index 3bf14ed..a6ff04c 100644 --- a/gui/core/colors.py +++ b/gui/core/colors.py @@ -46,9 +46,13 @@ else: DARKBLUE = SSD.rgb(0, 0, 90) WHITE = SSD.rgb(255, 255, 255) -# Color used when clearing the screen -BGCOLOR = BLACK - CIRCLE = 1 RECTANGLE = 2 CLIPPED_RECT = 3 + +FOCUS = 0 +PRECISION = 1 +FG = 2 +BG = 3 +GREY_OUT = 4 +color_map = [WHITE, YELLOW, WHITE, BLACK, GREY] diff --git a/gui/core/ugui.py b/gui/core/ugui.py index 3750e12..b3afade 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -21,7 +21,7 @@ display = None # Singleton instance ssd = None gc.collect() -__version__ = (0, 1, 1) +__version__ = (0, 1, 2) # Null function dolittle = lambda *_ : None @@ -37,8 +37,9 @@ _LAST = const(3) # Wrapper for ssd providing buttons and framebuf compatible methods class Display: + verbose = True - def __init__(self, objssd, nxt, sel, prev=None, up=None, down=None, encoder=False): + def __init__(self, objssd, nxt, sel, prev=None, incr=None, decr=None, encoder=False): global display, ssd self._next = Switch(nxt) self._sel = Switch(sel) @@ -58,18 +59,20 @@ class Display: self._prev = Switch(prev) self._prev.close_func(self._closure, (self._prev, Screen.ctrl_move, _PREV)) if encoder: - if up is None or down is None: + self.verbose and print('Using encoder.') + if incr is None or decr is None: raise ValueError('Must specify pins for encoder.') from gui.primitives.encoder import Encoder - self._enc = Encoder(up, down, div=encoder, callback=Screen.adjust) + self._enc = Encoder(incr, decr, div=encoder, callback=Screen.adjust) else: - # Up and down methods get the button as an arg. - if up is not None: - sup = Switch(up) + self.verbose and print('Using switches.') + # incr and decr methods get the button as an arg. + if incr is not None: + sup = Switch(incr) sup.close_func(self._closure, (sup, Screen.adjust, 1)) - if down is not None: - sdown = Switch(down) - sdown.close_func(self._closure, (sdown, Screen.adjust, -1)) + if decr is not None: + sdn = Switch(decr) + sdn.close_func(self._closure, (sdn, Screen.adjust, -1)) self._is_grey = False # Not greyed-out display = self # Populate globals ssd = objssd @@ -85,7 +88,7 @@ class Display: sl = writer.stringlen(text) writer.set_textpos(ssd, y - writer.height // 2, x - sl // 2) if self._is_grey: - fgcolor = GREY + fgcolor = color_map[GREY_OUT] writer.setcolor(fgcolor, bgcolor) writer.printstring(text, invert) writer.setcolor() # Restore defaults @@ -93,7 +96,7 @@ class Display: def print_left(self, writer, x, y, txt, fgcolor=None, bgcolor=None, invert=False): writer.set_textpos(ssd, y, x) if self._is_grey: - fgcolor = GREY + fgcolor = color_map[GREY_OUT] writer.setcolor(fgcolor, bgcolor) writer.printstring(txt, invert) writer.setcolor() # Restore defaults @@ -102,7 +105,7 @@ class Display: # It would be possible to do better with RGB565 but would need inverse transformation # to (r, g, b), scale and re-convert to integer. def _getcolor(self, color): # Takes in an integer color, bit size dependent on driver - return GREY if self._is_grey and color != BGCOLOR else color + return color_map[GREY_OUT] if self._is_grey and color != color_map[BG] else color def usegrey(self, val): # display.usegrey(True) sets greyed-out self._is_grey = val @@ -113,7 +116,7 @@ class Display: # These methods support greying out color overrides. # Clear screen. def clr_scr(self): - ssd.fill_rect(0, 0, self.width - 1, self.height - 1, BGCOLOR) + ssd.fill_rect(0, 0, self.width - 1, self.height - 1, color_map[BG]) def rect(self, x1, y1, w, h, color): ssd.rect(x1, y1, w, h, self._getcolor(color)) @@ -251,8 +254,7 @@ class Screen: cs_old.on_hide() # Optional method in subclass if forward: if isinstance(cls_new_screen, type): - # Instantiate new screen. __init__ must terminate - if cs_old is not None and cs_old.__name__ == 'Window': + if isinstance(cs_old, Window): raise ValueError('Windows are modal.') new_screen = cls_new_screen(*args, **kwargs) else: @@ -353,7 +355,7 @@ class Screen: # If opening a Screen from a Window just blank and redraw covered area if isinstance(old_screen, Window): x0, y0, x1, y1, w, h = old_screen._list_dims() - dev.fill_rect(x0, y0, w, h, BGCOLOR) # Blank to screen BG + dev.fill_rect(x0, y0, w, h, color_map[BG]) # Blank to screen BG for obj in [z for z in self.displaylist if z.overlaps(x0, y0, x1, y1)]: if obj.visible: obj.draw_border() @@ -484,8 +486,8 @@ class Window(Screen): self.height = height self.width = width self.draw_border = draw_border - self.fgcolor = fgcolor if fgcolor is not None else WHITE - self.bgcolor = bgcolor if bgcolor is not None else BGCOLOR + self.fgcolor = fgcolor if fgcolor is not None else color_map[FG] + self.bgcolor = bgcolor if bgcolor is not None else color_map[BG] def _do_open(self, old_screen): dev = display.usegrey(False) @@ -596,7 +598,7 @@ class Widget: dev = display.usegrey(self._greyed_out) x = self.col y = self.row - dev.fill_rect(x, y, self.width, self.height, BGCOLOR if black else self.bgcolor) + dev.fill_rect(x, y, self.width, self.height, color_map[BG] if black else self.bgcolor) return True # Called by Screen.show(). Draw background and bounding box if required. @@ -609,7 +611,7 @@ class Widget: w = self.width + 4 h = self.height + 4 if self.has_focus(): - color = WHITE + color = color_map[FOCUS] if hasattr(self, 'precision') and self.precision and self.prcolor is not None: color = self.prcolor dev.rect(x, y, w, h, color) @@ -617,7 +619,7 @@ class Widget: else: if isinstance(self.bdcolor, bool): # No border if self.has_border: # Border exists: erase it - dev.rect(x, y, w, h, BGCOLOR) + dev.rect(x, y, w, h, color_map[BG]) self.has_border = False elif self.bdcolor: # Border is required dev.rect(x, y, w, h, self.bdcolor) @@ -695,7 +697,7 @@ class LinearIO(Widget): # Precision mode can only be entered when the active control has focus. # In this state it will have a white border. By default this turns yellow # but subclass can be defeat this with WHITE or another color - self.prcolor = YELLOW if prcolor is None else prcolor + self.prcolor = color_map[PRECISION] if prcolor is None else prcolor # Adjust widget's value. Args: button pressed, amount of increment def do_adj(self, button, val): diff --git a/gui/demos/menu.py b/gui/demos/menu.py index 944084e..7cc70c8 100644 --- a/gui/demos/menu.py +++ b/gui/demos/menu.py @@ -29,8 +29,8 @@ class BaseScreen(Screen): Screen.change(DialogBox, kwargs = kwargs) super().__init__() - mnu = (('Gas', cb_sm, (0,), ('Argon','Neon','Xenon','Radon')), - ('Metal', cb_sm, (1,), ('Caesium', 'Lithium', 'Sodium', 'Potassium')), + mnu = (('Gas', cb_sm, (0,), ('Helium','Neon','Argon','Krypton','Xenon','Radon')), + ('Metal', cb_sm, (1,), ('Lithium', 'Sodium', 'Potassium','Rubidium','Caesium')), ('View', cb, (2,))) wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False) Menu(wri, bgcolor=BLUE, textcolor=WHITE, args = mnu) diff --git a/gui/demos/various.py b/gui/demos/various.py index 4c3c17b..4271857 100644 --- a/gui/demos/various.py +++ b/gui/demos/various.py @@ -25,7 +25,6 @@ import uasyncio as asyncio import utime import gc -defaults['focus'] = YELLOW class FooScreen(Screen): def __init__(self): diff --git a/gui/widgets/menu.py b/gui/widgets/menu.py index 1b2cbb9..8c1fa31 100644 --- a/gui/widgets/menu.py +++ b/gui/widgets/menu.py @@ -18,8 +18,8 @@ class SubMenu(Window): def __init__(self, menu, button, elements, cb, args): # menu is parent Menu wri = menu.writer - row = 10 - col = button.col + 4 # Drop down below top level menu button + row = button.height + 2 + col = button.col # Drop down below top level menu button # Need to determine Window dimensions from size of Listbox, which # depends on number and length of elements. entry_height, lb_height, textwidth = Listbox.dimensions(wri, elements) @@ -41,9 +41,10 @@ class SubMenu(Window): # A Menu is a set of Button objects at the top of the screen. On press, Buttons either run the # user callback or instantiate a SubMenu +# args: ((text, cb, (args,)),(text, cb, (args,), (elements,)), ...) class Menu: - def __init__(self, writer, *, height=25, bgcolor=None, fgcolor=None, textcolor=None, select_color=DARKBLUE, args): # ((text, cb, (args,)),(text, cb, (args,), (elements,)), ...) + def __init__(self, writer, *, height=25, bgcolor=None, fgcolor=None, textcolor=None, select_color=DARKBLUE, args): self.writer = writer self.select_color = select_color row = 2 diff --git a/hardware_setup.py b/hardware_setup.py index df02c1b..3049d80 100644 --- a/hardware_setup.py +++ b/hardware_setup.py @@ -51,4 +51,4 @@ sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value -display = Display(ssd, nxt, sel, prev, increase, decrease) +display = Display(ssd, nxt, sel, prev, increase, decrease, 5)