From 5fa17bb085f471c71c8652b045941a56d7731a95 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 10 Jul 2021 18:17:10 +0100 Subject: [PATCH] Meter class now replaces Tstat. --- README.md | 87 +++++++++++++++++------------ gui/core/colors.py | 4 +- gui/core/ugui.py | 2 + gui/demos/menu.py | 7 +++ gui/demos/tstat.py | 7 ++- gui/demos/various.py | 1 + gui/widgets/menu.py | 4 +- gui/widgets/meter.py | 13 ++++- gui/widgets/{tstat.py => region.py} | 27 +-------- 9 files changed, 79 insertions(+), 73 deletions(-) rename gui/widgets/{tstat.py => region.py} (83%) diff --git a/README.md b/README.md index 71b516e..7725b93 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ 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. @@ -104,9 +106,8 @@ of some display drivers. 13. [Dropdown widget](./README.md#13-dropdown-widget) Dropdown lists. 14. [DialogBox class](./README.md#14-dialogbox-class) Pop-up modal dialog boxes. 15. [Textbox widget](./README.md#15-textbox-widget) Scrolling text display. -16. [Meter widget](./README.md#16-meter-widget) Display floats on an analog meter. - 16.1 [Tstat widget](./README.md#161-tstat-widget) Meter subclass enables thermostats, alarms etc. -      16.1.1 [Region class](./README.md#1611-region-class) +16. [Meter widget](./README.md#16-meter-widget) Display floats on an analog meter, with data driven callbacks. + 16.1 [Region class](./README.md#161-region-class) 17. [Slider and HorizSlider widgets](./README.md#17-slider-and-horizslider-widgets) Linear potentiometer float data entry and display 18. [Scale widget](./README.md#18-scale-widget) High precision float entry and display. 19. [ScaleLog widget](./README.md#19-scalelog-widget) Wide dynamic range float entry and display. @@ -767,7 +768,10 @@ explicitly in code. This is a `Screen` subclass providing for modal windows. As such it has positional and dimension information. Usage consists of writing a user class -subclassed from `Window`. Example code is in `demos/screens.py`. +subclassed from `Window`. Example code is in `demos/screens.py`. Code in a +window must not attempt to open another `Window` or `Screen`. Doing so will +raise a `ValueError`. Modal behaviour means that the only valid screen change +is a return to the calling screen. ## 5.1 Constructor @@ -1392,13 +1396,32 @@ the oldest (topmost) being discarded as required. # 16. Meter widget +This `passive` widget displays a single floating point value on a vertical +linear scale. Optionally it can support data dependent callbacks. ```python from gui.widgets.meter import Meter ``` ![Image](./images/meter.JPG) - The two styles of `meter`, both showing a value of 0.65. This `passive` widget provides a vertical linear meter display of values scaled between 0.0 and 1.0. +In these examples each meter simply displays a data value. + +![Image](./images/tstat.JPG) +This example has two data sensitive regions, a control region with hysteresis +and an alarm region. Callbacks can run in response to specific changes in the +`Meter`'s value emulating data-dependent behaviour including alarms and +controls (like thermostats) having hysteresis. + +The class supports one or more `Region` instances. Visually these appear as +colored bands on the scale. If the meter's value enters, leaves or crosses one +of these bands a callback is triggered. This receives an arg indicating the +nature of the change which caused the trigger. For example an alarm might be +triggered when the value, initially below the region, enters it or crosses it. +The alarm might be cleared on exit or if crossed from above. Hysteresis as used +in thermostats is simple to implement. Examples of these techniques may be +found in `gui.demos.tstat.py`. + +Regions may be modified, added or removed programmatically. Constructor mandatory positional args: 1. `writer` The `Writer` instance (defines font) to use. @@ -1435,55 +1458,45 @@ Methods: Returns the current value. 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: * `text=None` The text to display. If `None` displays last value. - * ` invert=False` If true, show inverse text. + * `invert=False` If true, show inverse text. * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. * `bgcolor=None` Background color, as per foreground. * `bdcolor=None` Border color. As per above except that if `False` is passed, no border is displayed. This clears a previously drawn border. + 3. `del_region` Arg: a `Region` instance. Deletes the region. No callback will + run. ### Legends Depending on the font in use for legends additional space may be required above and below the `Meter` to display the top and bottom legends. -###### [Contents](./README.md#0-contents) - -## 16.1 Tstat widget +### Example of use of Regions ```python -from gui.widgets.tstat import Tstat +# Instantiate Meter +ts = Meter(wri, row, sl.mcol + 5, ptcolor=YELLOW, height=100, width=15, + style=Meter.BAR, legends=('0.0', '0.5', '1.0')) +# Instantiate two Regions and associate with the Meter instance. +reg = Region(ts, 0.4, 0.55, MAGENTA, ts_cb) +al = Region(ts, 0.9, 1.0, RED, al_cb) ``` -![Image](./images/tstat.JPG) -This example has two data sensitive regions, a control region with hysteresis -and an alarm region. +The callback `ts_cb` will run in response to data values between 0.4 and 0.55: +if the value enters that range having been outside it, if it leaves the range, +or if successive values are either side of the range. The `al_cb` callback +behaves similarly for data values between 0.9 and 1.0. -The `Tstat` subclass of `Meter` is a `passive` widget with callbacks which run -in response to specific changes in the object's value. Notionally based on a -thermostat, it can implement objects with a variety of data-dependent behaviour -including alarms and controls having hysteresis. +###### [Contents](./README.md#0-contents) -The class supports one or more `Region` instances. Visually these appear as -colored bands on the scale. If the meter's value enters, leaves or crosses one -of these bands a callback is triggered. This receives an arg indicating the -nature of the change which caused the trigger. For example an alarm might be -triggered when the value, initially below the region, enters it or crosses it. -The alarm might be cleared on exit or if crossed from above. Hysteresis as used -in thermostats is simple to implement. Examples of these techniques may be -found in `gui.demos.tstat.py`. +## 16.1 Region class -Regions may be modified, added or removed programmatically. +```python +from gui.widgets.region import Region +``` +Instantiating a `Region` associates it with a supporting widget (currently only +a `Meter`). Constructor positional args are as follows: -Constructor args and methods are as per `Meter`. The `Tstat` class adds the -following method: - 1. `del_region` Arg: a `Region` instance. Deletes the region. No callback will - run. - -### 16.1.1 Region class - -Instantiating a `Region` associates it with a `Tstat`. Constructor args are as -follows: - - * `tstat` The `Tstat` instance. + * `tstat` The parent instance. * `vlo` Low value (0 <= `vlo` <= 1.0). * `vhi` High value (`vlo` < `vhi` <= 1.0). * `color` For visible band. diff --git a/gui/core/colors.py b/gui/core/colors.py index 7066378..3bf14ed 100644 --- a/gui/core/colors.py +++ b/gui/core/colors.py @@ -1,7 +1,7 @@ # colors.py Micropython GUI library for TFT displays: colors and shapes # Released under the MIT License (MIT). See LICENSE. -# Copyright (c) 2019 Peter Hinch +# Copyright (c) 2019-2021 Peter Hinch from hardware_setup import SSD # Code can be portable between 4-bit and other drivers by calling create_color @@ -49,8 +49,6 @@ else: # Color used when clearing the screen BGCOLOR = BLACK -# RA8875 defines colors as 3-tuples with greying-out operating on those. -# Should we assign GREY to any color > 0? CIRCLE = 1 RECTANGLE = 2 CLIPPED_RECT = 3 diff --git a/gui/core/ugui.py b/gui/core/ugui.py index 8893c16..3750e12 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -252,6 +252,8 @@ class Screen: 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': + raise ValueError('Windows are modal.') new_screen = cls_new_screen(*args, **kwargs) else: raise ValueError('Must pass Screen class or subclass (not instance)') diff --git a/gui/demos/menu.py b/gui/demos/menu.py index ce5e633..944084e 100644 --- a/gui/demos/menu.py +++ b/gui/demos/menu.py @@ -11,6 +11,7 @@ from gui.core.writer import CWriter from gui.widgets.menu import Menu from gui.widgets.buttons import CloseButton from gui.core.colors import * +from gui.widgets.dialog import DialogBox class BaseScreen(Screen): @@ -19,7 +20,13 @@ class BaseScreen(Screen): print('Top level callback', n) def cb_sm(lb, n): + kwargs = {'writer' : wri, 'row': 60, 'col' : 2, + 'elements' : (('Yes', GREEN), ('No', RED), ('Foo', YELLOW)), + 'label' : 'Test dialog', + } print('Submenu callback', lb.value(), lb.textvalue(), n) + if lb.value() == 0: + Screen.change(DialogBox, kwargs = kwargs) super().__init__() mnu = (('Gas', cb_sm, (0,), ('Argon','Neon','Xenon','Radon')), diff --git a/gui/demos/tstat.py b/gui/demos/tstat.py index 67a5854..9e42d4f 100644 --- a/gui/demos/tstat.py +++ b/gui/demos/tstat.py @@ -13,7 +13,8 @@ from gui.core.ugui import Screen, ssd from gui.widgets.buttons import Button, CloseButton from gui.widgets.sliders import Slider from gui.widgets.label import Label -from gui.widgets.tstat import Tstat, Region +from gui.widgets.meter import Meter +from gui.widgets.region import Region from gui.widgets.led import LED from gui.core.writer import CWriter @@ -38,8 +39,8 @@ class BaseScreen(Screen): sl = Slider(wri, row, col, callback=self.slider_cb, bdcolor=RED, slotcolor=BLUE, legends=('0.0', '0.5', '1.0')) - self.ts = Tstat(wri, row, sl.mcol + 5, divisions = 4, ptcolor=YELLOW, height=100, width=15, - style=Tstat.BAR, legends=('0.0', '0.5', '1.0')) + self.ts = Meter(wri, row, sl.mcol + 5, ptcolor=YELLOW, height=100, width=15, + style=Meter.BAR, legends=('0.0', '0.5', '1.0')) reg = Region(self.ts, 0.4, 0.55, MAGENTA, self.ts_cb) al = Region(self.ts, 0.9, 1.0, RED, self.al_cb) col = self.ts.mcol + 5 diff --git a/gui/demos/various.py b/gui/demos/various.py index 4271857..4c3c17b 100644 --- a/gui/demos/various.py +++ b/gui/demos/various.py @@ -25,6 +25,7 @@ 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 fd901b2..1b2cbb9 100644 --- a/gui/widgets/menu.py +++ b/gui/widgets/menu.py @@ -11,9 +11,9 @@ from gui.widgets.buttons import Button from gui.widgets.listbox import Listbox from gui.core.colors import * +# A SubMenu is a Window containing a Listbox # Next and Prev close the listbox without running the callback. This is # handled by Screen .move bound method -# A SubMenu is a Window containing a Listbox class SubMenu(Window): def __init__(self, menu, button, elements, cb, args): # menu is parent Menu @@ -39,6 +39,8 @@ class SubMenu(Window): Screen.back() self.cb(obj_listbox, *self.args) # CB can access obj_listbox.value() or .textvalue() +# 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 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,)), ...) diff --git a/gui/widgets/meter.py b/gui/widgets/meter.py index 6b7fc2e..6b344f8 100644 --- a/gui/widgets/meter.py +++ b/gui/widgets/meter.py @@ -34,6 +34,7 @@ class Meter(Widget): yl -= dy mcol = max(mcol, l.mcol) self.mcol = mcol - 2 # For metrics. Legends never have border. + self.regions = set() self.value(value) def value(self, n=None, color=None): @@ -42,6 +43,8 @@ class Meter(Widget): n = super().value(min(1, max(0, n))) if color is not None: self.ptcolor = color + for r in self.regions: + r.check(n) return n def show(self): @@ -54,7 +57,10 @@ class Meter(Widget): x1 = self.col + width y0 = self.row y1 = self.row + height - self.preshow(x0, y1, width, height) # Subclass draws regions + for r in self.regions: + ht = round(height * (r.vhi - r.vlo)) + yr = y1 - round(height * r.vhi) + display.fill_rect(x0, yr, width, ht, r.color) if self.divisions > 0: dy = height / (self.divisions) # Tick marks for tick in range(self.divisions + 1): @@ -68,5 +74,6 @@ class Meter(Widget): w = width / 2 display.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) - def preshow(self, x, y, width, height): - pass # For subclass + def del_region(self, reg): + self.regions.discard(reg) + self.draw = True diff --git a/gui/widgets/tstat.py b/gui/widgets/region.py similarity index 83% rename from gui/widgets/tstat.py rename to gui/widgets/region.py index 9d9c6d7..ec1a939 100644 --- a/gui/widgets/tstat.py +++ b/gui/widgets/region.py @@ -1,4 +1,4 @@ -# tstat.py Extension to nanogui providing the Tstat class +# region.py Extension to nanogui providing the Region class # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2021 Peter Hinch @@ -100,28 +100,3 @@ class Region: self.wa = v < vlo # If it was in old region treat as if leaving it self.check(v) - - -class Tstat(Meter): - def __init__(self, *args, **kwargs): - self.regions = set() - super().__init__(*args, **kwargs) - - def del_region(self, reg): - self.regions.discard(reg) - self.draw = True - - # Called by subclass prior to drawing scale and data - def preshow(self, x, y, width, height): - for r in self.regions: - ht = round(height * (r.vhi - r.vlo)) - y1 = y - round(height * r.vhi) - display.fill_rect(x, y1, width, ht, r.color) - - def value(self, n=None, color=None): - if n is None: - return super().value() - v = super().value(n, color) - for r in self.regions: - r.check(v) - return v