CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
T9InputView.cpp
Go to the documentation of this file.
1
6
10#include "cdc_views/ToastView.h"
11#include "cdc_ui/ViewStack.h"
12#include "cdc_ui/I18n.h"
13#include "cdc_hal/IDisplay.h"
14#include "cdc_log.h"
15#include "esp_timer.h"
16#include <goodisplay/gdey029T94.h>
17#include <cstring>
18
19static const char* TAG = "T9InputView";
20
28static const char* t9_chars[] = {
29 " 0", // 0
30 ".@?!,;:'\"()-_#$%&*+=/\\<>[]{}|^~`1"
31 "\x9B\x9C\x9D\xA8\xAD\xAE\xAF\xAB\xAC\xF1\xF8\xFD\xE6\xF6", // 1: ¢ £ ¥ ¿ ¡ « » ½ ¼ ± ° ² µ ÷
32 "abcABC2\x84\xA0\x83\x85\x86\x91\x8E\x8F\x92\x87\x80", // 2: ä á â à å æ Ä Å Æ ç Ç
33 "defDEF3\x82\x8A\x88\x89\x90", // 3: é è ê ë É
34 "ghiGHI4\xA1\x8D\x8C\x8B", // 4: í ì î ï
35 "jklJKL5", // 5
36 "mnoMNO6\xA2\x95\x93\x94\x99\xA4\xA5", // 6: ó ò ô ö Ö ñ Ñ
37 "pqrsPQRS7\xE1", // 7: ß
38 "tuvTUV8\x81\x9A\xA3\x97\x96", // 8: ü Ü ú ù û
39 "wxyzWXYZ9\x98" // 9: ÿ
40};
41
45static constexpr int TITLE_Y = 5;
46static constexpr int TEXT_Y = 50;
47static constexpr int TEXT_MARGIN = 10;
48
49namespace cdc::ui {
50
58void T9InputView::init(const char* title, const char* initialText, uint16_t maxLen) {
59 if (title) {
60 strncpy(titleBuf_, title, TITLE_MAX_LEN);
63 } else {
64 titleBuf_[0] = '\0';
66 }
67 maxLen_ = maxLen > MAX_TEXT_LEN ? MAX_TEXT_LEN : maxLen;
68
69 // Copy initial text
70 if (initialText) {
71 strncpy(text_, initialText, maxLen_);
72 text_[maxLen_] = '\0';
73 len_ = strlen(text_);
74 } else {
75 text_[0] = '\0';
76 len_ = 0;
77 }
78
79 // Reset T9 state
80 lastKey_ = 0;
81 charIndex_ = 0;
82 lastPressMs_ = 0;
83 cursorActive_ = false;
84 onSave_ = nullptr;
85 onCancel_ = nullptr;
86 hintOverride_ = nullptr;
87 placeholder_ = nullptr;
88 dirty_ = true;
89
90 LOG_D(TAG, "init: title='%s', maxLen=%d", title ? title : "(null)", maxLen_);
91}
92
99char T9InputView::getChar(char key, uint8_t index) {
100 if (key < '0' || key > '9') return '\0';
101 const char* chars = t9_chars[key - '0'];
102 uint8_t count = strlen(chars);
103 return chars[index % count];
104}
105
111uint8_t T9InputView::getCharCount(char key) {
112 if (key < '0' || key > '9') return 0;
113 return strlen(t9_chars[key - '0']);
114}
115
122 if (key < '0' || key > '9') return false;
123
124 uint32_t now = static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
125 bool sameKey = (key == lastKey_);
126 bool timeout = (now - lastPressMs_) > TIMEOUT_MS;
127
128 if (sameKey && !timeout && len_ > 0) {
129 // Cycle through characters for the same key
130 charIndex_++;
131 uint8_t charCount = getCharCount(key);
132 if (charIndex_ >= charCount) {
133 charIndex_ = 0;
134 }
135 // Replace last character
136 text_[len_ - 1] = getChar(key, charIndex_);
137 cursorActive_ = true;
138 } else {
139 // New key or timeout - commit previous and add new
140 if (len_ < maxLen_) {
141 text_[len_++] = getChar(key, 0);
142 text_[len_] = '\0';
143 charIndex_ = 0;
144 cursorActive_ = true;
145 } else {
146 ui::showToastError(ui::tr("core.t9_full"), 800);
147 }
148 }
149
150 lastKey_ = key;
151 lastPressMs_ = now;
152 dirty_ = true;
153
154 return true;
155}
156
162 if (len_ > 0) {
163 len_--;
164 text_[len_] = '\0';
165 lastKey_ = 0;
166 cursorActive_ = false;
167 dirty_ = true;
168 }
169}
170
177 if (key < '0' || key > '9') return;
178
179 // A long-press is always preceded by a short-press of the same key, which
180 // already inserted the first T9 multi-tap character for that key. Replace
181 // that pending character with the literal digit instead of appending.
182 if (lastKey_ == key && len_ > 0) {
183 text_[len_ - 1] = key;
185 dirty_ = true;
186 LOG_D(TAG, "forceDigit (replace): key='%c', text='%s'", key, text_);
187 return;
188 }
189
191
192 if (len_ < maxLen_) {
193 text_[len_++] = key;
194 text_[len_] = '\0';
195 dirty_ = true;
196 LOG_D(TAG, "forceDigit (append): key='%c', text='%s'", key, text_);
197 } else {
198 ui::showToastError(ui::tr("core.t9_full"), 800);
199 }
200}
201
202uint16_t T9InputView::appendRaw(const char* text) {
203 if (!text) return 0;
205 uint16_t added = 0;
206 while (*text && len_ < maxLen_) {
207 text_[len_++] = *text++;
208 added++;
209 }
210 text_[len_] = '\0';
211 lastKey_ = 0;
212 charIndex_ = 0;
213 dirty_ = true;
214 return added;
215}
216
222 if (lastKey_ != 0) {
223 lastKey_ = 0;
224 cursorActive_ = false;
225 dirty_ = true;
226 }
227}
228
234void T9InputView::onTick(uint32_t nowMs) {
235 (void)nowMs; // Use own timestamp for consistent timing
236
237 // Check for timeout to commit character
238 if (lastKey_ != 0) {
239 uint32_t now = static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
240 // Safe comparison: only timeout if now > lastPressMs_ (avoid unsigned wrap)
241 if (now >= lastPressMs_ && (now - lastPressMs_) > TIMEOUT_MS) {
243 }
244 }
245}
246
253 switch (key) {
254 case KEY_YES: // Confirm
256 if (onSave_) {
257 // Pop ourselves FIRST, then call callback
258 // This prevents the callback's pushed view from being popped
260 onSave_(text_);
261 }
262 return InputResult::CONSUMED; // Already popped ourselves
263
264 case KEY_NO: // Backspace
265 backspace();
267
268 default:
269 if (key >= '0' && key <= '9') {
270 processKey(key);
272 }
274 }
275}
276
283 if (key == KEY_NO) {
284 // Cancel: pop ourselves FIRST, then notify, so a view pushed by the
285 // callback is not popped by the dispatcher (mirrors the confirm path).
287 if (onCancel_) {
288 onCancel_();
289 }
291 }
292
293 if (key >= '0' && key <= '9') {
294 // Force insert digit
295 forceDigit(key);
297 }
298
300}
301
306const char* T9InputView::getFooterHint() const {
307 return hintOverride_ ? hintOverride_ : ui::tr("core.hint_t9_input");
308}
309
315void T9InputView::render(bool partial) {
317 if (!display) return;
318
319 auto* gfx = static_cast<Gdey029T94*>(display->getNativeHandle());
320 if (!gfx) return;
321
322 const uint16_t width = display->getWidth();
323 const uint16_t height = display->getHeight();
324
325 if (!partial) {
326 gfx->fillScreen(EPD_WHITE);
327 }
328
329 gfx->setTextColor(EPD_BLACK);
330 gfx->setTextSize(1);
331
332 // Title + underline
334
335 // Text input area
336 gfx->fillRect(TEXT_MARGIN, TEXT_Y - 5, width - TEXT_MARGIN * 2, 30, EPD_WHITE);
337 gfx->drawRect(TEXT_MARGIN - 2, TEXT_Y - 7, width - TEXT_MARGIN * 2 + 4, 34, EPD_BLACK);
338
339 gfx->setCursor(TEXT_MARGIN + 2, TEXT_Y);
340
341 if (len_ == 0 && placeholder_) {
342 // Show placeholder when empty
343 gfx->setTextColor(EPD_DARKGREY);
345 gfx->setTextColor(EPD_BLACK);
346 } else {
347 // Show text with cursor
348 for (uint16_t i = 0; i < len_; i++) {
349 // If this is the last char and cursor is active, invert it
350 if (cursorActive_ && i == len_ - 1) {
351 int16_t x = gfx->getCursorX();
352 int16_t y = gfx->getCursorY();
353 gfx->fillRect(x, y - 2, 8, 14, EPD_BLACK);
354 gfx->setTextColor(EPD_WHITE);
355 gfx->print(text_[i]);
356 gfx->setTextColor(EPD_BLACK);
357 } else {
358 gfx->print(text_[i]);
359 }
360 }
361
362 // Show cursor at end if not in T9 cycle
363 if (!cursorActive_) {
364 gfx->print("|");
365 }
366 }
367
368 // Footer with hint
369 char countStr[16];
370 snprintf(countStr, sizeof(countStr), "%u/%u ", len_, maxLen_);
371 const char* hint = getFooterHint();
372 render::drawFooterBar(gfx, width, height, countStr, hint, true);
373
374 dirty_ = false;
375}
376
380
382
391T9InputView* showT9Input(const char* title, const char* initialText,
392 T9InputView::SaveCallback onSave, uint16_t maxLen) {
393 s_sharedT9Input.init(title, initialText, maxLen);
394 s_sharedT9Input.setOnSave(onSave);
396 return &s_sharedT9Input;
397}
398
399} // namespace cdc::ui
static const char * TAG
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
static constexpr int TEXT_MARGIN
Definition InfoView.cpp:28
static constexpr int TITLE_Y
Display layout constants.
static const char * t9_chars[]
T9 digit-to-character mapping table.
static constexpr int TEXT_Y
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
const char * placeholder_
Definition T9InputView.h:98
void backspace()
Removes the last character from the input buffer.
void commitCharacter()
Commits the currently active multi-tap character.
void(*)(const char *text) SaveCallback
Definition T9InputView.h:29
char text_[MAX_TEXT_LEN+1]
InputResult onLongPress(char key) override
Handles long-press actions for clear and forced digit insertion.
char titleBuf_[TITLE_MAX_LEN+1]
Definition T9InputView.h:96
SaveCallback onSave_
const char * getFooterHint() const override
Returns localized footer hint text.
void init(const char *title, const char *initialText=nullptr, uint16_t maxLen=MAX_TEXT_LEN)
Initializes T9 input state and optional initial text.
static constexpr uint32_t TIMEOUT_MS
Definition T9InputView.h:23
static constexpr uint16_t MAX_TEXT_LEN
Definition T9InputView.h:22
bool processKey(char key)
Processes a numeric key press using multi-tap logic.
CancelCallback onCancel_
const char * hintOverride_
Definition T9InputView.h:99
uint16_t appendRaw(const char *text)
void render(bool partial) override
Renders title, text entry box, cursor state, and footer.
void forceDigit(char key)
Inserts a numeric digit literally, bypassing multi-tap mapping.
static char getChar(char key, uint8_t index)
Returns the character for a key/index in the T9 mapping.
static uint8_t getCharCount(char key)
Returns the number of mapped characters for a key.
void onTick(uint32_t nowMs) override
Handles timeout-based commit for active multi-tap input.
const char * title_
Definition T9InputView.h:97
InputResult onKey(char key) override
Handles key input for save, backspace, and digit entry.
static constexpr uint16_t TITLE_MAX_LEN
Definition T9InputView.h:95
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
void push(IView *view, void *context=nullptr)
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
void drawFooterBar(Gdey029T94 *gfx, uint16_t width, uint16_t height, const char *prefix, const char *hint, bool force=false)
Draws footer bar with optional prefix and hint text.
void drawHeaderLeft(Gdey029T94 *gfx, const char *title, int x, int y, uint16_t width, int underlineOffset=18)
void printText(Gdey029T94 *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:208
static constexpr int TEXT_Y
Gdey029T94 * display
static T9InputView s_sharedT9Input
Convenience factory/helper function.
InputResult
Definition IView.h:10
static constexpr char KEY_NO
Cancel / Back / Backspace.
Definition KeyCodes.h:44
T9InputView * showT9Input(const char *title, const char *initialText, T9InputView::SaveCallback onSave, uint16_t maxLen=128)
Shows a shared T9 input view instance.
static constexpr int TITLE_Y
Layout constants mirror the ones used by T9InputView.
static const char * TAG
void showToastError(const char *message, uint16_t durationMs=1500)
Shows an error toast message.
static constexpr char KEY_YES
Confirm / OK / Save.
Definition KeyCodes.h:41
static constexpr int TEXT_MARGIN