diff --git a/lib/tdeck_ui/UI/LXMF/HomeScreen.cpp b/lib/tdeck_ui/UI/LXMF/HomeScreen.cpp index a3d26fd4..bbd4b9b2 100644 --- a/lib/tdeck_ui/UI/LXMF/HomeScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/HomeScreen.cpp @@ -61,13 +61,35 @@ HomeScreen::HomeScreen() { } HomeScreen::~HomeScreen() { if (_screen) lv_obj_del(_screen); } void HomeScreen::show() { - lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(_screen); + lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(_screen); auto* group = LVGL::LVGLInit::get_default_group(); - if (group) { for (auto* button : _buttons) lv_group_add_obj(group, button); lv_group_focus_obj(_buttons[0]); } + if (group) { + // Rebuild the ordered launcher group without letting the first add emit + // an automatic focus event. Messages becomes the sole final owner. + lv_group_focus_freeze(group, true); + for (auto* button : _buttons) lv_group_add_obj(group, button); + lv_group_focus_freeze(group, false); + lv_group_focus_obj(_buttons[0]); + } } void HomeScreen::hide() { auto* group = LVGL::LVGLInit::get_default_group(); - if (group) for (auto* button : _buttons) lv_group_remove_obj(button); + if (group) { + // Removing the current owner makes LVGL synchronously focus another + // member. Detach every other launcher tile first and the owner last so + // no later tile becomes a transient focus owner during navigation. + lv_obj_t* focused = lv_group_get_focused(group); + bool focused_is_launcher = false; + for (auto* button : _buttons) { + if (button == focused) { + focused_is_launcher = true; + } else { + lv_group_remove_obj(button); + } + } + if (focused_is_launcher) lv_group_remove_obj(focused); + } lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN); } void HomeScreen::clicked(lv_event_t* event) { diff --git a/tests/native/test_app_launcher_nomadnet.py b/tests/native/test_app_launcher_nomadnet.py index b51bd8fa..3b286797 100644 --- a/tests/native/test_app_launcher_nomadnet.py +++ b/tests/native/test_app_launcher_nomadnet.py @@ -154,6 +154,37 @@ def test_directory_rebuild_has_one_final_focus_owner(): assert rebuild.index("lv_group_focus_freeze(group,false);") < rebuild.rindex("lv_group_focus_obj") +def test_launcher_transition_has_one_final_focus_owner(): + """Leaving and restoring Home must not transiently focus later tiles.""" + home = (INCLUDE / "HomeScreen.cpp").read_text() + show_start = home.index("void HomeScreen::show()") + hide_start = home.index("void HomeScreen::hide()", show_start) + show = home[show_start:hide_start] + hide_end = home.index("void HomeScreen::clicked", hide_start) + hide = home[hide_start:hide_end] + + # Adding the first tile to an empty LVGL group auto-focuses it. Freeze the + # complete ordered rebuild and make Messages the sole explicit owner. + assert "lv_group_focus_freeze(group, true);" in show + assert "lv_group_focus_freeze(group, false);" in show + assert show.index("lv_group_focus_freeze(group, true);") < show.index("lv_group_add_obj") + assert show.index("lv_group_focus_freeze(group, false);") < show.index("lv_group_focus_obj(_buttons[0])") + + # Hide must detach every non-owner before the current owner; removing an + # owner early makes LVGL focus each surviving launcher tile synchronously. + assert "lv_obj_t* focused = lv_group_get_focused(group);" in hide + non_owner_remove = re.search( + r"if \(button == focused\)\s*\{.*?\}\s*else\s*\{\s*lv_group_remove_obj\(button\);", + hide, + re.S, + ) + assert non_owner_remove + assert "if (focused_is_launcher) lv_group_remove_obj(focused);" in hide + assert non_owner_remove.start() < hide.index( + "if (focused_is_launcher) lv_group_remove_obj(focused);" + ) + + def test_bounded_nomadnet_fonts_match_display_allowlist(): expected = set(range(0x20, 0x7F)) | set(range(0xA0, 0x180)) expected |= set(range(0x2190, 0x219A))