2021-06-09 16:11:48 +00:00
|
|
|
# buttons.py Extension to ugui providing pushbutton classes
|
|
|
|
|
|
|
|
# Released under the MIT License (MIT). See LICENSE.
|
|
|
|
# Copyright (c) 2021 Peter Hinch
|
|
|
|
|
|
|
|
import uasyncio as asyncio
|
|
|
|
from gui.core.ugui import Screen, Widget, display
|
|
|
|
from gui.core.colors import *
|
|
|
|
|
2024-07-19 13:01:08 +00:00
|
|
|
dolittle = lambda *_: None
|
2021-06-09 16:11:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Button(Widget):
|
|
|
|
lit_time = 1000
|
2024-07-19 13:01:08 +00:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
writer,
|
|
|
|
row,
|
|
|
|
col,
|
|
|
|
*,
|
|
|
|
shape=RECTANGLE,
|
|
|
|
height=20,
|
|
|
|
width=50,
|
|
|
|
fgcolor=None,
|
|
|
|
bgcolor=None,
|
|
|
|
bdcolor=False,
|
|
|
|
textcolor=None,
|
|
|
|
litcolor=None,
|
|
|
|
text="",
|
|
|
|
callback=dolittle,
|
|
|
|
args=[]
|
|
|
|
):
|
2021-06-09 16:11:48 +00:00
|
|
|
sl = writer.stringlen(text)
|
|
|
|
if shape == CIRCLE: # Only height need be specified
|
|
|
|
width = max(sl, height)
|
|
|
|
height = width
|
|
|
|
else:
|
2021-06-24 08:58:57 +00:00
|
|
|
width = max(sl + 10, width) # Allow 5 pixels before and after text
|
2021-06-09 16:11:48 +00:00
|
|
|
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, False, True)
|
|
|
|
self.shape = shape
|
|
|
|
self.radius = height // 2
|
|
|
|
self.litcolor = litcolor
|
|
|
|
self.textcolor = self.fgcolor if textcolor is None else textcolor
|
|
|
|
self.text = text
|
|
|
|
self.callback = callback
|
|
|
|
self.callback_args = args
|
|
|
|
|
|
|
|
def show(self):
|
|
|
|
if self.screen is not Screen.current_screen:
|
|
|
|
return
|
|
|
|
x = self.col
|
|
|
|
y = self.row
|
|
|
|
w = self.width
|
|
|
|
h = self.height
|
2024-07-19 13:01:08 +00:00
|
|
|
if not self.visible: # erase the button
|
2021-06-09 16:11:48 +00:00
|
|
|
display.usegrey(False)
|
|
|
|
display.fill_rect(x, y, w, h, BGCOLOR)
|
|
|
|
return
|
|
|
|
super().show() # Blank rectangle containing button
|
|
|
|
if self.shape == CIRCLE: # Button coords are of top left corner of bounding box
|
|
|
|
x += self.radius
|
|
|
|
y += self.radius
|
2021-07-08 12:18:05 +00:00
|
|
|
display.fillcircle(x, y, self.radius, self.bgcolor)
|
2021-06-09 16:11:48 +00:00
|
|
|
display.circle(x, y, self.radius, self.fgcolor)
|
|
|
|
if len(self.text):
|
|
|
|
display.print_centred(self.writer, x, y, self.text, self.textcolor, self.bgcolor)
|
|
|
|
else:
|
|
|
|
xc = x + w // 2
|
|
|
|
yc = y + h // 2
|
2024-07-19 13:01:08 +00:00
|
|
|
if self.shape == RECTANGLE: # rectangle
|
2021-07-08 12:18:05 +00:00
|
|
|
display.fill_rect(x, y, w, h, self.bgcolor)
|
2021-06-09 16:11:48 +00:00
|
|
|
display.rect(x, y, w, h, self.fgcolor)
|
|
|
|
if len(self.text):
|
2024-07-19 13:01:08 +00:00
|
|
|
display.print_centred(
|
|
|
|
self.writer, xc, yc, self.text, self.textcolor, self.bgcolor
|
|
|
|
)
|
|
|
|
elif self.shape == CLIPPED_RECT: # clipped rectangle
|
2021-07-08 12:18:05 +00:00
|
|
|
display.fill_clip_rect(x, y, w, h, self.bgcolor)
|
2021-06-09 16:11:48 +00:00
|
|
|
display.clip_rect(x, y, w, h, self.fgcolor)
|
|
|
|
if len(self.text):
|
2024-07-19 13:01:08 +00:00
|
|
|
display.print_centred(
|
|
|
|
self.writer, xc, yc, self.text, self.textcolor, self.bgcolor
|
|
|
|
)
|
2021-06-09 16:11:48 +00:00
|
|
|
|
2024-07-26 17:46:44 +00:00
|
|
|
async def shownormal(self): # Revert to normal color after a delay
|
|
|
|
try:
|
|
|
|
await asyncio.sleep_ms(Button.lit_time)
|
|
|
|
except asyncio.CancelledError: # Or prior to a screen change
|
|
|
|
pass
|
2021-07-08 12:18:05 +00:00
|
|
|
self.bgcolor = self.def_bgcolor
|
2021-06-09 16:11:48 +00:00
|
|
|
self.draw = True # Redisplay
|
|
|
|
|
2024-07-19 13:01:08 +00:00
|
|
|
def do_sel(self): # Select was pushed
|
|
|
|
self.callback(self, *self.callback_args) # CB takes self as 1st arg.
|
2021-06-09 16:11:48 +00:00
|
|
|
if self.litcolor is not None and self.has_focus(): # CB may have changed focus
|
2024-07-26 17:46:44 +00:00
|
|
|
if self.bgcolor != self.litcolor:
|
|
|
|
self.bgcolor = self.litcolor
|
|
|
|
self.draw = True # Redisplay
|
|
|
|
revert = asyncio.create_task(self.shownormal())
|
|
|
|
Screen.current_screen.reg_task(revert, True) # Cancel on screen change
|
2021-06-09 16:11:48 +00:00
|
|
|
|
2024-07-19 13:01:08 +00:00
|
|
|
|
2021-06-09 16:11:48 +00:00
|
|
|
# Preferred way to close a screen or dialog. Produces an X button at the top RHS.
|
|
|
|
# Note that if the bottom screen is closed, the application terminates.
|
|
|
|
class CloseButton(Button):
|
|
|
|
def __init__(self, writer, width=0, callback=dolittle, args=(), bgcolor=RED):
|
|
|
|
scr = Screen.current_screen
|
2021-06-24 08:58:57 +00:00
|
|
|
# Calculate the button width if not provided. Button allows
|
|
|
|
# 5 pixels either side.
|
2024-07-19 13:01:08 +00:00
|
|
|
wd = width if width else (writer.stringlen("X") + 10)
|
2021-06-09 16:11:48 +00:00
|
|
|
self.user_cb = callback
|
|
|
|
self.user_args = args
|
2024-07-19 13:01:08 +00:00
|
|
|
super().__init__(
|
|
|
|
writer,
|
|
|
|
*scr.locn(4, scr.width - wd - 4),
|
|
|
|
width=wd,
|
|
|
|
height=wd,
|
|
|
|
bgcolor=bgcolor,
|
|
|
|
callback=self.cb,
|
|
|
|
text="X"
|
|
|
|
)
|
2021-06-09 16:11:48 +00:00
|
|
|
|
|
|
|
def cb(self, _):
|
|
|
|
self.user_cb(self, *self.user_args)
|
|
|
|
Screen.back()
|
2024-07-19 13:01:08 +00:00
|
|
|
|
|
|
|
|
2021-06-09 16:11:48 +00:00
|
|
|
# Group of buttons, typically at same location, where pressing one shows
|
|
|
|
# the next e.g. start/stop toggle or sequential select from short list
|
|
|
|
class ButtonList:
|
2024-07-19 13:01:08 +00:00
|
|
|
def __init__(self, callback=dolittle, new_cb=False):
|
2021-06-09 16:11:48 +00:00
|
|
|
self.user_callback = callback
|
2024-07-19 13:01:08 +00:00
|
|
|
self._new_cb = new_cb
|
2021-06-09 16:11:48 +00:00
|
|
|
self.lstbuttons = []
|
2024-07-19 13:01:08 +00:00
|
|
|
self.current = None # No current button
|
2021-06-09 16:11:48 +00:00
|
|
|
self._greyed_out = False
|
|
|
|
|
|
|
|
def add_button(self, *args, **kwargs):
|
|
|
|
button = Button(*args, **kwargs)
|
|
|
|
self.lstbuttons.append(button)
|
2024-07-19 13:01:08 +00:00
|
|
|
active = self.current is None # 1st button added is active
|
2021-06-09 16:11:48 +00:00
|
|
|
button.visible = active
|
|
|
|
button.callback = self._callback
|
|
|
|
if active:
|
|
|
|
self.current = button
|
|
|
|
return button
|
|
|
|
|
|
|
|
def value(self, button=None, new_cb=False):
|
|
|
|
if button is not None and button is not self.current:
|
|
|
|
old = self.current
|
|
|
|
new = button
|
|
|
|
self.current = new
|
|
|
|
old.visible = False
|
|
|
|
new.visible = True
|
|
|
|
new.draw = True # Redisplay without changing currency
|
|
|
|
# Args for user callback: button instance followed by any specified.
|
2024-07-19 13:01:08 +00:00
|
|
|
# Normal behaviour is to run cb of old button (see docs).
|
|
|
|
# This may be overridden for programmatic value changes, or for
|
|
|
|
# physical button presses via constructor arg. See docs.
|
|
|
|
if new_cb or self._new_cb:
|
2021-06-09 16:11:48 +00:00
|
|
|
self.user_callback(new, *new.callback_args)
|
2024-07-19 13:01:08 +00:00
|
|
|
else:
|
2021-06-09 16:11:48 +00:00
|
|
|
self.user_callback(old, *old.callback_args)
|
|
|
|
return self.current
|
|
|
|
|
|
|
|
def greyed_out(self, val=None):
|
|
|
|
if val is not None and self._greyed_out != val:
|
|
|
|
self._greyed_out = val
|
|
|
|
for button in self.lstbuttons:
|
|
|
|
button.greyed_out(val)
|
|
|
|
self.current.draw = True
|
|
|
|
return self._greyed_out
|
|
|
|
|
|
|
|
def _callback(self, button, *_):
|
|
|
|
old = button
|
|
|
|
old_index = self.lstbuttons.index(button)
|
|
|
|
new = self.lstbuttons[(old_index + 1) % len(self.lstbuttons)]
|
|
|
|
self.current = new
|
|
|
|
old.visible = False
|
|
|
|
new.visible = True
|
|
|
|
Screen.select(new) # Move currency and redisplay
|
2024-07-19 13:01:08 +00:00
|
|
|
if self._new_cb:
|
|
|
|
self.user_callback(new, *new.callback_args)
|
|
|
|
else:
|
|
|
|
self.user_callback(old, *old.callback_args)
|
2021-06-09 16:11:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Group of buttons at different locations, where pressing one shows
|
|
|
|
# only current button highlighted and oes callback from current one
|
|
|
|
class RadioButtons:
|
|
|
|
def __init__(self, highlight, callback=dolittle, selected=0):
|
|
|
|
self.user_callback = callback
|
|
|
|
self.lstbuttons = []
|
2024-07-19 13:01:08 +00:00
|
|
|
self.current = None # No current button
|
2021-06-09 16:11:48 +00:00
|
|
|
self.highlight = highlight
|
|
|
|
self.selected = selected
|
|
|
|
self._greyed_out = False
|
|
|
|
|
|
|
|
def add_button(self, *args, **kwargs):
|
|
|
|
button = Button(*args, **kwargs)
|
|
|
|
self.lstbuttons.append(button)
|
|
|
|
button.callback = self._callback
|
|
|
|
active = len(self.lstbuttons) == self.selected + 1
|
2021-07-08 12:18:05 +00:00
|
|
|
button.bgcolor = self.highlight if active else button.def_bgcolor
|
2021-06-09 16:11:48 +00:00
|
|
|
if active:
|
|
|
|
self.current = button
|
|
|
|
return button
|
|
|
|
|
|
|
|
def value(self, button=None):
|
|
|
|
if button is not None and button is not self.current:
|
|
|
|
self._callback(button, *button.callback_args)
|
|
|
|
return self.current
|
|
|
|
|
|
|
|
def greyed_out(self, val=None):
|
|
|
|
if val is not None and self._greyed_out != val:
|
|
|
|
self._greyed_out = val
|
|
|
|
for button in self.lstbuttons:
|
|
|
|
button.greyed_out(val)
|
|
|
|
return self._greyed_out
|
|
|
|
|
|
|
|
def _callback(self, button, *args):
|
|
|
|
for but in self.lstbuttons:
|
|
|
|
if but is button:
|
|
|
|
but.bgcolor = self.highlight
|
|
|
|
self.current = button
|
|
|
|
else:
|
2021-07-08 12:18:05 +00:00
|
|
|
but.bgcolor = but.def_bgcolor
|
2021-06-09 16:11:48 +00:00
|
|
|
but.draw = True
|
2024-07-19 13:01:08 +00:00
|
|
|
self.user_callback(button, *args) # user gets button with args they specified
|