diff --git a/apps/esp_idf/CMakeLists.txt b/apps/esp_idf/CMakeLists.txt index cb21a52c..1fe1e485 100644 --- a/apps/esp_idf/CMakeLists.txt +++ b/apps/esp_idf/CMakeLists.txt @@ -90,6 +90,7 @@ set(trail_mate_idf_ui_shared_sources ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/menu/menu_layout.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/ui_boot.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/top_bar.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/busy_overlay.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/system_notification.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/toast/toast_widget.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp diff --git a/modules/ui_shared/include/ui/localization.h b/modules/ui_shared/include/ui/localization.h index eeade609..04600938 100644 --- a/modules/ui_shared/include/ui/localization.h +++ b/modules/ui_shared/include/ui/localization.h @@ -21,6 +21,14 @@ struct LocaleInfo bool builtin = true; }; +struct ImeInfo +{ + const char* id = nullptr; + const char* display_name = nullptr; + const char* backend = nullptr; + bool builtin = true; +}; + void reload_language(); std::size_t locale_count(); const LocaleInfo* locale_at(std::size_t index); @@ -33,6 +41,12 @@ const lv_font_t* active_ui_font_fallback(); const lv_font_t* active_content_font_fallback(); const lv_font_t* locale_preview_font(const char* locale_id, const lv_font_t* ascii_font = nullptr); bool ensure_content_font_for_text(const char* text); +std::size_t ime_count(); +const ImeInfo* ime_at(std::size_t index); +std::size_t enabled_ime_count(); +bool ime_enabled(const char* ime_id); +bool set_ime_enabled(const char* ime_id, bool enabled, bool persist = true); +bool any_enabled_script_input(); const char* active_ime_pack_id(); bool active_locale_supports_script_input(); const char* tr(const char* english); diff --git a/modules/ui_shared/include/ui/widgets/busy_overlay.h b/modules/ui_shared/include/ui/widgets/busy_overlay.h new file mode 100644 index 00000000..58170d97 --- /dev/null +++ b/modules/ui_shared/include/ui/widgets/busy_overlay.h @@ -0,0 +1,10 @@ +#pragma once + +namespace ui::widgets::busy_overlay +{ + +void show(const char* title, const char* detail = nullptr); +void hide(); +bool visible(); + +} // namespace ui::widgets::busy_overlay diff --git a/modules/ui_shared/src/ui/app_catalog_builder.cpp b/modules/ui_shared/src/ui/app_catalog_builder.cpp index 829e74e8..19ceb903 100644 --- a/modules/ui_shared/src/ui/app_catalog_builder.cpp +++ b/modules/ui_shared/src/ui/app_catalog_builder.cpp @@ -217,14 +217,14 @@ AppCatalog build(const FeatureFlags& flags) add(&s_sstv_app); #endif } - if (flags.include_settings) - { - add(&s_setting_app); - } if (flags.include_extensions) { add(&s_extensions_app); } + if (flags.include_settings) + { + add(&s_setting_app); + } }; if (flags.profile == CatalogProfile::IdfDefault) diff --git a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp index d674bbd3..48d421cb 100644 --- a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp +++ b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp @@ -14,6 +14,7 @@ #include "ui/assets/fonts/font_utils.h" #include "ui/runtime/memory_profile.h" #include "ui/support/lvgl_fs_utils.h" +#include "ui/widgets/busy_overlay.h" #if __has_include("lv_binfont_loader.h") #include "lv_binfont_loader.h" @@ -33,6 +34,7 @@ namespace constexpr const char* kLogTag = "[I18N]"; constexpr const char* kSettingsNamespace = "settings"; constexpr const char* kDisplayLocaleKey = "display_locale"; +constexpr const char* kEnabledImeKey = "enabled_imes"; constexpr const char* kLegacyDisplayLanguageKey = "display_language"; constexpr const char* kDefaultLocaleId = "en"; constexpr const char* kBuiltinEnglishName = "English"; @@ -41,6 +43,8 @@ constexpr const char* kPackRoot = "/trailmate/packs"; constexpr const char* kFontPackRoot = "/trailmate/packs/fonts"; constexpr const char* kLocalePackRoot = "/trailmate/packs/locales"; constexpr const char* kImePackRoot = "/trailmate/packs/ime"; +constexpr const char* kDisabledImeSentinel = "__none__"; +constexpr std::size_t kFontLoadOverlayThresholdBytes = 64U * 1024U; enum class FontPackUsage : uint8_t { @@ -109,6 +113,8 @@ std::vector s_font_packs; std::vector s_ime_packs; std::vector s_locale_packs; std::vector s_locale_views; +std::vector s_ime_views; +std::vector s_enabled_ime_ids; LocalePackRecord* s_active_locale = nullptr; FontPackRecord* s_active_ui_font_pack = nullptr; @@ -121,6 +127,44 @@ std::vector s_content_supplement_packs; bool s_registry_ready = false; +class ScopedFontLoadOverlay +{ + public: + explicit ScopedFontLoadOverlay(const FontPackRecord& pack) + : active_(should_show(pack)) + { + if (!active_) + { + return; + } + + ::ui::widgets::busy_overlay::show("Loading font pack...", pack.id.c_str()); + } + + ~ScopedFontLoadOverlay() + { + if (active_) + { + ::ui::widgets::busy_overlay::hide(); + } + } + + ScopedFontLoadOverlay(const ScopedFontLoadOverlay&) = delete; + ScopedFontLoadOverlay& operator=(const ScopedFontLoadOverlay&) = delete; + + private: + static bool should_show(const FontPackRecord& pack) + { + if (pack.builtin || pack.source_path.empty()) + { + return false; + } + return pack.estimated_ram_bytes == 0U || pack.estimated_ram_bytes >= kFontLoadOverlayThresholdBytes; + } + + bool active_ = false; +}; + const char* safe_text(const char* value) { return value ? value : ""; @@ -215,6 +259,24 @@ std::vector split_csv_strings(const char* value) return items; } +std::string join_csv_strings(const std::vector& items) +{ + std::string joined; + for (std::size_t index = 0; index < items.size(); ++index) + { + if (items[index].empty()) + { + continue; + } + if (!joined.empty()) + { + joined += ','; + } + joined += items[index]; + } + return joined; +} + std::string join_path(const std::string& base, const std::string& child) { if (base.empty()) @@ -447,6 +509,34 @@ const TPack* find_pack_by_id(const std::vector& packs, const char* id) return nullptr; } +bool string_list_contains(const std::vector& values, const char* value) +{ + if (!value || value[0] == '\0') + { + return false; + } + + return std::find(values.begin(), values.end(), value) != values.end(); +} + +void append_unique_string(std::vector& values, const std::string& value) +{ + if (!value.empty() && !string_list_contains(values, value.c_str())) + { + values.push_back(value); + } +} + +bool ime_backend_supports_runtime_input(const ImePackRecord& pack) +{ + return pack.backend == "builtin-pinyin"; +} + +bool ime_backend_can_be_enabled(const ImePackRecord& pack) +{ + return ime_backend_supports_runtime_input(pack); +} + std::size_t parse_size_value(const char* value) { if (!value || value[0] == '\0') @@ -962,6 +1052,7 @@ bool load_font_pack(FontPackRecord& pack) pack.id.c_str(), pack.source_path.c_str(), static_cast(pack.estimated_ram_bytes)); + ScopedFontLoadOverlay overlay(pack); pack.owned_font = lv_binfont_create(pack.source_path.c_str()); if (pack.owned_font == nullptr) { @@ -1260,6 +1351,27 @@ void rebuild_locale_views() } } +void rebuild_ime_views() +{ + s_ime_views.clear(); + s_ime_views.reserve(s_ime_packs.size()); + + for (const auto& pack : s_ime_packs) + { + if (!ime_backend_can_be_enabled(pack)) + { + continue; + } + + ImeInfo view{}; + view.id = pack.id.c_str(); + view.display_name = pack.display_name.c_str(); + view.backend = pack.backend.c_str(); + view.builtin = pack.builtin; + s_ime_views.push_back(view); + } +} + void add_builtin_font_packs() { FontPackRecord latin_pack{}; @@ -1723,6 +1835,82 @@ void prune_unresolved_locales() s_locale_packs.end()); } +ImePackRecord* resolve_enabled_ime_pack() +{ + if (s_active_ime_pack != nullptr && + string_list_contains(s_enabled_ime_ids, s_active_ime_pack->id.c_str()) && + ime_backend_supports_runtime_input(*s_active_ime_pack)) + { + return s_active_ime_pack; + } + + for (const std::string& ime_id : s_enabled_ime_ids) + { + ImePackRecord* pack = find_pack_by_id(s_ime_packs, ime_id.c_str()); + if (pack != nullptr && ime_backend_supports_runtime_input(*pack)) + { + return pack; + } + } + + return nullptr; +} + +void enable_default_ime_packs() +{ + s_enabled_ime_ids.clear(); + for (const auto& pack : s_ime_packs) + { + if (ime_backend_can_be_enabled(pack)) + { + s_enabled_ime_ids.push_back(pack.id); + } + } +} + +void load_enabled_ime_packs() +{ + s_enabled_ime_ids.clear(); + + std::string stored; + if (!::platform::ui::settings_store::get_string(kSettingsNamespace, kEnabledImeKey, stored)) + { + enable_default_ime_packs(); + std::printf("%s enabled ime packs source=default ids=%s\n", + kLogTag, + s_enabled_ime_ids.empty() ? "" : join_csv_strings(s_enabled_ime_ids).c_str()); + return; + } + + const std::vector stored_ids = split_csv_strings(stored.c_str()); + if (stored_ids.size() == 1U && stored_ids.front() == kDisabledImeSentinel) + { + std::printf("%s enabled ime packs source=stored ids=\n", kLogTag); + return; + } + + for (const std::string& ime_id : stored_ids) + { + const ImePackRecord* pack = find_pack_by_id(s_ime_packs, ime_id.c_str()); + if (pack != nullptr && ime_backend_can_be_enabled(*pack)) + { + append_unique_string(s_enabled_ime_ids, pack->id); + } + } + + std::printf("%s enabled ime packs source=stored ids=%s\n", + kLogTag, + s_enabled_ime_ids.empty() ? "" : join_csv_strings(s_enabled_ime_ids).c_str()); +} + +bool persist_enabled_ime_packs() +{ + const std::string stored_value = + s_enabled_ime_ids.empty() ? std::string(kDisabledImeSentinel) : join_csv_strings(s_enabled_ime_ids); + return ::platform::ui::settings_store::put_string( + kSettingsNamespace, kEnabledImeKey, stored_value.c_str()); +} + void remove_legacy_locale_key() { const char* keys[] = {kLegacyDisplayLanguageKey}; @@ -1785,6 +1973,8 @@ void clear_registry() s_ime_packs.clear(); s_locale_packs.clear(); s_locale_views.clear(); + s_ime_views.clear(); + s_enabled_ime_ids.clear(); s_active_locale = nullptr; s_active_ui_font_pack = nullptr; @@ -1840,13 +2030,15 @@ bool activate_locale_internal(LocalePackRecord* locale, FontPackRecord* preserve } rebuild_runtime_font_chains(); + ImePackRecord* effective_ime_pack = resolve_enabled_ime_pack(); - std::printf("%s active locale=%s ui_font=%s content_font=%s ime=%s ui_chain=%s content_chain=%s\n", + std::printf("%s active locale=%s ui_font=%s content_font=%s locale_ime=%s effective_ime=%s ui_chain=%s content_chain=%s\n", kLogTag, s_active_locale ? s_active_locale->id.c_str() : "", s_active_ui_font_pack ? s_active_ui_font_pack->id.c_str() : "", s_active_content_font_pack ? s_active_content_font_pack->id.c_str() : "", s_active_ime_pack ? s_active_ime_pack->id.c_str() : "", + effective_ime_pack ? effective_ime_pack->id.c_str() : "", s_ui_font_chain.desc.empty() ? "" : s_ui_font_chain.desc.c_str(), s_content_font_chain.desc.empty() ? "" : s_content_font_chain.desc.c_str()); return true; @@ -1865,6 +2057,8 @@ void rebuild_registry() catalog_external_packs(); prune_unresolved_locales(); rebuild_locale_views(); + rebuild_ime_views(); + load_enabled_ime_packs(); s_registry_ready = true; const std::string preferred_locale = migrate_legacy_locale_if_needed(); @@ -2067,17 +2261,80 @@ bool ensure_content_font_for_text(const char* text) return missing.empty(); } +std::size_t ime_count() +{ + ensure_registry(); + return s_ime_views.size(); +} + +const ImeInfo* ime_at(std::size_t index) +{ + ensure_registry(); + if (index >= s_ime_views.size()) + { + return nullptr; + } + return &s_ime_views[index]; +} + +std::size_t enabled_ime_count() +{ + ensure_registry(); + return s_enabled_ime_ids.size(); +} + +bool ime_enabled(const char* ime_id) +{ + ensure_registry(); + return string_list_contains(s_enabled_ime_ids, ime_id); +} + +bool set_ime_enabled(const char* ime_id, bool enabled, bool persist) +{ + ensure_registry(); + const ImePackRecord* pack = find_pack_by_id(s_ime_packs, ime_id); + if (pack == nullptr || !ime_backend_can_be_enabled(*pack)) + { + return false; + } + + const std::vector previous = s_enabled_ime_ids; + if (enabled) + { + append_unique_string(s_enabled_ime_ids, pack->id); + } + else + { + s_enabled_ime_ids.erase( + std::remove(s_enabled_ime_ids.begin(), s_enabled_ime_ids.end(), pack->id), + s_enabled_ime_ids.end()); + } + + if (persist && previous != s_enabled_ime_ids && !persist_enabled_ime_packs()) + { + s_enabled_ime_ids = previous; + return false; + } + + return true; +} + +bool any_enabled_script_input() +{ + ensure_registry(); + return resolve_enabled_ime_pack() != nullptr; +} + const char* active_ime_pack_id() { ensure_registry(); - return s_active_ime_pack ? s_active_ime_pack->id.c_str() : nullptr; + ImePackRecord* pack = resolve_enabled_ime_pack(); + return pack ? pack->id.c_str() : nullptr; } bool active_locale_supports_script_input() { - ensure_registry(); - return s_active_ime_pack != nullptr && !s_active_ime_pack->backend.empty() && - s_active_ime_pack->backend != "none"; + return any_enabled_script_input(); } const char* tr(const char* english) diff --git a/modules/ui_shared/src/ui/menu/menu_runtime.cpp b/modules/ui_shared/src/ui/menu/menu_runtime.cpp index 75ac3db1..acd5468c 100644 --- a/modules/ui_shared/src/ui/menu/menu_runtime.cpp +++ b/modules/ui_shared/src/ui/menu/menu_runtime.cpp @@ -59,6 +59,35 @@ bool formatMenuTime(char* out, size_t out_len) return s_runtime.hooks.format_time ? s_runtime.hooks.format_time(out, out_len) : false; } +bool resolveDisplayLocalTime(struct tm* out_tm) +{ + if (!out_tm) + { + return false; + } + + if (::platform::ui::time::localtime_now(out_tm)) + { + return true; + } + + const std::time_t now = std::time(nullptr); + if (now <= 0) + { + return false; + } + + const std::time_t local = ::platform::ui::time::apply_timezone_offset(now); + const std::tm* tmp = std::gmtime(&local); + if (!tmp) + { + return false; + } + + *out_tm = *tmp; + return true; +} + std::size_t used_bytes(std::size_t total_bytes, std::size_t free_bytes) { return total_bytes > free_bytes ? (total_bytes - free_bytes) : 0; @@ -186,16 +215,10 @@ void updateWatchFaceTime() const uint32_t self_id = app::messagingFacade().getSelfNodeId(); watchFaceSetNodeId(self_id); const int battery = s_runtime.watch_face_battery >= 0 ? s_runtime.watch_face_battery : -1; - if (!platform::ui::device::rtc_ready()) - { - watchFaceSetTime(-1, -1, -1, -1, nullptr, battery); - return; - } - struct tm info { }; - if (!::platform::ui::time::localtime_now(&info)) + if (!resolveDisplayLocalTime(&info)) { watchFaceSetTime(-1, -1, -1, -1, nullptr, battery); return; @@ -236,13 +259,6 @@ void refreshTimeLabel() return; } - if (!platform::ui::device::rtc_ready()) - { - lv_label_set_text(s_runtime.time_label, "--:--"); - updateWatchFaceTime(); - return; - } - char time_str[16]; if (formatMenuTime(time_str, sizeof(time_str))) { @@ -256,7 +272,7 @@ void refreshTimeLabel() } else { - lv_label_set_text(s_runtime.time_label, "??:??"); + lv_label_set_text(s_runtime.time_label, "--:--"); } updateWatchFaceTime(); diff --git a/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp b/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp index 585f4793..62617c27 100644 --- a/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp +++ b/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp @@ -16,12 +16,16 @@ #include "ui/page/page_profile.h" #include "ui/runtime/pack_repository.h" #include "ui/ui_theme.h" +#include "ui/widgets/busy_overlay.h" #include "ui/widgets/system_notification.h" #include "ui/widgets/top_bar.h" #if !defined(LV_FONT_MONTSERRAT_16) || !LV_FONT_MONTSERRAT_16 #define lv_font_montserrat_16 lv_font_montserrat_14 #endif +#if !defined(LV_FONT_MONTSERRAT_14) || !LV_FONT_MONTSERRAT_14 +#define lv_font_montserrat_14 lv_font_montserrat_16 +#endif namespace { @@ -52,7 +56,6 @@ struct RuntimeState lv_obj_t* header_card = nullptr; lv_obj_t* title_label = nullptr; lv_obj_t* status_label = nullptr; - lv_obj_t* refresh_btn = nullptr; lv_obj_t* body_panel = nullptr; lv_obj_t* detail_back_btn = nullptr; lv_obj_t* primary_action_btn = nullptr; @@ -67,6 +70,23 @@ struct RuntimeState RuntimeState s_runtime{}; +class ScopedBusyOverlay +{ + public: + ScopedBusyOverlay(const char* title, const char* detail = nullptr) + { + ::ui::widgets::busy_overlay::show(title, detail); + } + + ~ScopedBusyOverlay() + { + ::ui::widgets::busy_overlay::hide(); + } + + ScopedBusyOverlay(const ScopedBusyOverlay&) = delete; + ScopedBusyOverlay& operator=(const ScopedBusyOverlay&) = delete; +}; + void request_exit() { if (s_runtime.host) @@ -266,7 +286,15 @@ void set_header_title(const char* text) { return; } - ::ui::i18n::set_label_text(s_runtime.title_label, text ? text : ""); + if (text == nullptr || text[0] == '\0') + { + lv_obj_add_flag(s_runtime.title_label, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(s_runtime.title_label, ""); + return; + } + + lv_obj_clear_flag(s_runtime.title_label, LV_OBJ_FLAG_HIDDEN); + ::ui::i18n::set_label_text(s_runtime.title_label, text); } void set_status_text(const char* text) @@ -396,10 +424,6 @@ void sync_focus_group(lv_obj_t* preferred_focus = nullptr) { lv_group_add_obj(app_g, s_runtime.filter_btn); } - if (s_runtime.refresh_btn != nullptr) - { - lv_group_add_obj(app_g, s_runtime.refresh_btn); - } if (s_runtime.detail_back_btn != nullptr) { lv_group_add_obj(app_g, s_runtime.detail_back_btn); @@ -490,12 +514,6 @@ void on_filter_clicked(lv_event_t* event) show_list_view(); } -void on_refresh_clicked(lv_event_t* event) -{ - (void)event; - refresh_catalog_and_render(); -} - void on_package_clicked(lv_event_t* event) { const uintptr_t raw_index = reinterpret_cast(lv_event_get_user_data(event)); @@ -536,7 +554,9 @@ void on_primary_action_clicked(lv_event_t* event) } const packs::PackageRecord& package = s_runtime.packages[package_index]; + const std::string package_id = package.id; std::string error; + ScopedBusyOverlay overlay("Installing package...", package_id.c_str()); set_status_text("Installing package..."); if (!packs::install_package(package, error)) { @@ -546,7 +566,7 @@ void on_primary_action_clicked(lv_event_t* event) } ::ui::SystemNotification::show("Package installed", 2000); - s_runtime.selected_package_id = package.id; + s_runtime.selected_package_id = package_id; s_runtime.view = MainView::Detail; refresh_catalog_and_render(); } @@ -608,7 +628,7 @@ lv_obj_t* create_action_button(lv_obj_t* parent, void show_message_body(const char* text) { clear_body(); - set_header_title("Language Packs"); + set_header_title(nullptr); lv_obj_t* card = lv_obj_create(s_runtime.body_panel); style_detail_card(card); @@ -686,7 +706,7 @@ void create_package_list_item(const packs::PackageRecord& package, std::size_t p void render_list_view() { clear_body(); - set_header_title("Language Packs"); + set_header_title(nullptr); if (s_runtime.filtered_indices.empty()) { @@ -831,7 +851,7 @@ void refresh_catalog_and_render() { set_status_text(error); show_message_body(error.c_str()); - sync_focus_group(s_runtime.refresh_btn ? s_runtime.refresh_btn : s_runtime.filter_btn); + sync_focus_group(s_runtime.filter_btn); return; } @@ -887,39 +907,15 @@ void create_main_panel(lv_obj_t* parent) s_runtime.header_card = lv_obj_create(s_runtime.main_panel); style_detail_card(s_runtime.header_card); - lv_obj_set_style_pad_row(s_runtime.header_card, 4, 0); + lv_obj_set_style_pad_all(s_runtime.header_card, 6, 0); + lv_obj_set_style_pad_row(s_runtime.header_card, 2, 0); - lv_obj_t* header_row = lv_obj_create(s_runtime.header_card); - lv_obj_set_size(header_row, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_bg_opa(header_row, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(header_row, 0, 0); - lv_obj_set_style_pad_all(header_row, 0, 0); - lv_obj_set_style_pad_column(header_row, 8, 0); - lv_obj_set_flex_flow(header_row, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(header_row, - LV_FLEX_ALIGN_SPACE_BETWEEN, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER); - two_pane_layout::make_non_scrollable(header_row); - - s_runtime.title_label = lv_label_create(header_row); - lv_obj_set_width(s_runtime.title_label, 0); - lv_obj_set_flex_grow(s_runtime.title_label, 1); + s_runtime.title_label = lv_label_create(s_runtime.header_card); + lv_obj_set_width(s_runtime.title_label, LV_PCT(100)); lv_label_set_long_mode(s_runtime.title_label, LV_LABEL_LONG_WRAP); - two_pane_styles::apply_label_primary(s_runtime.title_label); - lv_obj_set_style_text_font(s_runtime.title_label, &lv_font_montserrat_16, 0); - ::ui::i18n::set_label_text(s_runtime.title_label, "Language Packs"); - - s_runtime.refresh_btn = lv_btn_create(header_row); - lv_obj_set_width(s_runtime.refresh_btn, ::ui::page_profile::resolve_control_button_min_width()); - lv_obj_set_height(s_runtime.refresh_btn, ::ui::page_profile::resolve_control_button_height()); - two_pane_layout::make_non_scrollable(s_runtime.refresh_btn); - two_pane_styles::apply_btn_basic(s_runtime.refresh_btn); - lv_obj_add_event_cb(s_runtime.refresh_btn, on_refresh_clicked, LV_EVENT_CLICKED, nullptr); - lv_obj_add_event_cb(s_runtime.refresh_btn, on_focus_scroll, LV_EVENT_FOCUSED, nullptr); - lv_obj_t* refresh_label = lv_label_create(s_runtime.refresh_btn); - ::ui::i18n::set_label_text(refresh_label, "Refresh"); - lv_obj_center(refresh_label); + two_pane_styles::apply_label_muted(s_runtime.title_label); + lv_obj_set_style_text_font(s_runtime.title_label, &lv_font_montserrat_14, 0); + lv_obj_add_flag(s_runtime.title_label, LV_OBJ_FLAG_HIDDEN); s_runtime.status_label = lv_label_create(s_runtime.header_card); lv_obj_set_width(s_runtime.status_label, LV_PCT(100)); diff --git a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp index a7f46c3a..49d282f1 100644 --- a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "app/app_config.h" @@ -73,8 +74,18 @@ struct OptionClick settings::ui::ItemWidget* widget; }; +struct ImeToggleClick +{ + const char* ime_id = nullptr; + settings::ui::ItemWidget* widget = nullptr; + lv_obj_t* state_label = nullptr; +}; + static OptionClick s_option_clicks[kMaxOptions]{}; +static constexpr size_t kMaxImeOptions = 16; +static ImeToggleClick s_ime_toggle_clicks[kMaxImeOptions]{}; static size_t s_option_click_count = 0; +static size_t s_ime_toggle_count = 0; static lv_group_t* s_modal_prev_group = nullptr; static int s_pending_category = -1; static bool s_category_update_scheduled = false; @@ -96,6 +107,7 @@ static wifi_runtime::ScanResult kWifiScanResults[kMaxWifiNetworks] = {}; static void update_item_value(settings::ui::ItemWidget& widget); static void open_factory_reset_modal(); +static void open_enabled_imes_modal(settings::ui::ItemWidget& widget); static bool option_labels_are_translated(const settings::ui::SettingItem& item); static bool option_labels_use_content_font(const settings::ui::SettingItem& item); static void apply_locale_preview_font(lv_obj_t* label, const settings::ui::SettingItem& item, int value); @@ -109,6 +121,51 @@ static void copy_bounded(char* out, size_t out_len, const char* text) std::snprintf(out, out_len, "%s", text ? text : ""); } +static void set_modal_toggle_state_label(lv_obj_t* label, bool enabled) +{ + if (!label) + { + return; + } + ::ui::i18n::set_label_text_raw(label, ::ui::i18n::tr(enabled ? "ON" : "OFF")); +} + +static void format_enabled_ime_summary(char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + const std::size_t available = ::ui::i18n::ime_count(); + const std::size_t enabled = ::ui::i18n::enabled_ime_count(); + if (available == 0) + { + std::snprintf(out, out_len, "%s", ::ui::i18n::tr("Unavailable")); + return; + } + if (enabled == 0) + { + std::snprintf(out, out_len, "%s", ::ui::i18n::tr("None")); + return; + } + if (enabled == 1) + { + for (std::size_t index = 0; index < available; ++index) + { + const ::ui::i18n::ImeInfo* ime = ::ui::i18n::ime_at(index); + if (ime != nullptr && ::ui::i18n::ime_enabled(ime->id)) + { + std::snprintf(out, out_len, "%s", ime->display_name ? ime->display_name : ime->id); + return; + } + } + } + + const std::string summary = ::ui::i18n::format("%d enabled", static_cast(enabled)); + std::snprintf(out, out_len, "%s", summary.c_str()); +} + static void clear_wifi_scan_options() { kWifiNetworkOptionCount = 0; @@ -929,7 +986,14 @@ static void format_value(const settings::ui::SettingItem& item, char* out, size_ } break; case settings::ui::SettingType::Action: - snprintf(out, out_len, "%s", ::ui::i18n::tr("Run")); + if (item.pref_key && std::strcmp(item.pref_key, "enabled_imes") == 0) + { + format_enabled_ime_summary(out, out_len); + } + else + { + snprintf(out, out_len, "%s", ::ui::i18n::tr("Run")); + } break; } } @@ -942,7 +1006,11 @@ static void update_item_value(settings::ui::ItemWidget& widget) } char value[48]; format_value(*widget.def, value, sizeof(value)); - if (option_labels_use_content_font(*widget.def)) + const bool use_content_font = + option_labels_use_content_font(*widget.def) || + (widget.def->pref_key && std::strcmp(widget.def->pref_key, "enabled_imes") == 0 && + ::ui::fonts::utf8_has_non_ascii(value)); + if (use_content_font) { ::ui::i18n::set_content_label_text_raw(widget.value_label, value); } @@ -994,6 +1062,7 @@ static void modal_close() g_state.editing_item = nullptr; g_state.editing_widget = nullptr; s_option_click_count = 0; + s_ime_toggle_count = 0; modal_restore_group(); } @@ -1811,6 +1880,12 @@ static void open_factory_reset_modal() lv_group_focus_obj(cancel_btn); } +static void on_enabled_imes_back_clicked(lv_event_t* e) +{ + (void)e; + modal_close(); +} + static void option_modal_focused_cb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_FOCUSED) return; @@ -1821,6 +1896,156 @@ static void option_modal_focused_cb(lv_event_t* e) } } +static void on_ime_toggle_clicked(lv_event_t* e) +{ + ImeToggleClick* payload = static_cast(lv_event_get_user_data(e)); + if (!payload || !payload->ime_id || !payload->widget) + { + return; + } + + const bool currently_enabled = ::ui::i18n::ime_enabled(payload->ime_id); + const bool next_enabled = !currently_enabled; + if (!::ui::i18n::set_ime_enabled(payload->ime_id, next_enabled, true)) + { + ::ui::SystemNotification::show(::ui::i18n::tr("IME setting update failed"), 3000); + return; + } + + lv_obj_t* btn = lv_event_get_target_obj(e); + if (btn) + { + if (next_enabled) + { + lv_obj_add_state(btn, LV_STATE_CHECKED); + } + else + { + lv_obj_clear_state(btn, LV_STATE_CHECKED); + } + } + + set_modal_toggle_state_label(payload->state_label, next_enabled); + update_item_value(*payload->widget); +} + +static void open_enabled_imes_modal(settings::ui::ItemWidget& widget) +{ + if (g_state.modal_root) + { + return; + } + + const std::size_t ime_total = ::ui::i18n::ime_count(); + if (ime_total == 0) + { + ::ui::SystemNotification::show(::ui::i18n::tr("No IME packs installed"), 3000); + return; + } + + modal_prepare_group(); + + const auto& profile = ::ui::page_profile::current(); + const lv_coord_t top_bar_h = profile.top_bar_height > 0 + ? profile.top_bar_height + : static_cast(::ui::widgets::kTopBarHeight); + const lv_coord_t gap_from_top_bar = 3; + const lv_coord_t content_h = lv_obj_get_height(g_state.root) - top_bar_h; + + g_state.modal_root = lv_obj_create(g_state.root); + lv_obj_set_size(g_state.modal_root, LV_PCT(100), content_h); + lv_obj_set_pos(g_state.modal_root, 0, top_bar_h); + style::apply_modal_bg(g_state.modal_root); + style::apply_modal_panel(g_state.modal_root); + lv_obj_set_style_border_width(g_state.modal_root, 0, LV_PART_MAIN); + lv_obj_set_style_radius(g_state.modal_root, 0, LV_PART_MAIN); + lv_obj_set_style_pad_all(g_state.modal_root, 0, LV_PART_MAIN); + lv_obj_clear_flag(g_state.modal_root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(g_state.modal_root, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_t* list = lv_obj_create(g_state.modal_root); + lv_obj_set_size(list, LV_PCT(100), content_h - gap_from_top_bar); + lv_obj_set_pos(list, 0, gap_from_top_bar); + lv_obj_set_style_pad_all(list, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(list, 0, LV_PART_MAIN); + lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_PART_MAIN); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(list, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_AUTO); + + s_ime_toggle_count = 0; + for (std::size_t index = 0; index < ime_total && s_ime_toggle_count < kMaxImeOptions; ++index) + { + const ::ui::i18n::ImeInfo* ime = ::ui::i18n::ime_at(index); + if (!ime) + { + continue; + } + + lv_obj_t* btn = lv_btn_create(list); + lv_obj_set_size(btn, LV_PCT(100), ::ui::page_profile::resolve_control_button_height()); + style::apply_btn_modal(btn); + lv_obj_set_style_pad_left(btn, 12, LV_PART_MAIN); + lv_obj_set_style_pad_right(btn, 12, LV_PART_MAIN); + lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(btn, + LV_FLEX_ALIGN_SPACE_BETWEEN, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + + lv_obj_t* name_label = lv_label_create(btn); + const char* display_name = ime->display_name ? ime->display_name : ime->id; + if (::ui::fonts::utf8_has_non_ascii(display_name)) + { + ::ui::i18n::set_content_label_text_raw(name_label, display_name); + } + else + { + ::ui::i18n::set_label_text_raw(name_label, display_name); + } + style::apply_label_primary(name_label); + + lv_obj_t* state_label = lv_label_create(btn); + set_modal_toggle_state_label(state_label, ::ui::i18n::ime_enabled(ime->id)); + style::apply_label_primary(state_label); + + s_ime_toggle_clicks[s_ime_toggle_count].ime_id = ime->id; + s_ime_toggle_clicks[s_ime_toggle_count].widget = &widget; + s_ime_toggle_clicks[s_ime_toggle_count].state_label = state_label; + lv_obj_add_event_cb(btn, + on_ime_toggle_clicked, + LV_EVENT_CLICKED, + &s_ime_toggle_clicks[s_ime_toggle_count]); + lv_obj_add_event_cb(btn, option_modal_focused_cb, LV_EVENT_FOCUSED, nullptr); + if (::ui::i18n::ime_enabled(ime->id)) + { + lv_obj_add_state(btn, LV_STATE_CHECKED); + } + lv_group_add_obj(g_state.modal_group, btn); + ++s_ime_toggle_count; + } + + lv_obj_t* back_btn = lv_btn_create(list); + lv_obj_set_size(back_btn, LV_PCT(100), ::ui::page_profile::resolve_control_button_height()); + style::apply_btn_modal(back_btn); + lv_obj_t* back_label = lv_label_create(back_btn); + ::ui::i18n::set_label_text(back_label, "Back"); + style::apply_label_primary(back_label); + lv_obj_center(back_label); + lv_obj_add_event_cb(back_btn, on_enabled_imes_back_clicked, LV_EVENT_CLICKED, nullptr); + lv_obj_add_event_cb(back_btn, option_modal_focused_cb, LV_EVENT_FOCUSED, nullptr); + lv_group_add_obj(g_state.modal_group, back_btn); + + if (s_ime_toggle_count > 0) + { + lv_group_focus_obj(lv_obj_get_child(list, 0)); + } + else + { + lv_group_focus_obj(back_btn); + } +} + static void open_option_modal(const settings::ui::SettingItem& item, settings::ui::ItemWidget& widget) { if (g_state.modal_root) @@ -2229,6 +2454,7 @@ static settings::ui::SettingItem kScreenItems[] = { {"Display Language", settings::ui::SettingType::Enum, kLocaleOptions, 0, &g_settings.display_locale_index, nullptr, nullptr, 0, false, "display_locale"}, + {"Enabled IMEs", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "enabled_imes"}, {"Screen Timeout", settings::ui::SettingType::Enum, kScreenTimeoutOptions, 4, &g_settings.screen_timeout_ms, nullptr, nullptr, 0, false, "screen_timeout"}, {"Screen Brightness", settings::ui::SettingType::Enum, kScreenBrightnessOptions, sizeof(kScreenBrightnessOptions) / sizeof(kScreenBrightnessOptions[0]), &g_settings.screen_brightness, nullptr, nullptr, 0, false, "screen_brightness"}, @@ -2726,7 +2952,11 @@ static bool activate_item_widget(settings::ui::ItemWidget& widget) } if (item.type == settings::ui::SettingType::Action) { - if (item.pref_key && strcmp(item.pref_key, "chat_reset_mesh") == 0) + if (item.pref_key && strcmp(item.pref_key, "enabled_imes") == 0) + { + open_enabled_imes_modal(widget); + } + else if (item.pref_key && strcmp(item.pref_key, "chat_reset_mesh") == 0) { reset_mesh_settings(); } diff --git a/modules/ui_shared/src/ui/startup_shell.cpp b/modules/ui_shared/src/ui/startup_shell.cpp index 7a5574ce..9d0d0de0 100644 --- a/modules/ui_shared/src/ui/startup_shell.cpp +++ b/modules/ui_shared/src/ui/startup_shell.cpp @@ -20,6 +20,35 @@ namespace ui::startup_shell namespace { +bool resolve_display_time(struct tm* out_tm) +{ + if (!out_tm) + { + return false; + } + + if (::platform::ui::time::localtime_now(out_tm)) + { + return true; + } + + const std::time_t now = std::time(nullptr); + if (now <= 0) + { + return false; + } + + const std::time_t local = ::platform::ui::time::apply_timezone_offset(now); + const std::tm* tmp = std::gmtime(&local); + if (!tmp) + { + return false; + } + + *out_tm = *tmp; + return true; +} + void present_boot_overlay_now() { lv_timer_handler(); @@ -38,7 +67,7 @@ bool format_menu_time(char* out, size_t out_len) struct tm info { }; - if (!::platform::ui::time::localtime_now(&info)) + if (!resolve_display_time(&info)) { return false; } diff --git a/modules/ui_shared/src/ui/widgets/busy_overlay.cpp b/modules/ui_shared/src/ui/widgets/busy_overlay.cpp new file mode 100644 index 00000000..ecfa5e57 --- /dev/null +++ b/modules/ui_shared/src/ui/widgets/busy_overlay.cpp @@ -0,0 +1,234 @@ +#include "ui/widgets/busy_overlay.h" + +#include + +#include "lvgl.h" +#include "platform/ui/screen_runtime.h" +#include "ui/page/page_profile.h" +#include "ui/ui_theme.h" + +#if !defined(LV_FONT_MONTSERRAT_12) || !LV_FONT_MONTSERRAT_12 +#define lv_font_montserrat_12 lv_font_montserrat_14 +#endif +#if !defined(LV_FONT_MONTSERRAT_16) || !LV_FONT_MONTSERRAT_16 +#define lv_font_montserrat_16 lv_font_montserrat_14 +#endif + +namespace ui::widgets::busy_overlay +{ +namespace +{ + +constexpr lv_coord_t kRequestedWidth = 220; +constexpr lv_coord_t kRequestedHeight = 118; +constexpr lv_coord_t kBarHeight = 8; +constexpr lv_coord_t kBarRadius = 4; +constexpr uint32_t kOverlayBgColor = 0x1B1208; +constexpr uint32_t kAnimTimeMs = 820; + +lv_obj_t* s_root = nullptr; +lv_obj_t* s_panel = nullptr; +lv_obj_t* s_title_label = nullptr; +lv_obj_t* s_detail_label = nullptr; +lv_obj_t* s_bar = nullptr; +uint32_t s_depth = 0; + +void refresh_now() +{ + lv_refr_now(nullptr); +} + +void set_bar_value(void* bar, int32_t value) +{ + lv_obj_t* bar_obj = static_cast(bar); + if (!bar_obj || !lv_obj_is_valid(bar_obj)) + { + return; + } + lv_bar_set_value(bar_obj, static_cast(value), LV_ANIM_OFF); +} + +void stop_bar_animation() +{ + if (s_bar && lv_obj_is_valid(s_bar)) + { + lv_anim_del(s_bar, set_bar_value); + } +} + +void start_bar_animation() +{ + if (!s_bar || !lv_obj_is_valid(s_bar)) + { + return; + } + + stop_bar_animation(); + lv_bar_set_range(s_bar, 0, 100); + lv_bar_set_value(s_bar, 12, LV_ANIM_OFF); + + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_var(&anim, s_bar); + lv_anim_set_values(&anim, 12, 100); + lv_anim_set_time(&anim, kAnimTimeMs); + lv_anim_set_playback_time(&anim, kAnimTimeMs); + lv_anim_set_repeat_count(&anim, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_exec_cb(&anim, set_bar_value); + lv_anim_start(&anim); +} + +void swallow_event_cb(lv_event_t* e) +{ + lv_event_stop_bubbling(e); + lv_event_stop_processing(e); + if (lv_indev_t* indev = lv_event_get_indev(e)) + { + lv_indev_stop_processing(indev); + } +} + +void destroy_overlay() +{ + stop_bar_animation(); + if (s_root && lv_obj_is_valid(s_root)) + { + lv_obj_del(s_root); + } + s_root = nullptr; + s_panel = nullptr; + s_title_label = nullptr; + s_detail_label = nullptr; + s_bar = nullptr; +} + +void ensure_overlay() +{ + if (s_root && lv_obj_is_valid(s_root)) + { + return; + } + + lv_obj_t* parent = lv_layer_top(); + if (!parent) + { + return; + } + + s_root = lv_obj_create(parent); + lv_obj_remove_style_all(s_root); + lv_obj_set_size(s_root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(s_root, lv_color_hex(kOverlayBgColor), 0); + lv_obj_set_style_bg_opa(s_root, LV_OPA_50, 0); + lv_obj_set_style_border_width(s_root, 0, 0); + lv_obj_clear_flag(s_root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(s_root, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(s_root, swallow_event_cb, LV_EVENT_ALL, nullptr); + + const auto size = ::ui::page_profile::resolve_modal_size(kRequestedWidth, kRequestedHeight, s_root); + const lv_coord_t pad = ::ui::page_profile::resolve_modal_pad(); + + s_panel = lv_obj_create(s_root); + lv_obj_set_size(s_panel, size.width, size.height); + lv_obj_center(s_panel); + lv_obj_set_style_bg_color(s_panel, ::ui::theme::surface(), 0); + lv_obj_set_style_bg_opa(s_panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_panel, 1, 0); + lv_obj_set_style_border_color(s_panel, ::ui::theme::border(), 0); + lv_obj_set_style_radius(s_panel, 10, 0); + lv_obj_set_style_pad_all(s_panel, pad, 0); + lv_obj_set_style_pad_row(s_panel, pad, 0); + lv_obj_set_flex_flow(s_panel, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(s_panel, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(s_panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(s_panel, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(s_panel, swallow_event_cb, LV_EVENT_ALL, nullptr); + + s_title_label = lv_label_create(s_panel); + lv_obj_set_width(s_title_label, LV_PCT(100)); + lv_obj_set_style_text_align(s_title_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(s_title_label, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(s_title_label, ::ui::theme::text(), 0); + lv_label_set_long_mode(s_title_label, LV_LABEL_LONG_WRAP); + + s_detail_label = lv_label_create(s_panel); + lv_obj_set_width(s_detail_label, LV_PCT(100)); + lv_obj_set_style_text_align(s_detail_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(s_detail_label, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(s_detail_label, ::ui::theme::text_muted(), 0); + lv_label_set_long_mode(s_detail_label, LV_LABEL_LONG_DOT); + + s_bar = lv_bar_create(s_panel); + lv_obj_set_width(s_bar, LV_PCT(100)); + lv_obj_set_height(s_bar, kBarHeight); + lv_obj_set_style_radius(s_bar, kBarRadius, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_bar, ::ui::theme::separator(), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_bar, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_border_width(s_bar, 0, LV_PART_MAIN); + lv_obj_set_style_radius(s_bar, kBarRadius, LV_PART_INDICATOR); + lv_obj_set_style_bg_color(s_bar, ::ui::theme::accent(), LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(s_bar, LV_OPA_COVER, LV_PART_INDICATOR); + + start_bar_animation(); + lv_obj_move_foreground(s_root); +} + +void set_label_text(lv_obj_t* label, const char* text) +{ + if (!label || !lv_obj_is_valid(label)) + { + return; + } + lv_label_set_text(label, text ? text : ""); +} + +} // namespace + +void show(const char* title, const char* detail) +{ + ++s_depth; + ::platform::ui::screen::disable_sleep(); + ensure_overlay(); + if (!s_root) + { + return; + } + + set_label_text(s_title_label, title ? title : "Loading..."); + set_label_text(s_detail_label, detail ? detail : ""); + if (detail && detail[0] != '\0') + { + lv_obj_clear_flag(s_detail_label, LV_OBJ_FLAG_HIDDEN); + } + else + { + lv_obj_add_flag(s_detail_label, LV_OBJ_FLAG_HIDDEN); + } + lv_obj_move_foreground(s_root); + refresh_now(); +} + +void hide() +{ + if (s_depth == 0) + { + return; + } + + --s_depth; + ::platform::ui::screen::enable_sleep(); + if (s_depth > 0) + { + return; + } + + destroy_overlay(); + refresh_now(); +} + +bool visible() +{ + return s_root != nullptr && lv_obj_is_valid(s_root); +} + +} // namespace ui::widgets::busy_overlay diff --git a/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp b/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp index 8bc580c3..cc964243 100644 --- a/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp +++ b/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp @@ -23,10 +23,13 @@ namespace ImeWidget* s_active_ime = nullptr; static constexpr int kCandidatesPerPage = 12; +static constexpr int kCompactCandidatesPerPage = 7; +static constexpr lv_coord_t kCompactImeRowHeight = 22; +static constexpr lv_coord_t kCompactImeControlHeight = 18; bool script_input_available() { - return ::ui::i18n::active_locale_supports_script_input(); + return ::ui::i18n::any_enabled_script_input(); } #if UI_SHARED_TOUCH_IME_ENABLED @@ -42,10 +45,12 @@ static const char* kTouchNumMap[] = { ".", ",", "?", "!", "'", "\"", "%", "+", "\n", "Space", ""}; #endif -std::string make_candidates_text(const std::vector& candidates, int active_idx) +std::string make_candidates_text(const std::vector& candidates, + int active_idx, + int max_show = kCandidatesPerPage, + const char* separator = " ") { std::string out; - int max_show = kCandidatesPerPage; int total = static_cast(candidates.size()); if (total <= 0) return out; if (active_idx < 0) active_idx = 0; @@ -61,7 +66,7 @@ std::string make_candidates_text(const std::vector& candidates, int for (int i = start; i < total && i < start + max_show; ++i) { - if (!out.empty()) out += " "; + if (!out.empty() && separator) out += separator; if (i == active_idx) { out += '['; @@ -244,19 +249,21 @@ void ImeWidget::init_compact_ui(lv_obj_t* parent) { container_ = lv_obj_create(parent); lv_obj_set_width(container_, LV_PCT(100)); - lv_obj_set_height(container_, 24); + lv_obj_set_height(container_, kCompactImeRowHeight); lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(container_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_left(container_, 6, 0); - lv_obj_set_style_pad_right(container_, 6, 0); - lv_obj_set_style_pad_column(container_, 6, 0); + lv_obj_set_style_pad_top(container_, 0, 0); + lv_obj_set_style_pad_bottom(container_, 0, 0); + lv_obj_set_style_pad_left(container_, 4, 0); + lv_obj_set_style_pad_right(container_, 4, 0); + lv_obj_set_style_pad_column(container_, 4, 0); lv_obj_set_style_bg_color(container_, lv_color_hex(0xFFF0D3), 0); lv_obj_set_style_bg_opa(container_, LV_OPA_COVER, 0); lv_obj_set_style_border_width(container_, 0, 0); lv_obj_clear_flag(container_, LV_OBJ_FLAG_SCROLLABLE); toggle_btn_ = lv_btn_create(container_); - lv_obj_set_size(toggle_btn_, 44, 18); + lv_obj_set_size(toggle_btn_, 42, kCompactImeControlHeight); lv_obj_set_style_radius(toggle_btn_, 4, LV_PART_MAIN); lv_obj_set_style_bg_color(toggle_btn_, lv_color_hex(0xFFF7E9), LV_PART_MAIN); lv_obj_set_style_bg_opa(toggle_btn_, LV_OPA_COVER, LV_PART_MAIN); @@ -286,9 +293,13 @@ void ImeWidget::init_compact_ui(lv_obj_t* parent) candidates_label_ = lv_label_create(container_); set_candidates_label_text(candidates_label_, ""); + lv_obj_set_width(candidates_label_, 0); + lv_obj_set_height(candidates_label_, kCompactImeControlHeight); lv_obj_set_style_text_color(candidates_label_, lv_color_hex(0x3A2A1A), 0); + lv_obj_set_style_text_line_space(candidates_label_, 0, 0); lv_obj_set_flex_grow(candidates_label_, 1); - lv_obj_set_style_text_align(candidates_label_, LV_TEXT_ALIGN_RIGHT, 0); + lv_label_set_long_mode(candidates_label_, LV_LABEL_LONG_CLIP); + lv_obj_set_style_text_align(candidates_label_, LV_TEXT_ALIGN_LEFT, 0); } #if UI_SHARED_TOUCH_IME_ENABLED @@ -891,7 +902,8 @@ void ImeWidget::refresh_candidates() set_candidates_label_text(candidates_label_, ""); return; } - std::string text = make_candidates_text(ime_.candidates(), ime_.candidateIndex()); + std::string text = + make_candidates_text(ime_.candidates(), ime_.candidateIndex(), kCompactCandidatesPerPage, " "); set_candidates_label_text(candidates_label_, text.c_str()); } diff --git a/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp index ca7eb813..25b7ab86 100644 --- a/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp +++ b/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp @@ -4,6 +4,17 @@ namespace platform::ui::time { +namespace +{ + +constexpr ::time_t kMinValidEpochSeconds = 1577836800; // 2020-01-01 UTC + +bool is_valid_epoch(::time_t value) +{ + return value >= kMinValidEpochSeconds; +} + +} // namespace int timezone_offset_min() { @@ -26,7 +37,12 @@ bool localtime_now(struct tm* out_tm) { return false; } - const ::time_t local = apply_timezone_offset(::time(nullptr)); + const ::time_t now = ::time(nullptr); + if (!is_valid_epoch(now)) + { + return false; + } + const ::time_t local = apply_timezone_offset(now); const ::tm* tmp = ::gmtime(&local); if (!tmp) { diff --git a/platform/esp/arduino_common/src/screen_sleep.cpp b/platform/esp/arduino_common/src/screen_sleep.cpp index 94dff7ce..a0b898f6 100644 --- a/platform/esp/arduino_common/src/screen_sleep.cpp +++ b/platform/esp/arduino_common/src/screen_sleep.cpp @@ -37,7 +37,7 @@ SemaphoreHandle_t s_activity_mutex = nullptr; TaskHandle_t s_screen_sleep_task_handle = nullptr; uint32_t s_last_user_activity_time = 0; bool s_screen_sleeping = false; -bool s_screen_sleep_disabled = false; +uint32_t s_screen_sleep_disable_depth = 0; uint8_t s_saved_screen_brightness = DEVICE_MAX_BRIGHTNESS_LEVEL; uint8_t s_saved_keyboard_brightness = 127; bool s_screen_saver_active = false; @@ -237,7 +237,8 @@ void screenSleepTask(void* pvParameters) const uint32_t time_since_activity = current_time - s_last_user_activity_time; const uint32_t current_timeout = getScreenSleepTimeout(); - if (s_screen_sleep_disabled) + const bool sleep_disabled = s_screen_sleep_disable_depth > 0; + if (sleep_disabled) { if (s_screen_sleeping) { @@ -395,7 +396,7 @@ bool isScreenSaverActive() void wakeScreenSaver() { - if (s_screen_sleep_disabled) + if (s_screen_sleep_disable_depth > 0) { updateUserActivity(); return; @@ -483,13 +484,18 @@ void disableScreenSleep() { if (xSemaphoreTake(s_activity_mutex, portMAX_DELAY) == pdTRUE) { - s_screen_sleep_disabled = true; - if (s_screen_saver_active) + const bool was_disabled = s_screen_sleep_disable_depth > 0; + if (s_screen_sleep_disable_depth < UINT32_MAX) + { + ++s_screen_sleep_disable_depth; + } + + if (!was_disabled && s_screen_saver_active) { s_screen_saver_active = false; hide_saver = true; } - if (s_screen_sleeping) + if (!was_disabled && s_screen_sleeping) { s_screen_sleeping = false; board.setBrightness(s_saved_screen_brightness); @@ -513,8 +519,14 @@ void enableScreenSleep() { if (xSemaphoreTake(s_activity_mutex, portMAX_DELAY) == pdTRUE) { - s_screen_sleep_disabled = false; - s_last_user_activity_time = millis(); + if (s_screen_sleep_disable_depth > 0) + { + --s_screen_sleep_disable_depth; + if (s_screen_sleep_disable_depth == 0) + { + s_last_user_activity_time = millis(); + } + } xSemaphoreGive(s_activity_mutex); } } @@ -527,7 +539,7 @@ bool isScreenSleepDisabled() { if (xSemaphoreTake(s_activity_mutex, pdMS_TO_TICKS(10)) == pdTRUE) { - disabled = s_screen_sleep_disabled; + disabled = s_screen_sleep_disable_depth > 0; xSemaphoreGive(s_activity_mutex); } } diff --git a/platform/esp/idf_common/src/platform_ui_time_runtime.cpp b/platform/esp/idf_common/src/platform_ui_time_runtime.cpp index db4f7952..d8a2d3c8 100644 --- a/platform/esp/idf_common/src/platform_ui_time_runtime.cpp +++ b/platform/esp/idf_common/src/platform_ui_time_runtime.cpp @@ -4,6 +4,17 @@ namespace platform::ui::time { +namespace +{ + +constexpr ::time_t kMinValidEpochSeconds = 1577836800; // 2020-01-01 UTC + +bool is_valid_epoch(::time_t value) +{ + return value >= kMinValidEpochSeconds; +} + +} // namespace int timezone_offset_min() { @@ -26,7 +37,12 @@ bool localtime_now(struct tm* out_tm) { return false; } - const time_t local = apply_timezone_offset(::time(nullptr)); + const ::time_t now = ::time(nullptr); + if (!is_valid_epoch(now)) + { + return false; + } + const ::time_t local = apply_timezone_offset(now); const tm* tmp = gmtime(&local); if (!tmp) { diff --git a/platform/nrf52/arduino_common/src/platform_ui_screen_runtime.cpp b/platform/nrf52/arduino_common/src/platform_ui_screen_runtime.cpp index b7b2171d..48c44f87 100644 --- a/platform/nrf52/arduino_common/src/platform_ui_screen_runtime.cpp +++ b/platform/nrf52/arduino_common/src/platform_ui_screen_runtime.cpp @@ -13,7 +13,7 @@ constexpr uint32_t kScreenTimeoutDefaultMs = 30000UL; constexpr uint32_t kScreenTimeoutMinMs = 15000UL; constexpr uint32_t kScreenTimeoutMaxMs = 300000UL; -bool s_sleep_disabled = false; +uint32_t s_sleep_disable_depth = 0; uint32_t normalize_timeout_ms(uint32_t timeout_ms) { @@ -62,7 +62,7 @@ bool is_sleeping() bool is_sleep_disabled() { - return s_sleep_disabled; + return s_sleep_disable_depth > 0; } bool is_saver_active() @@ -84,12 +84,18 @@ void update_user_activity() void disable_sleep() { - s_sleep_disabled = true; + if (s_sleep_disable_depth < UINT32_MAX) + { + ++s_sleep_disable_depth; + } } void enable_sleep() { - s_sleep_disabled = false; + if (s_sleep_disable_depth > 0) + { + --s_sleep_disable_depth; + } } } // namespace platform::ui::screen diff --git a/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp b/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp index dcaec110..830273f4 100644 --- a/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp +++ b/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp @@ -11,6 +11,7 @@ namespace constexpr const char* kSettingsNs = "settings"; constexpr const char* kTimezoneKey = "timezone_offset"; +constexpr time_t kMinValidEpochSeconds = 1577836800; // 2020-01-01 UTC int stored_timezone_offset_min() { @@ -44,7 +45,12 @@ bool localtime_now(struct tm* out_tm) { return false; } - const time_t now = apply_timezone_offset(static_cast(sys::epoch_seconds_now())); + const time_t utc_now = static_cast(sys::epoch_seconds_now()); + if (utc_now < kMinValidEpochSeconds) + { + return false; + } + const time_t now = apply_timezone_offset(utc_now); const tm* tmp = gmtime(&now); if (!tmp) {