You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Add AppearanceManager
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
# Most of this time is actually spent reading and parsing manifests.
|
||||
import lvgl as lv
|
||||
import mpos.apps
|
||||
from mpos import NOTIFICATION_BAR_HEIGHT, PackageManager, Activity, DisplayMetrics
|
||||
from mpos import AppearanceManager, PackageManager, Activity, DisplayMetrics
|
||||
import time
|
||||
import uhashlib
|
||||
import ubinascii
|
||||
@@ -28,9 +28,9 @@ class Launcher(Activity):
|
||||
main_screen = lv.obj()
|
||||
main_screen.set_style_border_width(0, lv.PART.MAIN)
|
||||
main_screen.set_style_radius(0, 0)
|
||||
main_screen.set_pos(0, NOTIFICATION_BAR_HEIGHT)
|
||||
main_screen.set_pos(0, AppearanceManager.NOTIFICATION_BAR_HEIGHT)
|
||||
main_screen.set_style_pad_hor(DisplayMetrics.pct_of_width(2), 0)
|
||||
main_screen.set_style_pad_ver(NOTIFICATION_BAR_HEIGHT, 0)
|
||||
main_screen.set_style_pad_ver(AppearanceManager.NOTIFICATION_BAR_HEIGHT, 0)
|
||||
main_screen.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
|
||||
self.setContentView(main_screen)
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ from .ui.testing import (
|
||||
|
||||
# UI utility functions
|
||||
from .ui.display_metrics import DisplayMetrics
|
||||
from .ui.appearance_manager import AppearanceManager
|
||||
from .ui.event import get_event_name, print_event
|
||||
from .ui.view import setContentView, back_screen
|
||||
from .ui.theme import set_theme
|
||||
from .ui.topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICATION_BAR_HEIGHT
|
||||
from .ui.topmenu import open_bar, close_bar, open_drawer, drawer_open
|
||||
from .ui.focus import save_and_clear_current_focusgroup
|
||||
from .ui.gesture_navigation import handle_back_swipe, handle_top_swipe
|
||||
from .ui.util import shutdown, set_foreground_app, get_foreground_app
|
||||
@@ -69,12 +69,12 @@ __all__ = [
|
||||
"SettingActivity", "SettingsActivity", "CameraActivity",
|
||||
# UI components
|
||||
"MposKeyboard",
|
||||
# UI utility - DisplayMetrics
|
||||
# UI utility - DisplayMetrics and AppearanceManager
|
||||
"DisplayMetrics",
|
||||
"AppearanceManager",
|
||||
"get_event_name", "print_event",
|
||||
"setContentView", "back_screen",
|
||||
"set_theme",
|
||||
"open_bar", "close_bar", "open_drawer", "drawer_open", "NOTIFICATION_BAR_HEIGHT",
|
||||
"open_bar", "close_bar", "open_drawer", "drawer_open",
|
||||
"save_and_clear_current_focusgroup",
|
||||
"handle_back_swipe", "handle_top_swipe",
|
||||
"shutdown", "set_foreground_app", "get_foreground_app",
|
||||
|
||||
@@ -8,6 +8,7 @@ import mpos.ui
|
||||
from . import ui
|
||||
from .content.package_manager import PackageManager
|
||||
from mpos.ui.display import init_rootscreen
|
||||
from mpos.ui.appearance_manager import AppearanceManager
|
||||
import mpos.ui.topmenu
|
||||
|
||||
# Auto-detect and initialize hardware
|
||||
@@ -41,7 +42,7 @@ mpos.fs_driver.fs_register(fs_drv, 'M')
|
||||
|
||||
prefs = mpos.config.SharedPreferences("com.micropythonos.settings")
|
||||
|
||||
mpos.ui.set_theme(prefs)
|
||||
AppearanceManager.init(prefs)
|
||||
init_rootscreen()
|
||||
mpos.ui.topmenu.create_notification_bar()
|
||||
mpos.ui.topmenu.create_drawer(mpos.ui.display)
|
||||
|
||||
@@ -3,8 +3,8 @@ 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 .appearance_manager import AppearanceManager
|
||||
from .topmenu import open_bar, close_bar, open_drawer, drawer_open
|
||||
from .focus import save_and_clear_current_focusgroup
|
||||
from .display_metrics import DisplayMetrics
|
||||
from .event import get_event_name, print_event
|
||||
@@ -20,8 +20,8 @@ main_display = None
|
||||
__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",
|
||||
"AppearanceManager",
|
||||
"open_bar", "close_bar", "open_drawer", "drawer_open",
|
||||
"save_and_clear_current_focusgroup",
|
||||
"DisplayMetrics",
|
||||
"get_event_name", "print_event",
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
# lib/mpos/ui/appearance_manager.py
|
||||
"""
|
||||
AppearanceManager - Android-inspired appearance management singleton.
|
||||
|
||||
Manages all aspects of the app's visual appearance:
|
||||
- Light/dark mode (UI appearance)
|
||||
- Theme colors (primary, secondary, accent)
|
||||
- UI dimensions (notification bar height, etc.)
|
||||
- LVGL theme initialization
|
||||
- Keyboard styling workarounds
|
||||
|
||||
This is a singleton implemented using class methods and class variables.
|
||||
No instance creation is needed - all methods are class methods.
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
|
||||
# Check light/dark mode
|
||||
if AppearanceManager.is_light_mode():
|
||||
print("Light mode enabled")
|
||||
|
||||
# Get UI dimensions
|
||||
bar_height = AppearanceManager.get_notification_bar_height()
|
||||
|
||||
# Initialize appearance from preferences
|
||||
AppearanceManager.init(prefs)
|
||||
"""
|
||||
|
||||
import lvgl as lv
|
||||
|
||||
|
||||
class AppearanceManager:
|
||||
"""
|
||||
Android-inspired appearance management singleton.
|
||||
|
||||
Centralizes all UI appearance settings including theme colors, light/dark mode,
|
||||
and UI dimensions. Follows the singleton pattern using class methods and class
|
||||
variables, similar to Android's Configuration and Resources classes.
|
||||
|
||||
All methods are class methods - no instance creation needed.
|
||||
"""
|
||||
|
||||
# ========== UI Dimensions ==========
|
||||
# These are constants that define the layout of the UI
|
||||
NOTIFICATION_BAR_HEIGHT = 24 # Height of the notification bar in pixels
|
||||
|
||||
# ========== Private Class Variables ==========
|
||||
# State variables shared across all "instances" (there is only one logical instance)
|
||||
_is_light_mode = True
|
||||
_primary_color = None
|
||||
_accent_color = None
|
||||
_keyboard_button_fix_style = None
|
||||
|
||||
# ========== Initialization ==========
|
||||
|
||||
@classmethod
|
||||
def init(cls, prefs):
|
||||
"""
|
||||
Initialize AppearanceManager from preferences.
|
||||
|
||||
Called during system startup to load theme settings from SharedPreferences
|
||||
and initialize the LVGL theme. This should be called once during boot.
|
||||
|
||||
Args:
|
||||
prefs: SharedPreferences object containing theme settings
|
||||
- "theme_light_dark": "light" or "dark" (default: "light")
|
||||
- "theme_primary_color": hex color string like "0xFF5722" or "#FF5722"
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
import mpos.config
|
||||
|
||||
prefs = mpos.config.get_shared_preferences()
|
||||
AppearanceManager.init(prefs)
|
||||
"""
|
||||
# Load light/dark mode preference
|
||||
theme_light_dark = prefs.get_string("theme_light_dark", "light")
|
||||
theme_dark_bool = (theme_light_dark == "dark")
|
||||
cls._is_light_mode = not theme_dark_bool
|
||||
|
||||
# Load primary color preference
|
||||
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"[AppearanceManager] Setting primary color: {color_int}")
|
||||
primary_color = lv.color_hex(color_int)
|
||||
cls._primary_color = primary_color
|
||||
except Exception as e:
|
||||
print(f"[AppearanceManager] Converting color setting '{color_string}' failed: {e}")
|
||||
|
||||
# Initialize LVGL theme with loaded settings
|
||||
# Get the display driver from the active screen
|
||||
screen = lv.screen_active()
|
||||
disp = screen.get_display()
|
||||
lv.theme_default_init(
|
||||
disp,
|
||||
primary_color,
|
||||
lv.color_hex(0xFBDC05), # Accent color (yellow)
|
||||
theme_dark_bool,
|
||||
lv.font_montserrat_12
|
||||
)
|
||||
|
||||
# Reset keyboard button fix style so it's recreated with new theme colors
|
||||
cls._keyboard_button_fix_style = None
|
||||
|
||||
print(f"[AppearanceManager] Initialized: light_mode={cls._is_light_mode}, primary_color={primary_color}")
|
||||
|
||||
# ========== Light/Dark Mode ==========
|
||||
|
||||
@classmethod
|
||||
def is_light_mode(cls):
|
||||
"""
|
||||
Check if light mode is currently enabled.
|
||||
|
||||
Returns:
|
||||
bool: True if light mode is enabled, False if dark mode is enabled
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
|
||||
if AppearanceManager.is_light_mode():
|
||||
print("Using light theme")
|
||||
else:
|
||||
print("Using dark theme")
|
||||
"""
|
||||
return cls._is_light_mode
|
||||
|
||||
@classmethod
|
||||
def set_light_mode(cls, is_light, prefs=None):
|
||||
"""
|
||||
Set light/dark mode and update the theme.
|
||||
|
||||
Args:
|
||||
is_light (bool): True for light mode, False for dark mode
|
||||
prefs (SharedPreferences, optional): If provided, saves the setting
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
|
||||
AppearanceManager.set_light_mode(False) # Switch to dark mode
|
||||
"""
|
||||
cls._is_light_mode = is_light
|
||||
|
||||
# Save to preferences if provided
|
||||
if prefs:
|
||||
theme_str = "light" if is_light else "dark"
|
||||
prefs.set_string("theme_light_dark", theme_str)
|
||||
|
||||
# Reinitialize LVGL theme with new mode
|
||||
if prefs:
|
||||
cls.init(prefs)
|
||||
|
||||
print(f"[AppearanceManager] Light mode set to: {is_light}")
|
||||
|
||||
# ========== Theme Colors ==========
|
||||
|
||||
@classmethod
|
||||
def get_primary_color(cls):
|
||||
"""
|
||||
Get the primary theme color.
|
||||
|
||||
Returns:
|
||||
lv.color_t: The primary color, or None if not set
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
|
||||
color = AppearanceManager.get_primary_color()
|
||||
if color:
|
||||
button.set_style_bg_color(color, 0)
|
||||
"""
|
||||
return cls._primary_color
|
||||
|
||||
@classmethod
|
||||
def set_primary_color(cls, color, prefs=None):
|
||||
"""
|
||||
Set the primary theme color.
|
||||
|
||||
Args:
|
||||
color (lv.color_t or int): The new primary color
|
||||
prefs (SharedPreferences, optional): If provided, saves the setting
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
import lvgl as lv
|
||||
|
||||
AppearanceManager.set_primary_color(lv.color_hex(0xFF5722))
|
||||
"""
|
||||
cls._primary_color = color
|
||||
|
||||
# Save to preferences if provided
|
||||
if prefs and isinstance(color, int):
|
||||
prefs.set_string("theme_primary_color", f"0x{color:06X}")
|
||||
|
||||
print(f"[AppearanceManager] Primary color set to: {color}")
|
||||
|
||||
# ========== UI Dimensions ==========
|
||||
|
||||
@classmethod
|
||||
def get_notification_bar_height(cls):
|
||||
"""
|
||||
Get the height of the notification bar.
|
||||
|
||||
The notification bar is the top bar that displays system information
|
||||
(time, battery, signal, etc.). This method returns its height in pixels.
|
||||
|
||||
Returns:
|
||||
int: Height of the notification bar in pixels (default: 24)
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
|
||||
bar_height = AppearanceManager.get_notification_bar_height()
|
||||
content_y = bar_height # Position content below the bar
|
||||
"""
|
||||
return cls.NOTIFICATION_BAR_HEIGHT
|
||||
|
||||
# ========== Keyboard Styling Workarounds ==========
|
||||
|
||||
@classmethod
|
||||
def get_keyboard_button_fix_style(cls):
|
||||
"""
|
||||
Get the keyboard button fix style for light mode.
|
||||
|
||||
The LVGL default theme applies bg_color_white to keyboard buttons,
|
||||
which makes them white-on-white (invisible) in light mode.
|
||||
This method returns a custom style to override that.
|
||||
|
||||
Returns:
|
||||
lv.style_t: Style to apply to keyboard buttons, or None if not needed
|
||||
|
||||
Note:
|
||||
This is a workaround for an LVGL/MicroPython issue. It only applies
|
||||
in light mode. In dark mode, the default LVGL styling is fine.
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
|
||||
style = AppearanceManager.get_keyboard_button_fix_style()
|
||||
if style:
|
||||
keyboard.add_style(style, lv.PART.ITEMS)
|
||||
"""
|
||||
# Only return style in light mode
|
||||
if not cls._is_light_mode:
|
||||
return None
|
||||
|
||||
# Create style if it doesn't exist
|
||||
if cls._keyboard_button_fix_style is None:
|
||||
cls._keyboard_button_fix_style = lv.style_t()
|
||||
cls._keyboard_button_fix_style.init()
|
||||
|
||||
# Set button background to light gray (matches LVGL's intended design)
|
||||
# This provides contrast against white background
|
||||
# Using palette_lighten gives us the same gray as used in the theme
|
||||
gray_color = lv.palette_lighten(lv.PALETTE.GREY, 2)
|
||||
cls._keyboard_button_fix_style.set_bg_color(gray_color)
|
||||
cls._keyboard_button_fix_style.set_bg_opa(lv.OPA.COVER)
|
||||
|
||||
return cls._keyboard_button_fix_style
|
||||
|
||||
@classmethod
|
||||
def apply_keyboard_fix(cls, keyboard):
|
||||
"""
|
||||
Apply keyboard button visibility fix to a keyboard instance.
|
||||
|
||||
Call this function after creating a keyboard to ensure buttons
|
||||
are visible in light mode.
|
||||
|
||||
Args:
|
||||
keyboard: The lv.keyboard instance to fix
|
||||
|
||||
Example:
|
||||
from mpos import AppearanceManager
|
||||
import lvgl as lv
|
||||
|
||||
keyboard = lv.keyboard(screen)
|
||||
AppearanceManager.apply_keyboard_fix(keyboard)
|
||||
"""
|
||||
style = cls.get_keyboard_button_fix_style()
|
||||
if style:
|
||||
keyboard.add_style(style, lv.PART.ITEMS)
|
||||
print(f"[AppearanceManager] Applied keyboard button fix for light mode")
|
||||
@@ -4,6 +4,7 @@ from .widget_animator import WidgetAnimator
|
||||
from .view import back_screen
|
||||
from mpos.ui import topmenu as topmenu
|
||||
from .display import DisplayMetrics
|
||||
from .appearance_manager import AppearanceManager
|
||||
|
||||
downbutton = None
|
||||
backbutton = None
|
||||
@@ -106,10 +107,10 @@ def _top_swipe_cb(event):
|
||||
def handle_back_swipe():
|
||||
global backbutton
|
||||
rect = lv.obj(lv.layer_top())
|
||||
rect.set_size(topmenu.NOTIFICATION_BAR_HEIGHT, lv.layer_top().get_height()-topmenu.NOTIFICATION_BAR_HEIGHT) # narrow because it overlaps buttons
|
||||
rect.set_size(AppearanceManager.NOTIFICATION_BAR_HEIGHT, lv.layer_top().get_height()-AppearanceManager.NOTIFICATION_BAR_HEIGHT) # narrow because it overlaps buttons
|
||||
rect.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
rect.set_scroll_dir(lv.DIR.NONE)
|
||||
rect.set_pos(0, topmenu.NOTIFICATION_BAR_HEIGHT)
|
||||
rect.set_pos(0, AppearanceManager.NOTIFICATION_BAR_HEIGHT)
|
||||
style = lv.style_t()
|
||||
style.init()
|
||||
style.set_bg_opa(lv.OPA.TRANSP)
|
||||
@@ -137,7 +138,7 @@ def handle_back_swipe():
|
||||
def handle_top_swipe():
|
||||
global downbutton
|
||||
rect = lv.obj(lv.layer_top())
|
||||
rect.set_size(lv.pct(100), topmenu.NOTIFICATION_BAR_HEIGHT)
|
||||
rect.set_size(lv.pct(100), AppearanceManager.NOTIFICATION_BAR_HEIGHT)
|
||||
rect.set_pos(0, 0)
|
||||
rect.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
style = lv.style_t()
|
||||
|
||||
@@ -3,12 +3,11 @@ import lvgl as lv
|
||||
import mpos.time
|
||||
import mpos.battery_voltage
|
||||
from .display_metrics import DisplayMetrics
|
||||
from .appearance_manager import AppearanceManager
|
||||
from .util import (get_foreground_app)
|
||||
from . import focus_direction
|
||||
from .widget_animator import WidgetAnimator
|
||||
|
||||
NOTIFICATION_BAR_HEIGHT=24
|
||||
|
||||
CLOCK_UPDATE_INTERVAL = 1000 # 10 or even 1 ms doesn't seem to change the framerate but 100ms is enough
|
||||
WIFI_ICON_UPDATE_INTERVAL = 1500
|
||||
BATTERY_ICON_UPDATE_INTERVAL = 15000 # not too often, but not too short, otherwise it takes a while to appear
|
||||
@@ -20,7 +19,7 @@ DRAWER_ANIM_DURATION=300
|
||||
|
||||
hide_bar_animation = None
|
||||
show_bar_animation = None
|
||||
show_bar_animation_start_value = -NOTIFICATION_BAR_HEIGHT
|
||||
show_bar_animation_start_value = -AppearanceManager.NOTIFICATION_BAR_HEIGHT
|
||||
show_bar_animation_end_value = 0
|
||||
hide_bar_animation_start_value = show_bar_animation_end_value
|
||||
hide_bar_animation_end_value = show_bar_animation_start_value
|
||||
@@ -80,7 +79,7 @@ def create_notification_bar():
|
||||
global notification_bar
|
||||
# Create notification bar
|
||||
notification_bar = lv.obj(lv.layer_top())
|
||||
notification_bar.set_size(lv.pct(100), NOTIFICATION_BAR_HEIGHT)
|
||||
notification_bar.set_size(lv.pct(100), AppearanceManager.NOTIFICATION_BAR_HEIGHT)
|
||||
notification_bar.set_pos(0, show_bar_animation_start_value)
|
||||
notification_bar.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
notification_bar.set_scroll_dir(lv.DIR.NONE)
|
||||
@@ -203,7 +202,7 @@ def create_notification_bar():
|
||||
hide_bar_animation = lv.anim_t()
|
||||
hide_bar_animation.init()
|
||||
hide_bar_animation.set_var(notification_bar)
|
||||
hide_bar_animation.set_values(0, -NOTIFICATION_BAR_HEIGHT)
|
||||
hide_bar_animation.set_values(0, -AppearanceManager.NOTIFICATION_BAR_HEIGHT)
|
||||
hide_bar_animation.set_duration(2000)
|
||||
hide_bar_animation.set_custom_exec_cb(lambda not_used, value : notification_bar.set_y(value))
|
||||
|
||||
@@ -222,7 +221,7 @@ def create_drawer(display=None):
|
||||
global drawer
|
||||
drawer=lv.obj(lv.layer_top())
|
||||
drawer.set_size(lv.pct(100),lv.pct(90))
|
||||
drawer.set_pos(0,NOTIFICATION_BAR_HEIGHT)
|
||||
drawer.set_pos(0,AppearanceManager.NOTIFICATION_BAR_HEIGHT)
|
||||
drawer.set_scroll_dir(lv.DIR.VER)
|
||||
drawer.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
drawer.set_style_pad_all(15, 0)
|
||||
@@ -381,7 +380,7 @@ def drawer_scroll_callback(event):
|
||||
elif event_code == lv.EVENT.SCROLL and scroll_start_y != None:
|
||||
diff = y - scroll_start_y
|
||||
#print(f"scroll distance: {diff}")
|
||||
if diff < -NOTIFICATION_BAR_HEIGHT:
|
||||
if diff < -AppearanceManager.NOTIFICATION_BAR_HEIGHT:
|
||||
close_drawer()
|
||||
elif event_code == lv.EVENT.SCROLL_END:
|
||||
scroll_start_y = None
|
||||
|
||||
Reference in New Issue
Block a user