kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
Add precision mode for float widgets.
rodzic
01e8d72601
commit
897a85d502
82
README.md
82
README.md
|
@ -2,7 +2,8 @@
|
|||
|
||||
This is a lightweight, portable, MicroPython GUI library for displays with
|
||||
drivers subclassed from `framebuf`. It allows input via pushbuttons or via a
|
||||
switch joystick.
|
||||
switch joystick. Written in Python it runs under a standard MicroPython
|
||||
firmware build.
|
||||
|
||||
It is larger and more complex than `nano-gui` owing to the support for input.
|
||||
It enables switching between screens and launching modal windows. In addition
|
||||
|
@ -20,8 +21,8 @@ to a wide range of displays. It is also portable between hosts.
|
|||
Touch GUI's have many advantages, however they have drawbacks, principally cost
|
||||
and the need for calibration. Note that the latter does not apply to the
|
||||
[official LCD160cr](https://store.micropython.org/product/LCD160CRv1.1H).
|
||||
Another problem is that touch controllers vary, magnifying the difficulty of
|
||||
writing a portable GUI.
|
||||
Another problem is that there are a number of types of touch controller,
|
||||
magnifying the difficulty of writing a portable GUI.
|
||||
|
||||
Pushbutton input works well and yields astonishingly low cost solutions. A
|
||||
network-connected board with a 135x240 color display can be built for under £20
|
||||
|
@ -41,6 +42,10 @@ The following are similar GUI repos with differing objectives.
|
|||
displays based on SSD1963 and XPT2046. High performance on large displays due
|
||||
to the parallel interface. Specific to STM hosts.
|
||||
|
||||
[LVGL](https://lvgl.io/) is a pretty icon-based GUI library. It is written in C
|
||||
with MicroPython bindings; consequently it requires the build system for your
|
||||
target and a C device driver (unless you can acquire a suitable binary).
|
||||
|
||||
# Project status
|
||||
|
||||
Code has been tested on ESP32 and Pi Pico. The API shuld be stable. I'm not
|
||||
|
@ -148,13 +153,8 @@ of `micro-gui` widgets which requires only the first two buttons.
|
|||
|
||||
Widgets such as `Listbox` objects, dropdown lists (`Dropdown`), and those for
|
||||
floating point data entry require the `Increase` and `Decrease` buttons to
|
||||
select a data item or to adjust the linear value.
|
||||
|
||||
A `LinearIO` is a `Widget` that responds to the `increase` and `decrease`
|
||||
buttons by running an `asyncio` task. These typically output floating point
|
||||
values using an accelerating algorithm responding to the duration of the button
|
||||
press. This enables floats with a wide dynamic range to be adjusted with
|
||||
precision.
|
||||
select a data item or to adjust the linear value. This is discussed in
|
||||
[Floating Point Widgets](./README.md#112-floating-point-widgets).
|
||||
|
||||
The currently selected `Widget` is identified by a white border: the `focus`
|
||||
moves between widgets via `Next` and `Prev`. Only `active` `Widget` instances
|
||||
|
@ -330,6 +330,31 @@ require a large (320x240) display. Demos are run by issuing (for example):
|
|||
* `various.py` Assorted widgets including the different types of pushbutton.
|
||||
* `vtest.py` Clock and compass styles of vector display.
|
||||
|
||||
## 1.12 Floating Point Widgets
|
||||
|
||||
The challenge is to devise a way, with just two pushbuttons, of adjusting a
|
||||
data value which may have an extremely large dynamic range. This is the ratio
|
||||
of the data value's total range to the smallest adjustment that can be made.
|
||||
The mechanism as currently implemented enables a precision of 0.05%.
|
||||
|
||||
Floating point widgets respond to a brief press of the `increase` or `decrease`
|
||||
buttons by adjusting the value by a small amount. A continued press causes the
|
||||
value to be repeatedly adjusted, with the amount of the adjustment increasing
|
||||
with time. This enables the entire range of the control to be accessed quickly,
|
||||
while allowing small changes of 0.5%. This works well. In many cases the level
|
||||
of precision will suffice.
|
||||
|
||||
Fine adjustments may be achieved by pressing the `select` button for at least
|
||||
one second. The GUI will respond by changing the border color from white
|
||||
(i.e. has focus) to yellow. In this mode a brief press of `increase` or
|
||||
`decrease` will have a reduced effect (0.05%). The fine mode may be cancelled
|
||||
by pressing `select` or by moving the focus to another control.
|
||||
|
||||
In the case of slider and knob controls the precision of fine mode exceeds that
|
||||
of the visual appearance of the widget: fine changes can be too small to see.
|
||||
Options are to use the [Scale widget](./README.md#18-scale-widget) or to have a
|
||||
linked `Label` showing the widget's exact value.
|
||||
|
||||
###### [Contents](./README.md#0-contents)
|
||||
|
||||
# 2. Usage
|
||||
|
@ -1213,9 +1238,12 @@ from gui.widgets.sliders import Slider, HorizSlider
|
|||
Different styles of slider.
|
||||
|
||||
These emulate linear potentiometers in order to display or control floating
|
||||
point values. Vertical `Slider` and horizontal `HorizSlider` variants are
|
||||
available. These are constructed and used similarly. The short forms (v) or (h)
|
||||
are used below to identify these variants.
|
||||
point values. A description of the user interface in the `active` case may be
|
||||
found in [Floating Point Widgets](./README.md#112-floating-point-widgets).
|
||||
|
||||
Vertical `Slider` and horizontal `HorizSlider` variants are available. These
|
||||
are constructed and used similarly. The short forms (v) or (h) are used below
|
||||
to identify these variants.
|
||||
|
||||
Constructor mandatory positional args:
|
||||
1. `writer` The `Writer` instance (defines font) to use.
|
||||
|
@ -1235,6 +1263,9 @@ Optional keyword only arguments:
|
|||
default is used.
|
||||
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
|
||||
color is provided, a border line will be drawn around the control.
|
||||
* `prcolor=None` If `active`, in precision mode the white focus border changes
|
||||
to yellow to for a visual indication. An alternative color can be provided.
|
||||
`WHITE` will defeat this change.
|
||||
* `fontcolor=None` Text color. Defaults to foreground color.
|
||||
* `slotcolor=None` Color for the slot: this is a thin rectangular region in
|
||||
the centre of the control along which the slider moves. Defaults to the
|
||||
|
@ -1282,6 +1313,10 @@ scale with (say) 200 graduations (ticks) to readily be visible on a small
|
|||
display, with sufficient resolution to enable the user to interpolate between
|
||||
ticks. Default settings enable estimation of a value to within about +-0.1%.
|
||||
|
||||
The `Scale` may be `active` or `passive`. A description of the user interface
|
||||
in the `active` case may be found in
|
||||
[Floating Point Widgets](./README.md#112-floating-point-widgets).
|
||||
|
||||
Legends for the scale are created dynamically as it scrolls past the window.
|
||||
The user may control this by means of a callback. The example `lscale.py` in
|
||||
`nano-gui` illustrates a variable with range 88.0 to 108.0, the callback
|
||||
|
@ -1308,6 +1343,9 @@ Keyword only arguments (all optional):
|
|||
default is used.
|
||||
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
|
||||
color is provided, a border line will be drawn around the control.
|
||||
* `prcolor=None` If `active`, in precision mode the white focus border changes
|
||||
to yellow to for a visual indication. An alternative color can be provided.
|
||||
`WHITE` will defeat this change.
|
||||
* `pointercolor=None` Color of pointer. Defaults to `.fgcolor`.
|
||||
* `fontcolor=None` Color of legends. Default `fgcolor`.
|
||||
* `value=0.0` Initial value.
|
||||
|
@ -1393,6 +1431,14 @@ The control is modelled on old radios where a large scale scrolls past a small
|
|||
window having a fixed pointer. The use of a logarithmic scale enables the
|
||||
display and input of a value which can change by many orders of magnitude.
|
||||
|
||||
The `Scale` may be `active` or `passive`. A description of the user interface
|
||||
in the `active` case may be found in
|
||||
[Floating Point Widgets](./README.md#112-floating-point-widgets). Owing to the
|
||||
logarithmic nature of the widget, the changes discussed in that reference are
|
||||
multiplicative rather than additive. Thus a long press of `increase` will
|
||||
multiply the widget's value by a progressively larger factor, enabling many
|
||||
decades to be traversed quickly.
|
||||
|
||||
Legends for the scale are created dynamically as it scrolls past the window,
|
||||
with one legend for each decade. The user may control this by means of a
|
||||
callback, for example to display units, e.g. `10nF`. A further callback
|
||||
|
@ -1421,6 +1467,9 @@ Keyword only arguments (all optional):
|
|||
default is used.
|
||||
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
|
||||
color is provided, a border line will be drawn around the control.
|
||||
* `prcolor=None` If `active`, in precision mode the white focus border changes
|
||||
to yellow to for a visual indication. An alternative color can be provided.
|
||||
`WHITE` will defeat this change.
|
||||
* `pointercolor=None` Color of pointer. Defaults to `.fgcolor`.
|
||||
* `fontcolor=None` Color of legends. Default `WHITE`.
|
||||
* `legendcb=None` Callback for populating scale legends (see below).
|
||||
|
@ -1620,7 +1669,9 @@ from gui.widgets.knob import Knob
|
|||
![Image](./images/knob.JPG)
|
||||
|
||||
This emulates a rotary control capable of being rotated through a predefined
|
||||
arc in order to display or set a floating point variable.
|
||||
arc in order to display or set a floating point variable. A `Knob` may be
|
||||
`active` or `passive`. A description of the user interface in the `active` case
|
||||
may be found in [Floating Point Widgets](./README.md#112-floating-point-widgets).
|
||||
|
||||
Constructor mandatory positional args:
|
||||
1. `writer` The `Writer` instance (defines font) to use.
|
||||
|
@ -1637,6 +1688,9 @@ Optional keyword only arguments:
|
|||
default is used.
|
||||
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
|
||||
color is provided, a border line will be drawn around the control.
|
||||
* `prcolor=None` If `active`, in precision mode the white focus border changes
|
||||
to yellow to for a visual indication. An alternative color can be provided.
|
||||
`WHITE` will defeat this change.
|
||||
* `color=None` Fill color for the control knob. Default: no fill.
|
||||
* `callback=dolittle` Callback function runs when the user moves the knob or
|
||||
the value is changed programmatically.
|
||||
|
|
|
@ -41,9 +41,11 @@ class Display:
|
|||
def __init__(self, ssd, nxt, sel, prev=None, up=None, down=None):
|
||||
self._next = Switch(nxt)
|
||||
self._sel = Switch(sel)
|
||||
self._last = None # Last switch pressed.
|
||||
# Mandatory buttons
|
||||
self._next.close_func(Screen.next_ctrl) # Call current screen bound method
|
||||
self._sel.close_func(Screen.sel_ctrl)
|
||||
# Call current screen bound method
|
||||
self._next.close_func(self._closure, (self._next, Screen.next_ctrl))
|
||||
self._sel.close_func(self._closure, (self._sel, Screen.sel_ctrl))
|
||||
self._sel.open_func(Screen.unsel)
|
||||
|
||||
self.height = ssd.height
|
||||
|
@ -53,18 +55,25 @@ class Display:
|
|||
self._prev = None
|
||||
if prev is not None:
|
||||
self._prev = Switch(prev)
|
||||
self._prev.close_func(Screen.prev_ctrl)
|
||||
self._prev.close_func(self._closure, (self._prev, Screen.prev_ctrl))
|
||||
# Up and down methods get the button as an arg.
|
||||
self._up = None
|
||||
if up is not None:
|
||||
self._up = Switch(up)
|
||||
self._up.close_func(self.do_up)
|
||||
self._up.close_func(self._closure, (self._up, self.do_up))
|
||||
self._down = None
|
||||
if down is not None:
|
||||
self._down = Switch(down)
|
||||
self._down.close_func(self.do_down)
|
||||
self._down.close_func(self._closure, (self._down, self.do_down))
|
||||
self._is_grey = False # Not greyed-out
|
||||
|
||||
|
||||
# Reject button presses where a button is already pressed.
|
||||
# Execute if initialising, if same switch re-pressed or if last switch released
|
||||
def _closure(self, switch, func):
|
||||
if (self._last is None) or (self._last == switch) or self._last():
|
||||
self._last = switch
|
||||
func()
|
||||
|
||||
def print_centred(self, writer, x, y, text, fgcolor=None, bgcolor=None, invert=False):
|
||||
sl = writer.stringlen(text)
|
||||
writer.set_textpos(ssd, y - writer.height // 2, x - sl // 2)
|
||||
|
@ -83,12 +92,10 @@ class Display:
|
|||
writer.setcolor() # Restore defaults
|
||||
|
||||
def do_up(self):
|
||||
if self._down is not None and self._down(): # Interlock. TODO More?
|
||||
Screen.up_ctrl(self._up)
|
||||
Screen.up_ctrl(self._up)
|
||||
|
||||
def do_down(self):
|
||||
if self._up is not None and self._up():
|
||||
Screen.down_ctrl(self._down)
|
||||
Screen.down_ctrl(self._down)
|
||||
|
||||
# Greying out has only one option given limitation of 4-bit display driver
|
||||
# It would be possible to do better with RGB565 but would need inverse transformation
|
||||
|
@ -603,7 +610,10 @@ class Widget:
|
|||
w = self.width + 4
|
||||
h = self.height + 4
|
||||
if self.has_focus():
|
||||
dev.rect(x, y, w, h, WHITE)
|
||||
color = WHITE
|
||||
if hasattr(self, 'precision') and self.precision and self.prcolor is not None:
|
||||
color = self.prcolor
|
||||
dev.rect(x, y, w, h, color)
|
||||
self.has_border = True
|
||||
else:
|
||||
if isinstance(self.bdcolor, bool): # No border
|
||||
|
@ -676,6 +686,14 @@ class LinearIO(Widget):
|
|||
super().__init__(writer, row, col, height, width,
|
||||
fgcolor, bgcolor, bdcolor,
|
||||
value, active)
|
||||
# Handle variable precision
|
||||
self.precision = False
|
||||
# 1 sec long press to set precise
|
||||
self.lpd = Delay_ms(self.precise, (True,))
|
||||
# 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 None or another color
|
||||
self.prcolor = YELLOW
|
||||
|
||||
def do_up(self, button):
|
||||
asyncio.create_task(self.btnhan(button, 1))
|
||||
|
@ -683,13 +701,37 @@ class LinearIO(Widget):
|
|||
def do_down(self, button):
|
||||
asyncio.create_task(self.btnhan(button, -1))
|
||||
|
||||
async def btnhan(self, button, up): # Note: textbox.py, scale_log.py overrides
|
||||
d = self.min_delta
|
||||
# Handle increase and decrease buttons. Redefined by textbox.py, scale_log.py
|
||||
async def btnhan(self, button, up):
|
||||
if self.precision:
|
||||
d = self.min_delta * 0.1
|
||||
maxd = self.max_delta
|
||||
else:
|
||||
d = self.min_delta
|
||||
maxd = d * 4 # Why move fast in slow mode?
|
||||
self.value(self.value() + up * d)
|
||||
t = ticks_ms()
|
||||
while not button():
|
||||
await asyncio.sleep_ms(0) # Quit fast on button release
|
||||
if ticks_diff(ticks_ms(), t) > 500: # Button was held down
|
||||
d = min(self.max_delta, d * 2)
|
||||
d = min(maxd, d * 2)
|
||||
self.value(self.value() + up * d)
|
||||
t = ticks_ms()
|
||||
|
||||
def precise(self, v): # Timed out while button pressed
|
||||
self.precision = v
|
||||
if self.prcolor is not None:
|
||||
self.draw = True
|
||||
|
||||
def do_sel(self): # Select button was pushed
|
||||
if self.precision: # Already in mode
|
||||
self.precise(False)
|
||||
else: # Require a long press to enter mode
|
||||
self.lpd.trigger()
|
||||
|
||||
def unsel(self): # Select button was released
|
||||
self.lpd.stop()
|
||||
|
||||
def leave(self): # Control has lost focus
|
||||
self.precise(False)
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ class BaseScreen(Screen):
|
|||
self.lbl = Label(wri, row, col, 70, bdcolor=RED)
|
||||
|
||||
self.vslider = Slider(wri, 2, 2, callback=self.slider_cb,
|
||||
bdcolor=RED, slotcolor=BLUE,
|
||||
bdcolor=RED, slotcolor=BLUE, prcolor=CYAN,
|
||||
legends=('0.0', '0.5', '1.0'), value=0.5)
|
||||
|
||||
col = 80
|
||||
|
@ -59,7 +59,8 @@ class BaseScreen(Screen):
|
|||
pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN,
|
||||
callback=self.cb, value=10, active=True)
|
||||
row = 120
|
||||
self.knob = Knob(wri, row, 2, callback = self.cb, bgcolor=DARKGREEN, color=LIGHTRED)
|
||||
self.knob = Knob(wri, row, 2, callback = self.cb,
|
||||
bgcolor=DARKGREEN, color=LIGHTRED)
|
||||
col = 150
|
||||
row = 185
|
||||
Checkbox(wri, row, col, callback=self.cbcb)
|
||||
|
|
|
@ -14,7 +14,7 @@ dolittle = lambda *_ : None
|
|||
|
||||
class Knob(LinearIO):
|
||||
def __init__(self, writer, row, col, *, height=70, arc=TWOPI, ticks=9, value=0.0,
|
||||
fgcolor=None, bgcolor=None, color=None, bdcolor=None,
|
||||
fgcolor=None, bgcolor=None, color=None, bdcolor=None, prcolor=None,
|
||||
callback=dolittle, args=[], active=True):
|
||||
super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active)
|
||||
super()._set_callbacks(callback, args)
|
||||
|
@ -29,6 +29,8 @@ class Knob(LinearIO):
|
|||
self.color = color
|
||||
self.draw = True # Ensure a redraw on next refresh
|
||||
if active: # Run callback (e.g. to set dynamic colors)
|
||||
if prcolor is not None:
|
||||
self.prcolor = prcolor # Option for different bdcolor in precision mode
|
||||
self.callback(self, *self.args)
|
||||
|
||||
def show(self):
|
||||
|
|
|
@ -18,7 +18,8 @@ class Scale(LinearIO):
|
|||
ticks=200, legendcb=None, tickcb=None,
|
||||
height=0, width=100, bdcolor=None, fgcolor=None, bgcolor=None,
|
||||
callback=dolittle, args=[],
|
||||
pointercolor=None, fontcolor=None, value=0.0, active=False):
|
||||
pointercolor=None, fontcolor=None, prcolor=None,
|
||||
value=0.0, active=False):
|
||||
if ticks % 2:
|
||||
raise ValueError('ticks arg must be divisible by 2')
|
||||
self.ticks = ticks
|
||||
|
@ -57,6 +58,8 @@ class Scale(LinearIO):
|
|||
self.ldy0 = ycl - self.ldl // 2
|
||||
self.draw = True # Ensure a redraw on next refresh
|
||||
if active: # Run callback (e.g. to set dynamic colors)
|
||||
if prcolor is not None:
|
||||
self.prcolor = prcolor # Option for different bdcolor in precision mode
|
||||
self.callback(self, *self.args)
|
||||
|
||||
def show(self):
|
||||
|
|
|
@ -26,7 +26,7 @@ class ScaleLog(LinearIO):
|
|||
def __init__(self, writer, row, col, *,
|
||||
decades=5, height=0, width=160,
|
||||
bdcolor=None, fgcolor=None, bgcolor=None,
|
||||
pointercolor=None, fontcolor=None,
|
||||
pointercolor=None, fontcolor=None, prcolor=None,
|
||||
legendcb=None, tickcb=None,
|
||||
callback=dolittle, args=[],
|
||||
value=1.0, delta=0.01, active=False):
|
||||
|
@ -70,6 +70,8 @@ class ScaleLog(LinearIO):
|
|||
self.dw = (self.x1 - self.x0) // 2 # Pixel width of a decade
|
||||
self.draw = True # Ensure a redraw on next refresh
|
||||
if active: # Run callback (e.g. to set dynamic colors)
|
||||
if prcolor is not None:
|
||||
self.prcolor = prcolor # Option for different bdcolor in precision mode
|
||||
self.callback(self, *self.args)
|
||||
|
||||
# Pre calculated log10(x) for x in range(1, 10)
|
||||
|
@ -144,8 +146,12 @@ class ScaleLog(LinearIO):
|
|||
|
||||
async def btnhan(self, button, up):
|
||||
up = up == 1
|
||||
delta = self.delta
|
||||
maxdelta = 0.64
|
||||
if self.precision:
|
||||
delta = self.delta * 0.1
|
||||
maxdelta = self.delta
|
||||
else:
|
||||
delta = self.delta
|
||||
maxdelta = 0.64
|
||||
smul= (1 + delta) if up else (1 / (1 + delta))
|
||||
self.value(self.value() * smul)
|
||||
t = ticks_ms()
|
||||
|
|
|
@ -20,7 +20,8 @@ _HALF_SLOT_WIDTH = const(2) # Width of slot /2
|
|||
class Slider(LinearIO):
|
||||
def __init__(self, writer, row, col, *,
|
||||
height=100, width=20, divisions=10, legends=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, bdcolor=None, slotcolor=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, bdcolor=None,
|
||||
slotcolor=None, prcolor=None,
|
||||
callback=dolittle, args=[], value=0.0, active=True):
|
||||
width &= 0xfe # ensure divisible by 2
|
||||
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active)
|
||||
|
@ -42,6 +43,8 @@ class Slider(LinearIO):
|
|||
self.slot_h = height - _SLIDE_DEPTH - 1
|
||||
self.draw = True # Ensure a redraw on next refresh
|
||||
if active: # Run callback (e.g. to set dynamic colors)
|
||||
if prcolor is not None:
|
||||
self.prcolor = prcolor # Option for different bdcolor in precision mode
|
||||
self.callback(self, *self.args)
|
||||
|
||||
def show(self):
|
||||
|
@ -91,7 +94,7 @@ class HorizSlider(LinearIO):
|
|||
def __init__(self, writer, row, col, *,
|
||||
height=20, width=100, divisions=10, legends=None,
|
||||
fgcolor=None, bgcolor=None, fontcolor=None, bdcolor=None,
|
||||
slotcolor=None,
|
||||
slotcolor=None, prcolor=None,
|
||||
callback=dolittle, args=[], value=0.0, active=True):
|
||||
height &= 0xfe # ensure divisible by 2
|
||||
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active)
|
||||
|
@ -113,6 +116,8 @@ class HorizSlider(LinearIO):
|
|||
self.slot_y0 = centre - _HALF_SLOT_WIDTH
|
||||
self.draw = True # Ensure a redraw on next refresh
|
||||
if active: # Run callback (e.g. to set dynamic colors)
|
||||
if prcolor is not None:
|
||||
self.prcolor = prcolor # Option for different bdcolor in precision mode
|
||||
self.callback(self, *self.args)
|
||||
|
||||
def show(self):
|
||||
|
|
Ładowanie…
Reference in New Issue