Fix underscore on keyboard and standardize activity (#138)

## Summary

* Fix underscore on keyboard
  * Remove special handling of special row characters
* Fix navigating between special row items
* Standardize keyboard activity to use standard loop
  * Fix issue with rendering keyboard non-stop

Fixes https://github.com/daveallie/crosspoint-reader/issues/131
This commit is contained in:
Dave Allie 2025-12-28 17:57:06 +10:00 committed by GitHub
parent 9023b262a1
commit 02350c6a9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 207 additions and 222 deletions

View File

@ -187,11 +187,21 @@ void WifiSelectionActivity::selectNetwork(const int index) {
if (selectedRequiresPassword) { if (selectedRequiresPassword) {
// Show password entry // Show password entry
state = WifiSelectionState::PASSWORD_ENTRY; state = WifiSelectionState::PASSWORD_ENTRY;
enterNewActivity(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password", enterNewActivity(new KeyboardEntryActivity(
renderer, inputManager, "Enter WiFi Password",
"", // No initial text "", // No initial text
50, // Y position
64, // Max password length 64, // Max password length
false // Show password by default (hard keyboard to use) false, // Show password by default (hard keyboard to use)
)); [this](const std::string& text) {
enteredPassword = text;
exitActivity();
},
[this] {
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
exitActivity();
}));
updateRequired = true; updateRequired = true;
} else { } else {
// Connect directly for open networks // Connect directly for open networks
@ -208,11 +218,6 @@ void WifiSelectionActivity::attemptConnection() {
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
// Get password from keyboard if we just entered it
if (subActivity && !usedSavedPassword) {
enteredPassword = static_cast<KeyboardEntryActivity*>(subActivity.get())->getText();
}
if (selectedRequiresPassword && !enteredPassword.empty()) { if (selectedRequiresPassword && !enteredPassword.empty()) {
WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str());
} else { } else {
@ -269,6 +274,11 @@ void WifiSelectionActivity::checkConnectionStatus() {
} }
void WifiSelectionActivity::loop() { void WifiSelectionActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
// Check scan progress // Check scan progress
if (state == WifiSelectionState::SCANNING) { if (state == WifiSelectionState::SCANNING) {
processWifiScanResults(); processWifiScanResults();
@ -281,27 +291,12 @@ void WifiSelectionActivity::loop() {
return; return;
} }
// Handle password entry state if (state == WifiSelectionState::PASSWORD_ENTRY) {
if (state == WifiSelectionState::PASSWORD_ENTRY && subActivity) { // Reach here once password entry finished in subactivity
const auto keyboard = static_cast<KeyboardEntryActivity*>(subActivity.get());
keyboard->handleInput();
if (keyboard->isComplete()) {
attemptConnection(); attemptConnection();
return; return;
} }
if (keyboard->isCancelled()) {
state = WifiSelectionState::NETWORK_LIST;
exitActivity();
updateRequired = true;
return;
}
updateRequired = true;
return;
}
// Handle save prompt state // Handle save prompt state
if (state == WifiSelectionState::SAVE_PROMPT) { if (state == WifiSelectionState::SAVE_PROMPT) {
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) { if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
@ -441,6 +436,10 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi
void WifiSelectionActivity::displayTaskLoop() { void WifiSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (subActivity) {
return;
}
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -461,9 +460,6 @@ void WifiSelectionActivity::render() const {
case WifiSelectionState::NETWORK_LIST: case WifiSelectionState::NETWORK_LIST:
renderNetworkList(); renderNetworkList();
break; break;
case WifiSelectionState::PASSWORD_ENTRY:
renderPasswordEntry();
break;
case WifiSelectionState::CONNECTING: case WifiSelectionState::CONNECTING:
renderConnecting(); renderConnecting();
break; break;
@ -561,23 +557,6 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", ""); renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", "");
} }
void WifiSelectionActivity::renderPasswordEntry() const {
// Draw header
renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD);
// Draw network name with good spacing from header
std::string networkInfo = "Network: " + selectedSSID;
if (networkInfo.length() > 30) {
networkInfo.replace(27, networkInfo.length() - 27, "...");
}
renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR);
// Draw keyboard
if (subActivity) {
static_cast<KeyboardEntryActivity*>(subActivity.get())->render(58);
}
}
void WifiSelectionActivity::renderConnecting() const { void WifiSelectionActivity::renderConnecting() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);

View File

@ -10,41 +10,55 @@ const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = {
// Keyboard layouts - uppercase/symbols // Keyboard layouts - uppercase/symbols
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"", const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
"ZXCVBNM<>?", "^ _____<OK"}; "ZXCVBNM<>?", "SPECIAL ROW"};
void KeyboardEntryActivity::setText(const std::string& newText) { void KeyboardEntryActivity::taskTrampoline(void* param) {
text = newText; auto* self = static_cast<KeyboardEntryActivity*>(param);
if (maxLength > 0 && text.length() > maxLength) { self->displayTaskLoop();
text.resize(maxLength);
}
} }
void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string& newInitialText) { void KeyboardEntryActivity::displayTaskLoop() {
if (!newTitle.empty()) { while (true) {
title = newTitle; if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
} }
text = newInitialText;
selectedRow = 0;
selectedCol = 0;
shiftActive = false;
complete = false;
cancelled = false;
} }
void KeyboardEntryActivity::onEnter() { void KeyboardEntryActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
// Reset state when entering the activity renderingMutex = xSemaphoreCreateMutex();
complete = false;
cancelled = false; // Trigger first update
updateRequired = true;
xTaskCreate(&KeyboardEntryActivity::taskTrampoline, "KeyboardEntryActivity",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void KeyboardEntryActivity::loop() { void KeyboardEntryActivity::onExit() {
handleInput(); Activity::onExit();
render(10);
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
int KeyboardEntryActivity::getRowLength(int row) const { int KeyboardEntryActivity::getRowLength(const int row) const {
if (row < 0 || row >= NUM_ROWS) return 0; if (row < 0 || row >= NUM_ROWS) return 0;
// Return actual length of each row based on keyboard layout // Return actual length of each row based on keyboard layout
@ -58,7 +72,7 @@ int KeyboardEntryActivity::getRowLength(int row) const {
case 3: case 3:
return 10; // zxcvbnm,./ return 10; // zxcvbnm,./
case 4: case 4:
return 10; // ^, space (5 wide), backspace, OK (2 wide) return 10; // caps (2 wide), space (5 wide), backspace (2 wide), OK
default: default:
return 0; return 0;
} }
@ -75,8 +89,8 @@ char KeyboardEntryActivity::getSelectedChar() const {
void KeyboardEntryActivity::handleKeyPress() { void KeyboardEntryActivity::handleKeyPress() {
// Handle special row (bottom row with shift, space, backspace, done) // Handle special row (bottom row with shift, space, backspace, done)
if (selectedRow == SHIFT_ROW) { if (selectedRow == SPECIAL_ROW) {
if (selectedCol == SHIFT_COL) { if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
// Shift toggle // Shift toggle
shiftActive = !shiftActive; shiftActive = !shiftActive;
return; return;
@ -90,7 +104,7 @@ void KeyboardEntryActivity::handleKeyPress() {
return; return;
} }
if (selectedCol == BACKSPACE_COL) { if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) {
// Backspace // Backspace
if (!text.empty()) { if (!text.empty()) {
text.pop_back(); text.pop_back();
@ -100,7 +114,6 @@ void KeyboardEntryActivity::handleKeyPress() {
if (selectedCol >= DONE_COL) { if (selectedCol >= DONE_COL) {
// Done button // Done button
complete = true;
if (onComplete) { if (onComplete) {
onComplete(text); onComplete(text);
} }
@ -109,8 +122,11 @@ void KeyboardEntryActivity::handleKeyPress() {
} }
// Regular character // Regular character
char c = getSelectedChar(); const char c = getSelectedChar();
if (c != '\0' && c != '^' && c != '_' && c != '<') { if (c == '\0') {
return;
}
if (maxLength == 0 || text.length() < maxLength) { if (maxLength == 0 || text.length() < maxLength) {
text += c; text += c;
// Auto-disable shift after typing a letter // Auto-disable shift after typing a letter
@ -118,33 +134,49 @@ void KeyboardEntryActivity::handleKeyPress() {
shiftActive = false; shiftActive = false;
} }
} }
}
} }
bool KeyboardEntryActivity::handleInput() { void KeyboardEntryActivity::loop() {
if (complete || cancelled) {
return false;
}
bool handled = false;
// Navigation // Navigation
if (inputManager.wasPressed(InputManager::BTN_UP)) { if (inputManager.wasPressed(InputManager::BTN_UP)) {
if (selectedRow > 0) { if (selectedRow > 0) {
selectedRow--; selectedRow--;
// Clamp column to valid range for new row // Clamp column to valid range for new row
int maxCol = getRowLength(selectedRow) - 1; const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol; if (selectedCol > maxCol) selectedCol = maxCol;
} }
handled = true; updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_DOWN)) { }
if (inputManager.wasPressed(InputManager::BTN_DOWN)) {
if (selectedRow < NUM_ROWS - 1) { if (selectedRow < NUM_ROWS - 1) {
selectedRow++; selectedRow++;
int maxCol = getRowLength(selectedRow) - 1; const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol; if (selectedCol > maxCol) selectedCol = maxCol;
} }
handled = true; updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_LEFT)) { }
if (inputManager.wasPressed(InputManager::BTN_LEFT)) {
// Special bottom row case
if (selectedRow == SPECIAL_ROW) {
// Bottom row has special key widths
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
// In shift key, do nothing
} else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
// In space bar, move to shift
selectedCol = SHIFT_COL;
} else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) {
// In backspace, move to space
selectedCol = SPACE_COL;
} else if (selectedCol >= DONE_COL) {
// At done button, move to backspace
selectedCol = BACKSPACE_COL;
}
updateRequired = true;
return;
}
if (selectedCol > 0) { if (selectedCol > 0) {
selectedCol--; selectedCol--;
} else if (selectedRow > 0) { } else if (selectedRow > 0) {
@ -152,9 +184,31 @@ bool KeyboardEntryActivity::handleInput() {
selectedRow--; selectedRow--;
selectedCol = getRowLength(selectedRow) - 1; selectedCol = getRowLength(selectedRow) - 1;
} }
handled = true; updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT)) { }
int maxCol = getRowLength(selectedRow) - 1;
if (inputManager.wasPressed(InputManager::BTN_RIGHT)) {
const int maxCol = getRowLength(selectedRow) - 1;
// Special bottom row case
if (selectedRow == SPECIAL_ROW) {
// Bottom row has special key widths
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
// In shift key, move to space
selectedCol = SPACE_COL;
} else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
// In space bar, move to backspace
selectedCol = BACKSPACE_COL;
} else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) {
// In backspace, move to done
selectedCol = DONE_COL;
} else if (selectedCol >= DONE_COL) {
// At done button, do nothing
}
updateRequired = true;
return;
}
if (selectedCol < maxCol) { if (selectedCol < maxCol) {
selectedCol++; selectedCol++;
} else if (selectedRow < NUM_ROWS - 1) { } else if (selectedRow < NUM_ROWS - 1) {
@ -162,35 +216,34 @@ bool KeyboardEntryActivity::handleInput() {
selectedRow++; selectedRow++;
selectedCol = 0; selectedCol = 0;
} }
handled = true; updateRequired = true;
} }
// Selection // Selection
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
handleKeyPress(); handleKeyPress();
handled = true; updateRequired = true;
} }
// Cancel // Cancel
if (inputManager.wasPressed(InputManager::BTN_BACK)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
cancelled = true;
if (onCancel) { if (onCancel) {
onCancel(); onCancel();
} }
handled = true; updateRequired = true;
} }
return handled;
} }
void KeyboardEntryActivity::render(int startY) const { void KeyboardEntryActivity::render() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
renderer.clearScreen();
// Draw title // Draw title
renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR);
// Draw input field // Draw input field
int inputY = startY + 22; const int inputY = startY + 22;
renderer.drawText(UI_FONT_ID, 10, inputY, "["); renderer.drawText(UI_FONT_ID, 10, inputY, "[");
std::string displayText; std::string displayText;
@ -204,9 +257,9 @@ void KeyboardEntryActivity::render(int startY) const {
displayText += "_"; displayText += "_";
// Truncate if too long for display - use actual character width from font // Truncate if too long for display - use actual character width from font
int charWidth = renderer.getSpaceWidth(UI_FONT_ID); int approxCharWidth = renderer.getSpaceWidth(UI_FONT_ID);
if (charWidth < 1) charWidth = 8; // Fallback to approximate width if (approxCharWidth < 1) approxCharWidth = 8; // Fallback to approximate width
int maxDisplayLen = (pageWidth - 40) / charWidth; const int maxDisplayLen = (pageWidth - 40) / approxCharWidth;
if (displayText.length() > static_cast<size_t>(maxDisplayLen)) { if (displayText.length() > static_cast<size_t>(maxDisplayLen)) {
displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3); displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3);
} }
@ -215,22 +268,22 @@ void KeyboardEntryActivity::render(int startY) const {
renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]"); renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]");
// Draw keyboard - use compact spacing to fit 5 rows on screen // Draw keyboard - use compact spacing to fit 5 rows on screen
int keyboardStartY = inputY + 25; const int keyboardStartY = inputY + 25;
const int keyWidth = 18; constexpr int keyWidth = 18;
const int keyHeight = 18; constexpr int keyHeight = 18;
const int keySpacing = 3; constexpr int keySpacing = 3;
const char* const* layout = shiftActive ? keyboardShift : keyboard; const char* const* layout = shiftActive ? keyboardShift : keyboard;
// Calculate left margin to center the longest row (13 keys) // Calculate left margin to center the longest row (13 keys)
int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
int leftMargin = (pageWidth - maxRowWidth) / 2; const int leftMargin = (pageWidth - maxRowWidth) / 2;
for (int row = 0; row < NUM_ROWS; row++) { for (int row = 0; row < NUM_ROWS; row++) {
int rowY = keyboardStartY + row * (keyHeight + keySpacing); const int rowY = keyboardStartY + row * (keyHeight + keySpacing);
// Left-align all rows for consistent navigation // Left-align all rows for consistent navigation
int startX = leftMargin; const int startX = leftMargin;
// Handle bottom row (row 4) specially with proper multi-column keys // Handle bottom row (row 4) specially with proper multi-column keys
if (row == 4) { if (row == 4) {
@ -240,64 +293,37 @@ void KeyboardEntryActivity::render(int startY) const {
int currentX = startX; int currentX = startX;
// CAPS key (logical col 0, spans 2 key widths) // CAPS key (logical col 0, spans 2 key widths)
int capsWidth = 2 * keyWidth + keySpacing; const bool capsSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
bool capsSelected = (selectedRow == 4 && selectedCol == SHIFT_COL); renderItemWithSelector(currentX + 2, rowY, shiftActive ? "CAPS" : "caps", capsSelected);
if (capsSelected) { currentX += 2 * (keyWidth + keySpacing);
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
renderer.drawText(UI_FONT_ID, currentX + capsWidth - 4, rowY, "]");
}
renderer.drawText(UI_FONT_ID, currentX + 2, rowY, shiftActive ? "CAPS" : "caps");
currentX += capsWidth + keySpacing;
// Space bar (logical cols 2-6, spans 5 key widths) // Space bar (logical cols 2-6, spans 5 key widths)
int spaceWidth = 5 * keyWidth + 4 * keySpacing; const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); const int spaceTextWidth = renderer.getTextWidth(UI_FONT_ID, "_____");
if (spaceSelected) { const int spaceXWidth = 5 * (keyWidth + keySpacing);
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2;
renderer.drawText(UI_FONT_ID, currentX + spaceWidth - 4, rowY, "]"); renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected);
} currentX += spaceXWidth;
// Draw centered underscores for space bar
int spaceTextX = currentX + (spaceWidth / 2) - 12;
renderer.drawText(UI_FONT_ID, spaceTextX, rowY, "_____");
currentX += spaceWidth + keySpacing;
// Backspace key (logical col 7, spans 2 key widths) // Backspace key (logical col 7, spans 2 key widths)
int bsWidth = 2 * keyWidth + keySpacing; const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL);
bool bsSelected = (selectedRow == 4 && selectedCol == BACKSPACE_COL); renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected);
if (bsSelected) { currentX += 2 * (keyWidth + keySpacing);
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
renderer.drawText(UI_FONT_ID, currentX + bsWidth - 4, rowY, "]");
}
renderer.drawText(UI_FONT_ID, currentX + 6, rowY, "<-");
currentX += bsWidth + keySpacing;
// OK button (logical col 9, spans 2 key widths) // OK button (logical col 9, spans 2 key widths)
int okWidth = 2 * keyWidth + keySpacing; const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL);
bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); renderItemWithSelector(currentX + 2, rowY, "OK", okSelected);
if (okSelected) {
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
renderer.drawText(UI_FONT_ID, currentX + okWidth - 4, rowY, "]");
}
renderer.drawText(UI_FONT_ID, currentX + 8, rowY, "OK");
} else { } else {
// Regular rows: render each key individually // Regular rows: render each key individually
for (int col = 0; col < getRowLength(row); col++) { for (int col = 0; col < getRowLength(row); col++) {
int keyX = startX + col * (keyWidth + keySpacing);
// Get the character to display // Get the character to display
char c = layout[row][col]; const char c = layout[row][col];
std::string keyLabel(1, c); std::string keyLabel(1, c);
const int charWidth = renderer.getTextWidth(UI_FONT_ID, keyLabel.c_str());
// Draw selection highlight const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2;
bool isSelected = (row == selectedRow && col == selectedCol); const bool isSelected = row == selectedRow && col == selectedCol;
renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected);
if (isSelected) {
renderer.drawText(UI_FONT_ID, keyX - 2, rowY, "[");
renderer.drawText(UI_FONT_ID, keyX + keyWidth - 4, rowY, "]");
}
renderer.drawText(UI_FONT_ID, keyX + 2, rowY, keyLabel.c_str());
} }
} }
} }
@ -305,4 +331,15 @@ void KeyboardEntryActivity::render(int startY) const {
// Draw help text at absolute bottom of screen (consistent with other screens) // Draw help text at absolute bottom of screen (consistent with other screens)
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK"); renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK");
renderer.displayBuffer();
}
void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item,
const bool isSelected) const {
if (isSelected) {
const int itemWidth = renderer.getTextWidth(UI_FONT_ID, item);
renderer.drawText(UI_FONT_ID, x - 6, y, "[");
renderer.drawText(UI_FONT_ID, x + itemWidth, y, "]");
}
renderer.drawText(UI_FONT_ID, x, y, item);
} }

View File

@ -1,9 +1,13 @@
#pragma once #pragma once
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <InputManager.h> #include <InputManager.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
#include <utility>
#include "../Activity.h" #include "../Activity.h"
@ -30,80 +34,44 @@ class KeyboardEntryActivity : public Activity {
* @param inputManager Reference to InputManager for handling input * @param inputManager Reference to InputManager for handling input
* @param title Title to display above the keyboard * @param title Title to display above the keyboard
* @param initialText Initial text to show in the input field * @param initialText Initial text to show in the input field
* @param startY Y position to start rendering the keyboard
* @param maxLength Maximum length of input text (0 for unlimited) * @param maxLength Maximum length of input text (0 for unlimited)
* @param isPassword If true, display asterisks instead of actual characters * @param isPassword If true, display asterisks instead of actual characters
* @param onComplete Callback invoked when input is complete
* @param onCancel Callback invoked when input is cancelled
*/ */
KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text", explicit KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, std::string title = "Enter Text",
const std::string& initialText = "", const size_t maxLength = 0, const bool isPassword = false) std::string initialText = "", const int startY = 10, const size_t maxLength = 0,
const bool isPassword = false, OnCompleteCallback onComplete = nullptr,
OnCancelCallback onCancel = nullptr)
: Activity("KeyboardEntry", renderer, inputManager), : Activity("KeyboardEntry", renderer, inputManager),
title(title), title(std::move(title)),
text(initialText), text(std::move(initialText)),
startY(startY),
maxLength(maxLength), maxLength(maxLength),
isPassword(isPassword) {} isPassword(isPassword),
onComplete(std::move(onComplete)),
/** onCancel(std::move(onCancel)) {}
* Handle button input. Call this in your main loop.
* @return true if input was handled, false otherwise
*/
bool handleInput();
/**
* Render the keyboard at the specified Y position.
* @param startY Y-coordinate where keyboard rendering starts (default 10)
*/
void render(int startY = 10) const;
/**
* Get the current text entered by the user.
*/
const std::string& getText() const { return text; }
/**
* Set the current text.
*/
void setText(const std::string& newText);
/**
* Check if the user has completed text entry (pressed OK on Done).
*/
bool isComplete() const { return complete; }
/**
* Check if the user has cancelled text entry.
*/
bool isCancelled() const { return cancelled; }
/**
* Reset the keyboard state for reuse.
*/
void reset(const std::string& newTitle = "", const std::string& newInitialText = "");
/**
* Set callback for when input is complete.
*/
void setOnComplete(OnCompleteCallback callback) { onComplete = callback; }
/**
* Set callback for when input is cancelled.
*/
void setOnCancel(OnCancelCallback callback) { onCancel = callback; }
// Activity overrides // Activity overrides
void onEnter() override; void onEnter() override;
void onExit() override;
void loop() override; void loop() override;
private: private:
std::string title; std::string title;
int startY;
std::string text; std::string text;
size_t maxLength; size_t maxLength;
bool isPassword; bool isPassword;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
// Keyboard state // Keyboard state
int selectedRow = 0; int selectedRow = 0;
int selectedCol = 0; int selectedCol = 0;
bool shiftActive = false; bool shiftActive = false;
bool complete = false;
bool cancelled = false;
// Callbacks // Callbacks
OnCompleteCallback onComplete; OnCompleteCallback onComplete;
@ -116,16 +84,17 @@ class KeyboardEntryActivity : public Activity {
static const char* const keyboardShift[NUM_ROWS]; static const char* const keyboardShift[NUM_ROWS];
// Special key positions (bottom row) // Special key positions (bottom row)
static constexpr int SHIFT_ROW = 4; static constexpr int SPECIAL_ROW = 4;
static constexpr int SHIFT_COL = 0; static constexpr int SHIFT_COL = 0;
static constexpr int SPACE_ROW = 4;
static constexpr int SPACE_COL = 2; static constexpr int SPACE_COL = 2;
static constexpr int BACKSPACE_ROW = 4;
static constexpr int BACKSPACE_COL = 7; static constexpr int BACKSPACE_COL = 7;
static constexpr int DONE_ROW = 4;
static constexpr int DONE_COL = 9; static constexpr int DONE_COL = 9;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
char getSelectedChar() const; char getSelectedChar() const;
void handleKeyPress(); void handleKeyPress();
int getRowLength(int row) const; int getRowLength(int row) const;
void render() const;
void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const;
}; };