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

@@ -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