Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ specter-diy-src
# i18n generated files (path-independent — matches any depth)
build
lang_*.bin
language_config.json
translation_keys.py
**/translation_keys.py
**/language_config.json

# Simulator runtime state files (generated at runtime, not for version control)
ui_state_config.json
**/ui_state_config.json

6 changes: 3 additions & 3 deletions scenarios/MockUI/src/MockUI/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# MockUI/__init__.py
from .basic import BTN_HEIGHT, BTN_WIDTH, MENU_PCT, PAD_SIZE, SWITCH_HEIGHT, SWITCH_WIDTH, STATUS_BTN_HEIGHT, STATUS_BTN_WIDTH, ONE_LETTER_SYMBOL_WIDTH, TWO_LETTER_SYMBOL_WIDTH, THREE_LETTER_SYMBOL_WIDTH, GREEN, ORANGE, RED
from .basic import MainMenu, LockedMenu, DeviceBar, WalletBar, ActionScreen, GenericMenu
from .basic import NavigationController
from .basic import SpecterGui
from .tour import UIExplainer, GuidedTour

from .helpers import UIState, SpecterState, Wallet
from .stubs import UIState, SpecterState, Wallet

from .wallet import (
WalletMenu,
Expand Down Expand Up @@ -59,7 +59,7 @@
"StorageMenu",
"SettingsMenu",
"PassphraseMenu",
"NavigationController",
"SpecterGui",
"UIExplainer",
"GuidedTour",
]
4 changes: 2 additions & 2 deletions scenarios/MockUI/src/MockUI/basic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .action_screen import ActionScreen
from .menu import GenericMenu
from .modal_overlay import ModalOverlay
from .navigation_controller import NavigationController
from .specter_gui import SpecterGui
from .symbol_lib import BTC_ICONS

__all__ = ["BTN_HEIGHT", "BTN_WIDTH", "BACK_BTN_HEIGHT", "BACK_BTN_WIDTH",
Expand All @@ -25,6 +25,6 @@
"ONE_LETTER_SYMBOL_WIDTH", "TWO_LETTER_SYMBOL_WIDTH", "THREE_LETTER_SYMBOL_WIDTH",
"GREEN", "ORANGE", "RED",
"GREEN_HEX", "ORANGE_HEX", "RED_HEX", "WHITE_HEX", "BLACK_HEX",
"MainMenu", "LockedMenu", "DeviceBar", "WalletBar", "ActionScreen", "GenericMenu", "TitledScreen", "ModalOverlay", "NavigationController",
"MainMenu", "LockedMenu", "DeviceBar", "WalletBar", "ActionScreen", "GenericMenu", "TitledScreen", "ModalOverlay", "SpecterGui",
"BTC_ICONS"
]
4 changes: 2 additions & 2 deletions scenarios/MockUI/src/MockUI/basic/action_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ def __init__(self, title, parent):
# TitledScreen creates title_bar (with optional back_btn + title_lbl) and body
super().__init__(title, parent)

# Get i18n manager from parent (always available via NavigationController)
self.t = parent.i18n.t
# Get i18n manager from shorthand (always available via SpecterGui)
self.t = self.i18n.t

# Message – placed inside body
self.msg = lv.label(self.body)
Expand Down
42 changes: 21 additions & 21 deletions scenarios/MockUI/src/MockUI/basic/device_bar.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import lvgl as lv
from ..helpers import Battery
from ..stubs import Battery
from .ui_consts import BTC_ICON_WIDTH, GREEN_HEX, ORANGE_HEX, RED_HEX, STATUS_BTN_HEIGHT, STATUS_BTN_WIDTH
from .symbol_lib import BTC_ICONS


class DeviceBar(lv.obj):
"""Device status bar showing system-level information. Designed to be ~5% of the screen height at the top."""

def __init__(self, parent, height_pct=5, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
def __init__(self, gui, height_pct=5, *args, **kwargs):
super().__init__(gui, *args, **kwargs)

self.parent = parent # for callback access
self.gui = gui # for callback access

self.set_width(lv.pct(100))
self.set_height(lv.pct(height_pct))
Expand Down Expand Up @@ -87,7 +87,7 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):

