Show message popup over VirtualKeyboard.

This commit is contained in:
whywilson
2025-09-05 11:37:08 +08:00
parent 64cd62d6af
commit b32293f2cb
4 changed files with 391 additions and 26 deletions

View File

@@ -149,6 +149,11 @@ void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
// Called to trigger a banner with custom message and duration
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
{
// Don't show overlay banner if virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
@@ -173,6 +178,11 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
// Called to trigger a banner with custom message and duration
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback)
{
// Don't show node picker if virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
@@ -196,6 +206,11 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
std::function<void(uint32_t)> bannerCallback)
{
// Don't show number picker if virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
@@ -1400,21 +1415,28 @@ 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
// Only wake/force display if the configuration allows it
if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw
// Always update frame list to include new message, but defer UI updates if virtual keyboard is active
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
// Only wake/force display if the configuration allows it
if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw
}
} else {
// Virtual keyboard is active - just mark that frames need regeneration when keyboard closes
// The devicestate and hasUnreadMessage are already set above, so message will appear later
LOG_INFO("Virtual keyboard active - deferring frame list update for new message");
}
// === Prepare banner content ===
// Show message alert - either as normal banner or as keyboard popup
// === Common variables ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
char banner[256];
// Check for bell character in message to determine alert type
bool isAlert = false;
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
@@ -1424,21 +1446,57 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
}
}
if (isAlert) {
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
// === Normal banner mode ===
char banner[256];
if (isAlert) {
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
} else {
strcpy(banner, "Alert Received");
}
} else {
strcpy(banner, "Alert Received");
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
} else {
strcpy(banner, "New Message");
}
}
screen->showSimpleBanner(banner, 3000);
} else {
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
// === Virtual keyboard popup mode ===
char title[64];
if (isAlert) {
if (longName && longName[0]) {
snprintf(title, sizeof(title), "Alert from %s", longName);
} else {
strcpy(title, "Alert Received");
}
} else {
strcpy(banner, "New Message");
if (longName && longName[0]) {
snprintf(title, sizeof(title), "%s", longName);
} else {
strcpy(title, "New Message");
}
}
}
screen->showSimpleBanner(banner, 3000);
// Prepare content - clean the message content
char content[200];
size_t contentLen = 0;
for (size_t i = 0; i < packet->decoded.payload.size && i < sizeof(content) - 1; i++) {
if (msgRaw[i] != '\x07' && msgRaw[i] != '\0') { // Skip bell character and null
content[contentLen++] = msgRaw[i];
}
}
content[contentLen] = '\0';
// Show popup with title and content on virtual keyboard
NotificationRenderer::showKeyboardMessagePopupWithTitle(title, content, 5000);
// Force display update to show the popup immediately
setFastFramerate();
forceDisplay();
}
}
}

View File

