From 4d9dbce55fed43c99243ac1fc511b5ec627425d7 Mon Sep 17 00:00:00 2001 From: csrutil Date: Mon, 1 Sep 2025 20:38:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20virtual=20keyboard=20?= =?UTF-8?q?implementation=20for=20text=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement VirtualKeyboard class with full keyboard functionality - Support cursor navigation, text input, and special keys (DEL, SPACE, CAPS, OK) - Compatible with both MESHCORE and MESHTASTIC display systems - Include multi-language layout support (EN/CN keyboards) - Add key press callback system for extensibility - Handle caps lock toggle and text buffer management --- src/graphics/VirtualKeyboard.cpp | 285 +++++++++++++++++++++++++++++++ src/graphics/VirtualKeyboard.h | 108 ++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 src/graphics/VirtualKeyboard.cpp create mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp new file mode 100644 index 000000000..b5b88980c --- /dev/null +++ b/src/graphics/VirtualKeyboard.cpp @@ -0,0 +1,285 @@ +/* + * VirtualKeyboard.cpp + * Author TSAO (hey@tsao.dev) 2025 + */ + +#include "VirtualKeyboard.h" + +#ifdef MESHCORE +#include "MeshCore.h" +#elif defined(MESHTASTIC) +#include "configuration.h" +#include "graphics/ScreenFonts.h" +#endif + +#include +#include + +#ifdef MESHCORE +VirtualKeyboard::VirtualKeyboard(DisplayDriver *displayDriver, int startX, int startY, int keyboardWidth, + int keyboardHeight) + : display(displayDriver) { +#elif defined(MESHTASTIC) +VirtualKeyboard::VirtualKeyboard(OLEDDisplay *displayDriver, int startX, int startY, int keyboardWidth, + int keyboardHeight) + : display(displayDriver) { +#endif + memset(inputBuffer, 0, sizeof(inputBuffer)); + bufferLength = 0; + + this->startX = startX; + this->startY = startY; + this->keyboardWidth = keyboardWidth; + this->keyboardHeight = keyboardHeight; +} + +void VirtualKeyboard::moveCursor(int deltaRow, int deltaColumn) { + cursorRow += deltaRow; + cursorColumn += deltaColumn; + + // Handle column overflow/underflow with row wrapping + if (cursorColumn >= getCols()) { + cursorColumn = 0; + cursorRow++; + } else if (cursorColumn < 0) { + cursorColumn = getCols() - 1; + cursorRow--; + } + + // Handle row overflow/underflow + if (cursorRow >= getRows()) { + cursorRow = 0; + } else if (cursorRow < 0) { + cursorRow = getRows() - 1; + } +} + +void VirtualKeyboard::moveRight(int steps) { + moveCursor(0, steps); +} + +void VirtualKeyboard::moveLeft(int steps) { + moveCursor(0, -steps); +} + +void VirtualKeyboard::moveUp(int steps) { + moveCursor(-steps, 0); +} + +void VirtualKeyboard::moveDown(int steps) { + moveCursor(steps, 0); +} + +const char *VirtualKeyboard::getKeyText(int row, int column) { + const char *keyString = getKeyAt(row, column); + if (!keyString || strlen(keyString) == 0) { + return ""; + } + + // Handle caps lock for letters + if (capsLockEnabled && strlen(keyString) == 1 && keyString[0] >= 'a' && keyString[0] <= 'z') { + static char uppercaseKey[2]; + uppercaseKey[0] = keyString[0] - 'a' + 'A'; + uppercaseKey[1] = '\0'; + return uppercaseKey; + } + + return keyString; +} + +const char *VirtualKeyboard::pressCurrentKey() { + const char *pressedKey = getKeyText(cursorRow, cursorColumn); + + if (strcmp(pressedKey, "DEL") == 0) { + // Delete/Backspace + if (bufferLength > 0) { + bufferLength--; + inputBuffer[bufferLength] = '\0'; + } + } else if (strcmp(pressedKey, "OK") == 0) { + // Enter - could trigger submission or new line + if (bufferLength < sizeof(inputBuffer) - 1) { + inputBuffer[bufferLength++] = '\n'; + inputBuffer[bufferLength] = '\0'; + } + } else if (strcmp(pressedKey, "SPACE") == 0) { + // Space + if (bufferLength < sizeof(inputBuffer) - 1) { + inputBuffer[bufferLength++] = ' '; + inputBuffer[bufferLength] = '\0'; + } + } else if (strcmp(pressedKey, "CAPS") == 0) { + // Toggle caps lock + capsLockEnabled = !capsLockEnabled; + } else { + // Regular character + int keyLength = strlen(pressedKey); + if (bufferLength + keyLength < sizeof(inputBuffer) - 1) { + strcat(inputBuffer, pressedKey); + bufferLength += keyLength; + } + } + + // Call user callback if one is set + if (keyPressCallback != nullptr) { + keyPressCallback(pressedKey, inputBuffer); + } + + return pressedKey; +} + +void VirtualKeyboard::clear() { + memset(inputBuffer, 0, sizeof(inputBuffer)); + bufferLength = 0; +} + +void VirtualKeyboard::reset() { + clear(); + cursorRow = 0; + cursorColumn = 0; +} + +void VirtualKeyboard::setKeyPressCallback(KeyPressCallback callback) { + keyPressCallback = callback; +} + +void VirtualKeyboard::getTextBounds(const char *text, uint16_t *width, uint16_t *height) { + if (!display || !text) { + if (width) *width = 0; + if (height) *height = 0; + return; + } + +#ifdef MESHCORE + Adafruit_SSD1306 *ssd1306Display = static_cast(display->getDisplay()); + if (ssd1306Display) { + int16_t x1, y1; + uint16_t w, h; + ssd1306Display->getTextBounds(text, 0, 0, &x1, &y1, &w, &h); + if (width) *width = w; + if (height) *height = h; + } else { + // Fallback if getDisplay() doesn't work + if (width) *width = display->getTextWidth(text); + if (height) *height = 8; // Default text height + } +#elif defined(MESHTASTIC) + if (width) *width = display->getStringWidth(text); + // FONT_SMALL is 7 by 🧐 + if (height) *height = 7; // FONT_HEIGHT_SMALL; +#endif +} + +void VirtualKeyboard::drawKeyboard() { + if (!display) return; + +#ifdef MESHCORE + display->setTextSize(1); +#elif defined(MESHTASTIC) + display->setFont(FONT_SMALL); + // display->clear(); +#endif + + // Calculate max standard key width (kw) + uint16_t kw = 0; + for (int row = 0; row < getRows(); row++) { + for (int col = 0; col < getCols() - 1; col++) { // Exclude rightmost column (control keys) + uint16_t keyWidth = 0; + const char *keyText = getKeyText(row, col); + getTextBounds(keyText, &keyWidth, nullptr); + if (keyWidth > kw) { + kw = keyWidth; + } + } + } + + // Calculate max control key width (cw) + uint16_t cw = 0; + for (int row = 0; row < getRows(); row++) { + int col = getCols() - 1; // Only check rightmost column + uint16_t keyWidth = 0; + const char *keyText = getKeyText(row, col); + getTextBounds(keyText, &keyWidth, nullptr); + if (keyWidth > cw) { + cw = keyWidth; + } + } + + // Calculate horizontal spacing + uint16_t totalKeyWidth = (getCols() - 1) * kw + cw; + uint16_t spacingX = (keyboardWidth - totalKeyWidth) / (getCols() - 1); + uint16_t fraction = (keyboardWidth - totalKeyWidth) % (getCols() - 1); + + // Calculate key height and vertical spacing + uint16_t keyH = 0; + getTextBounds(getKeyText(0, 0), nullptr, &keyH); + uint16_t spacingY = (keyboardHeight - getRows() * keyH) / (getRows() - 1); + +#ifdef MESHTASTIC + spacingY = 2; +#endif + + for (int row = 0; row < getRows(); row++) { + for (int col = 0; col < getCols(); col++) { + const char *label = getKeyText(row, col); + + uint16_t currentKeyWidth = (col == getCols() - 1) ? cw : kw; + + // Calculate x position dynamically + int currentX = startX + col * (kw + spacingX) + ((col == getCols() - 1) ? (cw - kw) : 0); + + if (col == getCols() - 1) { + currentX = keyboardWidth - cw; + // currentX += fraction * (getCols() - 1); + } + + // Calculate y position + int y = startY + row * keyH + row * spacingY; + + // Check if this is the currently selected key + bool selected = (row == cursorRow && col == cursorColumn); + + if (selected) { + // Highlight the selected key with inverted colors +#ifdef MESHCORE + display->setColor(DisplayDriver::LIGHT); + display->fillRect(currentX - 1, y - 1, currentKeyWidth + 2, keyH + 2); + display->setColor(DisplayDriver::DARK); // Dark text on light background +#elif defined(MESHTASTIC) + display->setColor(OLEDDISPLAY_COLOR::WHITE); + if (col == 0 && startX > 0) { + display->fillRect(currentX - 1, y + 2, currentKeyWidth + 1, keyH + 2); + } else { + display->fillRect(currentX - 2, y + 2, currentKeyWidth + 1, keyH + 2); + } + display->setColor(OLEDDISPLAY_COLOR::BLACK); // Dark text on light background +#endif + } else { +#ifdef MESHCORE + display->setColor(DisplayDriver::LIGHT); // Light text on dark background +#elif defined(MESHTASTIC) + display->setColor(OLEDDISPLAY_COLOR::WHITE); // Light text on dark background +#endif + } + + // Draw the key text at the key position +#ifdef MESHCORE + display->setCursor(currentX, y); + display->print(label); +#elif defined(MESHTASTIC) + display->drawString(currentX, y, label); +#endif + } + } + + // Reset text color to default +#ifdef MESHCORE + display->setColor(DisplayDriver::LIGHT); +#elif defined(MESHTASTIC) + display->setColor(OLEDDISPLAY_COLOR::WHITE); +#endif +} + +void VirtualKeyboard::render() { + drawKeyboard(); +} diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h new file mode 100644 index 000000000..2bcf82c88 --- /dev/null +++ b/src/graphics/VirtualKeyboard.h @@ -0,0 +1,108 @@ +/* + * VirtualKeyboard.h + * Author TSAO (hey@tsao.dev) 2025 + */ + +#pragma once + +#ifdef MESHCORE +#include +#include +#elif defined(MESHTASTIC) +#include "graphics/Screen.h" + +#include +#endif + +// Callback function type for key press events +typedef void (*KeyPressCallback)(const char *pressedKey, const char *currentText); + +#ifdef VIRTUAL_KEYBOARD_EN +static const int VIRTUAL_KEYBOARD_ROWS = 4; +static const int VIRTUAL_KEYBOARD_COLS = 11; + +static const char *keyboardLayout[VIRTUAL_KEYBOARD_ROWS][VIRTUAL_KEYBOARD_COLS] = { + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "DEL" }, + { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "OK" }, + { "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "SPACE" }, + { "z", "x", "c", "v", "b", "n", "m", ".", ",", "?", "CAPS" }, +}; +#endif + +#ifdef VIRTUAL_KEYBOARD_CN +static const int VIRTUAL_KEYBOARD_ROWS = 4; +static const int VIRTUAL_KEYBOARD_COLS = 11; + +static const char *keyboardLayout[VIRTUAL_KEYBOARD_ROWS][VIRTUAL_KEYBOARD_COLS] = { + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "DEL" }, + { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "OK" }, + { "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "SPACE" }, + { "z", "x", "c", "v", "b", "n", "m", ".", ",", "?", "EN/CN" }, +}; +#endif + +class VirtualKeyboard { +private: +#ifdef MESHCORE + DisplayDriver *display; +#elif defined(MESHTASTIC) + OLEDDisplay *display; +#endif + + int cursorRow = 0; + int cursorColumn = 0; + bool capsLockEnabled = false; + + char inputBuffer[160]; + int bufferLength = 0; + + // Keyboard positioning and dimensions + int startX = 0; + int startY = 0; + int keyboardWidth = 0; + int keyboardHeight = 0; + + // Callback function for key press events + KeyPressCallback keyPressCallback = nullptr; + + void drawKeyboard(); + const char *getKeyText(int row, int col); + +public: +#ifdef MESHCORE + VirtualKeyboard(DisplayDriver *display, int startX = 0, int startY = 0, int keyboardWidth = 0, + int keyboardHeight = 0); +#elif defined(MESHTASTIC) + VirtualKeyboard(OLEDDisplay *display, int startX = 0, int startY = 0, int keyboardWidth = 0, + int keyboardHeight = 0); +#endif + + void moveCursor(int deltaRow, int deltaColumn); + void moveRight(int steps = 1); + void moveLeft(int steps = 1); + void moveUp(int steps = 1); + void moveDown(int steps = 1); + + const char *pressCurrentKey(); + void clear(); + void reset(); + + const char *getText() const { return inputBuffer; } + int getTextLength() const { return bufferLength; } + + // Usage: kb.setKeyPressCallback([](const char* key, const char* text) { ... }); + void setKeyPressCallback(KeyPressCallback callback); + + void getTextBounds(const char *text, uint16_t *width, uint16_t *height); + void render(); + + int getRows() const { return VIRTUAL_KEYBOARD_ROWS; } + int getCols() const { return VIRTUAL_KEYBOARD_COLS; } + + const char *getKeyAt(int row, int col) const { + if (row >= 0 && row < VIRTUAL_KEYBOARD_ROWS && col >= 0 && col < VIRTUAL_KEYBOARD_COLS) { + return keyboardLayout[row][col]; + } + return ""; + } +};