# Battery icon
self.batt_icon = Battery(self.right_container)
self.batt_icon.VALUE = parent.specter_state.battery_pct
self.batt_icon.VALUE = gui.specter_state.battery_pct
self.batt_icon.update()

# Settings button
Expand All @@ -114,40 +114,40 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):

def power_cb(self, e):
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.specter_state.battery_pct is None:
self.parent.specter_state.battery_pct = 50
self.parent.refresh_ui()
if self.gui.specter_state.battery_pct is None:
self.gui.specter_state.battery_pct = 50
self.gui.refresh_ui()
else:
self.parent.specter_state.battery_pct = None
self.parent.refresh_ui()
self.gui.specter_state.battery_pct = None
self.gui.refresh_ui()

def lock_cb(self, e):
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.specter_state.is_locked:
if self.gui.specter_state.is_locked:
# unlocking should be handled by the locked screen's PIN flow
return
else:
# lock the device and force NavigationController to show the locked screen
self.parent.specter_state.lock()
# show_menu will detect is_locked and show the locked screen
self.parent.show_menu(None)
# lock the device and force SpecterGui to show the locked screen
self.gui.specter_state.lock()
# on_navigate will detect is_locked and show the locked screen
self.gui.on_navigate(None)

def peripheral_ico_clicked(self, e):
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.ui_state.current_menu_id != "interfaces":
self.parent.show_menu("interfaces")
if self.gui.ui_state.current_menu_id != "interfaces":
self.gui.on_navigate("interfaces")

def lang_clicked(self, e):
"""Navigate to language selection menu when language label is clicked."""
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.ui_state.current_menu_id != "select_language":
self.parent.show_menu("select_language")
if self.gui.ui_state.current_menu_id != "select_language":
self.gui.on_navigate("select_language")

def settings_cb(self, e):
"""Navigate to settings menu when settings button is clicked."""
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.ui_state.current_menu_id != "manage_settings":
self.parent.show_menu("manage_settings")
if self.gui.ui_state.current_menu_id != "manage_settings":
self.gui.on_navigate("manage_settings")

def refresh(self, state):
"""Update visual elements from a SpecterState-like object."""
Expand Down
16 changes: 8 additions & 8 deletions scenarios/MockUI/src/MockUI/basic/locked_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,19 @@ def _on_ok(self, e):
return
pin = self.pin_buf
# attempt unlock; SpecterState.unlock will check PIN
unlocked = self.parent.specter_state.unlock(pin)
unlocked = self.state.unlock(pin)
if unlocked:
# reset UI history and show main menu
self.parent.ui_state.clear_history()
self.gui.ui_state.clear_history()
# Ensure state updated
self.parent.specter_state.is_locked = False
self.state.is_locked = False
# load main menu fresh
self.parent.ui_state.current_menu_id = "main"
self.gui.ui_state.current_menu_id = "main"
# delete current and create main menu
if self.parent.current_screen:
self.parent.current_screen.delete()
self.parent.current_screen = None
self.parent.show_menu(None)
if self.gui.current_screen:
self.gui.current_screen.delete()
self.gui.current_screen = None
self.on_navigate(None)
else:
# clear buffer and indicate failure (simple UX)
self.pin_buf = ""
Expand Down
2 changes: 1 addition & 1 deletion scenarios/MockUI/src/MockUI/basic/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class GenericMenu(TitledScreen):
"""

def __init__(self, parent):
# TitledScreen sets self.parent, self.state, self.i18n, self.on_navigate, self.body, etc.
# TitledScreen sets self.gui, self.state, self.i18n, self.on_navigate, self.body, etc.
super().__init__("", parent)

title = self.get_title(self.i18n.t, self.state)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lvgl as lv

from ..helpers import UIState, SpecterState
from ..stubs import UIState, SpecterState
from .device_bar import DeviceBar
from .wallet_bar import WalletBar
from .action_screen import ActionScreen
Expand Down Expand Up @@ -34,7 +34,7 @@
from .keyboard_manager import KeyboardManager


class NavigationController(lv.obj):
class SpecterGui(lv.obj):
# Static tour step definitions: (element_spec, i18n_key, position)
# element_spec is None, a dotted attribute-path string, or a (x, y, w, h) tuple.
# Resolved to runtime objects by GuidedTour.resolve_steps() before use.
Expand Down
10 changes: 5 additions & 5 deletions scenarios/MockUI/src/MockUI/basic/titled_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ class TitledScreen(lv.obj):
"""Base class for all full-screen views.

