micropython-micro-gui/gui/widgets/buttons.py

202 wiersze
8.1 KiB
Python

# 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.primitives.delay_ms import Delay_ms
from gui.core.colors import *
dolittle = lambda *_ : None
class Button(Widget):
lit_time = 1000
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=[]):
sl = writer.stringlen(text)
if shape == CIRCLE: # Only height need be specified
width = max(sl, height)
height = width
else:
width = max(sl + 10, width) # Allow 5 pixels before and after text
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
if self.litcolor is not None:
self.delay = Delay_ms(self.shownormal)
def show(self):
if self.screen is not Screen.current_screen:
return
x = self.col
y = self.row
w = self.width
h = self.height
if not self.visible: # erase the button
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
display.fillcircle(x, y, self.radius, self.bgcolor)
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
if self.shape == RECTANGLE: # rectangle
display.fill_rect(x, y, w, h, self.bgcolor)
display.rect(x, y, w, h, self.fgcolor)
if len(self.text):
display.print_centred(self.writer, xc, yc, self.text, self.textcolor, self.bgcolor)
elif self.shape == CLIPPED_RECT: # clipped rectangle
display.fill_clip_rect(x, y, w, h, self.bgcolor)
display.clip_rect(x, y, w, h, self.fgcolor)
if len(self.text):
display.print_centred(self.writer, xc, yc, self.text, self.textcolor, self.bgcolor)
async def shownormal(self):
# Handle case where screen changed while timer was active: delay repaint
# until screen is current. Pathological app behaviour where another
# control caused a screen change while timer running.
while self.screen is not Screen.current_screen:
await asyncio.sleep_ms(500)
self.bgcolor = self.def_bgcolor
self.draw = True # Redisplay
def do_sel(self): # Select was pushed
self.callback(self, *self.callback_args) # CB takes self as 1st arg.
if self.litcolor is not None and self.has_focus(): # CB may have changed focus
self.bgcolor = self.litcolor
self.draw = True # Redisplay
self.delay.trigger(Button.lit_time)
# 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
# Calculate the button width if not provided. Button allows
# 5 pixels either side.
wd = width if width else (writer.stringlen('X') + 10)
self.user_cb = callback
self.user_args = args
super().__init__(writer, *scr.locn(4, scr.width - wd - 4),
width = wd, height = wd, bgcolor = bgcolor,
callback = self.cb, text = 'X')
def cb(self, _):
self.user_cb(self, *self.user_args)
Screen.back()
# 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:
def __init__(self, callback=dolittle):
self.user_callback = callback
self.lstbuttons = []
self.current = None # No current button
self._greyed_out = False
def add_button(self, *args, **kwargs):
button = Button(*args, **kwargs)
self.lstbuttons.append(button)
active = self.current is None # 1st button added is active
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.
# Normal behaviour is to run cb of old button: this mimics a button press.
# Optionally programmatic value changes can run the cb of new button.
if new_cb: # Forced value change, callback is that of new button
self.user_callback(new, *new.callback_args)
else: # A button was pressed
# Callback context is button just pressed, not the new one
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
# Callback context is button just pressed, not the new one
self.user_callback(old, *old.callback_args)
# 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 = []
self.current = None # No current button
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
button.bgcolor = self.highlight if active else button.def_bgcolor
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:
but.bgcolor = but.def_bgcolor
but.draw = True
self.user_callback(button, *args) # user gets button with args they specified