mirror of
https://github.com/meshtastic/firmware.git
synced 2025-10-27 15:02:41 +00:00
Merge 2c76078ac3 into 13c4c2037d
This commit is contained in:
commit
2ce6910801
@ -226,24 +226,9 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
||||
{
|
||||
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
|
||||
// Start OnScreenKeyboardModule session (non-touch variant)
|
||||
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, 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)
|
||||
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||
@ -1452,14 +1437,14 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Incoming message
|
||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||
hasUnreadMessage = true; // Enables mail icon in the header
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input)
|
||||
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
}
|
||||
// === Prepare banner content ===
|
||||
// === Prepare banner/popup content ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const meshtastic_Channel channel =
|
||||
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||
@ -1483,38 +1468,84 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
|
||||
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
|
||||
// 'mute' preferences set to any specific node or channel.
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "Alert Received");
|
||||
// If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Wake and force redraw so popup is visible immediately
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true);
|
||||
forceDisplay();
|
||||
}
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
||||
if (longName && longName[0]) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
strcpy(banner, "New Message");
|
||||
#else
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
#endif
|
||||
|
||||
// Build popup: title = message source name, content = message text (sanitized)
|
||||
// Title
|
||||
char titleBuf[64] = {0};
|
||||
if (longName && longName[0]) {
|
||||
// Sanitize sender name
|
||||
std::string t = sanitizeString(longName);
|
||||
strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1);
|
||||
} else {
|
||||
strcpy(banner, "New Message");
|
||||
strncpy(titleBuf, "Message", sizeof(titleBuf) - 1);
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
|
||||
// Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize
|
||||
char content[256] = {0};
|
||||
{
|
||||
std::string raw;
|
||||
raw.reserve(packet->decoded.payload.size);
|
||||
for (size_t i = 0; i < packet->decoded.payload.size; ++i) {
|
||||
char c = msgRaw[i];
|
||||
if (c == ASCII_BELL)
|
||||
continue; // strip bell
|
||||
raw.push_back(c);
|
||||
}
|
||||
std::string sanitized = sanitizeString(raw);
|
||||
strncpy(content, sanitized.c_str(), sizeof(content) - 1);
|
||||
}
|
||||
|
||||
NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000);
|
||||
|
||||
// Maintain existing buzzer behavior on M5 if applicable
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
|
||||
// - packet contains an alert and alert bell buzzer is enabled
|
||||
// - packet is a non-broadcast that is addressed to this node
|
||||
playLongBeep();
|
||||
}
|
||||
#else
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
#endif
|
||||
#endif
|
||||
} else {
|
||||
// No keyboard active: use regular banner flow, respecting mute settings
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "Alert Received");
|
||||
}
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
||||
if (longName && longName[0]) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
strcpy(banner, "New Message");
|
||||
#else
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
#endif
|
||||
} else {
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
|
||||
// - packet contains an alert and alert bell buzzer is enabled
|
||||
// - packet is a non-broadcast that is addressed to this node
|
||||
playLongBeep();
|
||||
}
|
||||
#else
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1596,6 +1627,12 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
showPrevFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
showNextFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
// Long press up button for fast frame switching
|
||||
showPrevFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
// Long press down button for fast frame switching
|
||||
showNextFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
|
||||
@ -13,12 +13,14 @@
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/KeyVerificationModule.h"
|
||||
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#include "mesh/Default.h"
|
||||
#include <functional>
|
||||
|
||||
extern uint16_t TFT_MESH;
|
||||
|
||||
@ -85,9 +85,13 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
|
||||
void NotificationRenderer::resetBanner()
|
||||
{
|
||||
notificationTypeEnum previousType = current_notification_type;
|
||||
|
||||
alertBannerMessage[0] = '\0';
|
||||
current_notification_type = notificationTypeEnum::none;
|
||||
|
||||
OnScreenKeyboardModule::instance().clearPopup();
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
inEvent.kbchar = 0;
|
||||
curSelected = 0;
|
||||
@ -100,6 +104,13 @@ void NotificationRenderer::resetBanner()
|
||||
currentNumber = 0;
|
||||
|
||||
nodeDB->pause_sort(false);
|
||||
|
||||
// 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
|
||||
if (previousType == notificationTypeEnum::text_input && screen) {
|
||||
OnScreenKeyboardModule::instance().stop(false);
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
@ -163,13 +174,15 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
|
||||
// modulo to extract
|
||||
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
if (this_digit == 9) {
|
||||
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
|
||||
} else {
|
||||
currentNumber += (pow_of_10(numDigits - curSelected - 1));
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
if (this_digit == 0) {
|
||||
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
|
||||
} else {
|
||||
@ -251,10 +264,10 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
|
||||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
curSelected--;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
|
||||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||
alertBannerCallback(selectedNodenum);
|
||||
@ -368,10 +381,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
||||
// Handle input
|
||||
if (alertBannerOptions > 0) {
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
|
||||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
curSelected--;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
|
||||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (optionsEnumPtr != nullptr) {
|
||||
@ -821,12 +834,27 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
}
|
||||
|
||||
// Re-check pointer before drawing to avoid use-after-free and crashes
|
||||
if (!virtualKeyboard) {
|
||||
// Ensure we exit text_input state and restore frames
|
||||
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||
resetBanner();
|
||||
}
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 transient popup overlay (if any) managed by OnScreenKeyboardModule
|
||||
OnScreenKeyboardModule::instance().drawPopupOverlay(display);
|
||||
} else {
|
||||
// If virtualKeyboard is null, reset the banner to avoid getting stuck
|
||||
LOG_INFO("Virtual keyboard is null - resetting banner");
|
||||
@ -839,5 +867,12 @@ bool NotificationRenderer::isOverlayBannerShowing()
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
}
|
||||
|
||||
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
|
||||
{
|
||||
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
|
||||
return;
|
||||
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@ -4,6 +4,7 @@
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
@ -31,6 +32,7 @@ class NotificationRenderer
|
||||
static bool pauseBanner;
|
||||
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
@ -88,6 +88,48 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
}
|
||||
}
|
||||
|
||||
if (directionDetected && directionStartTime > 0) {
|
||||
uint32_t directionDuration = millis() - directionStartTime;
|
||||
uint8_t directionPressedNow = 0;
|
||||
directionInterval++;
|
||||
|
||||
if (!digitalRead(_pinUp)) {
|
||||
directionPressedNow = TB_ACTION_UP;
|
||||
} else if (!digitalRead(_pinDown)) {
|
||||
directionPressedNow = TB_ACTION_DOWN;
|
||||
} else if (!digitalRead(_pinLeft)) {
|
||||
directionPressedNow = TB_ACTION_LEFT;
|
||||
} else if (!digitalRead(_pinRight)) {
|
||||
directionPressedNow = TB_ACTION_RIGHT;
|
||||
}
|
||||
|
||||
if (directionPressedNow < 3) {
|
||||
// Reset state
|
||||
directionDetected = false;
|
||||
directionStartTime = 0;
|
||||
directionInterval = 0;
|
||||
this->action = TB_ACTION_NONE;
|
||||
} else if (directionDuration >= LONG_PRESS_DURATION && directionPressedNow >= 3 && directionInterval >= 3) {
|
||||
// repeat event when long press these direction.
|
||||
switch (directionPressedNow) {
|
||||
case TB_ACTION_UP:
|
||||
e.inputEvent = this->_eventUp;
|
||||
break;
|
||||
case TB_ACTION_DOWN:
|
||||
e.inputEvent = this->_eventDown;
|
||||
break;
|
||||
case TB_ACTION_LEFT:
|
||||
e.inputEvent = this->_eventLeft;
|
||||
break;
|
||||
case TB_ACTION_RIGHT:
|
||||
e.inputEvent = this->_eventRight;
|
||||
break;
|
||||
}
|
||||
|
||||
directionInterval = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
||||
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
|
||||
// Start long press detection
|
||||
@ -113,17 +155,22 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
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");
|
||||
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) {
|
||||
directionDetected = true;
|
||||
directionStartTime = millis();
|
||||
e.inputEvent = this->_eventUp;
|
||||
} else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) {
|
||||
// LOG_DEBUG("Trackball event DOWN");
|
||||
// send event first,will automatically trigger every 50ms * 3 after 500ms
|
||||
} else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) {
|
||||
directionDetected = true;
|
||||
directionStartTime = millis();
|
||||
e.inputEvent = this->_eventDown;
|
||||
} else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) {
|
||||
// LOG_DEBUG("Trackball event LEFT");
|
||||
} else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) {
|
||||
directionDetected = true;
|
||||
directionStartTime = millis();
|
||||
e.inputEvent = this->_eventLeft;
|
||||
} else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) {
|
||||
// LOG_DEBUG("Trackball event RIGHT");
|
||||
} else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) {
|
||||
directionDetected = true;
|
||||
directionStartTime = millis();
|
||||
e.inputEvent = this->_eventRight;
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -49,10 +49,14 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
|
||||
// Long press detection for press button
|
||||
uint32_t pressStartTime = 0;
|
||||
uint32_t directionStartTime = 0;
|
||||
uint8_t directionInterval = 0;
|
||||
bool pressDetected = false;
|
||||
bool directionDetected = false;
|
||||
uint32_t lastLongPressEventTime = 0;
|
||||
uint32_t lastDirectionPressEventTime = 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
|
||||
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events
|
||||
|
||||
private:
|
||||
input_broker_event _eventDown = INPUT_BROKER_NONE;
|
||||
|
||||
@ -1008,8 +1008,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
// 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;
|
||||
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||
}
|
||||
|
||||
temporaryMessage = "";
|
||||
@ -1026,9 +1025,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
// 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::OnScreenKeyboardModule::instance().stop(false);
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
@ -1086,9 +1083,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
// 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::OnScreenKeyboardModule::instance().stop(false);
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
|
||||
282
src/modules/OnScreenKeyboardModule.cpp
Normal file
282
src/modules/OnScreenKeyboardModule.cpp
Normal file
@ -0,0 +1,282 @@
|
||||
#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,
|
||||
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;
|
||||
|
||||
// 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::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
|
||||
56
src/modules/OnScreenKeyboardModule.h
Normal file
56
src/modules/OnScreenKeyboardModule.h
Normal file
@ -0,0 +1,56 @@
|
||||
#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
|
||||
{
|
||||
class OnScreenKeyboardModule
|
||||
{
|
||||
public:
|
||||
static OnScreenKeyboardModule &instance();
|
||||
|
||||
void start(const char *header, const char *initialText,
|
||||
uint32_t,
|
||||
std::function<void(const std::string &)> callback);
|
||||
|
||||
void stop(bool callEmptyCallback);
|
||||
|
||||
|
||||
void handleInput(const InputEvent &event);
|
||||
bool draw(OLEDDisplay *display);
|
||||
|
||||
void showPopup(const char *title, const char *content, uint32_t durationMs);
|
||||
void clearPopup();
|
||||
// Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard)
|
||||
void drawPopupOverlay(OLEDDisplay *display);
|
||||
|
||||
private:
|
||||
OnScreenKeyboardModule() = default;
|
||||
~OnScreenKeyboardModule();
|
||||
OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete;
|
||||
OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete;
|
||||
|
||||
void onSubmit(const std::string &text);
|
||||
void onCancel();
|
||||
|
||||
void drawPopup(OLEDDisplay *display);
|
||||
|
||||
VirtualKeyboard *keyboard = nullptr;
|
||||
std::function<void(const std::string &)> callback;
|
||||
|
||||
char popupTitle[64] = {0};
|
||||
char popupMessage[256] = {0};
|
||||
uint32_t popupUntil = 0;
|
||||
bool popupVisible = false;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
Loading…
Reference in New Issue
Block a user