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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Board Support:
OS:
- Disable the repl on hardware uart for esp32s3 targets (USB serial still works)

Frameworks:
- SettingsActivity / SettingActivity: when a setting has `ui_options` (a list of `(label, value)` tuples used by `ui: "radiobuttons"` or `"dropdown"`), the row's value label now shows the human-readable label instead of the raw pref value — both on initial render and after a save. Previously a radiobuttons setting like `[("Lightning Piggy", "lightningpiggy"), ...]` would show "lightningpiggy" in the row beneath the title; now it shows "Lightning Piggy". Stored values not present in the current `ui_options` list pass through unchanged so stale prefs stay visible rather than collapsing to "(not set)".

0.9.5
=====

Expand Down
18 changes: 16 additions & 2 deletions internal_filesystem/lib/mpos/ui/setting_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,24 @@ def save_setting(self, setting):
editor.put_string(setting["key"], new_value)
editor.commit()

# Update model for UI
# Update model for UI. For settings with `ui_options`, the value_label
# should show the human-readable label (e.g. "Lightning Piggy"), not
# the raw stored value ("lightningpiggy"). Mirrors the list-view
# rendering in settings_activity.py:_value_label_for so the row stays
# consistent before and after a save.
value_label = setting.get("value_label")
if value_label:
value_label.set_text(new_value if new_value else "(not set)")
if not new_value:
value_label.set_text("(not set)")
else:
display = new_value
ui_options = setting.get("ui_options")
if ui_options:
for label, value in ui_options:
if value == new_value:
display = label
break
value_label.set_text(display)

# self.finish (= back action) should happen before callback, in case it happens to start a new activity
self.finish()
Expand Down
28 changes: 26 additions & 2 deletions internal_filesystem/lib/mpos/ui/settings_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@
from .setting_activity import SettingActivity
import mpos.ui


def _value_label_for(setting, stored_value):
"""Map a stored pref value to its human-readable display label using
the setting's `ui_options` list (a list of (label, value) tuples).
Returns the matching label if one is found, otherwise the raw value
unchanged.

Without this, settings with `ui_options` (radiobuttons, dropdown)
show the raw pref value in the row's value label — e.g.
"lightningpiggy" instead of "Lightning Piggy". The matching label
only exists in the picker activity itself, never on the list view.
"""
ui_options = setting.get("ui_options")
if ui_options:
for label, value in ui_options:
if value == stored_value:
return label
return stored_value


# Used to list and edit all settings:
class SettingsActivity(Activity):

Expand Down Expand Up @@ -72,11 +92,15 @@ def onResume(self, screen):
if stored_value is None:
default_value = setting.get("default_value")
if default_value is not None:
value_text = f"(defaults to {default_value})"
# Map default to its human-readable label too, when one exists.
value_text = f"(defaults to {_value_label_for(setting, default_value)})"
else:
value_text = "(not set)"
else:
value_text = stored_value
# Map stored value to its ui_options label when present
# (e.g. "lightningpiggy" → "Lightning Piggy"). No-op when
# no ui_options or the value isn't in the list.
value_text = _value_label_for(setting, stored_value)
value.set_text(value_text)
value.set_style_text_font(lv.font_montserrat_12, lv.PART.MAIN)
value.set_style_text_color(lv.color_hex(0x666666), lv.PART.MAIN)
Expand Down
77 changes: 77 additions & 0 deletions tests/test_settings_activity_label_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Unit tests for the `_value_label_for` helper in `mpos.ui.settings_activity`.

The helper maps a stored pref value to its human-readable display label
using the setting's `ui_options` list. This is what lets the row in a
SettingsActivity list show "Lightning Piggy" / "Bright" / "Dark" instead
of the raw pref value "lightningpiggy" / "light" / "dark".

Motivating case: Lightning Piggy's Customise screen with a Hero Image
row using `ui: "radiobuttons"` — the value_label was rendering
"lightningpiggy" before this helper was added.

Usage:
Desktop: ./tests/unittest.sh tests/test_settings_activity_label_mapping.py
Device: ./tests/unittest.sh tests/test_settings_activity_label_mapping.py --ondevice
"""

import unittest

from mpos.ui.settings_activity import _value_label_for


class TestValueLabelFor(unittest.TestCase):

def test_matched_value_returns_label(self):
setting = {
"ui_options": [
("Lightning Piggy", "lightningpiggy"),
("Lightning Penguin", "lightningpenguin"),
("None", "none"),
],
}
self.assertEqual(_value_label_for(setting, "lightningpiggy"), "Lightning Piggy")
self.assertEqual(_value_label_for(setting, "lightningpenguin"), "Lightning Penguin")
self.assertEqual(_value_label_for(setting, "none"), "None")

def test_unmatched_value_passes_through(self):
# A stored value that's not in ui_options (e.g. a stale value from
# before the option set changed) returns unchanged rather than
# disappearing into "(not set)". Lets users still see what's
# actually stored, even if it's not a current valid option.
setting = {
"ui_options": [("A", "a"), ("B", "b")],
}
self.assertEqual(_value_label_for(setting, "legacy_value"), "legacy_value")

def test_setting_without_ui_options_passes_through(self):
# Plain textarea / freeform settings have no ui_options — the raw
# value is the right thing to display.
setting = {}
self.assertEqual(_value_label_for(setting, "some_value"), "some_value")
# None ui_options should also pass through, not raise.
self.assertEqual(_value_label_for({"ui_options": None}, "v"), "v")

def test_empty_ui_options_passes_through(self):
# Edge case: ui_options = [] (degenerate). Same fall-through as None.
self.assertEqual(_value_label_for({"ui_options": []}, "v"), "v")

def test_none_stored_value_passes_through(self):
# The caller (SettingsActivity row-render code) handles the "None"
# case separately with "(not set)" — this helper should never be
# asked about None, but be defensive: don't crash.
setting = {"ui_options": [("Yes", True), ("No", False)]}
# None doesn't match any option → return None unchanged.
self.assertIsNone(_value_label_for(setting, None))

def test_first_match_wins_with_duplicate_values(self):
# Duplicate values in ui_options (config bug) — first match wins.
# Documents the precedence so callers know what to expect.
setting = {
"ui_options": [("First", "x"), ("Second", "x")],
}
self.assertEqual(_value_label_for(setting, "x"), "First")


if __name__ == "__main__":
unittest.main()
Loading