Attributes available to subclasses:
self.parent – the NavigationController that owns this screen
self.state – parent.specter_state shorthand
self.i18n – parent.i18n shorthand
self.on_navigate – navigation callback from parent NavigationController
self.gui – the SpecterGui that owns this screen
self.state – gui.specter_state shorthand
self.i18n – gui.i18n shorthand
self.on_navigate – navigation callback from gui.on_navigate
self.title_bar – lv.obj strip at the top, TITLE_ROW_HEIGHT tall
self.title_lbl – lv.label centred inside title_bar (alias: self.title)
self.back_btn – lv.button in title_bar (only when navigation history exists)
Expand All @@ -38,7 +38,7 @@ def __init__(self, title, parent):
lv_parent = getattr(parent, "content", parent)
super().__init__(lv_parent)

self.parent = parent
self.gui = parent
self.state = getattr(parent, "specter_state", None)
self.i18n = getattr(parent, "i18n", None)
self.on_navigate = getattr(parent, "on_navigate", None)
Expand Down
26 changes: 13 additions & 13 deletions scenarios/MockUI/src/MockUI/basic/wallet_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
class WalletBar(lv.obj):
"""Wallet status bar showing wallet-related information. Designed to be ~5% of the screen height at the bottom."""

def __init__(self, parent, height_pct=5, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
def __init__(self, gui, height_pct=5, *args, **kwargs):
super().__init__(gui, *args, **kwargs)

self.parent = parent # for callback access
self.gui = gui # for callback access

self.set_width(lv.pct(100))
self.set_height(lv.pct(height_pct))
Expand Down Expand Up @@ -65,21 +65,21 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):

def wallet_name_ico_clicked(self, e):
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.specter_state.active_wallet is None:
if self.parent.ui_state.current_menu_id != "add_wallet":
self.parent.show_menu("add_wallet")
if self.gui.specter_state.active_wallet is None:
if self.gui.ui_state.current_menu_id != "add_wallet":
self.gui.show_menu("add_wallet")
else:
if self.parent.ui_state.current_menu_id != "change_wallet":
self.parent.show_menu("change_wallet")
if self.gui.ui_state.current_menu_id != "change_wallet":
self.gui.show_menu("change_wallet")

def wallet_config_ico_clicked(self, e):
if e.get_code() == lv.EVENT.CLICKED:
if self.parent.specter_state.active_wallet is None:
if self.parent.ui_state.current_menu_id != "add_wallet":
self.parent.show_menu("add_wallet")
if self.gui.specter_state.active_wallet is None:
if self.gui.ui_state.current_menu_id != "add_wallet":
self.gui.show_menu("add_wallet")
else:
if self.parent.ui_state.current_menu_id != "manage_wallet":
self.parent.show_menu("manage_wallet")
if self.gui.ui_state.current_menu_id != "manage_wallet":
self.gui.show_menu("manage_wallet")

def refresh(self, state):
"""Update visual elements from a SpecterState-like object."""
Expand Down
7 changes: 3 additions & 4 deletions scenarios/MockUI/src/MockUI/device/interfaces_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class InterfacesMenu(TitledScreen):
"""Menu to enable/disable hardware interfaces."""

def __init__(self, parent):
# TitledScreen sets self.parent, self.state, self.i18n, self.on_navigate
# TitledScreen sets self.gui, self.state, self.i18n, self.on_navigate
super().__init__(parent.i18n.t("MENU_ENABLE_DISABLE_INTERFACES"), parent)

# Container for rows inside body
Expand Down Expand Up @@ -68,12 +68,11 @@ def _handler(e, attr):
sw_obj = e.get_target_obj()
is_on = bool(sw_obj.has_state(lv.STATE.CHECKED))

# update specter_state stored on this menu instance and in NavigationController
# update specter_state via shorthand
setattr(self.state, attr, is_on)
setattr(self.parent.specter_state, attr, is_on)

# refresh UI
self.parent.refresh_ui()
self.gui.refresh_ui()


sw.add_event_cb(lambda e, a=state_attr: _handler(e, a), lv.EVENT.VALUE_CHANGED, None)
10 changes: 5 additions & 5 deletions scenarios/MockUI/src/MockUI/device/language_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ class LanguageMenu(GenericMenu):
TITLE_KEY = "MENU_LANGUAGE"

def get_menu_items(self, t, state):
available_langs = self.parent.i18n.get_available_languages()
current_lang = self.parent.i18n.get_language()
available_langs = self.i18n.get_available_languages()
current_lang = self.i18n.get_language()

menu_items = []

for lang_code in available_langs:
label = self.parent.i18n.get_language_name(lang_code)
label = self.i18n.get_language_name(lang_code)
# Add checkmark for currently selected language
if lang_code == current_lang:
symbol = BTC_ICONS.CHECK
Expand All @@ -31,5 +31,5 @@ def get_menu_items(self, t, state):
def _on_language_selected(self, e, lang_code):
"""Handle language selection: change language and go back."""
if e.get_code() == lv.EVENT.CLICKED:
self.parent.change_language(lang_code)
self.parent.on_navigate(None)
self.gui.change_language(lang_code)
self.on_navigate(None)
2 changes: 1 addition & 1 deletion scenarios/MockUI/src/MockUI/device/settings_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SettingsMenu(GenericMenu):

def get_menu_items(self, t, state):
# Show current language code inline on the Language button
lang_code = self.parent.i18n.get_language()
lang_code = self.i18n.get_language()
lang_label = t("MENU_LANGUAGE") + " (" + lang_code.upper() + ")"

return [
Expand Down
2 changes: 1 addition & 1 deletion scenarios/MockUI/src/MockUI/fonts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ FontLoaderDE: Loaded 11/11 German umlaut fonts
## Next Steps

1. **For simulator (current setup):**
- Integrate `font_loader_de` into `mock_ui.py` or `NavigationController`
- Integrate `font_loader_de` into `mock_ui.py` or `SpecterGui`
- Update status bar and other components to use German fonts
- Test German text rendering with umlauts

Expand Down
8 changes: 4 additions & 4 deletions scenarios/MockUI/src/MockUI/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ The i18n system provides:
4. Binary (automatically) saved on device to `/flash/i18n/` → Persists across reboots
5. SD card not needed after initial load

#### Initialize in NavigationController
#### Initialize in SpecterGui

The i18n manager is automatically initialized in the NavigationController:
The i18n manager is automatically initialized in the SpecterGui:

```python
from ..i18n import I18nManager

class NavigationController(lv.obj):
class SpecterGui(lv.obj):
def __init__(self, specter_state=None, ui_state=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.i18n = I18nManager()
Expand All @@ -69,7 +69,7 @@ Example:

```python
def MyMenu(parent, *args, **kwargs):
# Get i18n manager from NavigationController
# Get i18n manager from SpecterGui
i18n = parent.i18n
t = i18n.t # Optional: make t() method available for convenience

Expand Down
2 changes: 1 addition & 1 deletion scenarios/MockUI/src/MockUI/i18n/i18n_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def load_language_from_json(self, json_path, lang_code=None):
return False


# Global instance (will be initialized by NavigationController or main app)
# Global instance (will be initialized by SpecterGui or main app)
_global_i18n_manager = None


Expand Down
6 changes: 3 additions & 3 deletions scenarios/MockUI/src/MockUI/tour/guided_tour.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ class GuidedTour:
Acts as the central controller - UIExplainer delegates navigation back here.

Usage:
steps = GuidedTour.resolve_steps(NavigationController.INTRO_TOUR_STEPS, nav)
steps = GuidedTour.resolve_steps(SpecterGui.INTRO_TOUR_STEPS, nav)
tour = GuidedTour(nav, steps)
tour.start()
"""

def __init__(self, nav_controller, steps):
"""Initialize the tour with a NavigationController and resolved steps.
"""Initialize the tour with a SpecterGui and resolved steps.

Args:
nav_controller: The NavigationController instance (must be fully constructed)
nav_controller: The SpecterGui instance (must be fully constructed)
steps: List of (element, text, position) tuples already resolved at runtime.
"""
self.nav = nav_controller
Expand Down
Loading
Loading