From 1bfe64580c882955186a1949b7f112209b44bc43 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 09:58:08 -0500 Subject: [PATCH] Kiosk: System Name entry in System Settings Adds a System Name text-entry page (hint 'e.g. Living Room', 1-40 char validation) and a data-driven System Settings row for it. Saving persists locally and nudges the background sync; conflict/invalid results surface the message and restore the previous name. Extends the settings-menu tests. --- CMakeLists.txt | 1 + src/settings_menu.cpp | 1 + src/settings_menu.h | 1 + src/system_name_page.cpp | 226 +++++++++++++++++++++++++++++++++++ src/system_name_page.h | 44 +++++++ src/system_settings_page.cpp | 4 + tests/test_settings_menu.cpp | 17 +-- 7 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 src/system_name_page.cpp create mode 100644 src/system_name_page.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 40e1e55..edddfe7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(radiantwave src/settings.cpp src/settings_menu.cpp src/system_settings_page.cpp + src/system_name_page.cpp src/wifi_setup_page.cpp src/audio_settings_page.cpp src/audio/audio_manager.cpp diff --git a/src/settings_menu.cpp b/src/settings_menu.cpp index cbf9e4f..a645bf0 100644 --- a/src/settings_menu.cpp +++ b/src/settings_menu.cpp @@ -6,6 +6,7 @@ std::vector systemSettingEntries() { return { {"Email Address", SystemSettingId::EmailAddress}, {"License Key", SystemSettingId::LicenseKey}, + {"System Name", SystemSettingId::SystemName}, {"WiFi Setup", SystemSettingId::WiFiSetup}, {"Time Zone Setup", SystemSettingId::Timezone}, {"Date Format", SystemSettingId::DateFormat}, diff --git a/src/settings_menu.h b/src/settings_menu.h index 96ca701..7165f13 100644 --- a/src/settings_menu.h +++ b/src/settings_menu.h @@ -9,6 +9,7 @@ namespace radiantwave::settings { enum class SystemSettingId { EmailAddress, LicenseKey, + SystemName, WiFiSetup, Timezone, DateFormat, diff --git a/src/system_name_page.cpp b/src/system_name_page.cpp new file mode 100644 index 0000000..b006e90 --- /dev/null +++ b/src/system_name_page.cpp @@ -0,0 +1,226 @@ +#include "system_name_page.h" +#include "app_state.h" +#include "network/system_name.h" +#include "logical_size.h" +#include + +namespace { + +constexpr size_t MAX_INPUT_LENGTH = 80; + +} // anonymous namespace + +void SystemNamePage::onPush(AppState& state) +{ + state_ = &state; + TTF_Init(); + createFonts(); + + storedValue_ = radiantwave::systemname::storedName(state.db); + input_ = storedValue_; + statusMessage_ = "Enter a System Name, then press ENTER"; + statusIsError_ = false; + + if (state.window) { + SDL_StartTextInput(state.window); + } +} + +void SystemNamePage::onResume(AppState& state) +{ + state_ = &state; + createFonts(); + storedValue_ = radiantwave::systemname::storedName(state.db); + input_ = storedValue_; + + if (state.window) { + SDL_StartTextInput(state.window); + } +} + +void SystemNamePage::onPause(AppState& state) +{ + state.keybinds.unbindAll(); + if (state.window) { + SDL_StopTextInput(state.window); + } + destroyFonts(); + destroyTextures(); +} + +void SystemNamePage::onPop(AppState& state) +{ + if (state.window) { + SDL_StopTextInput(state.window); + } + destroyFonts(); + destroyTextures(); + TTF_Quit(); +} + +void SystemNamePage::createFonts() +{ + destroyFonts(); + fontTitle_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 52); + fontHeader_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 28); + fontBody_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 32); + fontSmall_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 22); +} + +void SystemNamePage::destroyFonts() +{ + if (fontTitle_) { TTF_CloseFont(fontTitle_); fontTitle_ = nullptr; } + if (fontHeader_) { TTF_CloseFont(fontHeader_); fontHeader_ = nullptr; } + if (fontBody_) { TTF_CloseFont(fontBody_); fontBody_ = nullptr; } + if (fontSmall_) { TTF_CloseFont(fontSmall_); fontSmall_ = nullptr; } +} + +void SystemNamePage::destroyTextures() +{ + titleText_.destroy(); + storedText_.destroy(); + valueText_.destroy(); + statusText_.destroy(); + hintText_.destroy(); +} + +void SystemNamePage::submit() +{ + if (!state_) return; + + // Empty/whitespace is invalid and never clears the stored name. + std::string error; + if (!radiantwave::systemname::storeLocalName(state_->db, input_, &error)) { + statusMessage_ = error; + statusIsError_ = true; + return; + } + + storedValue_ = radiantwave::systemname::storedName(state_->db); + input_ = storedValue_; + statusMessage_ = "Saved. Syncing with the portal..."; + statusIsError_ = false; + + // Never block the UI: the background service performs the PUT and retries. + state_->systemNameSync.requestSync(); +} + +void SystemNamePage::pollSyncResult() +{ + if (!state_) return; + const std::string message = radiantwave::systemname::consumeMessage(state_->db); + if (message.empty()) return; + + statusMessage_ = message; + statusIsError_ = true; + storedValue_ = radiantwave::systemname::storedName(state_->db); + input_ = storedValue_; +} + +void SystemNamePage::handleEvent(const SDL_Event& event) +{ + if (!state_) return; + + if (event.type == SDL_EVENT_TEXT_INPUT) { + if (input_.size() < MAX_INPUT_LENGTH) { + input_ += event.text.text; + } + return; + } + + if (event.type == SDL_EVENT_KEY_DOWN) { + SDL_Keycode key = event.key.key; + + if (key == SDLK_BACKSPACE) { + if (!input_.empty()) { + input_.pop_back(); + } + } else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) { + submit(); + } else if (key == SDLK_ESCAPE) { + state_->pages.pop(*state_); + } + } +} + +void SystemNamePage::update(float dt) +{ + pollSyncResult(); +} + +void SystemNamePage::render(SDL_Renderer* renderer) +{ + int winW, winH; + radiantwave::getLayoutSize(renderer, &winW, &winH); + float fw = static_cast(winW); + float fh = static_cast(winH); + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255); + SDL_FRect bgRect{0, 0, fw, fh}; + SDL_RenderFillRect(renderer, &bgRect); + + if (!fontTitle_ || !fontHeader_ || !fontBody_ || !fontSmall_) return; + + SDL_Color white = {255, 255, 255, 255}; + SDL_Color yellow = {255, 255, 0, 255}; + SDL_Color cyan = {100, 220, 255, 255}; + SDL_Color gray = {180, 180, 180, 255}; + SDL_Color red = {240, 100, 100, 255}; + + float currentY = 30.0f; + + float boxW = 700.0f; + float boxH = 70.0f; + float boxX = (fw - boxW) / 2.0f; + + titleText_.ensure(renderer, fontTitle_, "System Name", white); + if (titleText_.texture) { + float x = (fw - titleText_.width) / 2.0f; + SDL_FRect dst{x, currentY, titleText_.width, titleText_.height}; + SDL_RenderTexture(renderer, titleText_.texture, nullptr, &dst); + currentY += titleText_.height + 30.0f; + } + + std::string storedStr = storedValue_.empty() ? "Not set" : storedValue_; + storedText_.ensure(renderer, fontHeader_, "Saved: " + storedStr, + storedValue_.empty() ? gray : yellow); + if (storedText_.texture) { + SDL_FRect dst{boxX, currentY, storedText_.width, storedText_.height}; + SDL_RenderTexture(renderer, storedText_.texture, nullptr, &dst); + } + currentY += 50.0f; + + SDL_SetRenderDrawColor(renderer, 35, 40, 60, 255); + SDL_FRect boxRect{boxX, currentY, boxW, boxH}; + SDL_RenderFillRect(renderer, &boxRect); + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + SDL_RenderRect(renderer, &boxRect); + + std::string display = input_ + "_"; + valueText_.ensure(renderer, fontBody_, display, cyan); + if (valueText_.texture) { + SDL_FRect dst{boxX + 20.0f, currentY + (boxH - valueText_.height) / 2.0f, + valueText_.width, valueText_.height}; + SDL_RenderTexture(renderer, valueText_.texture, nullptr, &dst); + } + currentY += boxH + 30.0f; + + if (!statusMessage_.empty()) { + statusText_.ensure(renderer, fontHeader_, statusMessage_, + statusIsError_ ? red : gray); + if (statusText_.texture) { + SDL_FRect dst{boxX, currentY, statusText_.width, statusText_.height}; + SDL_RenderTexture(renderer, statusText_.texture, nullptr, &dst); + } + currentY += 50.0f; + } + + hintText_.ensure(renderer, fontSmall_, + "e.g. Living Room [Type] Enter [ENTER] Save [BACKSPACE] Delete [ESC] Back", gray); + if (hintText_.texture) { + float x = (fw - hintText_.width) / 2.0f; + SDL_FRect dst{x, fh - 40.0f, hintText_.width, hintText_.height}; + SDL_RenderTexture(renderer, hintText_.texture, nullptr, &dst); + } +} diff --git a/src/system_name_page.h b/src/system_name_page.h new file mode 100644 index 0000000..8f60eef --- /dev/null +++ b/src/system_name_page.h @@ -0,0 +1,44 @@ +#pragma once + +#include "page.h" +#include "text_cache.h" +#include +#include + +// Text-entry page for the Kiosk's System Name (a human-readable label for this +// License Key). Saves locally first, then syncs in the background so the UI is +// never blocked on the network. +class SystemNamePage : public Page { +public: + void onPush(AppState& state) override; + void onResume(AppState& state) override; + void onPause(AppState& state) override; + void onPop(AppState& state) override; + void handleEvent(const SDL_Event& event) override; + void update(float dt) override; + void render(SDL_Renderer* renderer) override; + +private: + void createFonts(); + void destroyFonts(); + void destroyTextures(); + void submit(); + void pollSyncResult(); + + AppState* state_ = nullptr; + std::string storedValue_; + std::string input_; + std::string statusMessage_; + bool statusIsError_ = false; + + TTF_Font* fontTitle_ = nullptr; + TTF_Font* fontHeader_ = nullptr; + TTF_Font* fontBody_ = nullptr; + TTF_Font* fontSmall_ = nullptr; + + CachedText titleText_; + CachedText storedText_; + CachedText valueText_; + CachedText statusText_; + CachedText hintText_; +}; diff --git a/src/system_settings_page.cpp b/src/system_settings_page.cpp index fda054a..04fd05d 100644 --- a/src/system_settings_page.cpp +++ b/src/system_settings_page.cpp @@ -6,6 +6,7 @@ #include "logical_size.h" #include "timezone_page.h" #include "wifi_setup_page.h" +#include "system_name_page.h" #include #include #include @@ -76,6 +77,9 @@ void SystemSettingsPage::activateSelected(AppState &state) case radiantwave::settings::SystemSettingId::LicenseKey: state.pages.push(state, std::make_unique()); break; + case radiantwave::settings::SystemSettingId::SystemName: + state.pages.push(state, std::make_unique()); + break; case radiantwave::settings::SystemSettingId::WiFiSetup: state.pages.push(state, std::make_unique()); break; diff --git a/tests/test_settings_menu.cpp b/tests/test_settings_menu.cpp index 7a65950..0846b20 100644 --- a/tests/test_settings_menu.cpp +++ b/tests/test_settings_menu.cpp @@ -7,17 +7,19 @@ namespace settings = radiantwave::settings; TEST(SystemSettingsMenuTest, ListsTheSystemSettingsInOrder) { const auto entries = settings::systemSettingEntries(); - ASSERT_EQ(entries.size(), 5u); + ASSERT_EQ(entries.size(), 6u); EXPECT_EQ(entries[0].label, "Email Address"); EXPECT_EQ(entries[0].id, settings::SystemSettingId::EmailAddress); EXPECT_EQ(entries[1].label, "License Key"); EXPECT_EQ(entries[1].id, settings::SystemSettingId::LicenseKey); - EXPECT_EQ(entries[2].label, "WiFi Setup"); - EXPECT_EQ(entries[2].id, settings::SystemSettingId::WiFiSetup); - EXPECT_EQ(entries[3].label, "Time Zone Setup"); - EXPECT_EQ(entries[3].id, settings::SystemSettingId::Timezone); - EXPECT_EQ(entries[4].label, "Date Format"); - EXPECT_EQ(entries[4].id, settings::SystemSettingId::DateFormat); + EXPECT_EQ(entries[2].label, "System Name"); + EXPECT_EQ(entries[2].id, settings::SystemSettingId::SystemName); + EXPECT_EQ(entries[3].label, "WiFi Setup"); + EXPECT_EQ(entries[3].id, settings::SystemSettingId::WiFiSetup); + EXPECT_EQ(entries[4].label, "Time Zone Setup"); + EXPECT_EQ(entries[4].id, settings::SystemSettingId::Timezone); + EXPECT_EQ(entries[5].label, "Date Format"); + EXPECT_EQ(entries[5].id, settings::SystemSettingId::DateFormat); } TEST(SystemSettingsMenuTest, LabelsAreNonEmptyAndUnique) { @@ -38,6 +40,7 @@ TEST(SystemSettingsMenuTest, EveryEntryMapsToAKnownPage) { switch (entry.id) { case settings::SystemSettingId::EmailAddress: case settings::SystemSettingId::LicenseKey: + case settings::SystemSettingId::SystemName: case settings::SystemSettingId::WiFiSetup: case settings::SystemSettingId::Timezone: case settings::SystemSettingId::DateFormat: