mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-14 06:42:34 +00:00
Refactored the virtual keyboard implementation, migrated to OnScreenKeyboardModule, and removed the old NotificationRenderer related code.
This commit is contained in:
@@ -236,24 +236,10 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
|||||||
{
|
{
|
||||||
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||||
|
|
||||||
if (NotificationRenderer::virtualKeyboard) {
|
// Start OnScreenKeyboardModule session (non-touch variant)
|
||||||
delete NotificationRenderer::virtualKeyboard;
|
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
|
||||||
NotificationRenderer::virtualKeyboard = nullptr;
|
NotificationRenderer::virtualKeyboard = OnScreenKeyboardModule::instance().getKeyboard();
|
||||||
}
|
|
||||||
|
|
||||||
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::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)
|
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
||||||
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||||
|
|||||||
@@ -50,11 +50,6 @@ std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
|||||||
bool NotificationRenderer::pauseBanner = false;
|
bool NotificationRenderer::pauseBanner = false;
|
||||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||||
|
|
||||||
// Variables for message popup overlay on virtual keyboard
|
|
||||||
char NotificationRenderer::keyboardPopupMessage[256] = {0};
|
|
||||||
char NotificationRenderer::keyboardPopupTitle[64] = {0};
|
|
||||||
uint32_t NotificationRenderer::keyboardPopupUntil = 0;
|
|
||||||
bool NotificationRenderer::showKeyboardPopup = false;
|
|
||||||
uint32_t NotificationRenderer::numDigits = 0;
|
uint32_t NotificationRenderer::numDigits = 0;
|
||||||
uint32_t NotificationRenderer::currentNumber = 0;
|
uint32_t NotificationRenderer::currentNumber = 0;
|
||||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
@@ -96,11 +91,7 @@ void NotificationRenderer::resetBanner()
|
|||||||
alertBannerMessage[0] = '\0';
|
alertBannerMessage[0] = '\0';
|
||||||
current_notification_type = notificationTypeEnum::none;
|
current_notification_type = notificationTypeEnum::none;
|
||||||
|
|
||||||
// Reset keyboard popup message variables
|
OnScreenKeyboardModule::instance().clearPopup();
|
||||||
keyboardPopupMessage[0] = '\0';
|
|
||||||
keyboardPopupTitle[0] = '\0';
|
|
||||||
keyboardPopupUntil = 0;
|
|
||||||
showKeyboardPopup = false;
|
|
||||||
|
|
||||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||||
inEvent.kbchar = 0;
|
inEvent.kbchar = 0;
|
||||||
@@ -115,9 +106,10 @@ void NotificationRenderer::resetBanner()
|
|||||||
|
|
||||||
nodeDB->pause_sort(false);
|
nodeDB->pause_sort(false);
|
||||||
|
|
||||||
// If we're exiting from text_input (virtual keyboard), trigger frame update
|
// If we're exiting from text_input (virtual keyboard), stop module and trigger frame update
|
||||||
// to ensure any messages received during keyboard use are now displayed
|
// to ensure any messages received during keyboard use are now displayed
|
||||||
if (previousType == notificationTypeEnum::text_input && screen) {
|
if (previousType == notificationTypeEnum::text_input && screen) {
|
||||||
|
OnScreenKeyboardModule::instance().stop(false);
|
||||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,97 +621,26 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
|||||||
|
|
||||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
{
|
{
|
||||||
if (virtualKeyboard) {
|
// Delegate session to OnScreenKeyboardModule
|
||||||
// Check for timeout and auto-exit if needed
|
auto &osk = OnScreenKeyboardModule::instance();
|
||||||
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
|
if (!osk.isActive()) {
|
||||||
delete virtualKeyboard;
|
LOG_INFO("Virtual keyboard is not active - resetting banner");
|
||||||
virtualKeyboard = nullptr;
|
|
||||||
textInputCallback = nullptr;
|
|
||||||
resetBanner();
|
|
||||||
|
|
||||||
// Call callback after cleanup
|
|
||||||
if (callback) {
|
|
||||||
callback("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore normal overlays
|
|
||||||
if (screen) {
|
|
||||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
|
||||||
if (inEvent.inputEvent == INPUT_BROKER_UP) {
|
|
||||||
// high frequency for move cursor left/right than up/down with encoders
|
|
||||||
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
|
||||||
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
|
||||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
|
||||||
virtualKeyboard->moveCursorLeft();
|
|
||||||
} else {
|
|
||||||
virtualKeyboard->moveCursorUp();
|
|
||||||
}
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
|
|
||||||
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
|
||||||
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
|
||||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
|
||||||
virtualKeyboard->moveCursorRight();
|
|
||||||
} else {
|
|
||||||
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_UP_LONG) {
|
|
||||||
virtualKeyboard->moveCursorUp();
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
|
||||||
virtualKeyboard->moveCursorDown();
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
|
||||||
virtualKeyboard->moveCursorLeft();
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
|
||||||
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) {
|
|
||||||
auto callback = textInputCallback;
|
|
||||||
delete virtualKeyboard;
|
|
||||||
virtualKeyboard = nullptr;
|
|
||||||
textInputCallback = nullptr;
|
|
||||||
resetBanner();
|
|
||||||
if (callback) {
|
|
||||||
callback("");
|
|
||||||
}
|
|
||||||
if (screen) {
|
|
||||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume the event after processing for virtual keyboard
|
|
||||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the screen to avoid overlapping with underlying frames or overlays
|
|
||||||
display->setColor(BLACK);
|
|
||||||
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
|
||||||
display->setColor(WHITE);
|
|
||||||
// Draw the virtual keyboard
|
|
||||||
virtualKeyboard->draw(display, 0, 0);
|
|
||||||
|
|
||||||
// Draw message popup overlay if active
|
|
||||||
drawKeyboardMessagePopup(display);
|
|
||||||
} else {
|
|
||||||
// If virtualKeyboard is null, reset the banner to avoid getting stuck
|
|
||||||
LOG_INFO("Virtual keyboard is null - resetting banner");
|
|
||||||
resetBanner();
|
resetBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||||
|
osk.handleInput(inEvent);
|
||||||
|
inEvent.inputEvent = INPUT_BROKER_NONE; // consume
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw. If draw returns false, session ended (timeout or cancel)
|
||||||
|
if (!osk.draw(display)) {
|
||||||
|
// Session ended, ensure banner reset and restore frames
|
||||||
|
resetBanner();
|
||||||
|
if (screen)
|
||||||
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,135 +651,12 @@ bool NotificationRenderer::isOverlayBannerShowing()
|
|||||||
|
|
||||||
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
|
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
|
||||||
{
|
{
|
||||||
if (!title || !content || current_notification_type != notificationTypeEnum::text_input) {
|
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
|
||||||
return;
|
return;
|
||||||
}
|
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
|
||||||
|
|
||||||
strncpy(keyboardPopupTitle, title, 63);
|
|
||||||
keyboardPopupTitle[63] = '\0';
|
|
||||||
strncpy(keyboardPopupMessage, content, 255);
|
|
||||||
keyboardPopupMessage[255] = '\0';
|
|
||||||
keyboardPopupUntil = millis() + durationMs;
|
|
||||||
showKeyboardPopup = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationRenderer::drawKeyboardMessagePopup(OLEDDisplay *display)
|
// drawKeyboardMessagePopup removed; OnScreenKeyboardModule handles popup drawing within draw()
|
||||||
{
|
|
||||||
// Check if popup should be dismissed
|
|
||||||
if (!showKeyboardPopup || millis() > keyboardPopupUntil || keyboardPopupMessage[0] == '\0') {
|
|
||||||
showKeyboardPopup = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Use drawNotificationBox for true BannerOverlayOptions style ===
|
|
||||||
constexpr uint16_t maxContentLines = 3; // Maximum content lines
|
|
||||||
bool hasTitle = keyboardPopupTitle[0] != '\0';
|
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
|
|
||||||
// Conservative wrap width to ensure we never exceed screen after padding
|
|
||||||
const uint16_t maxWrapWidth = display->width() - 40; // Leave room for padding and bell icons
|
|
||||||
|
|
||||||
// Prepare content for display - build lines array for drawNotificationBox
|
|
||||||
std::vector<std::string> allLines;
|
|
||||||
|
|
||||||
// Parse message content into lines with word wrapping
|
|
||||||
char contentBuffer[256];
|
|
||||||
strncpy(contentBuffer, keyboardPopupMessage, 255);
|
|
||||||
contentBuffer[255] = '\0';
|
|
||||||
|
|
||||||
// Helper function to wrap text to fit within available width
|
|
||||||
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
|
|
||||||
std::vector<std::string> wrappedLines;
|
|
||||||
std::string currentLine;
|
|
||||||
std::string word;
|
|
||||||
const char *ptr = text;
|
|
||||||
|
|
||||||
while (*ptr && wrappedLines.size() < maxContentLines) {
|
|
||||||
// Skip whitespace and handle explicit newlines
|
|
||||||
while (*ptr && (*ptr == ' ' || *ptr == '\t' || *ptr == '\n' || *ptr == '\r')) {
|
|
||||||
if (*ptr == '\n') {
|
|
||||||
if (!currentLine.empty()) {
|
|
||||||
wrappedLines.push_back(currentLine);
|
|
||||||
currentLine.clear();
|
|
||||||
if (wrappedLines.size() >= maxContentLines)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
++ptr;
|
|
||||||
}
|
|
||||||
if (!*ptr || wrappedLines.size() >= maxContentLines)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Collect next word
|
|
||||||
word.clear();
|
|
||||||
while (*ptr && *ptr != ' ' && *ptr != '\t' && *ptr != '\n' && *ptr != '\r') {
|
|
||||||
word += *ptr++;
|
|
||||||
}
|
|
||||||
if (word.empty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Try to append word
|
|
||||||
std::string testLine = currentLine.empty() ? word : (currentLine + " " + word);
|
|
||||||
uint16_t testWidth = display->getStringWidth(testLine.c_str(), testLine.length(), true);
|
|
||||||
if (testWidth <= availableWidth) {
|
|
||||||
currentLine = testLine;
|
|
||||||
} else {
|
|
||||||
if (!currentLine.empty()) {
|
|
||||||
wrappedLines.push_back(currentLine);
|
|
||||||
currentLine = word;
|
|
||||||
if (wrappedLines.size() >= maxContentLines)
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// Single word longer than available width: hard cut down to fit
|
|
||||||
currentLine = word;
|
|
||||||
while (currentLine.size() > 1 &&
|
|
||||||
display->getStringWidth(currentLine.c_str(), currentLine.length(), true) > availableWidth) {
|
|
||||||
currentLine.pop_back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!currentLine.empty() && wrappedLines.size() < maxContentLines) {
|
|
||||||
wrappedLines.push_back(currentLine);
|
|
||||||
}
|
|
||||||
return wrappedLines;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add title if present
|
|
||||||
if (hasTitle) {
|
|
||||||
allLines.emplace_back(keyboardPopupTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split content by newlines first, then wrap each paragraph
|
|
||||||
std::vector<std::string> contentLines;
|
|
||||||
char *paragraph = strtok(contentBuffer, "\n");
|
|
||||||
while (paragraph != nullptr && contentLines.size() < maxContentLines) {
|
|
||||||
auto wrapped = wrapText(paragraph, maxWrapWidth);
|
|
||||||
for (const auto &ln : wrapped) {
|
|
||||||
if (contentLines.size() >= maxContentLines)
|
|
||||||
break;
|
|
||||||
contentLines.push_back(ln);
|
|
||||||
}
|
|
||||||
paragraph = strtok(nullptr, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add content lines to allLines
|
|
||||||
for (const auto &ln : contentLines) {
|
|
||||||
allLines.push_back(ln);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to const char* array for drawNotificationBox
|
|
||||||
std::vector<const char *> linePointers;
|
|
||||||
for (const auto &line : allLines) {
|
|
||||||
linePointers.push_back(line.c_str());
|
|
||||||
}
|
|
||||||
linePointers.push_back(nullptr); // null terminate
|
|
||||||
|
|
||||||
// Use a custom inverted color version for keyboard popup
|
|
||||||
drawInvertedNotificationBox(display, nullptr, linePointers.data(), allLines.size(), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom inverted color version for keyboard popup - black background with white text
|
// Custom inverted color version for keyboard popup - black background with white text
|
||||||
void NotificationRenderer::drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
void NotificationRenderer::drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "OLEDDisplayUi.h"
|
#include "OLEDDisplayUi.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/VirtualKeyboard.h"
|
#include "graphics/VirtualKeyboard.h"
|
||||||
|
#include "modules/OnScreenKeyboardModule.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#define MAX_LINES 5
|
#define MAX_LINES 5
|
||||||
@@ -30,14 +31,8 @@ class NotificationRenderer
|
|||||||
|
|
||||||
static bool pauseBanner;
|
static bool pauseBanner;
|
||||||
|
|
||||||
static char keyboardPopupMessage[256];
|
|
||||||
static char keyboardPopupTitle[64];
|
|
||||||
static uint32_t keyboardPopupUntil;
|
|
||||||
static bool showKeyboardPopup;
|
|
||||||
|
|
||||||
static void resetBanner();
|
static void resetBanner();
|
||||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||||
static void drawKeyboardMessagePopup(OLEDDisplay *display);
|
|
||||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
|||||||
@@ -1008,8 +1008,7 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr &&
|
if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr &&
|
||||||
graphics::NotificationRenderer::current_notification_type != graphics::notificationTypeEnum::text_input) {
|
graphics::NotificationRenderer::current_notification_type != graphics::notificationTypeEnum::text_input) {
|
||||||
LOG_INFO("Performing delayed virtual keyboard cleanup");
|
LOG_INFO("Performing delayed virtual keyboard cleanup");
|
||||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
temporaryMessage = "";
|
temporaryMessage = "";
|
||||||
@@ -1026,9 +1025,7 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
// Clean up virtual keyboard after sending
|
// Clean up virtual keyboard after sending
|
||||||
if (graphics::NotificationRenderer::virtualKeyboard) {
|
if (graphics::NotificationRenderer::virtualKeyboard) {
|
||||||
LOG_INFO("Cleaning up virtual keyboard after message send");
|
LOG_INFO("Cleaning up virtual keyboard after message send");
|
||||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
|
||||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
|
||||||
graphics::NotificationRenderer::resetBanner();
|
graphics::NotificationRenderer::resetBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1089,9 +1086,7 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
LOG_INFO("Virtual keyboard is active - not cleaning up due to CannedMessage timeout");
|
LOG_INFO("Virtual keyboard is active - not cleaning up due to CannedMessage timeout");
|
||||||
} else if (graphics::NotificationRenderer::virtualKeyboard) {
|
} else if (graphics::NotificationRenderer::virtualKeyboard) {
|
||||||
LOG_INFO("Cleaning up virtual keyboard due to module timeout");
|
LOG_INFO("Cleaning up virtual keyboard due to module timeout");
|
||||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
|
||||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
|
||||||
graphics::NotificationRenderer::resetBanner();
|
graphics::NotificationRenderer::resetBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
279
src/modules/OnScreenKeyboardModule.cpp
Normal file
279
src/modules/OnScreenKeyboardModule.cpp
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/draw/NotificationRenderer.h" // drawInvertedNotificationBox signature reuse
|
||||||
|
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||||
|
#include "input/UpDownInterruptImpl1.h"
|
||||||
|
#include "modules/OnScreenKeyboardModule.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
OnScreenKeyboardModule &OnScreenKeyboardModule::instance()
|
||||||
|
{
|
||||||
|
static OnScreenKeyboardModule inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnScreenKeyboardModule::~OnScreenKeyboardModule()
|
||||||
|
{
|
||||||
|
if (keyboard) {
|
||||||
|
delete keyboard;
|
||||||
|
keyboard = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t,
|
||||||
|
std::function<void(const std::string &)> cb)
|
||||||
|
{
|
||||||
|
if (keyboard) {
|
||||||
|
delete keyboard;
|
||||||
|
keyboard = nullptr;
|
||||||
|
}
|
||||||
|
keyboard = new VirtualKeyboard();
|
||||||
|
callback = cb;
|
||||||
|
if (header)
|
||||||
|
keyboard->setHeader(header);
|
||||||
|
if (initialText)
|
||||||
|
keyboard->setInputText(initialText);
|
||||||
|
|
||||||
|
// Route VK submission/cancel events back into the module
|
||||||
|
keyboard->setCallback([this](const std::string &text) {
|
||||||
|
if (text.empty()) {
|
||||||
|
this->onCancel();
|
||||||
|
} else {
|
||||||
|
this->onSubmit(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Maintain legacy compatibility hooks
|
||||||
|
NotificationRenderer::virtualKeyboard = keyboard;
|
||||||
|
NotificationRenderer::textInputCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::stop(bool callEmptyCallback)
|
||||||
|
{
|
||||||
|
auto cb = callback;
|
||||||
|
callback = nullptr;
|
||||||
|
if (keyboard) {
|
||||||
|
delete keyboard;
|
||||||
|
keyboard = nullptr;
|
||||||
|
}
|
||||||
|
// Keep NotificationRenderer legacy pointers in sync
|
||||||
|
NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
|
NotificationRenderer::textInputCallback = nullptr;
|
||||||
|
clearPopup();
|
||||||
|
if (callEmptyCallback && cb)
|
||||||
|
cb("");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OnScreenKeyboardModule::isActive() const
|
||||||
|
{
|
||||||
|
return keyboard != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::handleInput(const InputEvent &event)
|
||||||
|
{
|
||||||
|
if (!keyboard)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Auto-timeout check handled in draw() to centralize state transitions.
|
||||||
|
switch (event.inputEvent) {
|
||||||
|
case INPUT_BROKER_UP: {
|
||||||
|
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1)
|
||||||
|
keyboard->moveCursorLeft();
|
||||||
|
else
|
||||||
|
keyboard->moveCursorUp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case INPUT_BROKER_DOWN: {
|
||||||
|
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1)
|
||||||
|
keyboard->moveCursorRight();
|
||||||
|
else
|
||||||
|
keyboard->moveCursorDown();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case INPUT_BROKER_LEFT:
|
||||||
|
keyboard->moveCursorLeft();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_RIGHT:
|
||||||
|
keyboard->moveCursorRight();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_UP_LONG:
|
||||||
|
keyboard->moveCursorUp();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_DOWN_LONG:
|
||||||
|
keyboard->moveCursorDown();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_ALT_PRESS:
|
||||||
|
keyboard->moveCursorLeft();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_USER_PRESS:
|
||||||
|
keyboard->moveCursorRight();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_SELECT:
|
||||||
|
keyboard->handlePress();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_SELECT_LONG:
|
||||||
|
keyboard->handleLongPress();
|
||||||
|
break;
|
||||||
|
case INPUT_BROKER_CANCEL:
|
||||||
|
onCancel();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OnScreenKeyboardModule::draw(OLEDDisplay *display)
|
||||||
|
{
|
||||||
|
if (!keyboard)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
if (keyboard->isTimedOut()) {
|
||||||
|
onCancel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear full screen behind keyboard
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||||
|
display->setColor(WHITE);
|
||||||
|
keyboard->draw(display, 0, 0);
|
||||||
|
|
||||||
|
// Draw popup overlay if needed
|
||||||
|
drawPopup(display);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::onSubmit(const std::string &text)
|
||||||
|
{
|
||||||
|
auto cb = callback;
|
||||||
|
stop(false);
|
||||||
|
if (cb)
|
||||||
|
cb(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::onCancel()
|
||||||
|
{
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs)
|
||||||
|
{
|
||||||
|
if (!title || !content)
|
||||||
|
return;
|
||||||
|
strncpy(popupTitle, title, sizeof(popupTitle) - 1);
|
||||||
|
popupTitle[sizeof(popupTitle) - 1] = '\0';
|
||||||
|
strncpy(popupMessage, content, sizeof(popupMessage) - 1);
|
||||||
|
popupMessage[sizeof(popupMessage) - 1] = '\0';
|
||||||
|
popupUntil = millis() + durationMs;
|
||||||
|
popupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::clearPopup()
|
||||||
|
{
|
||||||
|
popupTitle[0] = '\0';
|
||||||
|
popupMessage[0] = '\0';
|
||||||
|
popupUntil = 0;
|
||||||
|
popupVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
|
||||||
|
{
|
||||||
|
if (!popupVisible)
|
||||||
|
return;
|
||||||
|
if (millis() > popupUntil || popupMessage[0] == '\0') {
|
||||||
|
popupVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build lines and leverage NotificationRenderer inverted box drawing for consistent style
|
||||||
|
constexpr uint16_t maxContentLines = 3;
|
||||||
|
const bool hasTitle = popupTitle[0] != '\0';
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
const uint16_t maxWrapWidth = display->width() - 40;
|
||||||
|
|
||||||
|
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> wrapped;
|
||||||
|
std::string current;
|
||||||
|
std::string word;
|
||||||
|
const char *p = text;
|
||||||
|
while (*p && wrapped.size() < maxContentLines) {
|
||||||
|
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
|
||||||
|
if (*p == '\n') {
|
||||||
|
if (!current.empty()) {
|
||||||
|
wrapped.push_back(current);
|
||||||
|
current.clear();
|
||||||
|
if (wrapped.size() >= maxContentLines)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
if (!*p || wrapped.size() >= maxContentLines)
|
||||||
|
break;
|
||||||
|
word.clear();
|
||||||
|
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
|
||||||
|
word += *p++;
|
||||||
|
if (word.empty())
|
||||||
|
continue;
|
||||||
|
std::string test = current.empty() ? word : (current + " " + word);
|
||||||
|
uint16_t w = display->getStringWidth(test.c_str(), test.length(), true);
|
||||||
|
if (w <= availableWidth)
|
||||||
|
current = test;
|
||||||
|
else {
|
||||||
|
if (!current.empty()) {
|
||||||
|
wrapped.push_back(current);
|
||||||
|
current = word;
|
||||||
|
if (wrapped.size() >= maxContentLines)
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
current = word;
|
||||||
|
while (current.size() > 1 &&
|
||||||
|
display->getStringWidth(current.c_str(), current.length(), true) > availableWidth)
|
||||||
|
current.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!current.empty() && wrapped.size() < maxContentLines)
|
||||||
|
wrapped.push_back(current);
|
||||||
|
return wrapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::string> allLines;
|
||||||
|
if (hasTitle)
|
||||||
|
allLines.emplace_back(popupTitle);
|
||||||
|
|
||||||
|
char buf[sizeof(popupMessage)];
|
||||||
|
strncpy(buf, popupMessage, sizeof(buf) - 1);
|
||||||
|
buf[sizeof(buf) - 1] = '\0';
|
||||||
|
char *paragraph = strtok(buf, "\n");
|
||||||
|
while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) {
|
||||||
|
auto wrapped = wrapText(paragraph, maxWrapWidth);
|
||||||
|
for (const auto &ln : wrapped) {
|
||||||
|
if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0))
|
||||||
|
break;
|
||||||
|
allLines.push_back(ln);
|
||||||
|
}
|
||||||
|
paragraph = strtok(nullptr, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const char *> ptrs;
|
||||||
|
for (const auto &ln : allLines)
|
||||||
|
ptrs.push_back(ln.c_str());
|
||||||
|
ptrs.push_back(nullptr);
|
||||||
|
|
||||||
|
// Use the inverted notification box already present in NotificationRenderer
|
||||||
|
NotificationRenderer::drawInvertedNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
#endif // HAS_SCREEN
|
||||||
69
src/modules/OnScreenKeyboardModule.h
Normal file
69
src/modules/OnScreenKeyboardModule.h
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
|
||||||
|
#include "graphics/Screen.h" // InputEvent
|
||||||
|
#include "graphics/VirtualKeyboard.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
// Lightweight UI module to manage on-screen keyboard (non-touch).
|
||||||
|
class OnScreenKeyboardModule
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static OnScreenKeyboardModule &instance();
|
||||||
|
|
||||||
|
// Begin a keyboard session
|
||||||
|
void start(const char *header, const char *initialText,
|
||||||
|
uint32_t /*durationMs unused here - NotificationRenderer controls banner timeout*/,
|
||||||
|
std::function<void(const std::string &)> callback);
|
||||||
|
|
||||||
|
// Stop current session (optionally call callback with empty string)
|
||||||
|
void stop(bool callEmptyCallback);
|
||||||
|
|
||||||
|
// Session status
|
||||||
|
bool isActive() const;
|
||||||
|
|
||||||
|
// Event handling + drawing
|
||||||
|
void handleInput(const InputEvent &event);
|
||||||
|
// Draw keyboard and any overlay popup; return false if session ended (timeout or submit/cancel)
|
||||||
|
bool draw(OLEDDisplay *display);
|
||||||
|
|
||||||
|
// Popup helpers (title/content shown above keyboard)
|
||||||
|
void showPopup(const char *title, const char *content, uint32_t durationMs);
|
||||||
|
void clearPopup();
|
||||||
|
|
||||||
|
// Compatibility: expose underlying keyboard pointer for existing callsites
|
||||||
|
VirtualKeyboard *getKeyboard() const { return keyboard; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
OnScreenKeyboardModule() = default;
|
||||||
|
~OnScreenKeyboardModule();
|
||||||
|
OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete;
|
||||||
|
OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete;
|
||||||
|
|
||||||
|
// Internal helpers
|
||||||
|
void onSubmit(const std::string &text);
|
||||||
|
void onCancel();
|
||||||
|
|
||||||
|
// Popup rendering
|
||||||
|
void drawPopup(OLEDDisplay *display);
|
||||||
|
|
||||||
|
VirtualKeyboard *keyboard = nullptr;
|
||||||
|
std::function<void(const std::string &)> callback;
|
||||||
|
|
||||||
|
// Popup state
|
||||||
|
char popupTitle[64] = {0};
|
||||||
|
char popupMessage[256] = {0};
|
||||||
|
uint32_t popupUntil = 0;
|
||||||
|
bool popupVisible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
#endif // HAS_SCREEN
|
||||||
Reference in New Issue
Block a user