From c339dc7d9770d24dfec9b8a9817a228f567f4436 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:42:18 +0200 Subject: [PATCH] refactor(ui): share single KeyboardWidget instance across screens Moves KeyboardWidget from a per-screen value member to a single instance owned by UITask, passed to SettingsScreen, QuickMsgScreen, and BotScreen as a pointer. Only one screen is ever active at a time, so the shared state is safe. Saves ~1.5 KB of always-resident heap. Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/ui-new/BotScreen.h | 20 ++++++++--------- .../companion_radio/ui-new/QuickMsgScreen.h | 22 +++++++++---------- .../companion_radio/ui-new/SettingsScreen.h | 16 +++++++------- examples/companion_radio/ui-new/UITask.cpp | 7 +++--- examples/companion_radio/ui-new/UITask.h | 2 ++ 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index 9be907bb..49596f17 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -13,8 +13,8 @@ class BotScreen : public UIScreen { bool _dirty; // keyboard state (reused for trigger and reply fields) - int _kb_field; // -1=off, 2=trigger, 3=reply DM, 4=reply Ch - KeyboardWidget _kb; + int _kb_field; // -1=off, 2=trigger, 3=reply DM, 4=reply Ch + KeyboardWidget* _kb; // channel cache (refreshed on enter) int _num_channels; @@ -38,7 +38,7 @@ class BotScreen : public UIScreen { } public: - BotScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {} + BotScreen(UITask* task, NodePrefs* prefs, KeyboardWidget* kb) : _task(task), _prefs(prefs), _kb(kb) {} void enter() { _sel = 0; @@ -52,7 +52,7 @@ public: display.setColor(DisplayDriver::LIGHT); if (_kb_field >= 0) { - return _kb.render(display); + return _kb->render(display); } int avail_h = display.height() - display.listStart(); @@ -109,17 +109,17 @@ public: bool cancel = (c == KEY_CANCEL || c == KEY_CONTEXT_MENU); if (_kb_field >= 0) { - auto res = _kb.handleInput(c); + auto res = _kb->handleInput(c); if (res == KeyboardWidget::DONE) { if (_kb_field == 2) { - strncpy(_prefs->bot_trigger, _kb.buf, sizeof(_prefs->bot_trigger) - 1); + strncpy(_prefs->bot_trigger, _kb->buf, sizeof(_prefs->bot_trigger) - 1); _prefs->bot_trigger[sizeof(_prefs->bot_trigger) - 1] = '\0'; for (char* p = _prefs->bot_trigger; *p; p++) *p = (char)tolower((uint8_t)*p); } else if (_kb_field == 3) { - strncpy(_prefs->bot_reply_dm, _kb.buf, sizeof(_prefs->bot_reply_dm) - 1); + strncpy(_prefs->bot_reply_dm, _kb->buf, sizeof(_prefs->bot_reply_dm) - 1); _prefs->bot_reply_dm[sizeof(_prefs->bot_reply_dm) - 1] = '\0'; } else { - strncpy(_prefs->bot_reply_ch, _kb.buf, sizeof(_prefs->bot_reply_ch) - 1); + strncpy(_prefs->bot_reply_ch, _kb->buf, sizeof(_prefs->bot_reply_ch) - 1); _prefs->bot_reply_ch[sizeof(_prefs->bot_reply_ch) - 1] = '\0'; } _dirty = true; @@ -180,9 +180,9 @@ public: initial = _prefs->bot_reply_ch; max = sizeof(_prefs->bot_reply_ch) - 1; } - _kb.begin(initial, max); + _kb->begin(initial, max); if (_sel == 2) { - _kb.clearPlaceholders(); // trigger is literal text — placeholders never match incoming msgs + _kb->clearPlaceholders(); // trigger is literal text — placeholders never match incoming msgs } else { kbAddSensorPlaceholders(_kb, &sensors); } diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index c3ee4ddc..32ca9dbe 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -45,7 +45,7 @@ class QuickMsgScreen : public UIScreen { static const int CH_HIST_MAX = 96; // KEYBOARD - KeyboardWidget _kb; + KeyboardWidget* _kb; // Context menu (opened by KEY_CONTEXT_MENU in CHANNEL_PICK / CONTACT_PICK / histories) PopupMenu _ctx_menu; @@ -152,7 +152,7 @@ class QuickMsgScreen : public UIScreen { void beginShareCompose(bool channel) { _sending_to_channel = channel; _reply_mode = false; - _kb.begin(_share_text); + _kb->begin(_share_text); _phase = KEYBOARD; } @@ -494,8 +494,8 @@ class QuickMsgScreen : public UIScreen { } public: - QuickMsgScreen(UITask* task) - : _task(task), _phase(MODE_SELECT), _mode_sel(0), + QuickMsgScreen(UITask* task, KeyboardWidget* kb) + : _task(task), _kb(kb), _phase(MODE_SELECT), _mode_sel(0), _contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false), _channel_sel(0), _channel_scroll(0), _num_channels(0), _sel_channel_idx(0), _sending_to_channel(false), @@ -1045,7 +1045,7 @@ public: if (_ctx_menu.active) _ctx_menu.render(display); } else if (_phase == KEYBOARD) { - return _kb.render(display); + return _kb->render(display); } else { // MSG_PICK char title[24]; @@ -1540,21 +1540,21 @@ public: } } else if (_phase == KEYBOARD) { - auto res = _kb.handleInput(c); + auto res = _kb->handleInput(c); if (res == KeyboardWidget::CANCELLED) { if (_share_mode) { _share_mode = false; _task->gotoHomeScreen(); } else { _phase = MSG_PICK; } } else if (res == KeyboardWidget::DONE) { int prefix_len = _reply_mode ? (int)strlen(_reply_prefix) : 0; - if (_kb.len > prefix_len) { + if (_kb->len > prefix_len) { // Expand only the body — prefix "@[nick] " is preserved verbatim, so a nick // that happens to contain a placeholder token isn't substituted. char expanded[KB_MAX_LEN + 1]; if (prefix_len > 0) { - memcpy(expanded, _kb.buf, prefix_len); - expandMsg(_kb.buf + prefix_len, expanded + prefix_len, sizeof(expanded) - prefix_len); + memcpy(expanded, _kb->buf, prefix_len); + expandMsg(_kb->buf + prefix_len, expanded + prefix_len, sizeof(expanded) - prefix_len); } else { - expandMsg(_kb.buf, expanded, sizeof(expanded)); + expandMsg(_kb->buf, expanded, sizeof(expanded)); } bool ok = sendText(expanded); afterSend(ok, expanded); @@ -1581,7 +1581,7 @@ public: } if (c == KEY_ENTER) { if (_msg_sel == 0) { - _kb.begin(_reply_mode ? _reply_prefix : ""); + _kb->begin(_reply_mode ? _reply_prefix : ""); kbAddSensorPlaceholders(_kb, &sensors); _phase = KEYBOARD; return true; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index c6e95e90..d8fe5dd0 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -572,12 +572,12 @@ class SettingsScreen : public UIScreen { } // Keyboard state for editing message slots - int _edit_slot; // -1 = not editing, 0..9 = slot being edited - KeyboardWidget _kb; + int _edit_slot; // -1 = not editing, 0..9 = slot being edited + KeyboardWidget* _kb; public: - SettingsScreen(UITask* task) - : _task(task), _selected(SECTION_DISPLAY), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) { + SettingsScreen(UITask* task, KeyboardWidget* kb) + : _task(task), _kb(kb), _selected(SECTION_DISPLAY), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) { buildVis(); } @@ -594,7 +594,7 @@ public: display.setTextSize(1); if (_edit_slot >= 0) { - return _kb.render(display); + return _kb->render(display); } int item_h = display.lineStep(); @@ -629,10 +629,10 @@ public: // Keyboard editing mode for message slots if (_edit_slot >= 0) { - auto res = _kb.handleInput(c); + auto res = _kb->handleInput(c); if (res == KeyboardWidget::DONE) { if (p) { - strncpy(p->custom_msgs[_edit_slot], _kb.buf, sizeof(p->custom_msgs[0]) - 1); + strncpy(p->custom_msgs[_edit_slot], _kb->buf, sizeof(p->custom_msgs[0]) - 1); p->custom_msgs[_edit_slot][sizeof(p->custom_msgs[0]) - 1] = '\0'; _dirty = true; } @@ -834,7 +834,7 @@ public: _edit_slot = slot; // Bound to the custom_msgs store (140 B) so the wider keyboard buffer // can't overflow it on save. - _kb.begin(p ? p->custom_msgs[slot] : "", + _kb->begin(p ? p->custom_msgs[slot] : "", p ? (int)sizeof(p->custom_msgs[slot]) - 1 : KB_MAX_LEN); kbAddSensorPlaceholders(_kb, &sensors); return true; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1ed0ace5..a7ab99ef 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -105,7 +105,6 @@ public: static const int QUICK_MSGS_MAX = 10; -#include "KeyboardWidget.h" #include "FullscreenMsgView.h" #include "SensorPlaceholders.h" #include "SettingsScreen.h" @@ -1198,11 +1197,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no splash = new SplashScreen(this); home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); - settings = new SettingsScreen(this); - quick_msg = new QuickMsgScreen(this); + settings = new SettingsScreen(this, &_kb); + quick_msg = new QuickMsgScreen(this, &_kb); tools_screen = new ToolsScreen(this); ringtone_edit = new RingtoneEditorScreen(this, node_prefs); - bot_screen = new BotScreen(this, node_prefs); + bot_screen = new BotScreen(this, node_prefs, &_kb); nearby_screen = new NearbyScreen(this); dashboard_config = new DashboardConfigScreen(this, node_prefs); auto_advert_screen = new AutoAdvertScreen(this, node_prefs); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 205ad75e..10936881 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -24,6 +24,7 @@ #include "../NodePrefs.h" #include "../Trail.h" #include "../Waypoint.h" +#include "KeyboardWidget.h" class UITask : public AbstractUITask { DisplayDriver* _display; @@ -43,6 +44,7 @@ class UITask : public AbstractUITask { bool _lock_seq_used; // true = suppress next back_btn CLICK (post-sequence release) char _alert[80]; char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies + KeyboardWidget _kb; // shared across all screens — only one active at a time unsigned long _alert_expiry; int _msgcount; int _room_unread;