From 168f1ec3748074df660b49363cb617f04cc2ab5b Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 15 Nov 2025 16:25:10 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 21 ++++++++------- CLAUDE.md | 14 +++++----- .../assets/confetti.py | 4 +-- internal_filesystem/boot.py | 10 +++---- internal_filesystem/boot_fri3d-2024.py | 14 +++++----- internal_filesystem/boot_unix.py | 6 ++--- .../assets/settings.py | 2 ++ internal_filesystem/lib/mpos/app/activity.py | 2 +- internal_filesystem/lib/mpos/ui/__init__.py | 2 ++ internal_filesystem/lib/mpos/ui/display.py | 11 ++++---- internal_filesystem/lib/mpos/ui/theme.py | 20 ++++++++++++++ internal_filesystem/lib/mpos/ui/topmenu.py | 8 +++--- internal_filesystem/main.py | 27 +++++-------------- tests/test_start_app.py | 2 +- 14 files changed, 78 insertions(+), 65 deletions(-) create mode 100644 internal_filesystem/lib/mpos/ui/theme.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 47fe4a9c..9a4e6d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ===== diff --git a/CLAUDE.md b/CLAUDE.md index a50e6c49..e62c3be4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py b/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py index 47f45360..5ec95d77 100644 --- a/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py +++ b/internal_filesystem/apps/com.micropythonos.confetti/assets/confetti.py @@ -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""" diff --git a/internal_filesystem/boot.py b/internal_filesystem/boot.py index 08351068..e594c80e 100644 --- a/internal_filesystem/boot.py +++ b/internal_filesystem/boot.py @@ -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 diff --git a/internal_filesystem/boot_fri3d-2024.py b/internal_filesystem/boot_fri3d-2024.py index f7602200..0aabb62c 100644 --- a/internal_filesystem/boot_fri3d-2024.py +++ b/internal_filesystem/boot_fri3d-2024.py @@ -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 diff --git a/internal_filesystem/boot_unix.py b/internal_filesystem/boot_unix.py index 9c7ca501..2ea8e0aa 100644 --- a/internal_filesystem/boot_unix.py +++ b/internal_filesystem/boot_unix.py @@ -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() diff --git a/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py b/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py index 0bb49ddf..16d94cab 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py @@ -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() diff --git a/internal_filesystem/lib/mpos/app/activity.py b/internal_filesystem/lib/mpos/app/activity.py index a46400f0..93d93b8e 100644 --- a/internal_filesystem/lib/mpos/app/activity.py +++ b/internal_filesystem/lib/mpos/app/activity.py @@ -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 diff --git a/internal_filesystem/lib/mpos/ui/__init__.py b/internal_filesystem/lib/mpos/ui/__init__.py index 05953006..0a7ce711 100644 --- a/internal_filesystem/lib/mpos/ui/__init__.py +++ b/internal_filesystem/lib/mpos/ui/__init__.py @@ -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", diff --git a/internal_filesystem/lib/mpos/ui/display.py b/internal_filesystem/lib/mpos/ui/display.py index e148dd9b..edda770a 100644 --- a/internal_filesystem/lib/mpos/ui/display.py +++ b/internal_filesystem/lib/mpos/ui/display.py @@ -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") diff --git a/internal_filesystem/lib/mpos/ui/theme.py b/internal_filesystem/lib/mpos/ui/theme.py new file mode 100644 index 00000000..0e80c738 --- /dev/null +++ b/internal_filesystem/lib/mpos/ui/theme.py @@ -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 diff --git a/internal_filesystem/lib/mpos/ui/topmenu.py b/internal_filesystem/lib/mpos/ui/topmenu.py index 4ad5e196..7d078d10 100644 --- a/internal_filesystem/lib/mpos/ui/topmenu.py +++ b/internal_filesystem/lib/mpos/ui/topmenu.py @@ -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") diff --git a/internal_filesystem/main.py b/internal_filesystem/main.py index f5990741..c5851ea0 100644 --- a/internal_filesystem/main.py +++ b/internal_filesystem/main.py @@ -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 diff --git a/tests/test_start_app.py b/tests/test_start_app.py index 975639ef..2a876d0a 100644 --- a/tests/test_start_app.py +++ b/tests/test_start_app.py @@ -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):