Doc Menu class. Various fixes/improvements.

pull/8/head
Peter Hinch 2021-07-12 14:47:18 +01:00
rodzic 5fa17bb085
commit cad9767688
7 zmienionych plików z 149 dodań i 73 usunięć

150
README.md
Wyświetl plik

@ -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

Wyświetl plik

@ -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]

Wyświetl plik

@ -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):

Wyświetl plik

@ -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)

Wyświetl plik

@ -25,7 +25,6 @@ import uasyncio as asyncio
import utime
import gc
defaults['focus'] = YELLOW
class FooScreen(Screen):
def __init__(self):

Wyświetl plik

@ -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

Wyświetl plik

@ -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)