mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-05-25 00:26:12 +00:00
v1.2.2: Fix trackball, add display name support, announce with app_data
- Fix trackball completely dead: remove gpio_reset_pin() calls that prevented GPIO interrupts from registering on ESP32-S3 - Add trackball left/right tab cycling when screen doesn't consume event - Add NameInputScreen: Ratspeak-branded first-boot name input - Add display name to all announces as MessagePack app_data - Add TEXT_INPUT setting type; expose Display Name in Settings - HomeScreen Enter key sends announce with toast notification - Bump version to 1.2.2
This commit is contained in:
+3
-3
@@ -5,9 +5,9 @@
|
||||
// =============================================================================
|
||||
|
||||
#define RATDECK_VERSION_MAJOR 1
|
||||
#define RATDECK_VERSION_MINOR 0
|
||||
#define RATDECK_VERSION_PATCH 0
|
||||
#define RATDECK_VERSION_STRING "1.0.0"
|
||||
#define RATDECK_VERSION_MINOR 2
|
||||
#define RATDECK_VERSION_PATCH 2
|
||||
#define RATDECK_VERSION_STRING "1.2.2"
|
||||
|
||||
// --- Feature Flags ---
|
||||
#define HAS_DISPLAY true
|
||||
|
||||
+1
-19
@@ -1,5 +1,4 @@
|
||||
#include "Trackball.h"
|
||||
#include <driver/gpio.h>
|
||||
|
||||
volatile int8_t Trackball::_deltaX = 0;
|
||||
volatile int8_t Trackball::_deltaY = 0;
|
||||
@@ -9,20 +8,6 @@ Trackball* Trackball::_instance = nullptr;
|
||||
bool Trackball::begin() {
|
||||
_instance = this;
|
||||
|
||||
// Detach any stale interrupts (GPIO state may persist after USB download mode reset)
|
||||
detachInterrupt(digitalPinToInterrupt(TBALL_UP));
|
||||
detachInterrupt(digitalPinToInterrupt(TBALL_DOWN));
|
||||
detachInterrupt(digitalPinToInterrupt(TBALL_LEFT));
|
||||
detachInterrupt(digitalPinToInterrupt(TBALL_RIGHT));
|
||||
detachInterrupt(digitalPinToInterrupt(TBALL_CLICK));
|
||||
|
||||
// Reset pins to a known state before configuring
|
||||
gpio_reset_pin((gpio_num_t)TBALL_UP);
|
||||
gpio_reset_pin((gpio_num_t)TBALL_DOWN);
|
||||
gpio_reset_pin((gpio_num_t)TBALL_LEFT);
|
||||
gpio_reset_pin((gpio_num_t)TBALL_RIGHT);
|
||||
gpio_reset_pin((gpio_num_t)TBALL_CLICK);
|
||||
|
||||
// Configure trackball GPIOs as inputs with pullup
|
||||
pinMode(TBALL_UP, INPUT_PULLUP);
|
||||
pinMode(TBALL_DOWN, INPUT_PULLUP);
|
||||
@@ -30,9 +15,6 @@ bool Trackball::begin() {
|
||||
pinMode(TBALL_RIGHT, INPUT_PULLUP);
|
||||
pinMode(TBALL_CLICK, INPUT_PULLUP);
|
||||
|
||||
// Small delay to let pullups stabilize
|
||||
delay(5);
|
||||
|
||||
// Attach interrupts for movement detection
|
||||
attachInterrupt(digitalPinToInterrupt(TBALL_UP), isrUp, FALLING);
|
||||
attachInterrupt(digitalPinToInterrupt(TBALL_DOWN), isrRight, FALLING); // Physical down pin = rightward
|
||||
@@ -40,7 +22,7 @@ bool Trackball::begin() {
|
||||
attachInterrupt(digitalPinToInterrupt(TBALL_RIGHT), isrDown, FALLING); // Physical right pin = downward
|
||||
attachInterrupt(digitalPinToInterrupt(TBALL_CLICK), isrClick, FALLING);
|
||||
|
||||
Serial.println("[TRACKBALL] Initialized (GPIO reset + interrupts attached)");
|
||||
Serial.println("[TRACKBALL] Initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+70
-14
@@ -26,6 +26,7 @@
|
||||
#include "ui/screens/SettingsScreen.h"
|
||||
#include "ui/screens/HelpOverlay.h"
|
||||
#include "ui/screens/MapScreen.h"
|
||||
#include "ui/screens/NameInputScreen.h"
|
||||
#include "storage/FlashStore.h"
|
||||
#include "storage/SDStore.h"
|
||||
#include "storage/MessageStore.h"
|
||||
@@ -90,6 +91,7 @@ MessageView messageView;
|
||||
SettingsScreen settingsScreen;
|
||||
HelpOverlay helpOverlay;
|
||||
MapScreen mapScreen;
|
||||
NameInputScreen nameInputScreen;
|
||||
|
||||
// Tab-screen mapping (5 tabs)
|
||||
Screen* tabScreens[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};
|
||||
@@ -110,6 +112,29 @@ constexpr unsigned long HEARTBEAT_INTERVAL_MS = 5000;
|
||||
unsigned long loopCycleStart = 0;
|
||||
unsigned long maxLoopTime = 0;
|
||||
|
||||
// =============================================================================
|
||||
// Announce with display name (MessagePack-encoded app_data)
|
||||
// =============================================================================
|
||||
|
||||
static RNS::Bytes encodeAnnounceName(const String& name) {
|
||||
if (name.isEmpty()) return {};
|
||||
size_t len = name.length();
|
||||
if (len > 31) len = 31;
|
||||
uint8_t buf[2 + 31];
|
||||
buf[0] = 0x91; // msgpack fixarray(1)
|
||||
buf[1] = 0xA0 | (uint8_t)len; // msgpack fixstr(len)
|
||||
memcpy(buf + 2, name.c_str(), len);
|
||||
return RNS::Bytes(buf, 2 + len);
|
||||
}
|
||||
|
||||
static void announceWithName() {
|
||||
RNS::Bytes appData = encodeAnnounceName(userConfig.settings().displayName);
|
||||
rns.announce(appData);
|
||||
ui.statusBar().flashAnnounce();
|
||||
ui.statusBar().showToast("Announce sent!");
|
||||
Serial.println("[ANNOUNCE] Sent with display name");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hotkey callbacks
|
||||
// =============================================================================
|
||||
@@ -131,9 +156,7 @@ void onHotkeySettings() {
|
||||
ui.setScreen(&settingsScreen);
|
||||
}
|
||||
void onHotkeyAnnounce() {
|
||||
rns.announce();
|
||||
ui.statusBar().flashAnnounce();
|
||||
ui.statusBar().showToast("Announce sent!");
|
||||
announceWithName();
|
||||
}
|
||||
void onHotkeyDiag() {
|
||||
Serial.println("=== DIAGNOSTIC DUMP ===");
|
||||
@@ -530,13 +553,15 @@ void setup() {
|
||||
delay(400);
|
||||
|
||||
bootComplete = true;
|
||||
ui.setBootMode(false);
|
||||
ui.statusBar().setTransportMode("Ratdeck");
|
||||
|
||||
// Wire up screen dependencies
|
||||
homeScreen.setReticulumManager(&rns);
|
||||
homeScreen.setRadio(&radio);
|
||||
homeScreen.setUserConfig(&userConfig);
|
||||
homeScreen.setAnnounceCallback([]() {
|
||||
announceWithName();
|
||||
});
|
||||
|
||||
nodesScreen.setAnnounceManager(announceManager);
|
||||
nodesScreen.setNodeSelectedCallback([](const std::string& peerHex) {
|
||||
@@ -586,13 +611,41 @@ void setup() {
|
||||
if (tabScreens[tab]) ui.setScreen(tabScreens[tab]);
|
||||
});
|
||||
|
||||
ui.setScreen(&homeScreen);
|
||||
ui.tabBar().setActiveTab(TabBar::TAB_HOME);
|
||||
// Name input screen (first boot only — when no display name is set)
|
||||
nameInputScreen.setDoneCallback([](const String& name) {
|
||||
userConfig.settings().displayName = name;
|
||||
userConfig.save(sdStore, flash);
|
||||
Serial.printf("[BOOT] Display name set: '%s'\n", name.c_str());
|
||||
|
||||
// Initial announce
|
||||
rns.announce();
|
||||
lastAutoAnnounce = millis();
|
||||
Serial.println("[BOOT] Initial announce sent");
|
||||
// Transition to home screen
|
||||
ui.setBootMode(false);
|
||||
ui.setScreen(&homeScreen);
|
||||
ui.tabBar().setActiveTab(TabBar::TAB_HOME);
|
||||
|
||||
// Initial announce with name
|
||||
RNS::Bytes appData = encodeAnnounceName(userConfig.settings().displayName);
|
||||
rns.announce(appData);
|
||||
lastAutoAnnounce = millis();
|
||||
ui.statusBar().flashAnnounce();
|
||||
Serial.println("[BOOT] Initial announce sent");
|
||||
});
|
||||
|
||||
if (userConfig.settings().displayName.isEmpty()) {
|
||||
// Show name input screen (boot mode keeps status/tab bars hidden)
|
||||
ui.setScreen(&nameInputScreen);
|
||||
Serial.println("[BOOT] Showing name input screen");
|
||||
} else {
|
||||
// Name already set — go straight to home
|
||||
ui.setBootMode(false);
|
||||
ui.setScreen(&homeScreen);
|
||||
ui.tabBar().setActiveTab(TabBar::TAB_HOME);
|
||||
|
||||
// Initial announce with name
|
||||
RNS::Bytes appData = encodeAnnounceName(userConfig.settings().displayName);
|
||||
rns.announce(appData);
|
||||
lastAutoAnnounce = millis();
|
||||
Serial.println("[BOOT] Initial announce sent");
|
||||
}
|
||||
|
||||
// Clear boot loop counter — we survived!
|
||||
{
|
||||
@@ -635,14 +688,16 @@ void loop() {
|
||||
// Screen gets the key next
|
||||
bool consumed = ui.handleKey(evt);
|
||||
|
||||
// Tab cycling: ,=left /=right (only if screen didn't consume)
|
||||
// Tab cycling: ,=left /=right OR trackball left/right (only if screen didn't consume)
|
||||
if (!consumed && !evt.ctrl) {
|
||||
if (evt.character == ',') {
|
||||
bool tabLeft = (evt.character == ',') || evt.left;
|
||||
bool tabRight = (evt.character == '/') || evt.right;
|
||||
if (tabLeft) {
|
||||
ui.tabBar().cycleTab(-1);
|
||||
int tab = ui.tabBar().getActiveTab();
|
||||
if (tabScreens[tab]) ui.setScreen(tabScreens[tab]);
|
||||
}
|
||||
if (evt.character == '/') {
|
||||
if (tabRight) {
|
||||
ui.tabBar().cycleTab(1);
|
||||
int tab = ui.tabBar().getActiveTab();
|
||||
if (tabScreens[tab]) ui.setScreen(tabScreens[tab]);
|
||||
@@ -657,7 +712,8 @@ void loop() {
|
||||
// 4. Auto-announce every 5 minutes
|
||||
if (bootComplete && millis() - lastAutoAnnounce >= ANNOUNCE_INTERVAL_MS) {
|
||||
lastAutoAnnounce = millis();
|
||||
rns.announce();
|
||||
RNS::Bytes appData = encodeAnnounceName(userConfig.settings().displayName);
|
||||
rns.announce(appData);
|
||||
ui.statusBar().flashAnnounce();
|
||||
Serial.println("[AUTO] Periodic announce");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ void HomeScreen::update() {
|
||||
}
|
||||
}
|
||||
|
||||
bool HomeScreen::handleKey(const KeyEvent& event) {
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_announceCb) _announceCb();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void HomeScreen::draw(LGFX_TDeck& gfx) {
|
||||
int x = 4;
|
||||
int y = Theme::CONTENT_Y + 4;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/UIManager.h"
|
||||
#include <functional>
|
||||
|
||||
class ReticulumManager;
|
||||
class SX1262;
|
||||
@@ -9,10 +10,12 @@ class UserConfig;
|
||||
class HomeScreen : public Screen {
|
||||
public:
|
||||
void update() override;
|
||||
bool handleKey(const KeyEvent& event) override;
|
||||
|
||||
void setReticulumManager(ReticulumManager* rns) { _rns = rns; }
|
||||
void setRadio(SX1262* radio) { _radio = radio; }
|
||||
void setUserConfig(UserConfig* cfg) { _cfg = cfg; }
|
||||
void setAnnounceCallback(std::function<void()> cb) { _announceCb = cb; }
|
||||
|
||||
const char* title() const override { return "Home"; }
|
||||
void draw(LGFX_TDeck& gfx) override;
|
||||
@@ -23,4 +26,5 @@ private:
|
||||
UserConfig* _cfg = nullptr;
|
||||
unsigned long _lastUptime = 0;
|
||||
uint32_t _lastHeap = 0;
|
||||
std::function<void()> _announceCb;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "NameInputScreen.h"
|
||||
#include "ui/Theme.h"
|
||||
#include "config/Config.h"
|
||||
#include "hal/Display.h"
|
||||
|
||||
void NameInputScreen::draw(LGFX_TDeck& gfx) {
|
||||
int cx = Theme::SCREEN_W / 2;
|
||||
|
||||
// Ratspeak branding
|
||||
gfx.setTextSize(2);
|
||||
gfx.setTextColor(Theme::PRIMARY, Theme::BG);
|
||||
const char* brand = "RATSPEAK";
|
||||
int bw = strlen(brand) * 12;
|
||||
gfx.setCursor(cx - bw / 2, 30);
|
||||
gfx.print(brand);
|
||||
|
||||
// .org subtitle
|
||||
gfx.setTextSize(1);
|
||||
gfx.setTextColor(Theme::ACCENT, Theme::BG);
|
||||
const char* sub = "ratspeak.org";
|
||||
int sw = strlen(sub) * 6;
|
||||
gfx.setCursor(cx - sw / 2, 52);
|
||||
gfx.print(sub);
|
||||
|
||||
// Prompt
|
||||
gfx.setTextColor(Theme::SECONDARY, Theme::BG);
|
||||
const char* prompt = "Enter your display name";
|
||||
int pw = strlen(prompt) * 6;
|
||||
gfx.setCursor(cx - pw / 2, 85);
|
||||
gfx.print(prompt);
|
||||
|
||||
gfx.setTextColor(Theme::MUTED, Theme::BG);
|
||||
const char* opt = "(Optional - press Enter to skip)";
|
||||
int ow = strlen(opt) * 6;
|
||||
gfx.setCursor(cx - ow / 2, 100);
|
||||
gfx.print(opt);
|
||||
|
||||
// Text input field
|
||||
int fieldW = 200;
|
||||
int fieldH = 20;
|
||||
int fieldX = cx - fieldW / 2;
|
||||
int fieldY = 125;
|
||||
gfx.drawRect(fieldX, fieldY, fieldW, fieldH, Theme::PRIMARY);
|
||||
gfx.fillRect(fieldX + 1, fieldY + 1, fieldW - 2, fieldH - 2, Theme::SELECTION_BG);
|
||||
|
||||
// Name text
|
||||
gfx.setTextSize(1);
|
||||
gfx.setTextColor(Theme::PRIMARY, Theme::SELECTION_BG);
|
||||
gfx.setCursor(fieldX + 6, fieldY + 6);
|
||||
gfx.print(_name);
|
||||
|
||||
// Cursor blink (simple solid cursor)
|
||||
int cursorX = fieldX + 6 + _nameLen * 6;
|
||||
if (cursorX < fieldX + fieldW - 8) {
|
||||
bool blink = (millis() / 500) % 2 == 0;
|
||||
if (blink) {
|
||||
gfx.fillRect(cursorX, fieldY + 4, 6, 12, Theme::PRIMARY);
|
||||
}
|
||||
}
|
||||
|
||||
// OK hint
|
||||
gfx.setTextColor(Theme::ACCENT, Theme::BG);
|
||||
const char* hint = "[Enter] OK";
|
||||
int hw = strlen(hint) * 6;
|
||||
gfx.setCursor(cx - hw / 2, 160);
|
||||
gfx.print(hint);
|
||||
|
||||
// Version at bottom
|
||||
gfx.setTextColor(Theme::MUTED, Theme::BG);
|
||||
char ver[32];
|
||||
snprintf(ver, sizeof(ver), "Ratdeck v%s", RATDECK_VERSION_STRING);
|
||||
int vw = strlen(ver) * 6;
|
||||
gfx.setCursor(cx - vw / 2, 190);
|
||||
gfx.print(ver);
|
||||
}
|
||||
|
||||
bool NameInputScreen::handleKey(const KeyEvent& event) {
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_doneCb) _doneCb(String(_name));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.del || event.character == 8) {
|
||||
if (_nameLen > 0) {
|
||||
_nameLen--;
|
||||
_name[_nameLen] = '\0';
|
||||
markDirty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (event.character >= 0x20 && event.character <= 0x7E && _nameLen < MAX_NAME_LEN) {
|
||||
_name[_nameLen] = event.character;
|
||||
_nameLen++;
|
||||
_name[_nameLen] = '\0';
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true; // Consume all keys on this screen
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/UIManager.h"
|
||||
#include <functional>
|
||||
|
||||
class NameInputScreen : public Screen {
|
||||
public:
|
||||
bool handleKey(const KeyEvent& event) override;
|
||||
const char* title() const override { return "Setup"; }
|
||||
void draw(LGFX_TDeck& gfx) override;
|
||||
|
||||
void setDoneCallback(std::function<void(const String&)> cb) { _doneCb = cb; }
|
||||
|
||||
static constexpr int MAX_NAME_LEN = 16;
|
||||
|
||||
private:
|
||||
char _name[MAX_NAME_LEN + 1] = {0};
|
||||
int _nameLen = 0;
|
||||
std::function<void(const String&)> _doneCb;
|
||||
};
|
||||
@@ -34,7 +34,8 @@ bool SettingsScreen::isEditable(int idx) const {
|
||||
if (idx < 0 || idx >= (int)_items.size()) return false;
|
||||
auto t = _items[idx].type;
|
||||
return t == SettingType::INTEGER || t == SettingType::TOGGLE
|
||||
|| t == SettingType::ENUM_CHOICE || t == SettingType::ACTION;
|
||||
|| t == SettingType::ENUM_CHOICE || t == SettingType::ACTION
|
||||
|| t == SettingType::TEXT_INPUT;
|
||||
}
|
||||
|
||||
void SettingsScreen::skipToNextEditable(int dir) {
|
||||
@@ -85,6 +86,15 @@ void SettingsScreen::buildItems() {
|
||||
[](int) { return String(RATDECK_VERSION_STRING); }});
|
||||
_items.push_back({"Identity", SettingType::READONLY, nullptr, nullptr,
|
||||
[this](int) { return _identityHash.substring(0, 16); }});
|
||||
{
|
||||
SettingItem nameItem;
|
||||
nameItem.label = "Display Name";
|
||||
nameItem.type = SettingType::TEXT_INPUT;
|
||||
nameItem.textGetter = [&s]() { return s.displayName; };
|
||||
nameItem.textSetter = [&s](const String& v) { s.displayName = v; };
|
||||
nameItem.maxTextLen = 16;
|
||||
_items.push_back(nameItem);
|
||||
}
|
||||
|
||||
// --- Display ---
|
||||
_items.push_back({"-- Display --", SettingType::HEADER, nullptr, nullptr, nullptr});
|
||||
@@ -168,8 +178,20 @@ void SettingsScreen::buildItems() {
|
||||
announceItem.type = SettingType::ACTION;
|
||||
announceItem.formatter = [](int) { return String("[Press Enter]"); };
|
||||
announceItem.action = [this]() {
|
||||
if (_rns) {
|
||||
_rns->announce();
|
||||
if (_rns && _cfg) {
|
||||
// Encode display name as msgpack app_data
|
||||
const String& name = _cfg->settings().displayName;
|
||||
RNS::Bytes appData;
|
||||
if (!name.isEmpty()) {
|
||||
size_t len = name.length();
|
||||
if (len > 31) len = 31;
|
||||
uint8_t buf[2 + 31];
|
||||
buf[0] = 0x91;
|
||||
buf[1] = 0xA0 | (uint8_t)len;
|
||||
memcpy(buf + 2, name.c_str(), len);
|
||||
appData = RNS::Bytes(buf, 2 + len);
|
||||
}
|
||||
_rns->announce(appData);
|
||||
if (_ui) {
|
||||
_ui->statusBar().flashAnnounce();
|
||||
_ui->statusBar().showToast("Announce sent!");
|
||||
@@ -268,6 +290,7 @@ void SettingsScreen::onEnter() {
|
||||
_selectedIdx = 0;
|
||||
_scrollOffset = 0;
|
||||
_editing = false;
|
||||
_textEditing = false;
|
||||
// Skip to first editable item
|
||||
if (!isEditable(_selectedIdx)) {
|
||||
skipToNextEditable(1);
|
||||
@@ -318,6 +341,27 @@ void SettingsScreen::draw(LGFX_TDeck& gfx) {
|
||||
gfx.setCursor(valX, y + 3);
|
||||
gfx.print(hint.c_str());
|
||||
}
|
||||
} else if (item.type == SettingType::TEXT_INPUT) {
|
||||
// Text input field
|
||||
gfx.setTextColor(Theme::SECONDARY, bgCol);
|
||||
gfx.setCursor(4, y + 3);
|
||||
gfx.print(item.label);
|
||||
|
||||
if (_textEditing && selected) {
|
||||
// Editing: show text with cursor
|
||||
gfx.setTextColor(Theme::WARNING_CLR, bgCol);
|
||||
gfx.setCursor(valX, y + 3);
|
||||
gfx.print(_editText.c_str());
|
||||
int curX = valX + _editText.length() * 6;
|
||||
if ((millis() / 500) % 2 == 0) {
|
||||
gfx.fillRect(curX, y + 3, 6, 8, Theme::WARNING_CLR);
|
||||
}
|
||||
} else {
|
||||
String val = item.textGetter ? item.textGetter() : "";
|
||||
gfx.setTextColor(val.isEmpty() ? Theme::MUTED : Theme::PRIMARY, bgCol);
|
||||
gfx.setCursor(valX, y + 3);
|
||||
gfx.print(val.isEmpty() ? "(not set)" : val.c_str());
|
||||
}
|
||||
} else {
|
||||
// Label
|
||||
gfx.setTextColor(Theme::SECONDARY, bgCol);
|
||||
@@ -378,6 +422,32 @@ void SettingsScreen::draw(LGFX_TDeck& gfx) {
|
||||
bool SettingsScreen::handleKey(const KeyEvent& event) {
|
||||
if (_items.empty()) return false;
|
||||
|
||||
if (_textEditing) {
|
||||
// Text edit mode
|
||||
auto& item = _items[_selectedIdx];
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (item.textSetter) item.textSetter(_editText);
|
||||
_textEditing = false;
|
||||
applyAndSave();
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
if (event.del || event.character == 8) {
|
||||
if (_editText.length() > 0) {
|
||||
_editText.remove(_editText.length() - 1);
|
||||
markDirty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.character >= 0x20 && event.character <= 0x7E
|
||||
&& (int)_editText.length() < item.maxTextLen) {
|
||||
_editText += (char)event.character;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return true; // Consume all keys in text edit mode
|
||||
}
|
||||
|
||||
if (_editing) {
|
||||
// Edit mode: left/right change value, enter confirms, backspace/del cancels
|
||||
auto& item = _items[_selectedIdx];
|
||||
@@ -432,6 +502,11 @@ bool SettingsScreen::handleKey(const KeyEvent& event) {
|
||||
// Execute action callback
|
||||
if (item.action) item.action();
|
||||
markDirty();
|
||||
} else if (item.type == SettingType::TEXT_INPUT) {
|
||||
// Enter text edit mode
|
||||
_textEditing = true;
|
||||
_editText = item.textGetter ? item.textGetter() : "";
|
||||
markDirty();
|
||||
} else if (item.type == SettingType::TOGGLE) {
|
||||
// Toggle immediately
|
||||
int val = item.getter ? item.getter() : 0;
|
||||
|
||||
@@ -21,7 +21,8 @@ enum class SettingType : uint8_t {
|
||||
INTEGER,
|
||||
TOGGLE,
|
||||
ENUM_CHOICE,
|
||||
ACTION // Button — triggers callback on Enter
|
||||
ACTION, // Button — triggers callback on Enter
|
||||
TEXT_INPUT // Editable text field
|
||||
};
|
||||
|
||||
struct SettingItem {
|
||||
@@ -37,6 +38,10 @@ struct SettingItem {
|
||||
std::vector<const char*> enumLabels;
|
||||
// For ACTION: callback on Enter
|
||||
std::function<void()> action;
|
||||
// For TEXT_INPUT: string getter/setter
|
||||
std::function<String()> textGetter;
|
||||
std::function<void(const String&)> textSetter;
|
||||
int maxTextLen = 16;
|
||||
};
|
||||
|
||||
class SettingsScreen : public Screen {
|
||||
@@ -87,4 +92,7 @@ private:
|
||||
int _scrollOffset = 0;
|
||||
bool _editing = false;
|
||||
int _editValue = 0;
|
||||
// For TEXT_INPUT editing
|
||||
bool _textEditing = false;
|
||||
String _editText;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user