Apply theme changes (dark mode, color) immediately after saving

Also:

- API: change "display" to mpos.ui.main_display
- API: change mpos.ui.th to mpos.ui.task_handler
This commit is contained in:
Thomas Farstrike
2025-11-15 16:25:10 +01:00
parent 6f86f57a70
commit 168f1ec374
14 changed files with 78 additions and 65 deletions
+12 -9
View File
@@ -1,17 +1,20 @@
0.3.4 (unreleased)
==================
- Apply theme changes (dark mode, color) immediately after saving
OSUpdate app: Major rework with improved reliability and user experience
- OSUpdate app: add WiFi monitoring - shows "Waiting for WiFi..." instead of error when no connection
- OSUpdate app: add automatic pause/resume on WiFi loss during downloads using HTTP Range headers
- OSUpdate app: add user-friendly error messages with specific guidance for each error type
- OSUpdate app: add "Check Again" button for easy retry after errors
- OSUpdate app: add state machine for better app state management
- OSUpdate app: add comprehensive test coverage (42 tests: 31 unit tests + 11 graphical tests)
- OSUpdate app: refactor code into testable components (NetworkMonitor, UpdateChecker, UpdateDownloader)
- OSUpdate app: improve download error recovery with progress preservation
- OSUpdate app: improve timeout handling (5-minute wait for WiFi with clear messaging)
- OSUpdate app: add WiFi monitoring - shows "Waiting for WiFi..." instead of error when no connection
- OSUpdate app: add automatic pause/resume on WiFi loss during downloads using HTTP Range headers
- OSUpdate app: add user-friendly error messages with specific guidance for each error type
- OSUpdate app: add "Check Again" button for easy retry after errors
- OSUpdate app: add state machine for better app state management
- OSUpdate app: add comprehensive test coverage (42 tests: 31 unit tests + 11 graphical tests)
- OSUpdate app: refactor code into testable components (NetworkMonitor, UpdateChecker, UpdateDownloader)
- OSUpdate app: improve download error recovery with progress preservation
- OSUpdate app: improve timeout handling (5-minute wait for WiFi with clear messaging)
- Tests: add test infrastructure with mock classes for network, HTTP, and partition operations
- Tests: add graphical test helper utilities for UI verification and screenshot capture
- API: change "display" to mpos.ui.main_display
- API: change mpos.ui.th to mpos.ui.task_handler
0.3.3
=====
+7 -7
View File
@@ -534,11 +534,11 @@ class MyAnimatedApp(Activity):
def onResume(self, screen):
# Register the frame callback
self.last_time = time.ticks_ms()
mpos.ui.th.add_event_cb(self.update_frame, 1)
mpos.ui.task_handler.add_event_cb(self.update_frame, 1)
def onPause(self, screen):
# Unregister when app goes to background
mpos.ui.th.remove_event_cb(self.update_frame)
mpos.ui.task_handler.remove_event_cb(self.update_frame)
def update_frame(self, a, b):
# Calculate delta time for framerate independence
@@ -670,7 +670,7 @@ def update_frame(self, a, b):
def start_animation(self):
self.spawn_timer = 0
self.spawn_interval = 0.15 # seconds between spawns
mpos.ui.th.add_event_cb(self.update_frame, 1)
mpos.ui.task_handler.add_event_cb(self.update_frame, 1)
def update_frame(self, a, b):
delta_time = time.ticks_diff(time.ticks_ms(), self.last_time) / 1000.0
@@ -803,7 +803,7 @@ def check_collision(self):
def start_animation(self):
self.animation_running = True
self.last_time = time.ticks_ms()
mpos.ui.th.add_event_cb(self.update_frame, 1)
mpos.ui.task_handler.add_event_cb(self.update_frame, 1)
# Optional: auto-stop after duration
lv.timer_create(self.stop_animation, 15000, None).set_repeat_count(1)
@@ -817,7 +817,7 @@ def update_frame(self, a, b):
# Stop when animation completes
if not self.animation_running and len(self.particles) == 0:
mpos.ui.th.remove_event_cb(self.update_frame)
mpos.ui.task_handler.remove_event_cb(self.update_frame)
print("Animation finished")
```
@@ -827,11 +827,11 @@ def onResume(self, screen):
# Only start if needed (e.g., game in progress)
if self.game_started and not self.game_over:
self.last_time = time.ticks_ms()
mpos.ui.th.add_event_cb(self.update_frame, 1)
mpos.ui.task_handler.add_event_cb(self.update_frame, 1)
def onPause(self, screen):
# Always stop when app goes to background
mpos.ui.th.remove_event_cb(self.update_frame)
mpos.ui.task_handler.remove_event_cb(self.update_frame)
```
### Performance Tips
@@ -45,10 +45,10 @@ class Confetti(Activity):
self.setContentView(self.screen)
def onResume(self, screen):
mpos.ui.th.add_event_cb(self.update_frame, 1)
mpos.ui.task_handler.add_event_cb(self.update_frame, 1)
def onPause(self, screen):
mpos.ui.th.remove_event_cb(self.update_frame)
mpos.ui.task_handler.remove_event_cb(self.update_frame)
def spawn_confetti(self):
"""Safely spawn a new confetti piece with unique img_idx"""
+5 -5
View File
@@ -59,7 +59,7 @@ _BUFFER_SIZE = const(28800)
fb1 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)
fb2 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)
display = st7789.ST7789(
mpos.ui.main_display = st7789.ST7789(
data_bus=display_bus,
frame_buffer1=fb1,
frame_buffer2=fb2,
@@ -71,9 +71,9 @@ display = st7789.ST7789(
color_byte_order=st7789.BYTE_ORDER_BGR,
rgb565_byte_swap=True,
)
display.init()
display.set_power(True)
display.set_backlight(100)
mpos.ui.main_display.init()
mpos.ui.main_display.set_power(True)
mpos.ui.main_display.set_backlight(100)
# Touch handling:
i2c_bus = i2c.I2C.Bus(host=I2C_BUS, scl=TP_SCL, sda=TP_SDA, freq=I2C_FREQ, use_locks=False)
@@ -81,7 +81,7 @@ touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=TP_ADDR, reg_bits=TP_REGBITS)
indev=cst816s.CST816S(touch_dev,startup_rotation=lv.DISPLAY_ROTATION._180) # button in top left, good
lv.init()
display.set_rotation(lv.DISPLAY_ROTATION._90) # must be done after initializing display and creating the touch drivers, to ensure proper handling
mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._90) # must be done after initializing display and creating the touch drivers, to ensure proper handling
# Battery voltage ADC measuring
import mpos.battery_voltage
+7 -7
View File
@@ -63,7 +63,7 @@ STATE_HIGH = 1
STATE_LOW = 0
# see ./lvgl_micropython/api_drivers/py_api_drivers/frozen/display/display_driver_framework.py
display = st7789.ST7789(
mpos.ui.main_display = st7789.ST7789(
data_bus=display_bus,
frame_buffer1=fb1,
frame_buffer2=fb2,
@@ -76,15 +76,15 @@ display = st7789.ST7789(
reset_state=STATE_LOW
)
display.init()
display.set_power(True)
display.set_backlight(100)
mpos.ui.main_display.init()
mpos.ui.main_display.set_power(True)
mpos.ui.main_display.set_backlight(100)
display.set_color_inversion(False)
mpos.ui.main_display.set_color_inversion(False)
lv.init()
display.set_rotation(lv.DISPLAY_ROTATION._270) # must be done after initializing display and creating the touch drivers, to ensure proper handling
display.set_params(0x36, bytearray([0x28]))
mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._270) # must be done after initializing display and creating the touch drivers, to ensure proper handling
mpos.ui.main_display.set_params(0x36, bytearray([0x28]))
# Button and joystick handling code:
from machine import ADC, Pin
+3 -3
View File
@@ -47,10 +47,10 @@ bus = lcd_bus.SDLBus(flags=0)
buf1 = bus.allocate_framebuffer(TFT_HOR_RES * TFT_VER_RES * 2, 0)
display = sdl_display.SDLDisplay(data_bus=bus,display_width=TFT_HOR_RES,display_height=TFT_VER_RES,frame_buffer1=buf1,color_space=lv.COLOR_FORMAT.RGB565)
# display.set_dpi(65) # doesn't seem to change the default 130...
display.init()
mpos.ui.main_display = sdl_display.SDLDisplay(data_bus=bus,display_width=TFT_HOR_RES,display_height=TFT_VER_RES,frame_buffer1=buf1,color_space=lv.COLOR_FORMAT.RGB565)
# display.set_dpi(65) # doesn't seem to change the default 130...
mpos.ui.main_display.init()
# main_display.set_dpi(65) # doesn't seem to change the default 130...
import sdl_pointer
mouse = sdl_pointer.SDLPointer()
@@ -327,4 +327,6 @@ class SettingActivity(Activity):
if changed_callback and old_value != new_value:
print(f"Setting {setting['key']} changed from {old_value} to {new_value}, calling changed_callback...")
changed_callback()
if setting["key"] == "theme_light_dark" or setting["key"] == "theme_primary_color":
mpos.ui.set_theme(self.prefs)
self.finish()
+1 -1
View File
@@ -19,7 +19,7 @@ class Activity:
def onResume(self, screen): # app goes to foreground
self._has_foreground = True
mpos.ui.th.add_event_cb(self.task_handler_callback, 1)
mpos.ui.task_handler.add_event_cb(self.task_handler_callback, 1)
def onPause(self, screen): # app goes to background
self._has_foreground = False
@@ -3,6 +3,7 @@ from .view import (
screen_stack, remove_and_stop_current_activity, remove_and_stop_all_activities
)
from .gesture_navigation import handle_back_swipe, handle_top_swipe
from .theme import set_theme
from .topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICATION_BAR_HEIGHT
from .focus import save_and_clear_current_focusgroup
from .display import (
@@ -17,6 +18,7 @@ from .util import shutdown, set_foreground_app, get_foreground_app
__all__ = [
"setContentView", "back_screen", "remove_and_stop_current_activity", "remove_and_stop_all_activities"
"handle_back_swipe", "handle_top_swipe",
"set_theme",
"open_bar", "close_bar", "open_drawer", "drawer_open", "NOTIFICATION_BAR_HEIGHT",
"save_and_clear_current_focusgroup",
"get_display_width", "get_display_height",
+6 -5
View File
@@ -12,17 +12,18 @@ def init_rootscreen():
_vertical_resolution = disp.get_vertical_resolution()
print(f"init_rootscreen set _vertical_resolution to {_vertical_resolution}")
# It seems this style
style = lv.style_t()
style.init()
style.set_bg_opa(lv.OPA.TRANSP)
#style.set_bg_opa(lv.OPA.TRANSP)
style.set_border_width(0)
style.set_outline_width(0)
style.set_shadow_width(0)
#style.set_outline_width(0)
#style.set_shadow_width(0)
style.set_pad_all(0)
style.set_radius(0)
screen.add_style(style, 0)
screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
screen.set_scroll_dir(lv.DIR.NONE)
#screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
#screen.set_scroll_dir(lv.DIR.NONE)
label = lv.label(screen)
label.set_text("Welcome to MicroPythonOS")
+20
View File
@@ -0,0 +1,20 @@
import lvgl as lv
import mpos.config
def set_theme(prefs):
# Load and set theme:
theme_light_dark = prefs.get_string("theme_light_dark", "light") # default to a light theme
theme_dark_bool = ( theme_light_dark == "dark" )
primary_color = lv.theme_get_color_primary(None)
color_string = prefs.get_string("theme_primary_color")
if color_string:
try:
color_string = color_string.replace("0x", "").replace("#", "").strip().lower()
color_int = int(color_string, 16)
print(f"Setting primary color: {color_int}")
primary_color = lv.color_hex(color_int)
except Exception as e:
print(f"Converting color setting '{color_string}' to lv_color_hex() got exception: {e}")
lv.theme_default_init(mpos.ui.main_display._disp_drv, primary_color, lv.color_hex(0xFBDC05), theme_dark_bool, lv.font_montserrat_12)
#mpos.ui.main_display.set_theme(theme) # not needed, default theme is applied immediately
+4 -4
View File
@@ -222,8 +222,8 @@ def create_drawer(display=None):
slider_label=lv.label(drawer)
prefs = mpos.config.SharedPreferences("com.micropythonos.settings")
brightness_int = prefs.get_int("display_brightness", 100)
if display:
display.set_backlight(brightness_int)
if mpos.ui.main_display:
mpos.ui.main_display.set_backlight(brightness_int)
slider_label.set_text(f"Brightness: {brightness_int}%")
slider_label.align(lv.ALIGN.TOP_MID,0,lv.pct(4))
slider=lv.slider(drawer)
@@ -234,8 +234,8 @@ def create_drawer(display=None):
def brightness_slider_changed(e):
brightness_int = slider.get_value()
slider_label.set_text(f"Brightness: {brightness_int}%")
if display:
display.set_backlight(brightness_int)
if mpos.ui.main_display:
mpos.ui.main_display.set_backlight(brightness_int)
def brightness_slider_released(e):
brightness_int = slider.get_value()
prefs = mpos.config.SharedPreferences("com.micropythonos.settings")
+6 -21
View File
@@ -16,26 +16,10 @@ from mpos.content.package_manager import PackageManager
prefs = mpos.config.SharedPreferences("com.micropythonos.settings")
# Load and set theme:
theme_light_dark = prefs.get_string("theme_light_dark", "light") # default to a light theme
theme_dark_bool = ( theme_light_dark == "dark" )
primary_color = lv.theme_get_color_primary(None)
color_string = prefs.get_string("theme_primary_color")
if color_string:
try:
color_string = color_string.replace("0x", "").replace("#", "").strip().lower()
color_int = int(color_string, 16)
print(f"Setting primary color: {color_int}")
primary_color = lv.color_hex(color_int)
except Exception as e:
print(f"Converting color setting '{color_string}' to lv_color_hex() got exception: {e}")
theme = lv.theme_default_init(display._disp_drv, primary_color, lv.color_hex(0xFBDC05), theme_dark_bool, lv.font_montserrat_12)
#display.set_theme(theme)
mpos.ui.set_theme(prefs)
init_rootscreen()
mpos.ui.topmenu.create_notification_bar()
mpos.ui.topmenu.create_drawer(display)
mpos.ui.topmenu.create_drawer(mpos.ui.display)
mpos.ui.handle_back_swipe()
mpos.ui.handle_top_swipe()
@@ -48,16 +32,17 @@ if focusgroup: # on esp32 this may not be set
# Can be passed to TaskHandler, currently unused:
def custom_exception_handler(e):
print(f"custom_exception_handler called: {e}")
mpos.ui.th.deinit()
mpos.ui.task_handler.deinit()
# otherwise it does focus_next and then crashes while doing lv.deinit()
focusgroup.remove_all_objs()
focusgroup.delete()
lv.deinit()
import sys
if sys.platform == "esp32":
mpos.ui.th = task_handler.TaskHandler(duration=5) # 1ms gives highest framerate on esp32-s3's but might have side effects?
mpos.ui.task_handler = task_handler.TaskHandler(duration=5) # 1ms gives highest framerate on esp32-s3's but might have side effects?
else:
mpos.ui.th = task_handler.TaskHandler(duration=5) # 5ms is recommended for MicroPython+LVGL on desktop (less results in lower framerate)
mpos.ui.task_handler = task_handler.TaskHandler(duration=5) # 5ms is recommended for MicroPython+LVGL on desktop (less results in lower framerate)
try:
import freezefs_mount_builtin
+1 -1
View File
@@ -24,7 +24,7 @@ class TestStartApp(unittest.TestCase):
init_rootscreen()
mpos.ui.topmenu.create_notification_bar()
mpos.ui.topmenu.create_drawer(display)
mpos.ui.th = task_handler.TaskHandler(duration=5) # 5ms is recommended for MicroPython+LVGL on desktop (less results in lower framerate)
mpos.ui.task_handler = task_handler.TaskHandler(duration=5) # 5ms is recommended for MicroPython+LVGL on desktop (less results in lower framerate)
def test_normal(self):