diff --git a/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp b/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp index e0a58b1c..546a53e3 100644 --- a/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp @@ -69,7 +69,8 @@ SettingsScreen::SettingsScreen(lv_obj_t* parent) _slider_lora_power(nullptr), _label_lora_power_value(nullptr), _lora_params_container(nullptr), _switch_auto_enabled(nullptr), _switch_ble_enabled(nullptr), _ta_announce_interval(nullptr), _ta_sync_interval(nullptr), _switch_gps_sync(nullptr), - _switch_transport_enabled(nullptr), _transport_warning_modal(nullptr), _transport_enable_confirmed(false), + _switch_transport_enabled(nullptr), _transport_warning_modal(nullptr), + _transport_modal_group(nullptr), _transport_enable_confirmed(false), _btn_propagation_nodes(nullptr), _switch_prop_fallback(nullptr), _switch_prop_only(nullptr), _gps(nullptr) { LVGL_LOCK(); @@ -974,19 +975,43 @@ void SettingsScreen::show_transport_warning() { lv_label_set_text(confirm_label, "ENABLE ANYWAY"); lv_obj_center(confirm_label); - lv_group_t* group = LVGL::LVGLInit::get_default_group(); - if (group) { - lv_group_add_obj(group, cancel); - lv_group_add_obj(group, confirm); + _transport_modal_group = lv_group_create(); + if (_transport_modal_group) { + lv_group_add_obj(_transport_modal_group, cancel); + lv_group_add_obj(_transport_modal_group, confirm); lv_group_focus_obj(cancel); + + // Isolate keyboard and trackball focus inside the warning. Otherwise + // users can navigate into and activate Settings controls behind the + // still-visible modal. + lv_indev_t* keyboard = LVGL::LVGLInit::get_keyboard(); + lv_indev_t* trackball = LVGL::LVGLInit::get_trackball(); + if (keyboard) lv_indev_set_group(keyboard, _transport_modal_group); + if (trackball) lv_indev_set_group(trackball, _transport_modal_group); } } void SettingsScreen::close_transport_warning() { if (!_transport_warning_modal) return; + + lv_group_t* default_group = LVGL::LVGLInit::get_default_group(); + lv_indev_t* keyboard = LVGL::LVGLInit::get_keyboard(); + lv_indev_t* trackball = LVGL::LVGLInit::get_trackball(); + if (keyboard) lv_indev_set_group(keyboard, default_group); + if (trackball) lv_indev_set_group(trackball, default_group); + + if (_transport_modal_group) { + lv_group_del(_transport_modal_group); + _transport_modal_group = nullptr; + } + lv_obj_t* modal = _transport_warning_modal; _transport_warning_modal = nullptr; lv_obj_del_async(modal); + + if (default_group && _switch_transport_enabled) { + lv_group_focus_obj(_switch_transport_enabled); + } } void SettingsScreen::load_settings() { diff --git a/lib/tdeck_ui/UI/LXMF/SettingsScreen.h b/lib/tdeck_ui/UI/LXMF/SettingsScreen.h index 28294111..f495b719 100644 --- a/lib/tdeck_ui/UI/LXMF/SettingsScreen.h +++ b/lib/tdeck_ui/UI/LXMF/SettingsScreen.h @@ -297,6 +297,7 @@ private: // Dangerous transport-mode section (must remain last in Settings) lv_obj_t* _switch_transport_enabled; lv_obj_t* _transport_warning_modal; + lv_group_t* _transport_modal_group; bool _transport_enable_confirmed; // Delivery/Propagation section diff --git a/sync_file_libdeps.py b/sync_file_libdeps.py index 247d71ea..8b42996d 100644 --- a/sync_file_libdeps.py +++ b/sync_file_libdeps.py @@ -48,6 +48,8 @@ def mirror(src: Path, dst: Path): return False src_files = 0 copied = 0 + removed = 0 + mirrored_paths = set() for root, dirs, files in os.walk(src): # Skip git, build artifacts. dirs[:] = [d for d in dirs if d not in (".git", ".pio", "__pycache__", "build")] @@ -57,6 +59,7 @@ def mirror(src: Path, dst: Path): continue sp = Path(root) / fn dp = dst / rel / fn + mirrored_paths.add((rel / fn).as_posix()) src_files += 1 # A freshly fetched dependency can have a newer mtime than an older # local checkout even when the local bytes contain the intended fix. @@ -68,8 +71,26 @@ def mirror(src: Path, dst: Path): # incremental PlatformIO builds recompile changed dependency code. dp.touch() copied += 1 - if copied > 0: - print(f"SYNC: {dst.name}: refreshed {copied}/{src_files} files from {src}") + + # A mirror must also remove source files deleted from the explicit local + # checkout. Otherwise PlatformIO can continue compiling stale translation + # units that no longer exist in the dependency being tested. + for root, dirs, files in os.walk(dst): + dirs[:] = [d for d in dirs if d not in (".git", ".pio", "__pycache__", "build")] + rel = Path(root).relative_to(dst) + for fn in files: + if fn == ".piopm" or fn.endswith((".pyc", ".o", ".a", ".elf", ".bin")): + continue + relative_path = (rel / fn).as_posix() + if relative_path not in mirrored_paths: + (Path(root) / fn).unlink() + removed += 1 + + if copied > 0 or removed > 0: + print( + f"SYNC: {dst.name}: refreshed {copied}/{src_files} files, " + f"removed {removed} stale files from {src}" + ) return True diff --git a/tests/build_scripts/test_sync_file_libdeps.py b/tests/build_scripts/test_sync_file_libdeps.py index fb2d0d88..8193f565 100644 --- a/tests/build_scripts/test_sync_file_libdeps.py +++ b/tests/build_scripts/test_sync_file_libdeps.py @@ -22,8 +22,10 @@ def test_local_override_uses_content_not_mtime(tmp_path, monkeypatch): source_file = source / "Transport.cpp" destination_file = destination / "Transport.cpp" + orphan_file = destination / "Removed.cpp" source_file.write_text("patched source\n") destination_file.write_text("stale fetched source\n") + orphan_file.write_text("removed local source\n") # A just-fetched dependency can have a newer mtime than the local checkout. # The override must still win based on bytes, not timestamps. @@ -43,6 +45,7 @@ def test_local_override_uses_content_not_mtime(tmp_path, monkeypatch): assert destination_file.read_text() == "patched source\n" assert destination_file.stat().st_mtime > 2 + assert not orphan_file.exists() def test_local_override_runs_before_dependency_patch_scripts(): diff --git a/tests/build_scripts/test_transport_mode_safety_contract.py b/tests/build_scripts/test_transport_mode_safety_contract.py index 7dfa5ea6..c597e846 100644 --- a/tests/build_scripts/test_transport_mode_safety_contract.py +++ b/tests/build_scripts/test_transport_mode_safety_contract.py @@ -34,3 +34,6 @@ def test_transport_toggle_is_last_and_requires_explicit_warning_confirmation(): assert "Takes effect after reboot" in SETTINGS_CPP assert "on_transport_enabled_changed" in SETTINGS_CPP assert "on_transport_confirm_enable" in SETTINGS_CPP + assert "lv_group_create()" in SETTINGS_CPP + assert "lv_indev_set_group(keyboard, _transport_modal_group)" in SETTINGS_CPP + assert "lv_indev_set_group(trackball, _transport_modal_group)" in SETTINGS_CPP