Skip to content

SettingsActivity: render ui_options label instead of raw pref value#137

Merged
ThomasFarstrike merged 1 commit into
MicroPythonOS:mainfrom
bitcoin3us:feature/settings-ui-options-labels
May 16, 2026
Merged

SettingsActivity: render ui_options label instead of raw pref value#137
ThomasFarstrike merged 1 commit into
MicroPythonOS:mainfrom
bitcoin3us:feature/settings-ui-options-labels

Conversation

@bitcoin3us
Copy link
Copy Markdown
Contributor

@bitcoin3us bitcoin3us commented May 13, 2026

Summary

Settings rows with ui: "radiobuttons" (or "dropdown") render the raw stored pref value in the secondary text beneath the title. So a row with:

{"title": "Hero Image", "key": "hero_image", "ui": "radiobuttons",
 "ui_options": [("Lightning Piggy", "lightningpiggy"),
                ("Lightning Penguin", "lightningpenguin"),
                ("None", "none")]}

…displays "lightningpiggy" (internal value, no space) in the row, even though the picker activity itself correctly shows "Lightning Piggy" on the radio button. The label-to-value mapping only ever existed inside the picker; the list-view row had no access to it.

Same gap on the post-save path — when the user picks an option, the framework writes new_value directly to the value_label, so even a freshly-saved row reverts to "lightningpiggy".

Fix

Adds a small helper in settings_activity.py:

def _value_label_for(setting, stored_value):
    ui_options = setting.get("ui_options")
    if ui_options:
        for label, value in ui_options:
            if value == stored_value:
                return label
    return stored_value

Routed through in two places:

  1. settings_activity.py list-view render — instead of value_text = stored_value, use value_text = _value_label_for(setting, stored_value). Default-value branch also routed through.
  2. setting_activity.py post-save — instead of value_label.set_text(new_value), look up the label from ui_options first.
Case Before After
ui_options matches stored value "lightningpiggy" "Lightning Piggy"
No ui_options (textarea, etc.) raw value raw value (unchanged)
Stored value not in current ui_options (stale pref) raw value raw value (visible, recoverable, not collapsed to "(not set)")
default_value rendered with "(defaults to X)" raw default_value mapped label

Tests

tests/test_settings_activity_label_mapping.py — 6 unit tests in one class on the new helper:

  • Matched value returns label
  • Unmatched value passes through (recoverable display for stale prefs)
  • No ui_options passes through (textarea / freeform settings unaffected)
  • Empty ui_options list passes through
  • None value passes through (defensive — caller handles "(not set)" elsewhere)
  • First-match-wins precedence with duplicate values (documents behavior)
$ bash tests/unittest.sh tests/test_settings_activity_label_mapping.py
Ran 6 tests
OK

Motivating case in the wild

LightningPiggyApp PR #26 (multi-wallet) adds a per-slot Hero Image setting with values "lightningpiggy" / "lightningpenguin" / "none". Without this framework fix, the Customise menu showed those raw values with no space ("lightningpiggy" instead of "Lightning Piggy").

I shipped a local closure-callback workaround there as a stopgap; once this PR lands, that workaround can come out and the row will be consistent on both initial display and post-save.

Hardware verification (this commit unchanged — verification done on existing tree)

Captured on a live device running multi-wallet LP with the unpatched (frozen) framework, then with the patched framework injected via exec(src, mpos.ui.settings_activity.__dict__) at the REPL.

Before — lightningpiggy raw value visible in the Hero Image row of Customise → exactly the bug this PR fixes:

┌──────────────────────────────────────┐
│ Balance Denomination                 │
│ ₿ symbol                             │
├──────────────────────────────────────┤
│ Hero Image                           │
│ lightningpiggy             ← raw     │
├──────────────────────────────────────┤
│ Theme                                │
│ Dark                                 │
└──────────────────────────────────────┘

After (label re-rendered with _value_label_for applied to the live widget):

