#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 #include 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 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::vector 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 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 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