Skip to content

Commit 5b5ac9c

Browse files
Add AppearanceManager
1 parent 9bbab6a commit 5b5ac9c

File tree

7 files changed

+309
-23
lines changed

7 files changed

+309
-23
lines changed

internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# Most of this time is actually spent reading and parsing manifests.
1111
import lvgl as lv
1212
import mpos.apps
13-
from mpos import NOTIFICATION_BAR_HEIGHT, PackageManager, Activity, DisplayMetrics
13+
from mpos import AppearanceManager, PackageManager, Activity, DisplayMetrics
1414
import time
1515
import uhashlib
1616
import ubinascii
@@ -28,9 +28,9 @@ def onCreate(self):
2828
main_screen = lv.obj()
2929
main_screen.set_style_border_width(0, lv.PART.MAIN)
3030
main_screen.set_style_radius(0, 0)
31-
main_screen.set_pos(0, NOTIFICATION_BAR_HEIGHT)
31+
main_screen.set_pos(0, AppearanceManager.NOTIFICATION_BAR_HEIGHT)
3232
main_screen.set_style_pad_hor(DisplayMetrics.pct_of_width(2), 0)
33-
main_screen.set_style_pad_ver(NOTIFICATION_BAR_HEIGHT, 0)
33+
main_screen.set_style_pad_ver(AppearanceManager.NOTIFICATION_BAR_HEIGHT, 0)
3434
main_screen.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
3535
self.setContentView(main_screen)
3636

internal_filesystem/lib/mpos/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232

3333
# UI utility functions
3434
from .ui.display_metrics import DisplayMetrics
35+
from .ui.appearance_manager import AppearanceManager
3536
from .ui.event import get_event_name, print_event
3637
from .ui.view import setContentView, back_screen
37-
from .ui.theme import set_theme
38-
from .ui.topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICATION_BAR_HEIGHT
38+
from .ui.topmenu import open_bar, close_bar, open_drawer, drawer_open
3939
from .ui.focus import save_and_clear_current_focusgroup
4040
from .ui.gesture_navigation import handle_back_swipe, handle_top_swipe
4141
from .ui.util import shutdown, set_foreground_app, get_foreground_app
@@ -69,12 +69,12 @@
6969
"SettingActivity", "SettingsActivity", "CameraActivity",
7070
# UI components
7171
"MposKeyboard",
72-
# UI utility - DisplayMetrics
72+
# UI utility - DisplayMetrics and AppearanceManager
7373
"DisplayMetrics",
74+
"AppearanceManager",
7475
"get_event_name", "print_event",
7576
"setContentView", "back_screen",
76-
"set_theme",
77-
"open_bar", "close_bar", "open_drawer", "drawer_open", "NOTIFICATION_BAR_HEIGHT",
77+
"open_bar", "close_bar", "open_drawer", "drawer_open",
7878
"save_and_clear_current_focusgroup",
7979
"handle_back_swipe", "handle_top_swipe",
8080
"shutdown", "set_foreground_app", "get_foreground_app",

internal_filesystem/lib/mpos/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from . import ui
99
from .content.package_manager import PackageManager
1010
from mpos.ui.display import init_rootscreen
11+
from mpos.ui.appearance_manager import AppearanceManager
1112
import mpos.ui.topmenu
1213

1314
# Auto-detect and initialize hardware
@@ -41,7 +42,7 @@
4142

4243
prefs = mpos.config.SharedPreferences("com.micropythonos.settings")
4344

44-
mpos.ui.set_theme(prefs)
45+
AppearanceManager.init(prefs)
4546
init_rootscreen()
4647
mpos.ui.topmenu.create_notification_bar()
4748
mpos.ui.topmenu.create_drawer(mpos.ui.display)

internal_filesystem/lib/mpos/ui/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
screen_stack, remove_and_stop_current_activity, remove_and_stop_all_activities
44
)
55
from .gesture_navigation import handle_back_swipe, handle_top_swipe
6-
from .theme import set_theme
7-
from .topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICATION_BAR_HEIGHT
6+
from .appearance_manager import AppearanceManager
7+
from .topmenu import open_bar, close_bar, open_drawer, drawer_open
88
from .focus import save_and_clear_current_focusgroup
99
from .display_metrics import DisplayMetrics
1010
from .event import get_event_name, print_event
@@ -20,8 +20,8 @@
2020
__all__ = [
2121
"setContentView", "back_screen", "remove_and_stop_current_activity", "remove_and_stop_all_activities",
2222
"handle_back_swipe", "handle_top_swipe",
23-
"set_theme",
24-
"open_bar", "close_bar", "open_drawer", "drawer_open", "NOTIFICATION_BAR_HEIGHT",
23+
"AppearanceManager",
24+
"open_bar", "close_bar", "open_drawer", "drawer_open",
2525
"save_and_clear_current_focusgroup",
2626
"DisplayMetrics",
2727
"get_event_name", "print_event",
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# lib/mpos/ui/appearance_manager.py
2+
"""
3+
AppearanceManager - Android-inspired appearance management singleton.
4+
5+
Manages all aspects of the app's visual appearance:
6+
- Light/dark mode (UI appearance)
7+
- Theme colors (primary, secondary, accent)
8+
- UI dimensions (notification bar height, etc.)
9+
- LVGL theme initialization
10+
- Keyboard styling workarounds
11+
12+
This is a singleton implemented using class methods and class variables.
13+
No instance creation is needed - all methods are class methods.
14+
15+
Example:
16+
from mpos import AppearanceManager
17+
18+
# Check light/dark mode
19+
if AppearanceManager.is_light_mode():
20+
print("Light mode enabled")
21+
22+
# Get UI dimensions
23+
bar_height = AppearanceManager.get_notification_bar_height()
24+
25+
# Initialize appearance from preferences
26+
AppearanceManager.init(prefs)
27+
"""
28+
29+
import lvgl as lv
30+
31+
32+
class AppearanceManager:
33+
"""
34+
Android-inspired appearance management singleton.
35+
36+
Centralizes all UI appearance settings including theme colors, light/dark mode,
37+
and UI dimensions. Follows the singleton pattern using class methods and class
38+
variables, similar to Android's Configuration and Resources classes.
39+
40+
All methods are class methods - no instance creation needed.
41+
"""
42+
43+
# ========== UI Dimensions ==========
44+
# These are constants that define the layout of the UI
45+
NOTIFICATION_BAR_HEIGHT = 24 # Height of the notification bar in pixels
46+
47+
# ========== Private Class Variables ==========
48+
# State variables shared across all "instances" (there is only one logical instance)
49+
_is_light_mode = True
50+
_primary_color = None
51+
_accent_color = None
52+
_keyboard_button_fix_style = None
53+
54+
# ========== Initialization ==========
55+
56+
@classmethod
57+
def init(cls, prefs):
58+
"""
59+
Initialize AppearanceManager from preferences.
60+
61+
Called during system startup to load theme settings from SharedPreferences
62+
and initialize the LVGL theme. This should be called once during boot.
63+
64+
Args:
65+
prefs: SharedPreferences object containing theme settings
66+
- "theme_light_dark": "light" or "dark" (default: "light")
67+
- "theme_primary_color": hex color string like "0xFF5722" or "#FF5722"
68+
69+
Example:
70+
from mpos import AppearanceManager
71+
import mpos.config
72+
73+
prefs = mpos.config.get_shared_preferences()
74+
AppearanceManager.init(prefs)
75+
"""
76+
# Load light/dark mode preference
77+
theme_light_dark = prefs.get_string("theme_light_dark", "light")
78+
theme_dark_bool = (theme_light_dark == "dark")
79+
cls._is_light_mode = not theme_dark_bool
80+
81+
# Load primary color preference
82+
primary_color = lv.theme_get_color_primary(None)
83+
color_string = prefs.get_string("theme_primary_color")
84+
if color_string:
85+
try:
86+
color_string = color_string.replace("0x", "").replace("#", "").strip().lower()
87+
color_int = int(color_string, 16)
88+
print(f"[AppearanceManager] Setting primary color: {color_int}")
89+
primary_color = lv.color_hex(color_int)
90+
cls._primary_color = primary_color
91+
except Exception as e:
92+
print(f"[AppearanceManager] Converting color setting '{color_string}' failed: {e}")
93+
94+
# Initialize LVGL theme with loaded settings
95+
# Get the display driver from the active screen
96+
screen = lv.screen_active()
97+
disp = screen.get_display()
98+
lv.theme_default_init(
99+
disp,
100+
primary_color,
101+
lv.color_hex(0xFBDC05), # Accent color (yellow)
102+
theme_dark_bool,
103+
lv.font_montserrat_12
104+
)
105+
106+
# Reset keyboard button fix style so it's recreated with new theme colors
107+
cls._keyboard_button_fix_style = None
108+
109+
print(f"[AppearanceManager] Initialized: light_mode={cls._is_light_mode}, primary_color={primary_color}")
110+
111+
# ========== Light/Dark Mode ==========
112+
113+
@classmethod
114+
def is_light_mode(cls):
115+
"""
116+
Check if light mode is currently enabled.
117+
118+
Returns:
119+
bool: True if light mode is enabled, False if dark mode is enabled
120+
121+
Example:
122+
from mpos import AppearanceManager
123+
124+
if AppearanceManager.is_light_mode():
125+
print("Using light theme")
126+
else:
127+
print("Using dark theme")
128+
"""
129+
return cls._is_light_mode
130+
131+
@classmethod
132+
def set_light_mode(cls, is_light, prefs=None):
133+
"""
134+
Set light/dark mode and update the theme.
135+
136+
Args:
137+
is_light (bool): True for light mode, False for dark mode
138+
prefs (SharedPreferences, optional): If provided, saves the setting
139+
140+
Example:
141+
from mpos import AppearanceManager
142+
143+
AppearanceManager.set_light_mode(False) # Switch to dark mode
144+
"""
145+
cls._is_light_mode = is_light
146+
147+
# Save to preferences if provided
148+
if prefs:
149+
theme_str = "light" if is_light else "dark"
150+
prefs.set_string("theme_light_dark", theme_str)
151+
152+
# Reinitialize LVGL theme with new mode
153+
if prefs:
154+
cls.init(prefs)
155+
156+
print(f"[AppearanceManager] Light mode set to: {is_light}")
157+
158+
# ========== Theme Colors ==========
159+
160+
@classmethod
161+
def get_primary_color(cls):
162+
"""
163+
Get the primary theme color.
164+
165+
Returns:
166+
lv.color_t: The primary color, or None if not set
167+
168+
Example:
169+
from mpos import AppearanceManager
170+
171+
color = AppearanceManager.get_primary_color()
172+
if color:
173+
button.set_style_bg_color(color, 0)
174+
"""
175+
return cls._primary_color
176+
177+
@classmethod
178+
def set_primary_color(cls, color, prefs=None):
179+
"""
180+
Set the primary theme color.
181+
182+
Args:
183+
color (lv.color_t or int): The new primary color
184+
prefs (SharedPreferences, optional): If provided, saves the setting
185+
186+
Example:
187+
from mpos import AppearanceManager
188+
import lvgl as lv
189+
190+
AppearanceManager.set_primary_color(lv.color_hex(0xFF5722))
191+
"""
192+
cls._primary_color = color
193+
194+
# Save to preferences if provided
195+
if prefs and isinstance(color, int):
196+
prefs.set_string("theme_primary_color", f"0x{color:06X}")
197+
198+
print(f"[AppearanceManager] Primary color set to: {color}")
199+
200+
# ========== UI Dimensions ==========
201+
202+
@classmethod
203+
def get_notification_bar_height(cls):
204+
"""
205+
Get the height of the notification bar.
206+
207+
The notification bar is the top bar that displays system information
208+
(time, battery, signal, etc.). This method returns its height in pixels.
209+
210+
Returns:
211+
int: Height of the notification bar in pixels (default: 24)
212+
213+
Example:
214+
from mpos import AppearanceManager
215+
216+
bar_height = AppearanceManager.get_notification_bar_height()
217+
content_y = bar_height # Position content below the bar
218+
"""
219+
return cls.NOTIFICATION_BAR_HEIGHT
220+
221+
# ========== Keyboard Styling Workarounds ==========
222+
223+
@classmethod
224+
def get_keyboard_button_fix_style(cls):
225+
"""
226+
Get the keyboard button fix style for light mode.
227+
228+
The LVGL default theme applies bg_color_white to keyboard buttons,
229+
which makes them white-on-white (invisible) in light mode.
230+
This method returns a custom style to override that.
231+
232+
Returns:
233+
lv.style_t: Style to apply to keyboard buttons, or None if not needed
234+
235+
Note:
236+
This is a workaround for an LVGL/MicroPython issue. It only applies
237+
in light mode. In dark mode, the default LVGL styling is fine.
238+
239+
Example:
240+
from mpos import AppearanceManager
241+
242+
style = AppearanceManager.get_keyboard_button_fix_style()
243+
if style:
244+
keyboard.add_style(style, lv.PART.ITEMS)
245+
"""
246+
# Only return style in light mode
247+
if not cls._is_light_mode:
248+
return None
249+
250+
# Create style if it doesn't exist
251+
if cls._keyboard_button_fix_style is None:
252+
cls._keyboard_button_fix_style = lv.style_t()
253+
cls._keyboard_button_fix_style.init()
254+
255+
# Set button background to light gray (matches LVGL's intended design)
256+
# This provides contrast against white background
257+
# Using palette_lighten gives us the same gray as used in the theme
258+
gray_color = lv.palette_lighten(lv.PALETTE.GREY, 2)
259+
cls._keyboard_button_fix_style.set_bg_color(gray_color)
260+
cls._keyboard_button_fix_style.set_bg_opa(lv.OPA.COVER)
261+
262+
return cls._keyboard_button_fix_style
263+
264+
@classmethod
265+
def apply_keyboard_fix(cls, keyboard):
266+
"""
267+
Apply keyboard button visibility fix to a keyboard instance.
268+
269+
Call this function after creating a keyboard to ensure buttons
270+
are visible in light mode.
271+
272+
Args:
273+
keyboard: The lv.keyboard instance to fix
274+
275+
Example:
276+
from mpos import AppearanceManager
277+
import lvgl as lv
278+
279+
keyboard = lv.keyboard(screen)
280+
AppearanceManager.apply_keyboard_fix(keyboard)
281+
"""
282+
style = cls.get_keyboard_button_fix_style()
283+
if style:
284+
keyboard.add_style(style, lv.PART.ITEMS)
285+
print(f"[AppearanceManager] Applied keyboard button fix for light mode")

0 commit comments

Comments
 (0)