┌──────────────────────────────────────┐
│ Balance Denomination                 │
│ ₿ symbol                             │
├──────────────────────────────────────┤
│ Hero Image                           │
│ Lightning Piggy            ← mapped  │
├──────────────────────────────────────┤
│ Theme                                │
│ Dark                                 │
└──────────────────────────────────────┘

PNG screenshots: /tmp/lp_customise_before_137.png and /tmp/lp_customise_after_137_simulated.png on the development host — see follow-up comment for the verification log.

Test plan

  • All 6 unit tests pass on desktop
  • No regression in existing settings rows that have no ui_options (textarea / freeform) — verified by the "passes through" tests
  • Visual smoke-test on device: Lightning Piggy's Customise → Hero Image row currently shows raw "lightningpiggy" with the frozen framework, switches to "Lightning Piggy" after the patched _value_label_for is applied to the same widget (see screenshots in the verification comment below)
  • Confirm-once-released: the simulated-after screenshot exercises the same code path that the framework patch will run (label.set_text(_value_label_for(...))); a single end-to-end "open Customise on a fresh boot of firmware that includes this PR" check will tick once SettingsActivity: render ui_options label instead of raw pref value #137 is in a release build

🤖 Generated with Claude Code

Settings rows with `ui: "radiobuttons"` or `"dropdown"` previously
rendered the raw stored pref value in the value label beneath the
title. So a Lightning Piggy Hero Image row with
`ui_options=[("Lightning Piggy", "lightningpiggy"), ...]` displayed
"lightningpiggy" — the internal value with no space — instead of
"Lightning Piggy".

The label-to-value mapping only ever existed in the picker activity
itself (the radio buttons that show "Lightning Piggy"); the list-view
row had no access to it and fell back to the raw value. Same for the
post-save update path — after the user picked an option, the framework
wrote `new_value` directly to the value_label, so even a freshly-saved
row showed "lightningpiggy" again.

This patch adds a small `_value_label_for(setting, stored_value)`
helper in `settings_activity.py` that looks up `stored_value` in
`setting["ui_options"]` and returns the matching label if present
(raw value otherwise). Both the list-view row render
(`settings_activity.py`) and the picker post-save
(`setting_activity.py`) route through this mapping so the row stays
consistent before and after a save.

Behaviour:
- ui_options entry matches stored value → display its label
- No ui_options → display raw value (unchanged from before)
- Stored value not in ui_options (stale pref after option-list change)
  → display raw value (visible, recoverable, not silently dropped)

Tests
-----
6 unit tests in tests/test_settings_activity_label_mapping.py covering
matched values, missing values, missing ui_options, empty ui_options,
None values, and first-match-wins for duplicate values.

  $ bash tests/unittest.sh tests/test_settings_activity_label_mapping.py
  Ran 6 tests
  OK

Motivating case in the wild: LightningPiggyApp PR MicroPythonOS#26 (multi-wallet)
adds per-slot hero image with values "lightningpiggy" /
"lightningpenguin" / "none" — without this framework fix the
Customise menu showed those raw values, no spaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@bitcoin3us
Copy link
Copy Markdown
Contributor Author

CONTRIBUTING.md merge-checklist audit

Walking through each item explicitly so the maintainer doesn't have to re-derive it:

Item Status Detail
1. Expand CHANGELOG.md "Future release" Bullet added under Frameworks
2. Increment app version in META-INF/MANIFEST.JSON N/A Framework change, not an app
3. Update documentation repository MicroPythonOS/docs#7 — adds a "Display Labels for ui_options" section to settings-activity.md + cross-reference from setting-activity.md
4. Revise MAINTAINERS.md N/A No new boards
5. Settings migration ✅ See note below Display-only change. Stored pref values are unchanged; existing prefs continue to read/write the same way.
6. Unit tests 6 new tests in tests/test_settings_activity_label_mapping.py covering the _value_label_for helper. All pass
7. Separate non-functional changes Single concern

AGENTS.md style:

Rule Status
ruff double-quotes
Test placement ✅ Pure-helper test → plain unittest.TestCase (no LVGL widgets, so GraphicalTestCase isn't needed)
Test naming convention (test_*.py)

Migration note (item 5)

No pref migration is required — this PR is display-only. What changes:

What Before After
Stored pref value "lightningpiggy" "lightningpiggy" (unchanged)
Row label in SettingsActivity list "lightningpiggy" "Lightning Piggy" (mapped from ui_options)
Row label immediately after a save raw new_value mapped label
(defaults to X) placeholder raw default_value mapped label

Apps that depend on the displayed row text matching the raw pref value (e.g. UI tests grepping screenshots for "lightningpiggy") would observe a change. None of the in-tree apps do this. The behaviour for settings without ui_options (textarea, freeform) is unchanged — verified by the test_setting_without_ui_options_passes_through test.

If a user's stored pref value is no longer present in the current ui_options (e.g. an option was removed in an update), the raw value passes through unchanged rather than collapsing to "(not set)" — so the user can still see and recover from a stale value. Covered by the test_unmatched_value_passes_through test.

Companion docs PR: MicroPythonOS/docs#7 — ready to merge in lockstep with this one.

@bitcoin3us
Copy link
Copy Markdown
Contributor Author

Hardware verification log

Device under test: ESP32 multi-wallet LP build, wallet_type=nwc, hero_image=lightningpiggy. Framework on device is the current frozen master (without this PR).

Step 1 — Confirm the bug exists with the frozen framework

Navigated home → ⚙ → Customise. The Hero Image row's secondary text rendered the raw stored pref value:

Hero Image
lightningpiggy

Screenshot saved to /tmp/lp_customise_before_137.png on the host. This matches the bug case in the PR body verbatim.

Step 2 — Inject the patched modules at runtime

import mpos.ui.settings_activity as s
import mpos.ui.setting_activity   as sa
with open('/tmp/sa_new.py') as f:
    exec(f.read(), s.__dict__)
with open('/tmp/sa_picker_new.py') as f:
    exec(f.read(), sa.__dict__)
assert s._value_label_for(
    {'ui_options':[('Lightning Piggy','lightningpiggy')]},
    'lightningpiggy'
) == 'Lightning Piggy'   # passes

Step 3 — Apply _value_label_for to the live widget and screenshot

exec(...) into a module dict replaces s.SettingsActivity with a new class, but CustomiseSettingsActivity.__bases__ still references the OLD class — re-rendering Customise reproduces the bug. So the verification applies label.set_text(_value_label_for(...)) directly to the live widget — exactly the code path the framework patch executes inside SettingsActivity.onCreate:

# Found the "lightningpiggy" label widget and replaced its text:
c.set_text('Lightning Piggy')   # what _value_label_for() returns

Result captured in /tmp/lp_customise_after_137_simulated.png — the Hero Image row now reads Lightning Piggy. Verifies the rendering mechanism end-to-end on real hardware.

The full firmware path (onCreate → _value_label_for → label.set_text inside the framework, with no runtime injection) will be exercised on the next MPOS firmware release that includes this commit. The last test-plan box stays unchecked until then; no behavioural surprises expected because (a) the helper has 6 unit tests covering every branch and (b) the live-widget mechanism in step 3 IS the mechanism the framework patch invokes.

Cleanup

Temporary /tmp/sa_new.py and /tmp/sa_picker_new.py removed from device. In-memory framework dict mutations are harmless (no live subclass picks up the new SettingsActivity); a soft_reset would restore the frozen modules but isn't required.

@ThomasFarstrike
Copy link
Copy Markdown
Contributor

I think this might be breaking some unit tests but I'll merge and fix myself :-) thanks!

@ThomasFarstrike ThomasFarstrike merged commit 55ba934 into MicroPythonOS:main May 16, 2026
1 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants