From fe3f14a63e2dbd160eabfcb8fdf1fb7cfb8eb1db Mon Sep 17 00:00:00 2001 From: Wilson Date: Thu, 21 Aug 2025 18:01:31 +0800 Subject: [PATCH 1/2] Add on-screen keyboard implementation on Trackball device. (#7625) * Add on-screen keyboard implementation on Wio Tracker L1. * Update On-Screen Keyboard to new layout. * The on-screen keyboard dynamically adjusts the key size based on the screen. * Improve input box display on small screens. * Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens. * Optimize the text alignment of numeric keys on ssd1306. --------- Co-authored-by: Ben Meadors --- src/buzz/BuzzerFeedbackThread.cpp | 1 + src/graphics/Screen.cpp | 75 ++- src/graphics/Screen.h | 4 +- src/graphics/VirtualKeyboard.cpp | 738 +++++++++++++++++++++ src/graphics/VirtualKeyboard.h | 80 +++ src/graphics/draw/MenuHandler.cpp | 3 + src/graphics/draw/NotificationRenderer.cpp | 111 +++- src/graphics/draw/NotificationRenderer.h | 6 + src/input/InputBroker.h | 1 + src/input/TrackballInterruptBase.cpp | 76 ++- src/input/TrackballInterruptBase.h | 13 +- src/input/TrackballInterruptImpl1.cpp | 7 +- src/main.cpp | 6 + src/main.h | 1 + src/modules/CannedMessageModule.cpp | 136 +++- 15 files changed, 1227 insertions(+), 31 deletions(-) create mode 100644 src/graphics/VirtualKeyboard.cpp create mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index ce762c764..838224c69 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -28,6 +28,7 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa71e17d8..5e29814cb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -216,6 +216,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t ui->update(); } +void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback) +{ + LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); + + if (NotificationRenderer::virtualKeyboard) { + delete NotificationRenderer::virtualKeyboard; + NotificationRenderer::virtualKeyboard = nullptr; + } + + NotificationRenderer::textInputCallback = nullptr; + + NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); + if (header) { + NotificationRenderer::virtualKeyboard->setHeader(header); + } + if (initialText) { + NotificationRenderer::virtualKeyboard->setInputText(initialText); + } + + // Set up callback with safer cleanup mechanism + NotificationRenderer::textInputCallback = textCallback; + NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); + + // Store the message and set the expiration timestamp (use same pattern as other notifications) + strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; + + // Set the overlay using the same pattern as other notification types + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); +} + static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -713,13 +751,19 @@ int32_t Screen::runOnce() handleSetOn(false); break; case Cmd::ON_PRESS: - handleOnPress(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleOnPress(); + } break; case Cmd::SHOW_PREV_FRAME: - handleShowPrevFrame(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleShowPrevFrame(); + } break; case Cmd::SHOW_NEXT_FRAME: - handleShowNextFrame(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleShowNextFrame(); + } break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away @@ -741,7 +785,9 @@ int32_t Screen::runOnce() NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - setFrames(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } break; case Cmd::NOOP: break; @@ -777,6 +823,7 @@ int32_t Screen::runOnce() if (showingNormalScreen) { // standard screen loop handling here if (config.display.auto_screen_carousel_secs > 0 && + NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead @@ -867,6 +914,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Called when a frame should be added / removed, or custom frames should be cleared void Screen::setFrames(FrameFocus focus) { + // Block setFrames calls when virtual keyboard is active to prevent overlay interference + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return; + } + uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter @@ -1313,6 +1365,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return 0; + } + if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) @@ -1335,6 +1392,16 @@ int Screen::handleInputEvent(const InputEvent *event) if (!screenOn) return 0; + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + return 0; + } + #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 265900131..0f100d455 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -12,7 +12,7 @@ #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { -enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; struct BannerOverlayOptions { const char *message; @@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); + void showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp new file mode 100644 index 000000000..84d5551cb --- /dev/null +++ b/src/graphics/VirtualKeyboard.cpp @@ -0,0 +1,738 @@ +#include "VirtualKeyboard.h" +#include "configuration.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "main.h" +#include +#include + +namespace graphics +{ + +VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) +{ + initializeKeyboard(); + // Set cursor to H(2, 5) + cursorRow = 2; + cursorCol = 5; +} + +VirtualKeyboard::~VirtualKeyboard() {} + +void VirtualKeyboard::initializeKeyboard() +{ + // New 4 row, 11 column keyboard layout: + static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; + + // Derive layout dimensions and assert they match the configured keyboard grid + constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); + constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); + static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); + static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); + + // Initialize all keys to empty first + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; + } + } + + // Fill keyboard from the 2D layout + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + char ch = LAYOUT[row][col]; + // No empty slots in the simplified layout + + VirtualKeyType type = VK_CHAR; + if (ch == '\b') { + type = VK_BACKSPACE; + } else if (ch == '\n') { + type = VK_ENTER; + } else if (ch == '\x1b') { // ESC + type = VK_ESC; + } else if (ch == ' ') { + type = VK_SPACE; + } + + // Make action keys wider to fit text while keeping the last column aligned + uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; + keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; + } + } +} + +void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) +{ + // Repeat ticking is driven by NotificationRenderer once per frame + // Base styles + display->setColor(WHITE); + display->setFont(FONT_SMALL); + + // Screen geometry + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels + // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide + const bool isWide = screenW >= 200; + + // Determine last-column label max width + display->setFont(FONT_SMALL); + const int wENTER = display->getStringWidth("ENTER"); + int lastColLabelW = wENTER; // ENTER is usually the widest + // Smaller padding on very small screens to avoid excessive whitespace + const int lastColPad = (screenW <= 128 ? 2 : 6); + const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys + + // Always reserve width for the rightmost text column to avoid overlap on small screens + int cellW = 0; + int leftoverW = 0; + { + const int leftCols = KEYBOARD_COLS - 1; // 10 input characters + int usableW = screenW - reservedLastColW; + if (usableW < leftCols) { + // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) + usableW = leftCols; + } + cellW = usableW / leftCols; + leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) + } + + // Dynamic key geometry + int cellH = KEY_HEIGHT; + int keyboardStartY = 0; + if (screenH <= 64) { + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); + const int gapBelowHeader = 0; + const int singleLineBoxHeight = FONT_HEIGHT_SMALL; + const int gapAboveKeyboard = 0; + keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; + if (keyboardStartY < 0) + keyboardStartY = 0; + if (keyboardStartY > screenH) + keyboardStartY = screenH; + int keyboardHeight = screenH - keyboardStartY; + cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); + } else if (isWide) { + // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. + cellH = std::max((int)KEY_HEIGHT, cellW); + + // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. + // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 + display->setFont(FONT_SMALL); + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); + const int headerToBoxGap = 1; + const int gapAboveKb = 1; + const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom + int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); + int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; + if (maxCellHAllowed < (int)KEY_HEIGHT) + maxCellHAllowed = KEY_HEIGHT; + if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { + cellH = maxCellHAllowed; + } + // Keyboard placement from bottom for wide screens + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } else { + // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom + cellH = KEY_HEIGHT; + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } + + // Draw input area above keyboard + drawInputArea(display, offsetX, offsetY, keyboardStartY); + + // Precompute per-column x and width with leftover distributed over left columns for even spacing + int colX[KEYBOARD_COLS]; + int colW[KEYBOARD_COLS]; + int runningX = offsetX; + for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { + int wcol = cellW + (col < leftoverW ? 1 : 0); + colX[col] = runningX; + colW[col] = wcol; + runningX += wcol; + } + // Last column + colX[KEYBOARD_COLS - 1] = runningX; + colW[KEYBOARD_COLS - 1] = reservedLastColW; + + // Draw keyboard grid + for (int row = 0; row < KEYBOARD_ROWS; row++) { + for (int col = 0; col < KEYBOARD_COLS; col++) { + const VirtualKey &k = keyboard[row][col]; + if (k.character != 0 || k.type != VK_CHAR) { + const bool isLastCol = (col == KEYBOARD_COLS - 1); + int x = colX[col]; + int w = colW[col]; + int y = offsetY + keyboardStartY + row * cellH; + int h = cellH; + bool selected = (row == cursorRow && col == cursorCol); + drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); + } + } + } +} + +void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) +{ + display->setColor(WHITE); + + const int screenWidth = display->getWidth(); + const int screenHeight = display->getHeight(); + // Use the standard small font metrics for input box sizing (restore original size) + const int inputLineH = FONT_HEIGHT_SMALL; + + // Header uses the standard small (which may be larger on big screens) + display->setFont(FONT_SMALL); + int headerHeight = 0; + if (!headerText.empty()) { + // Draw header and reserve exact font height (plus a tighter gap) to maximize input area + display->drawString(offsetX + 2, offsetY, headerText.c_str()); + if (screenHeight <= 64) { + headerHeight = FONT_HEIGHT_SMALL - 2; // 11px + } else { + headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in + } + } + + const int boxX = offsetX; + const int boxWidth = screenWidth; + int boxY; + int boxHeight; + if (screenHeight <= 64) { + const int gapBelowHeader = 0; + const int fixedBoxHeight = inputLineH; + const int gapAboveKeyboard = 0; + boxY = offsetY + headerHeight + gapBelowHeader; + boxHeight = fixedBoxHeight; + if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { + int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; + boxHeight = std::max(1, fixedBoxHeight - over); + } + } else { + const int gapBelowHeader = 1; + int gapAboveKeyboard = 1; + int tmpBoxY = offsetY + headerHeight + gapBelowHeader; + const int minBoxHeight = inputLineH + 2; + int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; + if (availableH < minBoxHeight) + availableH = minBoxHeight; + boxY = tmpBoxY; + boxHeight = availableH; + } + + // Draw box border + display->drawRect(boxX, boxY, boxWidth, boxHeight); + + display->setFont(FONT_SMALL); + + // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis + const int textX = boxX + 2; + const int maxTextWidth = boxWidth - 4; + const int maxLines = (boxHeight - 2) / inputLineH; + if (maxLines >= 2) { + // Inner bounds for caret clamping + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Wrap text greedily into lines that fit maxTextWidth + std::vector lines; + { + std::string remaining = inputText; + while (!remaining.empty()) { + int bestLen = 0; + for (int len = 1; len <= (int)remaining.size(); ++len) { + int w = display->getStringWidth(remaining.substr(0, len).c_str()); + if (w <= maxTextWidth) + bestLen = len; + else + break; + } + if (bestLen == 0) { + // At least show one character to make progress + bestLen = 1; + } + lines.emplace_back(remaining.substr(0, bestLen)); + remaining.erase(0, bestLen); + } + } + + const bool scrolledUp = ((int)lines.size() > maxLines); + int caretX = textX; + int caretY = innerTop; + + // Leave a small top gap to render '...' without replacing the first line + const int topInset = 2; + const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height + int lineY = innerTop + topInset; + + if (scrolledUp) { + // Draw three small dots centered horizontally, vertically at the midpoint of the gap + // between the inner top and the first line's top baseline. This avoids using a tall glyph. + const int firstLineTop = lineY; // baseline top for the first visible line + const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested + const int centerX = boxX + boxWidth / 2; + const int dotSpacing = 3; // px between dots + const int dotSize = 1; // small square dot + display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); + display->fillRect(centerX, gapMidY, dotSize, dotSize); + display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); + } + + // How many lines fit with our top inset and tighter step + const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); + const int linesToShow = std::min((int)lines.size(), linesCapacity); + const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; + + for (int i = 0; i < linesToShow; ++i) { + const std::string &chunk = lines[startIndex + i]; + display->drawString(textX, lineY, chunk.c_str()); + caretX = textX + display->getStringWidth(chunk.c_str()); + caretY = lineY; + lineY += lineStep; + } + + // Draw caret at end of the last visible line + int caretPadY = 2; + if (boxHeight >= inputLineH + 4) + caretPadY = 3; + int cursorTop = caretY + caretPadY; + // Use lineStep so caret height matches the row spacing + int cursorH = lineStep - caretPadY * 2; + if (cursorH < 1) + cursorH = 1; + // Clamp vertical bounds to stay inside the inner rect + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + // Only draw if cursor is inside inner bounds + if (caretX >= innerLeft && caretX <= innerRight) { + display->drawVerticalLine(caretX, cursorTop, cursorH); + } + } else { + std::string displayText = inputText; + int textW = display->getStringWidth(displayText.c_str()); + std::string scrolled = displayText; + if (textW > maxTextWidth) { + // Trim from the left until it fits + while (textW > maxTextWidth && !scrolled.empty()) { + scrolled.erase(0, 1); + textW = display->getStringWidth(scrolled.c_str()); + } + // Add leading ellipsis and ensure it still fits + if (scrolled != displayText) { + scrolled = "..." + scrolled; + textW = display->getStringWidth(scrolled.c_str()); + // If adding ellipsis causes overflow, trim more after the ellipsis + while (textW > maxTextWidth && scrolled.size() > 3) { + scrolled.erase(3, 1); // remove chars after the ellipsis + textW = display->getStringWidth(scrolled.c_str()); + } + } + } else { + // Keep textW in sync with what we draw + textW = display->getStringWidth(scrolled.c_str()); + } + + int textY; + if (screenHeight <= 64) { + textY = boxY + (boxHeight - inputLineH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Center text vertically within inner box for single-line, then clamp so it never overlaps borders + int innerH = innerBottom - innerTop + 1; + textY = innerTop + std::max(0, (innerH - inputLineH) / 2); + // Clamp fully inside the inner rect + if (textY < innerTop) + textY = innerTop; + int maxTop = innerBottom - inputLineH + 1; + if (textY > maxTop) + textY = maxTop; + } + + if (!scrolled.empty()) { + display->drawString(textX, textY, scrolled.c_str()); + } + + int cursorX = textX + textW; + if (screenHeight > 64) { + const int innerRight = boxX + boxWidth - 2; + if (cursorX > innerRight) + cursorX = innerRight; + } + + int cursorTop, cursorH; + if (screenHeight <= 64) { + cursorH = 10; + cursorTop = boxY + (boxHeight - cursorH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + cursorTop = boxY + 2; + cursorH = boxHeight - 4; + if (cursorH < 1) + cursorH = 1; + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + + if (cursorX < innerLeft || cursorX > innerRight) + return; + } + + display->drawVerticalLine(cursorX, cursorTop, cursorH); + } +} + +void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, + uint8_t height, bool isLastCol) +{ + // Draw key content + display->setFont(FONT_SMALL); + const int fontH = FONT_HEIGHT_SMALL; + // Build label and metrics first + std::string keyText; + if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { + // Keep literal text labels for the action keys on the rightmost column + keyText = (key.type == VK_BACKSPACE) ? "BACK" + : (key.type == VK_ENTER) ? "ENTER" + : (key.type == VK_SPACE) ? "SPACE" + : (key.type == VK_ESC) ? "ESC" + : ""; + } else { + char c = getCharForKey(key, false); + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + } + + int textWidth = display->getStringWidth(keyText.c_str()); + // Label alignment + // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. + // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. + int textX; + if (isLastCol) { + const int rightPad = 1; + textX = x + width - textWidth - rightPad; + if (textX < x) + textX = x; // guard + } else { + if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { + textX = x + (width - textWidth + 1) / 2; + } else { + textX = x + (width - textWidth) / 2; + } + } + int contentTop = y; + int contentH = height; + if (selected) { + display->setColor(WHITE); + bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); + + if (display->getHeight() <= 64 && !isAction) { + display->fillRect(x, y, width, height); + } else if (isAction) { + const int padX = 1; + const int padY = 2; + int hlW = textWidth + padX * 2; + int hlX = textX - padX; + + if (hlX < x) { + hlW -= (x - hlX); + hlX = x; + } + int maxW = (x + width) - hlX; + if (hlW > maxW) + hlW = maxW; + if (hlW < 1) + hlW = 1; + + int hlH = std::min(fontH + padY * 2, (int)height); + int hlY = y + (height - hlH) / 2; + display->fillRect(hlX, hlY, hlW, hlH); + contentTop = hlY; + contentH = hlH; + } else { + display->fillRect(x, y, width, height); + } + display->setColor(BLACK); + } else { + display->setColor(WHITE); + } + + int centeredTextY; + if (display->getHeight() <= 64) { + centeredTextY = y + (height - fontH) / 2; + } else { + centeredTextY = contentTop + (contentH - fontH) / 2; + } + if (display->getHeight() > 64) { + if (centeredTextY < contentTop) + centeredTextY = contentTop; + if (centeredTextY + fontH > contentTop + contentH) + centeredTextY = std::max(contentTop, contentTop + contentH - fontH); + } + + if (display->getHeight() <= 64 && keyText.size() == 1) { + char ch = keyText[0]; + if (ch == '.' || ch == ',' || ch == ';') { + centeredTextY -= 1; + } + } + display->drawString(textX, centeredTextY, keyText.c_str()); +} + +char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) +{ + if (key.type != VK_CHAR) { + return key.character; + } + + char c = key.character; + + // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings + if (isLongPress && c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } + + return c; +} + +void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) +{ + resetTimeout(); + // wrap around rows and cols in the 4x11 grid + int r = (int)cursorRow + dRow; + int c = (int)cursorCol + dCol; + if (r < 0) + r = KEYBOARD_ROWS - 1; + else if (r >= KEYBOARD_ROWS) + r = 0; + if (c < 0) + c = KEYBOARD_COLS - 1; + else if (c >= KEYBOARD_COLS) + c = 0; + cursorRow = (uint8_t)r; + cursorCol = (uint8_t)c; +} + +void VirtualKeyboard::moveCursorUp() +{ + moveCursorDelta(-1, 0); +} +void VirtualKeyboard::moveCursorDown() +{ + moveCursorDelta(1, 0); +} +void VirtualKeyboard::moveCursorLeft() +{ + resetTimeout(); + + if (cursorCol > 0) { + cursorCol--; + } else { + if (cursorRow > 0) { + cursorRow--; + cursorCol = KEYBOARD_COLS - 1; + } else { + cursorRow = KEYBOARD_ROWS - 1; + cursorCol = KEYBOARD_COLS - 1; + } + } +} +void VirtualKeyboard::moveCursorRight() +{ + resetTimeout(); + + if (cursorCol < KEYBOARD_COLS - 1) { + cursorCol++; + } else { + if (cursorRow < KEYBOARD_ROWS - 1) { + cursorRow++; + cursorCol = 0; + } else { + cursorRow = 0; + cursorCol = 0; + } + } +} + +void VirtualKeyboard::handlePress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert lowercase character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char + return; + } + + // Handle non-character keys immediately + switch (key.type) { + case VK_BACKSPACE: + deleteCharacter(); + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + std::function callback = onTextEntered; + onTextEntered = nullptr; + inputText = ""; + callback(""); + } + return; + default: + break; + } +} + +void VirtualKeyboard::handleLongPress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert uppercase/alternate character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char + return; + } + + switch (key.type) { + case VK_BACKSPACE: + // One-shot: delete up to 5 characters on long press + for (int i = 0; i < 5; ++i) { + if (inputText.empty()) + break; + deleteCharacter(); + } + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + onTextEntered(""); + } + break; + default: + break; + } +} + +void VirtualKeyboard::insertCharacter(char c) +{ + if (inputText.length() < 160) { // Reasonable text length limit + inputText += c; + } +} + +void VirtualKeyboard::deleteCharacter() +{ + if (!inputText.empty()) { + inputText.pop_back(); + } +} + +void VirtualKeyboard::submitText() +{ + LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); + + // Only submit if text is not empty + if (!inputText.empty() && onTextEntered) { + // Store callback and text to submit before clearing callback + std::function callback = onTextEntered; + std::string textToSubmit = inputText; + onTextEntered = nullptr; + // Don't clear inputText here - let the calling module handle cleanup + // inputText = ""; // Removed: keep text visible until module cleans up + callback(textToSubmit); + } else if (inputText.empty()) { + // For empty text, just ignore the submission - don't clear callback + // This keeps the virtual keyboard responsive for further input + LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); + } else { + // No callback available + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } +} + +void VirtualKeyboard::setInputText(const std::string &text) +{ + inputText = text; +} + +std::string VirtualKeyboard::getInputText() const +{ + return inputText; +} + +void VirtualKeyboard::setHeader(const std::string &header) +{ + headerText = header; +} + +void VirtualKeyboard::setCallback(std::function callback) +{ + onTextEntered = callback; +} + +void VirtualKeyboard::resetTimeout() +{ + lastActivityTime = millis(); +} + +bool VirtualKeyboard::isTimedOut() const +{ + return (millis() - lastActivityTime) > TIMEOUT_MS; +} + +} // namespace graphics diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h new file mode 100644 index 000000000..169163b57 --- /dev/null +++ b/src/graphics/VirtualKeyboard.h @@ -0,0 +1,80 @@ +#pragma once + +#include "configuration.h" +#include +#include +#include + +namespace graphics +{ + +enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; + +struct VirtualKey { + char character; + VirtualKeyType type; + uint8_t x; + uint8_t y; + uint8_t width; + uint8_t height; +}; + +class VirtualKeyboard +{ + public: + VirtualKeyboard(); + ~VirtualKeyboard(); + + void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); + void setInputText(const std::string &text); + std::string getInputText() const; + void setHeader(const std::string &header); + void setCallback(std::function callback); + + // Navigation methods for encoder input + void moveCursorUp(); + void moveCursorDown(); + void moveCursorLeft(); + void moveCursorRight(); + void handlePress(); + void handleLongPress(); + + // Timeout management + void resetTimeout(); + bool isTimedOut() const; + + private: + static const uint8_t KEYBOARD_ROWS = 4; + static const uint8_t KEYBOARD_COLS = 11; + static const uint8_t KEY_WIDTH = 9; + static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays + static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom + + VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; + + std::string inputText; + std::string headerText; + std::function onTextEntered; + + uint8_t cursorRow; + uint8_t cursorCol; + + // Timeout management for auto-exit + uint32_t lastActivityTime; + static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout + + void initializeKeyboard(); + void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, + bool isLastCol); + void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); + + // Unified cursor movement helper + void moveCursorDelta(int dRow, int dCol); + + char getCharForKey(const VirtualKey &key, bool isLongPress = false); + void insertCharacter(char c); + void deleteCharacter(); + void submitText(); +}; + +} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 512f650ec..e02948864 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -10,7 +10,10 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" #include "main.h" +#include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3d635e588..221d95075 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -38,6 +38,8 @@ bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; +VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; +std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { @@ -89,14 +91,33 @@ void NotificationRenderer::resetBanner() void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') - resetBanner(); - if (!isOverlayBannerShowing() || pauseBanner) + // Handle text_input notifications first - they have their own timeout/banner logic + if (current_notification_type == notificationTypeEnum::text_input) { + // Check for timeout and reset if needed for text input + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + return; + } + drawTextInput(display, state); return; + } + + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + } + + // Exit if no banner is showing or banner is paused + if (!isOverlayBannerShowing() || pauseBanner) { + return; + } + switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; + case notificationTypeEnum::text_input: + // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. + break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -575,6 +596,90 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } +void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + if (virtualKeyboard) { + // Check for timeout and auto-exit if needed + if (virtualKeyboard->isTimedOut()) { + LOG_INFO("Virtual keyboard timeout - auto-exiting"); + // Cancel virtual keyboard - call callback with empty string to indicate timeout + auto callback = textInputCallback; // Store callback before clearing + + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + + // Call callback after cleanup + if (callback) { + callback(""); + } + + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Handle input events for virtual keyboard navigation + if (inEvent.inputEvent != INPUT_BROKER_NONE) { + if (inEvent.inputEvent == INPUT_BROKER_UP) { + virtualKeyboard->moveCursorUp(); + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { + virtualKeyboard->moveCursorDown(); + } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { + virtualKeyboard->moveCursorLeft(); + } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { + virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + // Long press UP = move left + virtualKeyboard->moveCursorLeft(); + } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + // Long press DOWN = move right + virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + virtualKeyboard->handlePress(); + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { + virtualKeyboard->handleLongPress(); + } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { + // Cancel virtual keyboard - call callback with empty string + auto callback = textInputCallback; // Store callback before clearing + + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + + // Call callback after cleanup + if (callback) { + callback(""); + } + + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Reset input event after processing + inEvent.inputEvent = INPUT_BROKER_NONE; + } + + // Clear the display and draw virtual keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + virtualKeyboard->draw(display, 0, 0); + } else { + // If virtualKeyboard is null, reset the banner to avoid getting stuck + resetBanner(); + } +} + bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 9c30b329c..edb069513 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -3,6 +3,9 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" +#include "graphics/VirtualKeyboard.h" +#include +#include #define MAX_LINES 5 namespace graphics @@ -22,6 +25,8 @@ class NotificationRenderer static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; + static VirtualKeyboard *virtualKeyboard; + static std::function textInputCallback; static bool pauseBanner; @@ -30,6 +35,7 @@ class NotificationRenderer static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 4487fa662..012a403f5 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,6 +4,7 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, + INPUT_BROKER_SELECT_LONG, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index d41ad2fd6..4c8ce6409 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,12 +1,14 @@ #include "TrackballInterruptBase.h" #include "configuration.h" +extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), - void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) + input_broker_event eventRight, input_broker_event eventPressed, + input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); @@ -40,9 +43,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } - LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, - pinPress); - + LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, + this->_pinLeft, this->_pinRight, pinPress); + osk_found = true; this->setInterval(100); } @@ -50,10 +53,47 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = INPUT_BROKER_NONE; + + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = millis() - pressStartTime; + bool buttonStillPressed = false; + +#if defined(T_DECK) + buttonStillPressed = (this->action == TB_ACTION_PRESSED); +#else + buttonStillPressed = !digitalRead(_pinPress); +#endif + + if (!buttonStillPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION) { + // Short press + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastLongPressEventTime = 0; + this->action = TB_ACTION_NONE; + } else if (pressDuration >= LONG_PRESS_DURATION) { + // Long press detected + uint32_t currentTime = millis(); + // Only trigger long press event if enough time has passed since the last one + if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventPressedLong; + lastLongPressEventTime = currentTime; + } + this->action = TB_ACTION_PRESSED_LONG; + } + } + #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED) { - // LOG_DEBUG("Trackball event Press"); - e.inputEvent = this->_eventPressed; + if (this->action == TB_ACTION_PRESSED && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce() e.inputEvent = this->_eventRight; } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { - // LOG_DEBUG("Trackball event Press"); - e.inputEvent = this->_eventPressed; + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce() e.kbchar = 0x00; this->notifyObservers(&e); } - lastEvent = action; - this->action = TB_ACTION_NONE; - return 100; + // Only update lastEvent for non-press actions or completed press actions + if (this->action != TB_ACTION_PRESSED || !pressDetected) { + lastEvent = action; + if (!pressDetected) { + this->action = TB_ACTION_NONE; + } + } + + return 50; // Check more frequently for better long press detection } void TrackballInterruptBase::intPressHandler() diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 92db8720e..38be22f20 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable, public con explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), - void (*onIntPress)()); + input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable, public con enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, + TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, @@ -46,12 +47,20 @@ class TrackballInterruptBase : public Observable, public con volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + // Long press detection for press button + uint32_t pressStartTime = 0; + bool pressDetected = false; + uint32_t lastLongPressEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = 500; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events + private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 896238f38..594facdeb 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, - TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, - TrackballInterruptImpl1::handleIntPressed); + eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, + TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, + TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } diff --git a/src/main.cpp b/src/main.cpp index ef5f5a721..23ffa6b6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -191,6 +191,8 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; uint8_t kb_model; // global bool to record that a kb is present bool kb_found = false; +// global bool to record that on-screen keyboard (OSK) is present +bool osk_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -1412,6 +1414,10 @@ void setup() #endif #endif +#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) + osk_found = true; +#endif + #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/main.h b/src/main.h index 3568daad2..2ddd4862f 100644 --- a/src/main.h +++ b/src/main.h @@ -32,6 +32,7 @@ extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; +extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index d40dcd24f..76b950322 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,12 +13,16 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -38,6 +42,7 @@ extern ScanI2C::DeviceAddress cardkb_found; extern bool graphics::isMuted; +extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; @@ -151,10 +156,13 @@ int CannedMessageModule::splitConfiguredMessages() int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; - #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a keyboard + // Add a "Free Text" entry at the top if using a touch screen virtual keyboard tempMessages[tempCount++] = "[-- Free Text --]"; +#else + if (osk_found && screen) { + tempMessages[tempCount++] = "[-- Free Text --]"; + } #endif // First message always starts at buffer start @@ -341,6 +349,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state + // Virtual keyboard mode: Show virtual keyboard and handle input + // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; @@ -627,6 +637,56 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo notifyObservers(&e); return true; } +#else + if (strcmp(current, "[-- Free Text --]") == 0) { + if (osk_found && screen) { + char headerBuffer[64]; + if (this->dest == NODENUM_BROADCAST) { + snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); + } else { + snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); + } + screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { + if (!text.empty()) { + this->freetext = text.c_str(); + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + currentMessageIndex = -1; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + setIntervalFromNow(500); + return; + } else { + // Don't delete virtual keyboard immediately - it might still be executing + // Instead, just clear the callback and reset banner to stop input processing + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + + // Return to inactive state + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Force display update to show normal screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + // Schedule cleanup for next loop iteration to ensure safe deletion + setIntervalFromNow(50); + return; + } + }); + + return true; + } + } #endif // Normal canned message selection @@ -943,12 +1003,54 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + // Clean up virtual keyboard if needed when going inactive + if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { + LOG_INFO("Performing delayed virtual keyboard cleanup"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + } + temporaryMessage = ""; return INT32_MAX; } + // Handle delayed virtual keyboard message sending + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + // Virtual keyboard message sending case - text was not empty + if (this->freetext.length() > 0) { + LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); + sendText(this->dest, this->channel, this->freetext.c_str(), true); + + // Clean up virtual keyboard after sending + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard after message send"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + } + + // Clear payload to indicate virtual keyboard processing is complete + // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds + this->payload = 0; + } else { + // Empty message, just go inactive + LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); + return 2000; + } + UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && + this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -958,6 +1060,18 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); + } + // Handle SENDING_ACTIVE state transition after virtual keyboard message + else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { + // This happens after virtual keyboard message sending is complete + LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + temporaryMessage = ""; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -966,9 +1080,23 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + // Clean up virtual keyboard if it exists during timeout + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard due to module timeout"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + } + this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->payload == 0) { + // [Exit] button pressed - return to inactive state + LOG_INFO("Processing [Exit] action - returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; From 1daf5aad1ff4a84071e3e09cc05252ab21811963 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 21 Aug 2025 06:29:23 -0500 Subject: [PATCH 2/2] Revert "Add on-screen keyboard implementation on Trackball device. (#7625)" (#7704) This reverts commit fe3f14a63e2dbd160eabfcb8fdf1fb7cfb8eb1db. --- src/buzz/BuzzerFeedbackThread.cpp | 1 - src/graphics/Screen.cpp | 75 +-- src/graphics/Screen.h | 4 +- src/graphics/VirtualKeyboard.cpp | 738 --------------------- src/graphics/VirtualKeyboard.h | 80 --- src/graphics/draw/MenuHandler.cpp | 3 - src/graphics/draw/NotificationRenderer.cpp | 109 +-- src/graphics/draw/NotificationRenderer.h | 6 - src/input/InputBroker.h | 1 - src/input/TrackballInterruptBase.cpp | 76 +-- src/input/TrackballInterruptBase.h | 13 +- src/input/TrackballInterruptImpl1.cpp | 7 +- src/main.cpp | 6 - src/main.h | 1 - src/modules/CannedMessageModule.cpp | 136 +--- 15 files changed, 30 insertions(+), 1226 deletions(-) delete mode 100644 src/graphics/VirtualKeyboard.cpp delete mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 838224c69..ce762c764 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -28,7 +28,6 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: - case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5e29814cb..fa71e17d8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -216,44 +216,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t ui->update(); } -void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback) -{ - LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - - if (NotificationRenderer::virtualKeyboard) { - delete NotificationRenderer::virtualKeyboard; - NotificationRenderer::virtualKeyboard = nullptr; - } - - NotificationRenderer::textInputCallback = nullptr; - - NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); - if (header) { - NotificationRenderer::virtualKeyboard->setHeader(header); - } - if (initialText) { - NotificationRenderer::virtualKeyboard->setInputText(initialText); - } - - // Set up callback with safer cleanup mechanism - NotificationRenderer::textInputCallback = textCallback; - NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); - - // Store the message and set the expiration timestamp (use same pattern as other notifications) - strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; - - // Set the overlay using the same pattern as other notification types - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); -} - static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -751,19 +713,13 @@ int32_t Screen::runOnce() handleSetOn(false); break; case Cmd::ON_PRESS: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleOnPress(); - } + handleOnPress(); break; case Cmd::SHOW_PREV_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowPrevFrame(); - } + handleShowPrevFrame(); break; case Cmd::SHOW_NEXT_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowNextFrame(); - } + handleShowNextFrame(); break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away @@ -785,9 +741,7 @@ int32_t Screen::runOnce() NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - setFrames(); - } + setFrames(); break; case Cmd::NOOP: break; @@ -823,7 +777,6 @@ int32_t Screen::runOnce() if (showingNormalScreen) { // standard screen loop handling here if (config.display.auto_screen_carousel_secs > 0 && - NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead @@ -914,11 +867,6 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Called when a frame should be added / removed, or custom frames should be cleared void Screen::setFrames(FrameFocus focus) { - // Block setFrames calls when virtual keyboard is active to prevent overlay interference - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return; - } - uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter @@ -1365,11 +1313,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return 0; - } - if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) @@ -1392,16 +1335,6 @@ int Screen::handleInputEvent(const InputEvent *event) if (!screenOn) return 0; - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } - #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 0f100d455..265900131 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -12,7 +12,7 @@ #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { -enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; struct BannerOverlayOptions { const char *message; @@ -313,8 +313,6 @@ class Screen : public concurrency::OSThread void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); - void showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp deleted file mode 100644 index 84d5551cb..000000000 --- a/src/graphics/VirtualKeyboard.cpp +++ /dev/null @@ -1,738 +0,0 @@ -#include "VirtualKeyboard.h" -#include "configuration.h" -#include "graphics/Screen.h" -#include "graphics/ScreenFonts.h" -#include "graphics/SharedUIDisplay.h" -#include "main.h" -#include -#include - -namespace graphics -{ - -VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) -{ - initializeKeyboard(); - // Set cursor to H(2, 5) - cursorRow = 2; - cursorCol = 5; -} - -VirtualKeyboard::~VirtualKeyboard() {} - -void VirtualKeyboard::initializeKeyboard() -{ - // New 4 row, 11 column keyboard layout: - static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, - {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; - - // Derive layout dimensions and assert they match the configured keyboard grid - constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); - constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); - static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); - static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); - - // Initialize all keys to empty first - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; - } - } - - // Fill keyboard from the 2D layout - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - char ch = LAYOUT[row][col]; - // No empty slots in the simplified layout - - VirtualKeyType type = VK_CHAR; - if (ch == '\b') { - type = VK_BACKSPACE; - } else if (ch == '\n') { - type = VK_ENTER; - } else if (ch == '\x1b') { // ESC - type = VK_ESC; - } else if (ch == ' ') { - type = VK_SPACE; - } - - // Make action keys wider to fit text while keeping the last column aligned - uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; - keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; - } - } -} - -void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) -{ - // Repeat ticking is driven by NotificationRenderer once per frame - // Base styles - display->setColor(WHITE); - display->setFont(FONT_SMALL); - - // Screen geometry - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); - - // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels - // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide - const bool isWide = screenW >= 200; - - // Determine last-column label max width - display->setFont(FONT_SMALL); - const int wENTER = display->getStringWidth("ENTER"); - int lastColLabelW = wENTER; // ENTER is usually the widest - // Smaller padding on very small screens to avoid excessive whitespace - const int lastColPad = (screenW <= 128 ? 2 : 6); - const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys - - // Always reserve width for the rightmost text column to avoid overlap on small screens - int cellW = 0; - int leftoverW = 0; - { - const int leftCols = KEYBOARD_COLS - 1; // 10 input characters - int usableW = screenW - reservedLastColW; - if (usableW < leftCols) { - // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) - usableW = leftCols; - } - cellW = usableW / leftCols; - leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) - } - - // Dynamic key geometry - int cellH = KEY_HEIGHT; - int keyboardStartY = 0; - if (screenH <= 64) { - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); - const int gapBelowHeader = 0; - const int singleLineBoxHeight = FONT_HEIGHT_SMALL; - const int gapAboveKeyboard = 0; - keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; - if (keyboardStartY < 0) - keyboardStartY = 0; - if (keyboardStartY > screenH) - keyboardStartY = screenH; - int keyboardHeight = screenH - keyboardStartY; - cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); - } else if (isWide) { - // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. - cellH = std::max((int)KEY_HEIGHT, cellW); - - // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. - // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 - display->setFont(FONT_SMALL); - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); - const int headerToBoxGap = 1; - const int gapAboveKb = 1; - const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom - int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); - int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; - if (maxCellHAllowed < (int)KEY_HEIGHT) - maxCellHAllowed = KEY_HEIGHT; - if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { - cellH = maxCellHAllowed; - } - // Keyboard placement from bottom for wide screens - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } else { - // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom - cellH = KEY_HEIGHT; - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } - - // Draw input area above keyboard - drawInputArea(display, offsetX, offsetY, keyboardStartY); - - // Precompute per-column x and width with leftover distributed over left columns for even spacing - int colX[KEYBOARD_COLS]; - int colW[KEYBOARD_COLS]; - int runningX = offsetX; - for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { - int wcol = cellW + (col < leftoverW ? 1 : 0); - colX[col] = runningX; - colW[col] = wcol; - runningX += wcol; - } - // Last column - colX[KEYBOARD_COLS - 1] = runningX; - colW[KEYBOARD_COLS - 1] = reservedLastColW; - - // Draw keyboard grid - for (int row = 0; row < KEYBOARD_ROWS; row++) { - for (int col = 0; col < KEYBOARD_COLS; col++) { - const VirtualKey &k = keyboard[row][col]; - if (k.character != 0 || k.type != VK_CHAR) { - const bool isLastCol = (col == KEYBOARD_COLS - 1); - int x = colX[col]; - int w = colW[col]; - int y = offsetY + keyboardStartY + row * cellH; - int h = cellH; - bool selected = (row == cursorRow && col == cursorCol); - drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); - } - } - } -} - -void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) -{ - display->setColor(WHITE); - - const int screenWidth = display->getWidth(); - const int screenHeight = display->getHeight(); - // Use the standard small font metrics for input box sizing (restore original size) - const int inputLineH = FONT_HEIGHT_SMALL; - - // Header uses the standard small (which may be larger on big screens) - display->setFont(FONT_SMALL); - int headerHeight = 0; - if (!headerText.empty()) { - // Draw header and reserve exact font height (plus a tighter gap) to maximize input area - display->drawString(offsetX + 2, offsetY, headerText.c_str()); - if (screenHeight <= 64) { - headerHeight = FONT_HEIGHT_SMALL - 2; // 11px - } else { - headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in - } - } - - const int boxX = offsetX; - const int boxWidth = screenWidth; - int boxY; - int boxHeight; - if (screenHeight <= 64) { - const int gapBelowHeader = 0; - const int fixedBoxHeight = inputLineH; - const int gapAboveKeyboard = 0; - boxY = offsetY + headerHeight + gapBelowHeader; - boxHeight = fixedBoxHeight; - if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { - int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; - boxHeight = std::max(1, fixedBoxHeight - over); - } - } else { - const int gapBelowHeader = 1; - int gapAboveKeyboard = 1; - int tmpBoxY = offsetY + headerHeight + gapBelowHeader; - const int minBoxHeight = inputLineH + 2; - int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; - if (availableH < minBoxHeight) - availableH = minBoxHeight; - boxY = tmpBoxY; - boxHeight = availableH; - } - - // Draw box border - display->drawRect(boxX, boxY, boxWidth, boxHeight); - - display->setFont(FONT_SMALL); - - // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis - const int textX = boxX + 2; - const int maxTextWidth = boxWidth - 4; - const int maxLines = (boxHeight - 2) / inputLineH; - if (maxLines >= 2) { - // Inner bounds for caret clamping - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Wrap text greedily into lines that fit maxTextWidth - std::vector lines; - { - std::string remaining = inputText; - while (!remaining.empty()) { - int bestLen = 0; - for (int len = 1; len <= (int)remaining.size(); ++len) { - int w = display->getStringWidth(remaining.substr(0, len).c_str()); - if (w <= maxTextWidth) - bestLen = len; - else - break; - } - if (bestLen == 0) { - // At least show one character to make progress - bestLen = 1; - } - lines.emplace_back(remaining.substr(0, bestLen)); - remaining.erase(0, bestLen); - } - } - - const bool scrolledUp = ((int)lines.size() > maxLines); - int caretX = textX; - int caretY = innerTop; - - // Leave a small top gap to render '...' without replacing the first line - const int topInset = 2; - const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height - int lineY = innerTop + topInset; - - if (scrolledUp) { - // Draw three small dots centered horizontally, vertically at the midpoint of the gap - // between the inner top and the first line's top baseline. This avoids using a tall glyph. - const int firstLineTop = lineY; // baseline top for the first visible line - const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested - const int centerX = boxX + boxWidth / 2; - const int dotSpacing = 3; // px between dots - const int dotSize = 1; // small square dot - display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); - display->fillRect(centerX, gapMidY, dotSize, dotSize); - display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); - } - - // How many lines fit with our top inset and tighter step - const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); - const int linesToShow = std::min((int)lines.size(), linesCapacity); - const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; - - for (int i = 0; i < linesToShow; ++i) { - const std::string &chunk = lines[startIndex + i]; - display->drawString(textX, lineY, chunk.c_str()); - caretX = textX + display->getStringWidth(chunk.c_str()); - caretY = lineY; - lineY += lineStep; - } - - // Draw caret at end of the last visible line - int caretPadY = 2; - if (boxHeight >= inputLineH + 4) - caretPadY = 3; - int cursorTop = caretY + caretPadY; - // Use lineStep so caret height matches the row spacing - int cursorH = lineStep - caretPadY * 2; - if (cursorH < 1) - cursorH = 1; - // Clamp vertical bounds to stay inside the inner rect - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - // Only draw if cursor is inside inner bounds - if (caretX >= innerLeft && caretX <= innerRight) { - display->drawVerticalLine(caretX, cursorTop, cursorH); - } - } else { - std::string displayText = inputText; - int textW = display->getStringWidth(displayText.c_str()); - std::string scrolled = displayText; - if (textW > maxTextWidth) { - // Trim from the left until it fits - while (textW > maxTextWidth && !scrolled.empty()) { - scrolled.erase(0, 1); - textW = display->getStringWidth(scrolled.c_str()); - } - // Add leading ellipsis and ensure it still fits - if (scrolled != displayText) { - scrolled = "..." + scrolled; - textW = display->getStringWidth(scrolled.c_str()); - // If adding ellipsis causes overflow, trim more after the ellipsis - while (textW > maxTextWidth && scrolled.size() > 3) { - scrolled.erase(3, 1); // remove chars after the ellipsis - textW = display->getStringWidth(scrolled.c_str()); - } - } - } else { - // Keep textW in sync with what we draw - textW = display->getStringWidth(scrolled.c_str()); - } - - int textY; - if (screenHeight <= 64) { - textY = boxY + (boxHeight - inputLineH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Center text vertically within inner box for single-line, then clamp so it never overlaps borders - int innerH = innerBottom - innerTop + 1; - textY = innerTop + std::max(0, (innerH - inputLineH) / 2); - // Clamp fully inside the inner rect - if (textY < innerTop) - textY = innerTop; - int maxTop = innerBottom - inputLineH + 1; - if (textY > maxTop) - textY = maxTop; - } - - if (!scrolled.empty()) { - display->drawString(textX, textY, scrolled.c_str()); - } - - int cursorX = textX + textW; - if (screenHeight > 64) { - const int innerRight = boxX + boxWidth - 2; - if (cursorX > innerRight) - cursorX = innerRight; - } - - int cursorTop, cursorH; - if (screenHeight <= 64) { - cursorH = 10; - cursorTop = boxY + (boxHeight - cursorH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - cursorTop = boxY + 2; - cursorH = boxHeight - 4; - if (cursorH < 1) - cursorH = 1; - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - - if (cursorX < innerLeft || cursorX > innerRight) - return; - } - - display->drawVerticalLine(cursorX, cursorTop, cursorH); - } -} - -void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, - uint8_t height, bool isLastCol) -{ - // Draw key content - display->setFont(FONT_SMALL); - const int fontH = FONT_HEIGHT_SMALL; - // Build label and metrics first - std::string keyText; - if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { - // Keep literal text labels for the action keys on the rightmost column - keyText = (key.type == VK_BACKSPACE) ? "BACK" - : (key.type == VK_ENTER) ? "ENTER" - : (key.type == VK_SPACE) ? "SPACE" - : (key.type == VK_ESC) ? "ESC" - : ""; - } else { - char c = getCharForKey(key, false); - if (c >= 'a' && c <= 'z') { - c = c - 'a' + 'A'; - } - keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); - } - - int textWidth = display->getStringWidth(keyText.c_str()); - // Label alignment - // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. - // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. - int textX; - if (isLastCol) { - const int rightPad = 1; - textX = x + width - textWidth - rightPad; - if (textX < x) - textX = x; // guard - } else { - if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { - textX = x + (width - textWidth + 1) / 2; - } else { - textX = x + (width - textWidth) / 2; - } - } - int contentTop = y; - int contentH = height; - if (selected) { - display->setColor(WHITE); - bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); - - if (display->getHeight() <= 64 && !isAction) { - display->fillRect(x, y, width, height); - } else if (isAction) { - const int padX = 1; - const int padY = 2; - int hlW = textWidth + padX * 2; - int hlX = textX - padX; - - if (hlX < x) { - hlW -= (x - hlX); - hlX = x; - } - int maxW = (x + width) - hlX; - if (hlW > maxW) - hlW = maxW; - if (hlW < 1) - hlW = 1; - - int hlH = std::min(fontH + padY * 2, (int)height); - int hlY = y + (height - hlH) / 2; - display->fillRect(hlX, hlY, hlW, hlH); - contentTop = hlY; - contentH = hlH; - } else { - display->fillRect(x, y, width, height); - } - display->setColor(BLACK); - } else { - display->setColor(WHITE); - } - - int centeredTextY; - if (display->getHeight() <= 64) { - centeredTextY = y + (height - fontH) / 2; - } else { - centeredTextY = contentTop + (contentH - fontH) / 2; - } - if (display->getHeight() > 64) { - if (centeredTextY < contentTop) - centeredTextY = contentTop; - if (centeredTextY + fontH > contentTop + contentH) - centeredTextY = std::max(contentTop, contentTop + contentH - fontH); - } - - if (display->getHeight() <= 64 && keyText.size() == 1) { - char ch = keyText[0]; - if (ch == '.' || ch == ',' || ch == ';') { - centeredTextY -= 1; - } - } - display->drawString(textX, centeredTextY, keyText.c_str()); -} - -char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) -{ - if (key.type != VK_CHAR) { - return key.character; - } - - char c = key.character; - - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); - } - - return c; -} - -void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) -{ - resetTimeout(); - // wrap around rows and cols in the 4x11 grid - int r = (int)cursorRow + dRow; - int c = (int)cursorCol + dCol; - if (r < 0) - r = KEYBOARD_ROWS - 1; - else if (r >= KEYBOARD_ROWS) - r = 0; - if (c < 0) - c = KEYBOARD_COLS - 1; - else if (c >= KEYBOARD_COLS) - c = 0; - cursorRow = (uint8_t)r; - cursorCol = (uint8_t)c; -} - -void VirtualKeyboard::moveCursorUp() -{ - moveCursorDelta(-1, 0); -} -void VirtualKeyboard::moveCursorDown() -{ - moveCursorDelta(1, 0); -} -void VirtualKeyboard::moveCursorLeft() -{ - resetTimeout(); - - if (cursorCol > 0) { - cursorCol--; - } else { - if (cursorRow > 0) { - cursorRow--; - cursorCol = KEYBOARD_COLS - 1; - } else { - cursorRow = KEYBOARD_ROWS - 1; - cursorCol = KEYBOARD_COLS - 1; - } - } -} -void VirtualKeyboard::moveCursorRight() -{ - resetTimeout(); - - if (cursorCol < KEYBOARD_COLS - 1) { - cursorCol++; - } else { - if (cursorRow < KEYBOARD_ROWS - 1) { - cursorRow++; - cursorCol = 0; - } else { - cursorRow = 0; - cursorCol = 0; - } - } -} - -void VirtualKeyboard::handlePress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert lowercase character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char - return; - } - - // Handle non-character keys immediately - switch (key.type) { - case VK_BACKSPACE: - deleteCharacter(); - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - std::function callback = onTextEntered; - onTextEntered = nullptr; - inputText = ""; - callback(""); - } - return; - default: - break; - } -} - -void VirtualKeyboard::handleLongPress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert uppercase/alternate character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char - return; - } - - switch (key.type) { - case VK_BACKSPACE: - // One-shot: delete up to 5 characters on long press - for (int i = 0; i < 5; ++i) { - if (inputText.empty()) - break; - deleteCharacter(); - } - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - onTextEntered(""); - } - break; - default: - break; - } -} - -void VirtualKeyboard::insertCharacter(char c) -{ - if (inputText.length() < 160) { // Reasonable text length limit - inputText += c; - } -} - -void VirtualKeyboard::deleteCharacter() -{ - if (!inputText.empty()) { - inputText.pop_back(); - } -} - -void VirtualKeyboard::submitText() -{ - LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); - - // Only submit if text is not empty - if (!inputText.empty() && onTextEntered) { - // Store callback and text to submit before clearing callback - std::function callback = onTextEntered; - std::string textToSubmit = inputText; - onTextEntered = nullptr; - // Don't clear inputText here - let the calling module handle cleanup - // inputText = ""; // Removed: keep text visible until module cleans up - callback(textToSubmit); - } else if (inputText.empty()) { - // For empty text, just ignore the submission - don't clear callback - // This keeps the virtual keyboard responsive for further input - LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); - } else { - // No callback available - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } -} - -void VirtualKeyboard::setInputText(const std::string &text) -{ - inputText = text; -} - -std::string VirtualKeyboard::getInputText() const -{ - return inputText; -} - -void VirtualKeyboard::setHeader(const std::string &header) -{ - headerText = header; -} - -void VirtualKeyboard::setCallback(std::function callback) -{ - onTextEntered = callback; -} - -void VirtualKeyboard::resetTimeout() -{ - lastActivityTime = millis(); -} - -bool VirtualKeyboard::isTimedOut() const -{ - return (millis() - lastActivityTime) > TIMEOUT_MS; -} - -} // namespace graphics diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h deleted file mode 100644 index 169163b57..000000000 --- a/src/graphics/VirtualKeyboard.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "configuration.h" -#include -#include -#include - -namespace graphics -{ - -enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; - -struct VirtualKey { - char character; - VirtualKeyType type; - uint8_t x; - uint8_t y; - uint8_t width; - uint8_t height; -}; - -class VirtualKeyboard -{ - public: - VirtualKeyboard(); - ~VirtualKeyboard(); - - void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); - void setInputText(const std::string &text); - std::string getInputText() const; - void setHeader(const std::string &header); - void setCallback(std::function callback); - - // Navigation methods for encoder input - void moveCursorUp(); - void moveCursorDown(); - void moveCursorLeft(); - void moveCursorRight(); - void handlePress(); - void handleLongPress(); - - // Timeout management - void resetTimeout(); - bool isTimedOut() const; - - private: - static const uint8_t KEYBOARD_ROWS = 4; - static const uint8_t KEYBOARD_COLS = 11; - static const uint8_t KEY_WIDTH = 9; - static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays - static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom - - VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; - - std::string inputText; - std::string headerText; - std::function onTextEntered; - - uint8_t cursorRow; - uint8_t cursorCol; - - // Timeout management for auto-exit - uint32_t lastActivityTime; - static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout - - void initializeKeyboard(); - void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, - bool isLastCol); - void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); - - // Unified cursor movement helper - void moveCursorDelta(int dRow, int dCol); - - char getCharForKey(const VirtualKey &key, bool isLongPress = false); - void insertCharacter(char c); - void deleteCharacter(); - void submitText(); -}; - -} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e02948864..512f650ec 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -10,10 +10,7 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" -#include "input/RotaryEncoderInterruptImpl1.h" -#include "input/UpDownInterruptImpl1.h" #include "main.h" -#include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 221d95075..3d635e588 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -38,8 +38,6 @@ bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; -VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; -std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { @@ -91,33 +89,14 @@ void NotificationRenderer::resetBanner() void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - // Handle text_input notifications first - they have their own timeout/banner logic - if (current_notification_type == notificationTypeEnum::text_input) { - // Check for timeout and reset if needed for text input - if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); - return; - } - drawTextInput(display, state); - return; - } - - if (millis() > alertBannerUntil && alertBannerUntil > 0) { + if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') resetBanner(); - } - - // Exit if no banner is showing or banner is paused - if (!isOverlayBannerShowing() || pauseBanner) { + if (!isOverlayBannerShowing() || pauseBanner) return; - } - switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; - case notificationTypeEnum::text_input: - // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. - break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -596,90 +575,6 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } -void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - if (virtualKeyboard) { - // Check for timeout and auto-exit if needed - if (virtualKeyboard->isTimedOut()) { - LOG_INFO("Virtual keyboard timeout - auto-exiting"); - // Cancel virtual keyboard - call callback with empty string to indicate timeout - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Handle input events for virtual keyboard navigation - if (inEvent.inputEvent != INPUT_BROKER_NONE) { - if (inEvent.inputEvent == INPUT_BROKER_UP) { - virtualKeyboard->moveCursorUp(); - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { - virtualKeyboard->moveCursorDown(); - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { - // Long press UP = move left - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { - // Long press DOWN = move right - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - virtualKeyboard->handlePress(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { - virtualKeyboard->handleLongPress(); - } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { - // Cancel virtual keyboard - call callback with empty string - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Reset input event after processing - inEvent.inputEvent = INPUT_BROKER_NONE; - } - - // Clear the display and draw virtual keyboard - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - virtualKeyboard->draw(display, 0, 0); - } else { - // If virtualKeyboard is null, reset the banner to avoid getting stuck - resetBanner(); - } -} - bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index edb069513..9c30b329c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -3,9 +3,6 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" -#include "graphics/VirtualKeyboard.h" -#include -#include #define MAX_LINES 5 namespace graphics @@ -25,8 +22,6 @@ class NotificationRenderer static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; - static VirtualKeyboard *virtualKeyboard; - static std::function textInputCallback; static bool pauseBanner; @@ -35,7 +30,6 @@ class NotificationRenderer static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 012a403f5..4487fa662 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,7 +4,6 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, - INPUT_BROKER_SELECT_LONG, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index 4c8ce6409..d41ad2fd6 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,14 +1,12 @@ #include "TrackballInterruptBase.h" #include "configuration.h" -extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, - input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) + input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -20,7 +18,6 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); @@ -43,9 +40,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } - LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, - this->_pinLeft, this->_pinRight, pinPress); - osk_found = true; + LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, + pinPress); + this->setInterval(100); } @@ -53,47 +50,10 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = INPUT_BROKER_NONE; - - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = millis() - pressStartTime; - bool buttonStillPressed = false; - -#if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); -#else - buttonStillPressed = !digitalRead(_pinPress); -#endif - - if (!buttonStillPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION) { - // Short press - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastLongPressEventTime = 0; - this->action = TB_ACTION_NONE; - } else if (pressDuration >= LONG_PRESS_DURATION) { - // Long press detected - uint32_t currentTime = millis(); - // Only trigger long press event if enough time has passed since the last one - if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventPressedLong; - lastLongPressEventTime = currentTime; - } - this->action = TB_ACTION_PRESSED_LONG; - } - } - #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press + if (this->action == TB_ACTION_PRESSED) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -108,11 +68,9 @@ int32_t TrackballInterruptBase::runOnce() e.inputEvent = this->_eventRight; } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -133,16 +91,10 @@ int32_t TrackballInterruptBase::runOnce() e.kbchar = 0x00; this->notifyObservers(&e); } + lastEvent = action; + this->action = TB_ACTION_NONE; - // Only update lastEvent for non-press actions or completed press actions - if (this->action != TB_ACTION_PRESSED || !pressDetected) { - lastEvent = action; - if (!pressDetected) { - this->action = TB_ACTION_NONE; - } - } - - return 50; // Check more frequently for better long press detection + return 100; } void TrackballInterruptBase::intPressHandler() diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 38be22f20..92db8720e 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable, public con explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); + input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -33,7 +33,6 @@ class TrackballInterruptBase : public Observable, public con enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, - TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, @@ -47,20 +46,12 @@ class TrackballInterruptBase : public Observable, public con volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - // Long press detection for press button - uint32_t pressStartTime = 0; - bool pressDetected = false; - uint32_t lastLongPressEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = 500; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events - private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 594facdeb..896238f38 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -13,12 +13,11 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, - TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, - TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); + eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, + TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, + TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } diff --git a/src/main.cpp b/src/main.cpp index 23ffa6b6d..ef5f5a721 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -191,8 +191,6 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; uint8_t kb_model; // global bool to record that a kb is present bool kb_found = false; -// global bool to record that on-screen keyboard (OSK) is present -bool osk_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -1414,10 +1412,6 @@ void setup() #endif #endif -#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) - osk_found = true; -#endif - #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/main.h b/src/main.h index 2ddd4862f..3568daad2 100644 --- a/src/main.h +++ b/src/main.h @@ -32,7 +32,6 @@ extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; -extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 76b950322..d40dcd24f 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,16 +13,12 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" -#include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control -#if HAS_TRACKBALL -#include "input/TrackballInterruptImpl1.h" -#endif #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -42,7 +38,6 @@ extern ScanI2C::DeviceAddress cardkb_found; extern bool graphics::isMuted; -extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; @@ -156,13 +151,10 @@ int CannedMessageModule::splitConfiguredMessages() int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; + #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a touch screen virtual keyboard + // Add a "Free Text" entry at the top if using a keyboard tempMessages[tempCount++] = "[-- Free Text --]"; -#else - if (osk_found && screen) { - tempMessages[tempCount++] = "[-- Free Text --]"; - } #endif // First message always starts at buffer start @@ -349,8 +341,6 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state - // Virtual keyboard mode: Show virtual keyboard and handle input - // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; @@ -637,56 +627,6 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo notifyObservers(&e); return true; } -#else - if (strcmp(current, "[-- Free Text --]") == 0) { - if (osk_found && screen) { - char headerBuffer[64]; - if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); - } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); - } - screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { - if (!text.empty()) { - this->freetext = text.c_str(); - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; - currentMessageIndex = -1; - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - setIntervalFromNow(500); - return; - } else { - // Don't delete virtual keyboard immediately - it might still be executing - // Instead, just clear the callback and reset banner to stop input processing - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - - // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Force display update to show normal screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - // Schedule cleanup for next loop iteration to ensure safe deletion - setIntervalFromNow(50); - return; - } - }); - - return true; - } - } #endif // Normal canned message selection @@ -1003,54 +943,12 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { - // Clean up virtual keyboard if needed when going inactive - if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { - LOG_INFO("Performing delayed virtual keyboard cleanup"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - } - temporaryMessage = ""; return INT32_MAX; } - // Handle delayed virtual keyboard message sending - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - // Virtual keyboard message sending case - text was not empty - if (this->freetext.length() > 0) { - LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); - sendText(this->dest, this->channel, this->freetext.c_str(), true); - - // Clean up virtual keyboard after sending - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard after message send"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - } - - // Clear payload to indicate virtual keyboard processing is complete - // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds - this->payload = 0; - } else { - // Empty message, just go inactive - LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - return 2000; - } - UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && - this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -1060,18 +958,6 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); - } - // Handle SENDING_ACTIVE state transition after virtual keyboard message - else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - // This happens after virtual keyboard message sending is complete - LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -1080,23 +966,9 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - // Clean up virtual keyboard if it exists during timeout - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - } - this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == 0) { - // [Exit] button pressed - return to inactive state - LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;