mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 07:12:34 +00:00
* 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>
273 lines
7.7 KiB
C++
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
|