Add connect to Wifi and File Manager Webserver (#41)
## Summary
- **What is the goal of this PR?**
Implements wireless EPUB file management via a built-in web server,
enabling users to upload, browse, organize, and delete EPUB files from
any device on the same WiFi network without needing a computer cable
connection.
- **What changes are included?**
- **New Web Server**
([`CrossPointWebServer.cpp`](src/CrossPointWebServer.cpp),
[`CrossPointWebServer.h`](src/CrossPointWebServer.h)):
- HTTP server on port 80 with a responsive HTML/CSS interface
- Home page showing device status (version, IP, free memory)
- File Manager with folder navigation and breadcrumb support
- EPUB file upload with progress tracking
- Folder creation and file/folder deletion
- XSS protection via HTML escaping
- Hidden system folders (`.` prefixed, "System Volume Information",
"XTCache")
- **WiFi Screen** ([`WifiScreen.cpp`](src/screens/WifiScreen.cpp),
[`WifiScreen.h`](src/screens/WifiScreen.h)):
- Network scanning with signal strength indicators
- Visual indicators for encrypted (`*`) and saved (`+`) networks
- State machine managing: scanning, network selection, password entry,
connecting, save/forget prompts
- 15-second connection timeout handling
- Integration with web server (starts on connect, stops on exit)
- **WiFi Credential Storage**
([`WifiCredentialStore.cpp`](src/WifiCredentialStore.cpp),
[`WifiCredentialStore.h`](src/WifiCredentialStore.h)):
- Persistent storage in `/sd/.crosspoint/wifi.bin`
- XOR obfuscation for stored passwords (basic protection against casual
reading)
- Up to 8 saved networks with add/remove/update operations
- **On-Screen Keyboard**
([`OnScreenKeyboard.cpp`](src/screens/OnScreenKeyboard.cpp),
[`OnScreenKeyboard.h`](src/screens/OnScreenKeyboard.h)):
- Reusable QWERTY keyboard component with shift support
- Special keys: Shift, Space, Backspace, Done
- Support for password masking mode
- **Settings Screen Integration**
([`SettingsScreen.h`](src/screens/SettingsScreen.h)):
- Added WiFi action to navigate to the new WiFi screen
- **Documentation** ([`docs/webserver.md`](docs/webserver.md)):
- Comprehensive user guide covering WiFi setup, web interface usage,
file management, troubleshooting, and security notes
- See this for more screenshots!
- Working "displays the right way in GitHub" on my repo:
https://github.com/olearycrew/crosspoint-reader/blob/feature/connect-to-wifi/docs/webserver.md
**Video demo**
https://github.com/user-attachments/assets/283e32dc-2d9f-4ae2-848e-01f41166a731
## Additional Context
- **Security considerations**: The web server has no
authentication—anyone on the same WiFi network can access files. This is
documented as a limitation, recommending use only on trusted private
networks. Password obfuscation in the credential store is XOR-based, not
cryptographically secure.
- **Memory implications**: The web server and WiFi stack consume
significant memory. The implementation properly cleans up (stops server,
disconnects WiFi, sets `WIFI_OFF` mode) when exiting the WiFi screen to
free resources.
- **Async operations**: Network scanning and connection use async
patterns with FreeRTOS tasks to prevent blocking the UI. The display
task handles rendering on a dedicated thread with mutex protection.
- **Browser compatibility**: The web interface uses standard
HTML5/CSS3/JavaScript and is tested to work with all modern browsers on
desktop and mobile.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-19 09:05:43 -05:00
|
|
|
#include "KeyboardEntryActivity.h"
|
|
|
|
|
|
|
|
|
|
#include "../../config.h"
|
|
|
|
|
|
|
|
|
|
// Keyboard layouts - lowercase
|
|
|
|
|
const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = {
|
|
|
|
|
"`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./",
|
|
|
|
|
"^ _____<OK" // ^ = shift, _ = space, < = backspace, OK = done
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Keyboard layouts - uppercase/symbols
|
|
|
|
|
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
|
|
|
|
|
"ZXCVBNM<>?", "^ _____<OK"};
|
|
|
|
|
|
|
|
|
|
void KeyboardEntryActivity::setText(const std::string& newText) {
|
|
|
|
|
text = newText;
|
|
|
|
|
if (maxLength > 0 && text.length() > maxLength) {
|
2025-12-21 15:43:53 +11:00
|
|
|
text.resize(maxLength);
|
Add connect to Wifi and File Manager Webserver (#41)
## Summary
- **What is the goal of this PR?**
Implements wireless EPUB file management via a built-in web server,
enabling users to upload, browse, organize, and delete EPUB files from
any device on the same WiFi network without needing a computer cable
connection.
- **What changes are included?**
- **New Web Server**
([`CrossPointWebServer.cpp`](src/CrossPointWebServer.cpp),
[`CrossPointWebServer.h`](src/CrossPointWebServer.h)):
- HTTP server on port 80 with a responsive HTML/CSS interface
- Home page showing device status (version, IP, free memory)
- File Manager with folder navigation and breadcrumb support
- EPUB file upload with progress tracking
- Folder creation and file/folder deletion
- XSS protection via HTML escaping
- Hidden system folders (`.` prefixed, "System Volume Information",
"XTCache")
- **WiFi Screen** ([`WifiScreen.cpp`](src/screens/WifiScreen.cpp),
[`WifiScreen.h`](src/screens/WifiScreen.h)):
- Network scanning with signal strength indicators
- Visual indicators for encrypted (`*`) and saved (`+`) networks
- State machine managing: scanning, network selection, password entry,
connecting, save/forget prompts
- 15-second connection timeout handling
- Integration with web server (starts on connect, stops on exit)
- **WiFi Credential Storage**
([`WifiCredentialStore.cpp`](src/WifiCredentialStore.cpp),
[`WifiCredentialStore.h`](src/WifiCredentialStore.h)):
- Persistent storage in `/sd/.crosspoint/wifi.bin`
- XOR obfuscation for stored passwords (basic protection against casual
reading)
- Up to 8 saved networks with add/remove/update operations
- **On-Screen Keyboard**
([`OnScreenKeyboard.cpp`](src/screens/OnScreenKeyboard.cpp),
[`OnScreenKeyboard.h`](src/screens/OnScreenKeyboard.h)):
- Reusable QWERTY keyboard component with shift support
- Special keys: Shift, Space, Backspace, Done
- Support for password masking mode
- **Settings Screen Integration**
([`SettingsScreen.h`](src/screens/SettingsScreen.h)):
- Added WiFi action to navigate to the new WiFi screen
- **Documentation** ([`docs/webserver.md`](docs/webserver.md)):
- Comprehensive user guide covering WiFi setup, web interface usage,
file management, troubleshooting, and security notes
- See this for more screenshots!
- Working "displays the right way in GitHub" on my repo:
https://github.com/olearycrew/crosspoint-reader/blob/feature/connect-to-wifi/docs/webserver.md
**Video demo**
https://github.com/user-attachments/assets/283e32dc-2d9f-4ae2-848e-01f41166a731
## Additional Context
- **Security considerations**: The web server has no
authentication—anyone on the same WiFi network can access files. This is
documented as a limitation, recommending use only on trusted private
networks. Password obfuscation in the credential store is XOR-based, not
cryptographically secure.
- **Memory implications**: The web server and WiFi stack consume
significant memory. The implementation properly cleans up (stops server,
disconnects WiFi, sets `WIFI_OFF` mode) when exiting the WiFi screen to
free resources.
- **Async operations**: Network scanning and connection use async
patterns with FreeRTOS tasks to prevent blocking the UI. The display
task handles rendering on a dedicated thread with mutex protection.
- **Browser compatibility**: The web interface uses standard
HTML5/CSS3/JavaScript and is tested to work with all modern browsers on
desktop and mobile.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-19 09:05:43 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string& newInitialText) {
|
|
|
|
|
if (!newTitle.empty()) {
|
|
|
|
|
title = newTitle;
|
|
|
|
|
}
|
|
|
|
|
text = newInitialText;
|
|
|
|
|
selectedRow = 0;
|
|
|
|
|
selectedCol = 0;
|
|
|
|
|
shiftActive = false;
|
|
|
|
|
complete = false;
|
|
|
|
|
cancelled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void KeyboardEntryActivity::onEnter() {
|
2025-12-21 21:17:00 +11:00
|
|
|
Activity::onEnter();
|
|
|
|
|
|
Add connect to Wifi and File Manager Webserver (#41)
## Summary
- **What is the goal of this PR?**
Implements wireless EPUB file management via a built-in web server,
enabling users to upload, browse, organize, and delete EPUB files from
any device on the same WiFi network without needing a computer cable
connection.
- **What changes are included?**
- **New Web Server**
([`CrossPointWebServer.cpp`](src/CrossPointWebServer.cpp),
[`CrossPointWebServer.h`](src/CrossPointWebServer.h)):
- HTTP server on port 80 with a responsive HTML/CSS interface
- Home page showing device status (version, IP, free memory)
- File Manager with folder navigation and breadcrumb support
- EPUB file upload with progress tracking
- Folder creation and file/folder deletion
- XSS protection via HTML escaping
- Hidden system folders (`.` prefixed, "System Volume Information",
"XTCache")
- **WiFi Screen** ([`WifiScreen.cpp`](src/screens/WifiScreen.cpp),
[`WifiScreen.h`](src/screens/WifiScreen.h)):
- Network scanning with signal strength indicators
- Visual indicators for encrypted (`*`) and saved (`+`) networks
- State machine managing: scanning, network selection, password entry,
connecting, save/forget prompts
- 15-second connection timeout handling
- Integration with web server (starts on connect, stops on exit)
- **WiFi Credential Storage**
([`WifiCredentialStore.cpp`](src/WifiCredentialStore.cpp),
[`WifiCredentialStore.h`](src/WifiCredentialStore.h)):
- Persistent storage in `/sd/.crosspoint/wifi.bin`
- XOR obfuscation for stored passwords (basic protection against casual
reading)
- Up to 8 saved networks with add/remove/update operations
- **On-Screen Keyboard**
([`OnScreenKeyboard.cpp`](src/screens/OnScreenKeyboard.cpp),
[`OnScreenKeyboard.h`](src/screens/OnScreenKeyboard.h)):
- Reusable QWERTY keyboard component with shift support
- Special keys: Shift, Space, Backspace, Done
- Support for password masking mode
- **Settings Screen Integration**
([`SettingsScreen.h`](src/screens/SettingsScreen.h)):
- Added WiFi action to navigate to the new WiFi screen
- **Documentation** ([`docs/webserver.md`](docs/webserver.md)):
- Comprehensive user guide covering WiFi setup, web interface usage,
file management, troubleshooting, and security notes
- See this for more screenshots!
- Working "displays the right way in GitHub" on my repo:
https://github.com/olearycrew/crosspoint-reader/blob/feature/connect-to-wifi/docs/webserver.md
**Video demo**
https://github.com/user-attachments/assets/283e32dc-2d9f-4ae2-848e-01f41166a731
## Additional Context
- **Security considerations**: The web server has no
authentication—anyone on the same WiFi network can access files. This is
documented as a limitation, recommending use only on trusted private
networks. Password obfuscation in the credential store is XOR-based, not
cryptographically secure.
- **Memory implications**: The web server and WiFi stack consume
significant memory. The implementation properly cleans up (stops server,
disconnects WiFi, sets `WIFI_OFF` mode) when exiting the WiFi screen to
free resources.
- **Async operations**: Network scanning and connection use async
patterns with FreeRTOS tasks to prevent blocking the UI. The display
task handles rendering on a dedicated thread with mutex protection.
- **Browser compatibility**: The web interface uses standard
HTML5/CSS3/JavaScript and is tested to work with all modern browsers on
desktop and mobile.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-19 09:05:43 -05:00
|
|
|
// Reset state when entering the activity
|
|
|
|
|
complete = false;
|
|
|
|
|
cancelled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void KeyboardEntryActivity::loop() {
|
|
|
|
|
handleInput();
|
|
|
|
|
render(10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int KeyboardEntryActivity::getRowLength(int row) const {
|
|
|
|
|
if (row < 0 || row >= NUM_ROWS) return 0;
|
|
|
|
|
|
|
|
|
|
// Return actual length of each row based on keyboard layout
|
|
|
|
|
switch (row) {
|
|
|
|
|
case 0:
|
|
|
|
|
return 13; // `1234567890-=
|
|
|
|
|
case 1:
|
|
|
|
|
return 13; // qwertyuiop[]backslash
|
|
|
|
|
case 2:
|
|
|
|
|
return 11; // asdfghjkl;'
|
|
|
|
|
case 3:
|
|
|
|
|
return 10; // zxcvbnm,./
|
|
|
|
|
case 4:
|
|
|
|
|
return 10; // ^, space (5 wide), backspace, OK (2 wide)
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char KeyboardEntryActivity::getSelectedChar() const {
|
|
|
|
|
const char* const* layout = shiftActive ? keyboardShift : keyboard;
|
|
|
|
|
|
|
|
|
|
if (selectedRow < 0 || selectedRow >= NUM_ROWS) return '\0';
|
|
|
|
|
if (selectedCol < 0 || selectedCol >= getRowLength(selectedRow)) return '\0';
|
|
|
|
|
|
|
|
|
|
return layout[selectedRow][selectedCol];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void KeyboardEntryActivity::handleKeyPress() {
|
|
|
|
|
// Handle special row (bottom row with shift, space, backspace, done)
|
|
|
|
|
if (selectedRow == SHIFT_ROW) {
|
|
|
|
|
if (selectedCol == SHIFT_COL) {
|
|
|
|
|
// Shift toggle
|
|
|
|
|
shiftActive = !shiftActive;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
|
|
|
|
|
// Space bar
|
|
|
|
|
if (maxLength == 0 || text.length() < maxLength) {
|
|
|
|
|
text += ' ';
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedCol == BACKSPACE_COL) {
|
|
|
|
|
// Backspace
|
|
|
|
|
if (!text.empty()) {
|
|
|
|
|
text.pop_back();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedCol >= DONE_COL) {
|
|
|
|
|
// Done button
|
|
|
|
|
complete = true;
|
|
|
|
|
if (onComplete) {
|
|
|
|
|
onComplete(text);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Regular character
|
|
|
|
|
char c = getSelectedChar();
|
|
|
|
|
if (c != '\0' && c != '^' && c != '_' && c != '<') {
|
|
|
|
|
if (maxLength == 0 || text.length() < maxLength) {
|
|
|
|
|
text += c;
|
|
|
|
|
// Auto-disable shift after typing a letter
|
|
|
|
|
if (shiftActive && ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) {
|
|
|
|
|
shiftActive = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool KeyboardEntryActivity::handleInput() {
|
|
|
|
|
if (complete || cancelled) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool handled = false;
|
|
|
|
|
|
|
|
|
|
// Navigation
|
|
|
|
|
if (inputManager.wasPressed(InputManager::BTN_UP)) {
|
|
|
|
|
if (selectedRow > 0) {
|
|
|
|
|
selectedRow--;
|
|
|
|
|
// Clamp column to valid range for new row
|
|
|
|
|
int maxCol = getRowLength(selectedRow) - 1;
|
|
|
|
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
|
|
|
}
|
|
|
|
|
handled = true;
|
|
|
|
|
} else if (inputManager.wasPressed(InputManager::BTN_DOWN)) {
|
|
|
|
|
if (selectedRow < NUM_ROWS - 1) {
|
|
|
|
|
selectedRow++;
|
|
|
|
|
int maxCol = getRowLength(selectedRow) - 1;
|
|
|
|
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
|
|
|
}
|
|
|
|
|
handled = true;
|
|
|
|
|
} else if (inputManager.wasPressed(InputManager::BTN_LEFT)) {
|
|
|
|
|
if (selectedCol > 0) {
|
|
|
|
|
selectedCol--;
|
|
|
|
|
} else if (selectedRow > 0) {
|
|
|
|
|
// Wrap to previous row
|
|
|
|
|
selectedRow--;
|
|
|
|
|
selectedCol = getRowLength(selectedRow) - 1;
|
|
|
|
|
}
|
|
|
|
|
handled = true;
|
|
|
|
|
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT)) {
|
|
|
|
|
int maxCol = getRowLength(selectedRow) - 1;
|
|
|
|
|
if (selectedCol < maxCol) {
|
|
|
|
|
selectedCol++;
|
|
|
|
|
} else if (selectedRow < NUM_ROWS - 1) {
|
|
|
|
|
// Wrap to next row
|
|
|
|
|
selectedRow++;
|
|
|
|
|
selectedCol = 0;
|
|
|
|
|
}
|
|
|
|
|
handled = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Selection
|
|
|
|
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
|
|
|
|
handleKeyPress();
|
|
|
|
|
handled = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel
|
|
|
|
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
if (onCancel) {
|
|
|
|
|
onCancel();
|
|
|
|
|
}
|
|
|
|
|
handled = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return handled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void KeyboardEntryActivity::render(int startY) const {
|
|
|
|
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
|
|
|
|
|
|
|
|
|
// Draw title
|
|
|
|
|
renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR);
|
|
|
|
|
|
|
|
|
|
// Draw input field
|
|
|
|
|
int inputY = startY + 22;
|
|
|
|
|
renderer.drawText(UI_FONT_ID, 10, inputY, "[");
|
|
|
|
|
|
|
|
|
|
std::string displayText;
|
|
|
|
|
if (isPassword) {
|
|
|
|
|
displayText = std::string(text.length(), '*');
|
|
|
|
|
} else {
|
|
|
|
|
displayText = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show cursor at end
|
|
|
|
|
displayText += "_";
|
|
|
|
|
|
|
|
|
|
// Truncate if too long for display - use actual character width from font
|
|
|
|
|
int charWidth = renderer.getSpaceWidth(UI_FONT_ID);
|
|
|
|
|
if (charWidth < 1) charWidth = 8; // Fallback to approximate width
|
|
|
|
|
int maxDisplayLen = (pageWidth - 40) / charWidth;
|
|
|
|
|
if (displayText.length() > static_cast<size_t>(maxDisplayLen)) {
|
|
|
|
|
displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderer.drawText(UI_FONT_ID, 20, inputY, displayText.c_str());
|
|
|
|
|
renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]");
|
|
|
|
|
|
|
|
|
|
// Draw keyboard - use compact spacing to fit 5 rows on screen
|
|
|
|
|
int keyboardStartY = inputY + 25;
|
|
|
|
|
const int keyWidth = 18;
|
|
|
|
|
const int keyHeight = 18;
|
|
|
|
|
const int keySpacing = 3;
|
|
|
|
|
|
|
|
|
|
const char* const* layout = shiftActive ? keyboardShift : keyboard;
|
|
|
|
|
|
|
|
|
|
// Calculate left margin to center the longest row (13 keys)
|
|
|
|
|
int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
|
|
|
|
|
int leftMargin = (pageWidth - maxRowWidth) / 2;
|
|
|
|
|
|
|
|
|
|
for (int row = 0; row < NUM_ROWS; row++) {
|
|
|
|
|
int rowY = keyboardStartY + row * (keyHeight + keySpacing);
|
|
|
|
|
|
|
|
|
|
// Left-align all rows for consistent navigation
|
|
|
|
|
int startX = leftMargin;
|
|
|
|
|
|
|
|
|
|
// Handle bottom row (row 4) specially with proper multi-column keys
|
|
|
|
|
if (row == 4) {
|
|
|
|
|
// Bottom row layout: CAPS (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
|
|
|
|
|
// Total: 11 visual columns, but we use logical positions for selection
|
|
|
|
|
|
|
|
|
|
int currentX = startX;
|
|
|
|
|
|
|
|
|
|
// CAPS key (logical col 0, spans 2 key widths)
|
|
|
|
|
int capsWidth = 2 * keyWidth + keySpacing;
|
|
|
|
|
bool capsSelected = (selectedRow == 4 && selectedCol == SHIFT_COL);
|
|
|
|
|
if (capsSelected) {
|
|
|
|
|
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)
|
|
|
|
|
int spaceWidth = 5 * keyWidth + 4 * keySpacing;
|
|
|
|
|
bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
|
|
|
|
|
if (spaceSelected) {
|
|
|
|
|
renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "[");
|
|
|
|
|
renderer.drawText(UI_FONT_ID, currentX + spaceWidth - 4, rowY, "]");
|
|
|
|
|
}
|
|
|
|
|
// 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)
|
|
|
|
|
int bsWidth = 2 * keyWidth + keySpacing;
|
|
|
|
|
bool bsSelected = (selectedRow == 4 && selectedCol == BACKSPACE_COL);
|
|
|
|
|
if (bsSelected) {
|
|
|
|
|
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)
|
|
|
|
|
int okWidth = 2 * keyWidth + keySpacing;
|
|
|
|
|
bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL);
|
|
|
|
|
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 {
|
|
|
|
|
// Regular rows: render each key individually
|
|
|
|
|
for (int col = 0; col < getRowLength(row); col++) {
|
|
|
|
|
int keyX = startX + col * (keyWidth + keySpacing);
|
|
|
|
|
|
|
|
|
|
// Get the character to display
|
|
|
|
|
char c = layout[row][col];
|
|
|
|
|
std::string keyLabel(1, c);
|
|
|
|
|
|
|
|
|
|
// Draw selection highlight
|
|
|
|
|
bool isSelected = (row == selectedRow && col == selectedCol);
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw help text at absolute bottom of screen (consistent with other screens)
|
|
|
|
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK");
|
|
|
|
|
}
|