mirror of
https://github.com/meshtastic/firmware.git
synced 2025-10-05 06:33:56 +00:00
Add virtural keyboard for wio tracker L1.
This commit is contained in:
parent
25b8d9b0ca
commit
2fcb08a20a
@ -28,6 +28,7 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
case INPUT_BROKER_ALT_PRESS:
|
||||
case INPUT_BROKER_SELECT:
|
||||
case INPUT_BROKER_SELECT_LONG:
|
||||
playBeep(); // Confirmation feedback
|
||||
break;
|
||||
|
||||
|
@ -216,6 +216,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
ui->update();
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
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
|
||||
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);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
uint8_t module_frame;
|
||||
@ -706,13 +744,19 @@ int32_t Screen::runOnce()
|
||||
handleSetOn(false);
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleOnPress();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowPrevFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowNextFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::START_ALERT_FRAME: {
|
||||
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
||||
@ -734,7 +778,9 @@ int32_t Screen::runOnce()
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames();
|
||||
}
|
||||
break;
|
||||
case Cmd::NOOP:
|
||||
break;
|
||||
@ -770,6 +816,7 @@ int32_t Screen::runOnce()
|
||||
if (showingNormalScreen) {
|
||||
// standard screen loop handling here
|
||||
if (config.display.auto_screen_carousel_secs > 0 &&
|
||||
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
|
||||
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
||||
|
||||
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
||||
@ -860,6 +907,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
// Called when a frame should be added / removed, or custom frames should be cleared
|
||||
void Screen::setFrames(FrameFocus focus)
|
||||
{
|
||||
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
||||
uint8_t previousFrameCount = framesetInfo.frameCount;
|
||||
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
||||
@ -1307,6 +1359,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Triggered by MeshModules
|
||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
{
|
||||
// Block UI frame events when virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (showingNormalScreen) {
|
||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||
@ -1329,6 +1386,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (!screenOn)
|
||||
return 0;
|
||||
|
||||
// Handle text input notifications specially - pass input to virtual keyboard
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
||||
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
||||
|
@ -12,7 +12,7 @@
|
||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||
namespace graphics
|
||||
{
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
|
||||
{
|
||||
|
569
src/graphics/VirtualKeyboard.cpp
Normal file
569
src/graphics/VirtualKeyboard.cpp
Normal file
@ -0,0 +1,569 @@
|
||||
#include "VirtualKeyboard.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
VirtualKeyboard::VirtualKeyboard()
|
||||
: cursorRow(0), cursorCol(0), closeButtonX(0), closeButtonY(0), closeButtonWidth(0), closeButtonHeight(0),
|
||||
cursorOnCloseButton(false), lastActivityTime(millis())
|
||||
{
|
||||
initializeKeyboard();
|
||||
// Set cursor to Q(0, 0)
|
||||
cursorRow = 0;
|
||||
cursorCol = 0;
|
||||
}
|
||||
|
||||
VirtualKeyboard::~VirtualKeyboard() {}
|
||||
|
||||
void VirtualKeyboard::initializeKeyboard()
|
||||
{
|
||||
// Initialize all keys to empty first
|
||||
for (int row = 0; row < KEYBOARD_ROWS; row++) {
|
||||
for (int col = 0; col < KEYBOARD_COLS; col++) {
|
||||
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
// Row 0: q w e r t y u i o p 0 1 2 3
|
||||
const char *row0 = "qwertyuiop0123";
|
||||
for (int i = 0; i < 14; i++) {
|
||||
keyboard[0][i] = {row0[i], VK_CHAR, (uint8_t)(i * KEY_WIDTH), 0, KEY_WIDTH, KEY_HEIGHT};
|
||||
}
|
||||
|
||||
// Row 1: a s d f g h j k l ← 4 5 6
|
||||
const char *row1 = "asdfghjkl";
|
||||
for (int i = 0; i < 9; i++) {
|
||||
keyboard[1][i] = {row1[i], VK_CHAR, (uint8_t)(i * KEY_WIDTH), KEY_HEIGHT, KEY_WIDTH, KEY_HEIGHT};
|
||||
}
|
||||
// Backspace key (2 chars wide)
|
||||
keyboard[1][9] = {'\b', VK_BACKSPACE, 9 * KEY_WIDTH, KEY_HEIGHT, KEY_WIDTH * 2, KEY_HEIGHT};
|
||||
// Numbers 4, 5, 6
|
||||
const char *numbers456 = "456";
|
||||
for (int i = 0; i < 3; i++) {
|
||||
keyboard[1][11 + i] = {numbers456[i], VK_CHAR, (uint8_t)((11 + i) * KEY_WIDTH), KEY_HEIGHT, KEY_WIDTH, KEY_HEIGHT};
|
||||
}
|
||||
|
||||
// Row 2: z x c v b n m _ . OK 7 8 9
|
||||
const char *row2 = "zxcvbnm_.";
|
||||
for (int i = 0; i < 9; i++) {
|
||||
keyboard[2][i] = {row2[i], VK_CHAR, (uint8_t)(i * KEY_WIDTH), 2 * KEY_HEIGHT, KEY_WIDTH, KEY_HEIGHT};
|
||||
}
|
||||
// OK key (Enter) - 2 chars wide
|
||||
keyboard[2][9] = {'\n', VK_ENTER, 9 * KEY_WIDTH, 2 * KEY_HEIGHT, KEY_WIDTH * 2, KEY_HEIGHT};
|
||||
// Numbers 7, 8, 9
|
||||
const char *numbers789 = "789";
|
||||
for (int i = 0; i < 3; i++) {
|
||||
keyboard[2][11 + i] = {numbers789[i], VK_CHAR, (uint8_t)((11 + i) * KEY_WIDTH), 2 * KEY_HEIGHT, KEY_WIDTH, KEY_HEIGHT};
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
// Set initial color and font
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Draw input area (header + input box)
|
||||
drawInputArea(display, offsetX, offsetY);
|
||||
|
||||
// Draw keyboard with proper QWERTY layout
|
||||
for (int row = 0; row < KEYBOARD_ROWS; row++) {
|
||||
for (int col = 0; col < KEYBOARD_COLS; col++) {
|
||||
if (keyboard[row][col].character != 0 || keyboard[row][col].type != VK_CHAR) { // Include special keys
|
||||
bool selected = (row == cursorRow && col == cursorCol && !cursorOnCloseButton);
|
||||
drawKey(display, keyboard[row][col], selected, offsetX, offsetY + KEYBOARD_START_Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawCloseButton(display, offsetX, offsetY, cursorOnCloseButton);
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawCloseButton(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, bool selected)
|
||||
{
|
||||
if (closeButtonX == 0 && closeButtonY == 0) {
|
||||
// Close button position not set yet
|
||||
return;
|
||||
}
|
||||
|
||||
display->setColor(WHITE);
|
||||
|
||||
if (selected) {
|
||||
// Draw highlighted close button background
|
||||
display->drawRect(closeButtonX - 1, closeButtonY - 1, closeButtonWidth + 2, closeButtonHeight + 2);
|
||||
display->fillRect(closeButtonX, closeButtonY, closeButtonWidth, closeButtonHeight);
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
// Draw the X symbol
|
||||
display->drawLine(closeButtonX, closeButtonY, closeButtonX + closeButtonWidth, closeButtonY + closeButtonHeight);
|
||||
display->drawLine(closeButtonX + closeButtonWidth, closeButtonY, closeButtonX, closeButtonY + closeButtonHeight);
|
||||
|
||||
// Reset color
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
int screenWidth = display->getWidth();
|
||||
|
||||
int headerHeight = 0;
|
||||
if (!headerText.empty()) {
|
||||
display->drawString(offsetX + 2, offsetY, headerText.c_str());
|
||||
|
||||
// Set close button position
|
||||
closeButtonX = screenWidth - 12;
|
||||
closeButtonY = offsetY;
|
||||
closeButtonWidth = 8;
|
||||
closeButtonHeight = 8;
|
||||
|
||||
drawCloseButton(display, offsetX, offsetY, false);
|
||||
|
||||
headerHeight = 10;
|
||||
}
|
||||
|
||||
// Draw input box - positioned just below header
|
||||
int boxWidth = screenWidth - 4;
|
||||
int boxY = offsetY + headerHeight + 2;
|
||||
int boxHeight = 14; // Increased by 2 pixels
|
||||
|
||||
// Draw box border
|
||||
display->drawRect(offsetX + 2, boxY, boxWidth, boxHeight);
|
||||
|
||||
// Prepare display text
|
||||
std::string displayText = inputText;
|
||||
if (displayText.empty()) {
|
||||
displayText = ""; // Don't show placeholder when empty
|
||||
}
|
||||
|
||||
// Handle text overflow with scrolling
|
||||
int textPadding = 4;
|
||||
int maxWidth = boxWidth - textPadding;
|
||||
int textWidth = display->getStringWidth(displayText.c_str());
|
||||
|
||||
std::string scrolledText = displayText;
|
||||
if (textWidth > maxWidth) {
|
||||
// Scroll text to show the end (cursor position)
|
||||
while (textWidth > maxWidth && !scrolledText.empty()) {
|
||||
scrolledText = scrolledText.substr(1);
|
||||
textWidth = display->getStringWidth(scrolledText.c_str());
|
||||
}
|
||||
if (scrolledText != displayText) {
|
||||
scrolledText = "..." + scrolledText;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text inside the box - properly centered vertically in the input box
|
||||
int textX = offsetX + 4;
|
||||
int textY = boxY - 1; // Moved down by 1 pixel
|
||||
|
||||
if (!scrolledText.empty()) {
|
||||
display->drawString(textX, textY, scrolledText.c_str());
|
||||
}
|
||||
|
||||
// Draw cursor at the end of visible text - aligned with text baseline
|
||||
if (!inputText.empty() || true) { // Always show cursor for visibility
|
||||
int cursorX = textX + display->getStringWidth(scrolledText.c_str());
|
||||
// Ensure cursor stays within box bounds
|
||||
if (cursorX < offsetX + boxWidth - 2) {
|
||||
// Align cursor properly with the text baseline and height - moved down by 2 pixels
|
||||
display->drawVerticalLine(cursorX, textY + 3, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
int x = offsetX + key.x;
|
||||
int y = offsetY + key.y;
|
||||
|
||||
// Draw border for OK key or selected keys (NOT for backspace key)
|
||||
bool drawBorder = selected || (key.type == VK_ENTER);
|
||||
|
||||
if (drawBorder) {
|
||||
if (selected) {
|
||||
if (key.type == VK_BACKSPACE) {
|
||||
display->fillRect(x, y + 3, key.width, 10);
|
||||
} else if (key.type == VK_ENTER) {
|
||||
display->fillRect(x, y + 3, key.width, 10);
|
||||
} else {
|
||||
display->fillRect(x, y + 3, key.width, key.height);
|
||||
}
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
// Draw key content
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
if (key.type == VK_BACKSPACE) {
|
||||
int centerX = x + key.width / 2;
|
||||
int centerY = y + key.height / 2;
|
||||
|
||||
display->drawLine(centerX - 3, centerY + 1, centerX + 2, centerY + 1); // horizontal line
|
||||
display->drawLine(centerX - 3, centerY + 1, centerX - 1, centerY - 1); // upper diagonal
|
||||
display->drawLine(centerX - 3, centerY + 1, centerX - 1, centerY + 3); // lower diagonal
|
||||
} else if (key.type == VK_ENTER) {
|
||||
std::string keyText = "OK";
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
int textX = x + (key.width - textWidth) / 2;
|
||||
int textY = y + 2;
|
||||
display->drawString(textX, textY - 1, keyText.c_str());
|
||||
display->drawRect(textX - 1, textY, textWidth + 3, 11);
|
||||
} else {
|
||||
std::string keyText;
|
||||
char c = getCharForKey(key, false); // Pass false for display purposes
|
||||
|
||||
if (key.character == ' ') {
|
||||
keyText = "_"; // Show underscore for space
|
||||
} else if (key.character == '_') {
|
||||
keyText = "_"; // Show underscore for underscore character
|
||||
} else {
|
||||
keyText = c;
|
||||
}
|
||||
|
||||
// Center text in key with perfect horizontal and vertical alignment
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
int textX = x + (key.width - textWidth) / 2;
|
||||
int textY = y; // Fixed position for optimal centering in 12px height
|
||||
|
||||
// If the character is a digit, adjust X position by +1
|
||||
if (key.character >= '0' && key.character <= '9') {
|
||||
textX += 1;
|
||||
textY += 1;
|
||||
}
|
||||
|
||||
display->drawString(textX, textY + 1, keyText.c_str());
|
||||
}
|
||||
|
||||
// Reset color after drawing
|
||||
if (selected) {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
{
|
||||
if (key.type != VK_CHAR) {
|
||||
return key.character;
|
||||
}
|
||||
|
||||
char c = key.character;
|
||||
|
||||
if (isLongPress) {
|
||||
if (c == '_') {
|
||||
return ' ';
|
||||
} else if (c == '.') {
|
||||
return ',';
|
||||
} else if (c >= 'a' && c <= 'z') {
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorUp()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
// If we're on the close button, move to keyboard
|
||||
if (cursorOnCloseButton) {
|
||||
cursorOnCloseButton = false;
|
||||
cursorRow = 0;
|
||||
cursorCol = KEYBOARD_COLS - 1; // Move to rightmost key in top row
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t originalRow = cursorRow;
|
||||
if (cursorRow > 0) {
|
||||
cursorRow--;
|
||||
} else {
|
||||
// From top row, move to close button if on rightmost position
|
||||
if (cursorCol >= KEYBOARD_COLS - 3) { // Close to right edge
|
||||
cursorOnCloseButton = true;
|
||||
return;
|
||||
}
|
||||
cursorRow = KEYBOARD_ROWS - 1;
|
||||
}
|
||||
|
||||
// If the new position is empty, find the nearest valid key in this row
|
||||
if (keyboard[cursorRow][cursorCol].character == 0) {
|
||||
// First try to move left to find a valid key
|
||||
uint8_t originalCol = cursorCol;
|
||||
while (cursorCol > 0 && keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorCol--;
|
||||
}
|
||||
// If we still don't have a valid key, try moving right from original position
|
||||
if (keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorCol = originalCol;
|
||||
while (cursorCol < KEYBOARD_COLS - 1 && keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorCol++;
|
||||
}
|
||||
}
|
||||
// If still no valid key, go back to original row
|
||||
if (keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorRow = originalRow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorDown()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
uint8_t originalRow = cursorRow;
|
||||
if (cursorRow < KEYBOARD_ROWS - 1) {
|
||||
cursorRow++;
|
||||
} else {
|
||||
cursorRow = 0;
|
||||
}
|
||||
|
||||
// If the new position is empty, find the nearest valid key in this row
|
||||
if (keyboard[cursorRow][cursorCol].character == 0) {
|
||||
// First try to move left to find a valid key
|
||||
uint8_t originalCol = cursorCol;
|
||||
while (cursorCol > 0 && keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorCol--;
|
||||
}
|
||||
// If we still don't have a valid key, try moving right from original position
|
||||
if (keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorCol = originalCol;
|
||||
while (cursorCol < KEYBOARD_COLS - 1 && keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorCol++;
|
||||
}
|
||||
}
|
||||
// If still no valid key, go back to original row
|
||||
if (keyboard[cursorRow][cursorCol].character == 0) {
|
||||
cursorRow = originalRow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorLeft()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
// Find the previous valid key position
|
||||
do {
|
||||
if (cursorCol > 0) {
|
||||
cursorCol--;
|
||||
} else {
|
||||
if (cursorRow > 0) {
|
||||
cursorRow--;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
} else {
|
||||
cursorRow = KEYBOARD_ROWS - 1;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
}
|
||||
}
|
||||
} while ((keyboard[cursorRow][cursorCol].character == 0 && keyboard[cursorRow][cursorCol].type == VK_CHAR) &&
|
||||
!(cursorRow == 0 && cursorCol == 0)); // Prevent infinite loop
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorRight()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
// If we're on the close button, go back to keyboard
|
||||
if (cursorOnCloseButton) {
|
||||
cursorOnCloseButton = false;
|
||||
cursorRow = 0;
|
||||
cursorCol = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the next valid key position
|
||||
do {
|
||||
if (cursorCol < KEYBOARD_COLS - 1) {
|
||||
cursorCol++;
|
||||
} else {
|
||||
// From top row's rightmost position, check if we should go to close button
|
||||
if (cursorRow == 0) {
|
||||
cursorOnCloseButton = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursorRow < KEYBOARD_ROWS - 1) {
|
||||
cursorRow++;
|
||||
cursorCol = 0;
|
||||
} else {
|
||||
cursorRow = 0;
|
||||
cursorCol = 0;
|
||||
}
|
||||
}
|
||||
} while ((keyboard[cursorRow][cursorCol].character == 0 && keyboard[cursorRow][cursorCol].type == VK_CHAR) &&
|
||||
!(cursorRow == 0 && cursorCol == 0)); // Prevent infinite loop
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handlePress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
// Handle close button press
|
||||
if (cursorOnCloseButton) {
|
||||
LOG_INFO("Virtual keyboard: close button pressed, cancelling");
|
||||
if (onTextEntered) {
|
||||
// Store callback before clearing to prevent use-after-free
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
onTextEntered = nullptr; // Clear immediately to prevent re-entry
|
||||
inputText = ""; // Clear input
|
||||
|
||||
// Call callback with empty string to signal cancellation
|
||||
callback("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert lowercase character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle non-character keys immediately
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
deleteCharacter();
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handleLongPress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
// Handle close button long press (same as regular press for now)
|
||||
if (cursorOnCloseButton) {
|
||||
// Call callback with empty string to indicate cancel/close
|
||||
if (onTextEntered) {
|
||||
onTextEntered("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert uppercase/alternate character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-character keys, long press behaves the same as regular press
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
deleteCharacter();
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::insertCharacter(char c)
|
||||
{
|
||||
if (inputText.length() < 160) { // Reasonable text length limit
|
||||
inputText += c;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::deleteCharacter()
|
||||
{
|
||||
if (!inputText.empty()) {
|
||||
inputText.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::submitText()
|
||||
{
|
||||
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
|
||||
|
||||
// Only submit if text is not empty
|
||||
if (!inputText.empty() && onTextEntered) {
|
||||
// Store callback and text to submit before clearing callback
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
std::string textToSubmit = inputText;
|
||||
onTextEntered = nullptr;
|
||||
// Don't clear inputText here - let the calling module handle cleanup
|
||||
// inputText = ""; // Removed: keep text visible until module cleans up
|
||||
callback(textToSubmit);
|
||||
} else if (inputText.empty()) {
|
||||
// For empty text, just ignore the submission - don't clear callback
|
||||
// This keeps the virtual keyboard responsive for further input
|
||||
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
|
||||
} else {
|
||||
// No callback available
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setInputText(const std::string &text)
|
||||
{
|
||||
inputText = text;
|
||||
}
|
||||
|
||||
std::string VirtualKeyboard::getInputText() const
|
||||
{
|
||||
return inputText;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setHeader(const std::string &header)
|
||||
{
|
||||
headerText = header;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
|
||||
{
|
||||
onTextEntered = callback;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::resetTimeout()
|
||||
{
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
|
||||
bool VirtualKeyboard::isTimedOut() const
|
||||
{
|
||||
return (millis() - lastActivityTime) > TIMEOUT_MS;
|
||||
}
|
||||
|
||||
bool VirtualKeyboard::isCursorOnCloseButton() const
|
||||
{
|
||||
return cursorOnCloseButton;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
85
src/graphics/VirtualKeyboard.h
Normal file
85
src/graphics/VirtualKeyboard.h
Normal file
@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT };
|
||||
|
||||
struct VirtualKey {
|
||||
char character;
|
||||
VirtualKeyType type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
};
|
||||
|
||||
class VirtualKeyboard
|
||||
{
|
||||
public:
|
||||
VirtualKeyboard();
|
||||
~VirtualKeyboard();
|
||||
|
||||
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void setInputText(const std::string &text);
|
||||
std::string getInputText() const;
|
||||
void setHeader(const std::string &header);
|
||||
void setCallback(std::function<void(const std::string &)> callback);
|
||||
|
||||
// Navigation methods for encoder input
|
||||
void moveCursorUp();
|
||||
void moveCursorDown();
|
||||
void moveCursorLeft();
|
||||
void moveCursorRight();
|
||||
void handlePress();
|
||||
void handleLongPress();
|
||||
|
||||
// Timeout management
|
||||
void resetTimeout();
|
||||
bool isTimedOut() const;
|
||||
|
||||
// Check cursor position for input handling
|
||||
bool isCursorOnCloseButton() const;
|
||||
|
||||
private:
|
||||
static const uint8_t KEYBOARD_ROWS = 3;
|
||||
static const uint8_t KEYBOARD_COLS = 14;
|
||||
static const uint8_t KEY_WIDTH = 9;
|
||||
static const uint8_t KEY_HEIGHT = 12; // Optimized for FONT_SMALL text with minimal padding
|
||||
static const uint8_t KEYBOARD_START_Y = 25;
|
||||
|
||||
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||
|
||||
std::string inputText;
|
||||
std::string headerText;
|
||||
std::function<void(const std::string &)> onTextEntered;
|
||||
|
||||
uint8_t cursorRow;
|
||||
uint8_t cursorCol;
|
||||
|
||||
// Close button position for cursor navigation
|
||||
int closeButtonX, closeButtonY, closeButtonWidth, closeButtonHeight;
|
||||
bool cursorOnCloseButton;
|
||||
|
||||
// Timeout management for auto-exit
|
||||
uint32_t lastActivityTime;
|
||||
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||
|
||||
void initializeKeyboard();
|
||||
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t offsetX, int16_t offsetY);
|
||||
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void drawCloseButton(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, bool selected);
|
||||
void drawCursor(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
|
||||
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||
void insertCharacter(char c);
|
||||
void deleteCharacter();
|
||||
void submitText();
|
||||
};
|
||||
|
||||
} // namespace graphics
|
@ -10,7 +10,10 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/KeyVerificationModule.h"
|
||||
|
@ -38,6 +38,8 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
uint32_t pow_of_10(uint32_t n)
|
||||
{
|
||||
@ -89,10 +91,26 @@ void NotificationRenderer::resetBanner()
|
||||
|
||||
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
|
||||
// Handle text_input notifications first - they have their own timeout/banner logic
|
||||
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Check for timeout and reset if needed for text input
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
if (!isOverlayBannerShowing() || pauseBanner)
|
||||
return;
|
||||
}
|
||||
drawTextInput(display, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
}
|
||||
|
||||
// Exit if no banner is showing or banner is paused
|
||||
if (!isOverlayBannerShowing() || pauseBanner) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (current_notification_type) {
|
||||
case notificationTypeEnum::none:
|
||||
// Do nothing - no notification to display
|
||||
@ -570,6 +588,90 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
||||
"Please be patient and do not power off.");
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (virtualKeyboard) {
|
||||
// Check for timeout and auto-exit if needed
|
||||
if (virtualKeyboard->isTimedOut()) {
|
||||
LOG_INFO("Virtual keyboard timeout - auto-exiting");
|
||||
// Cancel virtual keyboard - call callback with empty string to indicate timeout
|
||||
auto callback = textInputCallback; // Store callback before clearing
|
||||
|
||||
// Clean up first to prevent re-entry
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
|
||||
// Call callback after cleanup
|
||||
if (callback) {
|
||||
callback("");
|
||||
}
|
||||
|
||||
// Restore normal overlays
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle input events for virtual keyboard navigation
|
||||
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP) {
|
||||
virtualKeyboard->moveCursorUp();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
|
||||
virtualKeyboard->moveCursorDown();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
// Long press UP = move left
|
||||
virtualKeyboard->moveCursorLeft();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
// Long press DOWN = move right
|
||||
virtualKeyboard->moveCursorRight();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||
virtualKeyboard->handlePress();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
|
||||
virtualKeyboard->handleLongPress();
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
|
||||
// Cancel virtual keyboard - call callback with empty string
|
||||
auto callback = textInputCallback; // Store callback before clearing
|
||||
|
||||
// Clean up first to prevent re-entry
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
|
||||
// Call callback after cleanup
|
||||
if (callback) {
|
||||
callback("");
|
||||
}
|
||||
|
||||
// Restore normal overlays
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset input event after processing
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
}
|
||||
|
||||
// Clear the display and draw virtual keyboard
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||
display->setColor(WHITE);
|
||||
virtualKeyboard->draw(display, 0, 0);
|
||||
} else {
|
||||
// If virtualKeyboard is null, reset the banner to avoid getting stuck
|
||||
resetBanner();
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
|
@ -3,6 +3,9 @@
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
|
||||
namespace graphics
|
||||
@ -22,6 +25,8 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
@ -30,6 +35,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
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);
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
enum input_broker_event {
|
||||
INPUT_BROKER_NONE = 0,
|
||||
INPUT_BROKER_SELECT = 10,
|
||||
INPUT_BROKER_SELECT_LONG,
|
||||
INPUT_BROKER_UP = 17,
|
||||
INPUT_BROKER_DOWN = 18,
|
||||
INPUT_BROKER_LEFT = 19,
|
||||
|
@ -5,8 +5,9 @@ TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::
|
||||
|
||||
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
|
||||
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
|
||||
input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(),
|
||||
void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
|
||||
input_broker_event eventRight, input_broker_event eventPressed,
|
||||
input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
|
||||
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
|
||||
{
|
||||
this->_pinDown = pinDown;
|
||||
this->_pinUp = pinUp;
|
||||
@ -18,6 +19,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
||||
this->_eventLeft = eventLeft;
|
||||
this->_eventRight = eventRight;
|
||||
this->_eventPressed = eventPressed;
|
||||
this->_eventPressedLong = eventPressedLong;
|
||||
|
||||
if (pinPress != 255) {
|
||||
pinMode(pinPress, INPUT_PULLUP);
|
||||
@ -50,10 +52,42 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
{
|
||||
InputEvent e;
|
||||
e.inputEvent = INPUT_BROKER_NONE;
|
||||
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
||||
if (this->action == TB_ACTION_PRESSED) {
|
||||
// LOG_DEBUG("Trackball event Press");
|
||||
|
||||
// Handle long press detection for press button
|
||||
if (pressDetected && pressStartTime > 0) {
|
||||
uint32_t pressDuration = millis() - pressStartTime;
|
||||
bool buttonStillPressed = false;
|
||||
|
||||
#if defined(T_DECK)
|
||||
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
|
||||
#else
|
||||
buttonStillPressed = !digitalRead(_pinPress);
|
||||
#endif
|
||||
|
||||
if (!buttonStillPressed) {
|
||||
// Button released
|
||||
if (pressDuration < LONG_PRESS_DURATION) {
|
||||
// Short press
|
||||
e.inputEvent = this->_eventPressed;
|
||||
}
|
||||
// Reset state
|
||||
pressDetected = false;
|
||||
pressStartTime = 0;
|
||||
this->action = TB_ACTION_NONE;
|
||||
} else if (pressDuration >= LONG_PRESS_DURATION) {
|
||||
// Long press detected
|
||||
e.inputEvent = this->_eventPressedLong;
|
||||
this->action = TB_ACTION_PRESSED_LONG;
|
||||
// Keep pressDetected true to avoid repeated long press events
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
||||
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
|
||||
// Start long press detection
|
||||
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 && lastEvent == TB_ACTION_UP) {
|
||||
// LOG_DEBUG("Trackball event UP");
|
||||
e.inputEvent = this->_eventUp;
|
||||
@ -68,9 +102,11 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
e.inputEvent = this->_eventRight;
|
||||
}
|
||||
#else
|
||||
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) {
|
||||
// LOG_DEBUG("Trackball event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) {
|
||||
// Start long press detection
|
||||
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");
|
||||
e.inputEvent = this->_eventUp;
|
||||
@ -91,10 +127,16 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
e.kbchar = 0x00;
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
lastEvent = action;
|
||||
this->action = TB_ACTION_NONE;
|
||||
|
||||
return 100;
|
||||
// Only update lastEvent for non-press actions or completed press actions
|
||||
if (this->action != TB_ACTION_PRESSED || !pressDetected) {
|
||||
lastEvent = action;
|
||||
if (!pressDetected) {
|
||||
this->action = TB_ACTION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
return 50; // Check more frequently for better long press detection
|
||||
}
|
||||
|
||||
void TrackballInterruptBase::intPressHandler()
|
||||
|
@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
explicit TrackballInterruptBase(const char *name);
|
||||
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown,
|
||||
input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight,
|
||||
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(),
|
||||
void (*onIntPress)());
|
||||
input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
|
||||
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)());
|
||||
void intPressHandler();
|
||||
void intDownHandler();
|
||||
void intUpHandler();
|
||||
@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
enum TrackballInterruptBaseActionType {
|
||||
TB_ACTION_NONE,
|
||||
TB_ACTION_PRESSED,
|
||||
TB_ACTION_PRESSED_LONG,
|
||||
TB_ACTION_UP,
|
||||
TB_ACTION_DOWN,
|
||||
TB_ACTION_LEFT,
|
||||
@ -46,12 +47,18 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
||||
|
||||
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
|
||||
|
||||
// Long press detection for press button
|
||||
uint32_t pressStartTime = 0;
|
||||
bool pressDetected = false;
|
||||
static const uint32_t LONG_PRESS_DURATION = 500; // ms
|
||||
|
||||
private:
|
||||
input_broker_event _eventDown = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventLeft = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventRight = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
||||
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||
const char *_originName;
|
||||
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
|
||||
};
|
||||
|
@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
|
||||
input_broker_event eventLeft = INPUT_BROKER_LEFT;
|
||||
input_broker_event eventRight = INPUT_BROKER_RIGHT;
|
||||
input_broker_event eventPressed = INPUT_BROKER_SELECT;
|
||||
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
|
||||
|
||||
TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight,
|
||||
eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp,
|
||||
TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight,
|
||||
TrackballInterruptImpl1::handleIntPressed);
|
||||
eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown,
|
||||
TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft,
|
||||
TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed);
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,16 @@
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/NotificationRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h" // for cardkb_found
|
||||
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "GPS.h"
|
||||
#endif
|
||||
@ -122,8 +126,20 @@ int CannedMessageModule::splitConfiguredMessages()
|
||||
tempMessages[tempCount++] = "[Select Destination]";
|
||||
|
||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||
// Add a "Free Text" entry at the top if using a keyboard
|
||||
// Add a "Free Text" entry at the top if using a touch screen virtual keyboard
|
||||
tempMessages[tempCount++] = "[-- Free Text --]";
|
||||
#else
|
||||
// For devices with encoder input or trackball, also add Free Text option
|
||||
#if HAS_TRACKBALL
|
||||
extern TrackballInterruptImpl1 *trackballInterruptImpl1;
|
||||
if (trackballInterruptImpl1) {
|
||||
#else
|
||||
extern RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
extern UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
if (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1) {
|
||||
#endif
|
||||
tempMessages[tempCount++] = "[-- Free Text --]";
|
||||
}
|
||||
#endif
|
||||
|
||||
// First message always starts at buffer start
|
||||
@ -310,6 +326,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
|
||||
case CANNED_MESSAGE_RUN_STATE_FREETEXT:
|
||||
return handleFreeTextInput(event); // All allowed input for this state
|
||||
|
||||
// Virtual keyboard mode: Show virtual keyboard and handle input
|
||||
|
||||
// If sending, block all input except global/system (handled above)
|
||||
case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE:
|
||||
return 1;
|
||||
@ -589,6 +607,60 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
|
||||
notifyObservers(&e);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
if (strcmp(current, "[-- Free Text --]") == 0) {
|
||||
#if HAS_TRACKBALL
|
||||
extern TrackballInterruptImpl1 *trackballInterruptImpl1;
|
||||
if (trackballInterruptImpl1 && screen) {
|
||||
#else
|
||||
extern RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||
extern UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||
if ((rotaryEncoderInterruptImpl1 || upDownInterruptImpl1) && screen) {
|
||||
#endif
|
||||
screen->showTextInput("Free Text", "", 300000, [this](const std::string &text) {
|
||||
LOG_INFO("Free text submitted: '%s'", text.c_str());
|
||||
if (!text.empty()) {
|
||||
LOG_INFO("Storing message for delayed sending: '%s'", text.c_str());
|
||||
this->freetext = text.c_str();
|
||||
this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
||||
runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
||||
currentMessageIndex = -1;
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->notifyObservers(&e);
|
||||
screen->forceDisplay();
|
||||
|
||||
setIntervalFromNow(500);
|
||||
LOG_INFO("Free text callback completed safely");
|
||||
return;
|
||||
} else {
|
||||
// Don't delete virtual keyboard immediately - it might still be executing
|
||||
// Instead, just clear the callback and reset banner to stop input processing
|
||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
|
||||
// Return to inactive state
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
|
||||
// Force display update to show normal screen
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->notifyObservers(&e);
|
||||
screen->forceDisplay();
|
||||
|
||||
// Schedule cleanup for next loop iteration to ensure safe deletion
|
||||
setIntervalFromNow(50);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Normal canned message selection
|
||||
@ -883,12 +955,54 @@ 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) {
|
||||
LOG_INFO("Performing delayed virtual keyboard cleanup");
|
||||
delete graphics::NotificationRenderer::virtualKeyboard;
|
||||
graphics::NotificationRenderer::virtualKeyboard = nullptr;
|
||||
}
|
||||
|
||||
temporaryMessage = "";
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
// Handle delayed virtual keyboard message sending
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
// Virtual keyboard message sending case - text was not empty
|
||||
if (this->freetext.length() > 0) {
|
||||
LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str());
|
||||
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
||||
|
||||
// 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::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
// Clear payload to indicate virtual keyboard processing is complete
|
||||
// But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds
|
||||
this->payload = 0;
|
||||
} else {
|
||||
// Empty message, just go inactive
|
||||
LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
}
|
||||
|
||||
UIFrameEvent e;
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
return 2000;
|
||||
}
|
||||
|
||||
UIFrameEvent e;
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 &&
|
||||
this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) ||
|
||||
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) ||
|
||||
(this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) {
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
@ -898,6 +1012,18 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
// Handle SENDING_ACTIVE state transition after virtual keyboard message
|
||||
else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) {
|
||||
// This happens after virtual keyboard message sending is complete
|
||||
LOG_INFO("Virtual keyboard message sending completed, returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
temporaryMessage = "";
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
|
||||
!Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) {
|
||||
// Reset module on inactivity
|
||||
@ -906,9 +1032,23 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
// 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::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
this->notifyObservers(&e);
|
||||
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) {
|
||||
if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->payload == 0) {
|
||||
// [Exit] button pressed - return to inactive state
|
||||
LOG_INFO("Processing [Exit] action - returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
} else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->freetext.length() > 0) {
|
||||
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
||||
|
Loading…
Reference in New Issue
Block a user