Files
firmware/src/modules/OnScreenKeyboardModule.cpp
Wilson eb087849c0 OnScreenKeyboard Improvement with Joystick and UpDown Encoder (#8379)
* Add mesh/Default.h include.

* Reflacter OnScreenKeyBoard Module, do not interrupt keyboard when new message comes.

* feat: Add long press scrolling for Joystick and upDown Encoder on baseUI frames and menus.

* refactor: Clean up code formatting and improve readability in Screen and OnScreenKeyboardModule

* Fix navigation on UpDownEncoder, default was RotaryEncoder while bringing the T_LORA_PAGER

* Update src/graphics/draw/MenuHandler.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/OnScreenKeyboardModule.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/draw/NotificationRenderer.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Optimize the detection logic for repeated events of the arrow keys.

* Fixed parameter names in the OnScreenKeyboardModule::start

* Trunk fix

* Reflator OnScreenKeyboard Input checking, make it simple

* Simplify long press logic in OnScreenKeyboardModule.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 05:40:30 -06:00

273 lines
7.7 KiB
C++

#include "configuration.h"
#if HAS_SCREEN
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/NotificationRenderer.h"
#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 durationMs,
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("");
}
void OnScreenKeyboardModule::handleInput(const InputEvent &event)
{
if (!keyboard)
return;
if (processVirtualKeyboardInput(event, keyboard))
return;
if (event.inputEvent == INPUT_BROKER_CANCEL)
onCancel();
}
bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard)
{
if (!targetKeyboard)
return false;
switch (event.inputEvent) {
case INPUT_BROKER_UP:
case INPUT_BROKER_UP_LONG:
targetKeyboard->moveCursorUp();
return true;
case INPUT_BROKER_DOWN:
case INPUT_BROKER_DOWN_LONG:
targetKeyboard->moveCursorDown();
return true;
case INPUT_BROKER_LEFT:
case INPUT_BROKER_ALT_PRESS:
targetKeyboard->moveCursorLeft();
return true;
case INPUT_BROKER_RIGHT:
case INPUT_BROKER_USER_PRESS:
targetKeyboard->moveCursorRight();
return true;
case INPUT_BROKER_SELECT:
targetKeyboard->handlePress();
return true;
case INPUT_BROKER_SELECT_LONG:
targetKeyboard->handleLongPress();
return true;
default:
return false;
}
}
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::drawPopupOverlay(OLEDDisplay *display)
{
// Only render the popup overlay (without drawing the keyboard)
drawPopup(display);
}
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 standard notification box drawing from NotificationRenderer
NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
}
} // namespace graphics
#endif // HAS_SCREEN