From 7e34d3c3abb93e8ad7299d27658ed99ab65b8beb Mon Sep 17 00:00:00 2001 From: Matthew Wilkes Date: Tue, 28 May 2024 23:47:01 +0100 Subject: [PATCH] Add basic read-only settings app This also adds some framework for more complex layouts, that the settings app uses --- modules/app_components/layout.py | 196 ++++++++++++++++++++++++++ modules/app_components/tokens.py | 11 +- modules/app_components/utils.py | 21 +++ modules/firmware_apps/settings_app.py | 51 +++++++ modules/system/launcher/app.py | 1 + 5 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 modules/app_components/layout.py create mode 100644 modules/app_components/utils.py create mode 100644 modules/firmware_apps/settings_app.py diff --git a/modules/app_components/layout.py b/modules/app_components/layout.py new file mode 100644 index 0000000..1b3e2f0 --- /dev/null +++ b/modules/app_components/layout.py @@ -0,0 +1,196 @@ +from events.input import BUTTON_TYPES +from . import tokens, utils + + +class Layoutable: + height: int # Available after draw + + def __init__(self): + self.height = 0 + + def draw(self, ctx, focused=False): + return + + async def button_event(self, event): + return False + + +class TextDisplay(Layoutable): + def __init__(self, text, font_size=None, rgb=None): + super().__init__() + self.text = text + if font_size is None: + font_size = tokens.label_font_size + self.font_size = font_size + self.lines = None + if rgb is None: + rgb = tokens.ui_colors["label"] + self.rgb = rgb + + def draw(self, ctx, focused=False): + ctx.save() + if self.lines is None: + self.lines = utils.wrap_text(ctx, self.text) + self.height = len(self.lines) * ctx.font_size + ctx.text_align = ctx.LEFT + if self.rgb: + ctx.rgb(*self.rgb) + for i, line in enumerate(self.lines): + ctx.move_to(0, i * ctx.font_size) + ctx.text(line) + ctx.restore() + + +class ButtonDisplay(Layoutable): + def __init__(self, text, font_size=None, rgb=None): + self.text = text + self.height = 40 + + def draw(self, ctx, focused=False): + ctx.save() + + # Draw button + ctx.translate(30, 5) + ctx.scale(0.75, 0.75) + if focused: + bg = tokens.ui_colors["active_button_background"] + fg = tokens.ui_colors["active_button_text"] + else: + bg = tokens.ui_colors["button_background"] + fg = tokens.ui_colors["active_button_text"] + ctx.rgb(*bg) + ctx.round_rectangle(0, 0, tokens.display_x, 40, 30).fill() + + # Draw text + ctx.rgb(*fg) + ctx.move_to(120, 30) + ctx.text_align = ctx.CENTER + ctx.text(self.text) + + ctx.restore() + + +class DefinitionDisplay(Layoutable): + def __init__(self, label, value): + self.label = label + self.value = value + self.height = 0 + + def draw(self, ctx, focused=False): + ctx.save() + self.height = 0 + + # Draw heading + ctx.font_size = tokens.one_pt * 8 + ctx.text_align = ctx.LEFT + if focused: + ctx.rgb(*tokens.colors["orange"]) + else: + ctx.rgb(*tokens.colors["yellow"]) + + # Draw label + label_lines = utils.wrap_text(ctx, self.label) + for line in label_lines: + ctx.move_to(0, self.height) + ctx.text(line) + self.height += ctx.font_size + + ctx.rgb(*tokens.ui_colors["label"]) + + # Draw value + ctx.font_size = tokens.ten_pt + value_lines = utils.wrap_text(ctx, self.value, width=230) + for line in value_lines: + ctx.move_to(10, self.height) + ctx.text(line) + self.height += ctx.font_size + + ctx.restore() + + +class LinearLayout(Layoutable): + def __init__(self, items): + self.items = items + self.y_offset = 120 + self.scale_factor = 0.9 + super().__init__() + + def draw(self, ctx): + focused_child = self.centred_component() + self.height = 0 + + ctx.save() + # Clip to the screen to be shown + ctx.rectangle(-120, -120, 240, 240).clip() + + # Re-centre so the origin is in the top left + # Use y_offset to move this down + ctx.translate(-120, -120 + self.y_offset) + + # Scale to 90% and centre + ctx.scale(self.scale_factor, self.scale_factor) + ctx.translate(12, 0) + + # Draw each item in turn + for item in self.items: + item.draw(ctx, focused=item == focused_child) + ctx.translate(0, item.height) + self.height += item.height + + ctx.restore() + + def centred_component(self): + cumulative_height = 0 + for item in self.items: + cumulative_height += item.height * self.scale_factor + print(f"Y: {self.y_offset}, cumulative: {cumulative_height}") + if round(cumulative_height) > round(120 - self.y_offset): + return item + return item + + async def button_event(self, event) -> bool: + focused = self.centred_component() + to_jump = min(focused.height * self.scale_factor, 60) + print(f"Y: {self.y_offset}, Jump: {to_jump}") + if BUTTON_TYPES["UP"] in event.button: + self.y_offset += to_jump + if self.y_offset > 120: + self.y_offset = 120 + return True + elif BUTTON_TYPES["DOWN"] in event.button: + self.y_offset -= to_jump + if self.y_offset < 120 - self.height: + self.y_offset = 120 - self.height + return True + else: + return await focused.button_event(event) + + +a = """ +import display, time +import app_components.layout + +text = app_components.layout.TextDisplay("Lorem ipsum " + "abcde "*30) +foo = app_components.layout.ButtonDisplay("foo") +bar = app_components.layout.DefinitionDisplay("Wifi", "emfcamp") +layout = app_components.layout.LinearLayout([text, foo, bar]) + +ctx=display.get_ctx() +app_components.clear_background(ctx) +ctx.rgb(1,1,1) +layout.draw(ctx) +display.end_frame(ctx) +""" + +""" +def scroll(): + for i in range(20): + ctx=display.get_ctx() + app_components.clear_background(ctx) + ctx.rgb(1,1,1) + layout.draw(ctx) + display.end_frame(ctx) + time.sleep_ms(100) + layout.y_offset -= 10 + +""" diff --git a/modules/app_components/tokens.py b/modules/app_components/tokens.py index 717f0c9..70ae173 100644 --- a/modules/app_components/tokens.py +++ b/modules/app_components/tokens.py @@ -26,13 +26,22 @@ "orange": (246, 127, 2), "pink": (245, 80, 137), "blue": (46, 173, 217), + "black": (0, 0, 0), + "white": (255, 255, 255), } colors = { name: (c[0] / 256.0, c[1] / 256.0, c[2] / 256.0) for (name, c) in colors.items() } -ui_colors = {"background": colors["dark_green"], "label": (232, 230, 227)} +ui_colors = { + "background": colors["dark_green"], + "label": colors["white"], + "button_background": colors["pale_green"], + "button_text": colors["black"], + "active_button_background": colors["yellow"], + "active_button_text": colors["black"], +} def clear_background(ctx): diff --git a/modules/app_components/utils.py b/modules/app_components/utils.py new file mode 100644 index 0000000..de229d5 --- /dev/null +++ b/modules/app_components/utils.py @@ -0,0 +1,21 @@ +def fill_line(ctx, text, width_for_line): + extra_text = "" + text_that_fits = text + text_width = ctx.text_width(text_that_fits) + while text_width > width_for_line: + character = text_that_fits[-1] + text_that_fits = text_that_fits[:-1] + extra_text = character + extra_text + text_width = ctx.text_width(text_that_fits) + return text_that_fits, extra_text + + +def wrap_text(ctx, text, width=None): + if width is None: + width = 240 + remaining_text = text + lines = [] + while remaining_text: + line, remaining_text = fill_line(ctx, remaining_text, width) + lines.append(line) + return lines diff --git a/modules/firmware_apps/settings_app.py b/modules/firmware_apps/settings_app.py new file mode 100644 index 0000000..7d4c814 --- /dev/null +++ b/modules/firmware_apps/settings_app.py @@ -0,0 +1,51 @@ +import settings +import app +from app_components import layout, tokens +from events.input import BUTTON_TYPES, ButtonDownEvent +from system.eventbus import eventbus +from system.scheduler.events import RequestForegroundPushEvent + + +def string_formatter(value): + if value is None: + return "Default" + else: + return str(value) + + +class SettingsApp(app.App): + def __init__(self): + self.layout = layout.LinearLayout(items=[]) + self.make_layout_children() + eventbus.on_async(ButtonDownEvent, self._button_handler, self) + eventbus.on(RequestForegroundPushEvent, self.make_layout_children, self) + + async def _button_handler(self, event): + layout_handled = await self.layout.button_event(event) + if not layout_handled: + if BUTTON_TYPES["CANCEL"] in event.button: + self.minimise() + + def make_layout_children(self, event=None): + self.layout.items = [] + for id, label, formatter in self.settings_options(): + value = settings.get(id) + self.layout.items.append(layout.DefinitionDisplay(label, formatter(value))) + + def settings_options(self): + return [ + ("name", "Name", string_formatter), + ("pattern", "LED Pattern", string_formatter), + ("wifi_tx_power", "WiFi TX power", string_formatter), + ("wifi_connection_timeout", "WiFi connection timeout", string_formatter), + ("wifi_ssid", "WiFi SSID", string_formatter), + ("wifi_password", "WiFi password", string_formatter), + ("wifi_wpa2ent_username", "WPA2 Enterprise Username", string_formatter), + ] + + def update(self, delta): + return True + + def draw(self, ctx): + tokens.clear_background(ctx) + self.layout.draw(ctx) diff --git a/modules/system/launcher/app.py b/modules/system/launcher/app.py index eee2f89..2afeb2f 100644 --- a/modules/system/launcher/app.py +++ b/modules/system/launcher/app.py @@ -104,6 +104,7 @@ def list_core_apps(self): # ("Magnetometer", "magnet_app", "Magnetometer"), ("Update", "system.ota.ota", "OtaUpdate"), ("Power Off", "firmware_apps.poweroff", "PowerOff"), + ("Settings", "firmware_apps.settings_app", "SettingsApp"), # ("Settings", "settings_app", "SettingsApp"), ] core_apps = []