@@ -49,6 +49,12 @@ const int *NotificationRenderer::optionsEnumPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
bool NotificationRenderer::pauseBanner = false;
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::currentNumber = 0;
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
@@ -85,9 +91,17 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::resetBanner()
{
notificationTypeEnum previousType = current_notification_type;
alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none;
// Reset keyboard popup message variables
keyboardPopupMessage[0] = '\0';
keyboardPopupTitle[0] = '\0';
keyboardPopupUntil = 0;
showKeyboardPopup = false;
inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0;
curSelected = 0;
@@ -100,6 +114,12 @@ void NotificationRenderer::resetBanner()
currentNumber = 0;
nodeDB->pause_sort(false);
// If we're exiting from text_input (virtual keyboard), trigger frame update
// to ensure any messages received during keyboard use are now displayed
if (previousType == notificationTypeEnum::text_input && screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
}
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
@@ -693,6 +713,9 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
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");
@@ -705,5 +728,275 @@ 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;
}
strncpy(keyboardPopupTitle, title, 63);
keyboardPopupTitle[63] = '\0';
strncpy(keyboardPopupMessage, content, 255);
keyboardPopupMessage[255] = '\0';
keyboardPopupUntil = millis() + durationMs;
showKeyboardPopup = true;
}
void NotificationRenderer::drawKeyboardMessagePopup(OLEDDisplay *display)
{
// 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
void NotificationRenderer::drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
{
bool is_picker = false;
uint16_t lineCount = 0;
// === Layout Configuration ===
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
bool needs_bell = false;
uint16_t lineWidths[totalLines] = {0};
uint16_t lineLengths[totalLines] = {0};
if (maxWidth != 0)
is_picker = true;
// Setup font and alignment
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]);
else
lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
if (!is_picker) {
if (lineWidths[lineCount] > maxWidth)
maxWidth = lineWidths[lineCount];
}
lineCount++;
}
uint16_t boxWidth = hPadding * 2 + maxWidth;
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
}
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) {
boxHeight += (isHighResolution) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) {
boxWidth += (isHighResolution) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw Box with INVERTED COLORS ===
// Add outer separation pixels (1-pixel white background around the box)
display->setColor(WHITE);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
// Make outer corners round by filling back with背景色 (BLACK for separation)
display->setColor(BLACK);
// Top-left outer corner
display->fillRect(boxLeft - 1, boxTop - 1, 1, 1);
// Top-right outer corner
display->fillRect(boxLeft + boxWidth, boxTop - 1, 1, 1);
// Bottom-left outer corner
display->fillRect(boxLeft - 1, boxTop + boxHeight, 1, 1);
// Bottom-right outer corner
display->fillRect(boxLeft + boxWidth, boxTop + boxHeight, 1, 1);
// Draw single pixel black border
display->setColor(BLACK);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
// Make inner corners round by filling white pixels at corners
display->setColor(WHITE);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
// Fill interior with BLACK (inverted)
display->setColor(BLACK);
display->fillRect(boxLeft + 1, boxTop + 1, boxWidth - 2, boxHeight - 2);
// === Draw Content with WHITE text on BLACK background ===
display->setColor(WHITE);
int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
char lineBuffer[lineLengths[i] + 1];
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
// For keyboard popup, treat first line as title if it's different from others
if (i == 0 && lineCount > 1) {
// Title line - use inverted colors (white background, black text)
display->setColor(WHITE);
int background_yOffset = 1;
// Check for low hanging characters
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
background_yOffset = -1;
}
display->fillRect(boxLeft + 1, boxTop + 1, boxWidth - 2, effectiveLineHeight - background_yOffset);
display->setColor(BLACK);
int yOffset = 3;
display->drawString(textX, lineY - yOffset, lineBuffer);
display->setColor(WHITE); // Reset to white for next lines
lineY += (effectiveLineHeight - 2 - background_yOffset);
} else {
// Content lines - white text on black background
display->drawString(textX, lineY, lineBuffer);
lineY += effectiveLineHeight;
}
}
// === Scroll Bar (if needed) ===
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->setColor(WHITE);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
}
} // namespace graphics
#endif

View File

@@ -30,7 +30,14 @@ class NotificationRenderer
static bool pauseBanner;
static char keyboardPopupMessage[256];
static char keyboardPopupTitle[64];
static uint32_t keyboardPopupUntil;
static bool showKeyboardPopup;
static void resetBanner();
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 drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
@@ -38,6 +45,8 @@ class NotificationRenderer
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);
static void drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);

View File

@@ -653,11 +653,12 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
currentMessageIndex = -1;
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->notifyObservers(&e);
screen->forceDisplay();
// IMPORTANT: Don't delete virtual keyboard here - it's still executing!
// Just clear the callback to prevent further input
graphics::NotificationRenderer::textInputCallback = nullptr;
// Schedule keyboard cleanup for next run cycle
// This allows the current submitText() method to complete safely
setIntervalFromNow(500);
return;
} else {
@@ -1003,8 +1004,9 @@ 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) {
// Clean up virtual keyboard if needed when going inactive, but only if virtual keyboard is not actively being used
if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr &&
graphics::NotificationRenderer::current_notification_type != graphics::notificationTypeEnum::text_input) {
LOG_INFO("Performing delayed virtual keyboard cleanup");
delete graphics::NotificationRenderer::virtualKeyboard;
graphics::NotificationRenderer::virtualKeyboard = nullptr;
@@ -1081,8 +1083,11 @@ int32_t CannedMessageModule::runOnce()
this->cursor = 0;
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
// Clean up virtual keyboard if it exists during timeout
if (graphics::NotificationRenderer::virtualKeyboard) {
// Clean up virtual keyboard if it exists during timeout, but only if it's not actively being used by another process
if (graphics::NotificationRenderer::virtualKeyboard &&
graphics::NotificationRenderer::current_notification_type == graphics::notificationTypeEnum::text_input) {
LOG_INFO("Virtual keyboard is active - not cleaning up due to CannedMessage timeout");
} else if (graphics::NotificationRenderer::virtualKeyboard) {
LOG_INFO("Cleaning up virtual keyboard due to module timeout");
delete graphics::NotificationRenderer::virtualKeyboard;
graphics::NotificationRenderer::virtualKeyboard = nullptr;