# 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