Skip to content
162 changes: 155 additions & 7 deletions src/graphics/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1058,13 +1058,7 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(chirpy_small);
}

#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (!hiddenFrames.wifi && isWifiAvailable()) {
fsi.positions.wifi = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
indicatorIcons.push_back(icon_wifi);
}
#endif
// WiFi frame removed from carousel - access via WiFi Config menu only

// Beware of what changes you make in this code!
// We pass numframes into GetMeshModulesWithUIFrames() which is highly important!
Expand Down Expand Up @@ -1228,6 +1222,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
if (frameName == "chirpy") {
hiddenFrames.chirpy = !hiddenFrames.chirpy;
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (frameName == "wifi") {
hiddenFrames.wifi = !hiddenFrames.wifi;
}
#endif
}

bool Screen::isFrameHidden(const std::string &frameName) const
Expand Down Expand Up @@ -1258,6 +1257,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return hiddenFrames.show_favorites;
if (frameName == "chirpy")
return hiddenFrames.chirpy;
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (frameName == "wifi")
return hiddenFrames.wifi;
#endif

return false;
}
Expand Down Expand Up @@ -1561,6 +1564,31 @@ int Screen::handleInputEvent(const InputEvent *event)
return 0;
}

#if HAS_WIFI && !defined(ARCH_PORTDUINO)
// Handle input when WiFi status screen is showing
if (graphics::UIRenderer::showingWifiStatus) {
if (event->inputEvent == INPUT_BROKER_BACK || event->inputEvent == INPUT_BROKER_CANCEL ||
event->inputEvent == INPUT_BROKER_SELECT) {
// Exit WiFi status screen and return to normal frames
graphics::UIRenderer::showingWifiStatus = false;
setFrames(FOCUS_DEFAULT);
return 0;
}
}

// Handle input when MQTT status screen is showing
if (graphics::UIRenderer::showingMqttStatus) {
if (event->inputEvent == INPUT_BROKER_BACK || event->inputEvent == INPUT_BROKER_CANCEL ||
event->inputEvent == INPUT_BROKER_SELECT) {
// Exit MQTT status screen and return to normal frames
graphics::UIRenderer::showingMqttStatus = false;
hiddenFrames.mqtt = false; // Restore MQTT frame to carousel
setFrames(FOCUS_DEFAULT);
return 0;
}
}
#endif

// Use left or right input from a keyboard to move between frames,
// so long as a mesh module isn't using these events for some other purpose
if (showingNormalScreen) {
Expand Down Expand Up @@ -1647,6 +1675,126 @@ bool Screen::isOverlayBannerShowing()
return NotificationRenderer::isOverlayBannerShowing();
}

#if HAS_WIFI && !defined(ARCH_PORTDUINO)
void Screen::openMqttInfoScreen()
{
// Hide MQTT frame from carousel when showing status screen
hiddenFrames.mqtt = true;

// Set flag to track MQTT status screen is showing
graphics::UIRenderer::showingMqttStatus = true;

// Create a FrameCallback with the drawMqttInfoDirect function
setFrameImmediateDraw(new FrameCallback([](OLEDDisplay *d, OLEDDisplayUiState *s, int16_t x, int16_t y) {
graphics::UIRenderer::drawMqttInfoDirect(d, s, x, y);
}));
}

void Screen::openWifiInfoScreen()
{
// Hide WiFi frame from carousel when showing status screen
hiddenFrames.wifi = true;

// Set flag to track WiFi status screen is showing
graphics::UIRenderer::showingWifiStatus = true;

// Create a FrameCallback with the drawWifiInfoDirect function
setFrameImmediateDraw(new FrameCallback([](OLEDDisplay *d, OLEDDisplayUiState *s, int16_t x, int16_t y) {
graphics::UIRenderer::drawWifiInfoDirect(d, s, x, y);
}));
}

void Screen::hideFrame(const std::string &frameName)
{
#ifndef USE_EINK
if (frameName == "nodelist") {
hiddenFrames.nodelist = true;
}
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard") {
hiddenFrames.nodelist_lastheard = true;
}
if (frameName == "nodelist_hopsignal") {
hiddenFrames.nodelist_hopsignal = true;
}
if (frameName == "nodelist_distance") {
hiddenFrames.nodelist_distance = true;
}
#endif
#if HAS_GPS
if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = true;
}
if (frameName == "gps") {
hiddenFrames.gps = true;
}
#endif
if (frameName == "lora") {
hiddenFrames.lora = true;
}
if (frameName == "clock") {
hiddenFrames.clock = true;
}
if (frameName == "show_favorites") {
hiddenFrames.show_favorites = true;
}
if (frameName == "chirpy") {
hiddenFrames.chirpy = true;
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (frameName == "wifi") {
hiddenFrames.wifi = true;
}
#endif
}

void Screen::showFrame(const std::string &frameName)
{
#ifndef USE_EINK
if (frameName == "nodelist") {
hiddenFrames.nodelist = false;
}
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard") {
hiddenFrames.nodelist_lastheard = false;
}
if (frameName == "nodelist_hopsignal") {
hiddenFrames.nodelist_hopsignal = false;
}
if (frameName == "nodelist_distance") {
hiddenFrames.nodelist_distance = false;
}
#endif
#if HAS_GPS
if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = false;
}
if (frameName == "gps") {
hiddenFrames.gps = false;
}
#endif
if (frameName == "lora") {
hiddenFrames.lora = false;
}
if (frameName == "clock") {
hiddenFrames.clock = false;
}
if (frameName == "show_favorites") {
hiddenFrames.show_favorites = false;
}
if (frameName == "chirpy") {
hiddenFrames.chirpy = false;
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (frameName == "wifi") {
hiddenFrames.wifi = false;
}
#endif
}
#endif

} // namespace graphics

#else
Expand Down
8 changes: 8 additions & 0 deletions src/graphics/Screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ class Screen : public concurrency::OSThread
void showSimpleBanner(const char *message, uint32_t durationMs = 0);
void showOverlayBanner(BannerOverlayOptions);

#if HAS_WIFI && !defined(ARCH_PORTDUINO)
void openMqttInfoScreen(); // Opens direct the MQTT status info screen
void openWifiInfoScreen(); // Opens direct the WiFi status info screen
#endif

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,
Expand Down Expand Up @@ -591,6 +596,8 @@ class Screen : public concurrency::OSThread

// Menu-driven Show / Hide Toggle
void toggleFrameVisibility(const std::string &frameName);
void hideFrame(const std::string &frameName);
void showFrame(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;

#ifdef USE_EINK
Expand Down Expand Up @@ -676,6 +683,7 @@ class Screen : public concurrency::OSThread
bool textMessage = false;
bool waypoint = false;
bool wifi = false;
bool mqtt = false;
bool system = false;
bool home = false;
bool clock = false;
Expand Down
73 changes: 52 additions & 21 deletions src/graphics/VirtualKeyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace graphics
{

VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), upperCaseLayer(false), lastActivityTime(millis())
{
initializeKeyboard();
// Set cursor to H(2, 5)
Expand All @@ -22,15 +22,25 @@ VirtualKeyboard::~VirtualKeyboard() {}

void VirtualKeyboard::initializeKeyboard()
{
// New 4 row, 11 column keyboard layout:
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
// Define two layouts: lowercase and uppercase/symbols
static const char LAYOUT_LOWER[KEYBOARD_ROWS][KEYBOARD_COLS] = {
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '?', ' '},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '\x01', '\x1b'}};

// Uppercase layout with WiFi-friendly symbols in top row
static const char LAYOUT_UPPER[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'!', '@', '#', '$', '%', '&', '*', '-', '_', '+', '\b'},
{'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '\n'},
{'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', ' '},
{'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', '\x1b'}};

// Choose current layout based on layer state
const char(*currentLayout)[KEYBOARD_COLS] = upperCaseLayer ? LAYOUT_UPPER : LAYOUT_LOWER;

// Derive layout dimensions and assert they match the configured keyboard grid
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT_LOWER) / sizeof(LAYOUT_LOWER[0]));
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT_LOWER[0]) / sizeof(LAYOUT_LOWER[0][0]));
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");

Expand All @@ -41,11 +51,10 @@ void VirtualKeyboard::initializeKeyboard()
}
}

// Fill keyboard from the 2D layout
// Fill keyboard from the current layout
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
char ch = LAYOUT[row][col];
// No empty slots in the simplified layout
char ch = currentLayout[row][col];

VirtualKeyType type = VK_CHAR;
if (ch == '\b') {
Expand All @@ -56,10 +65,14 @@ void VirtualKeyboard::initializeKeyboard()
type = VK_ESC;
} else if (ch == ' ') {
type = VK_SPACE;
} else if (ch == '\x01') { // SHIFT (using control character as marker)
type = VK_SHIFT;
}

// Make action keys wider to fit text while keeping the last column aligned
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
uint8_t width =
(type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE || type == VK_ESC) ? (KEY_WIDTH * 3) : KEY_WIDTH;

keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
}
}
Expand Down Expand Up @@ -205,6 +218,11 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
}
}

// Draw layer indicator in top right corner
std::string layerIndicator = upperCaseLayer ? "ABC" : "abc";
int indicatorWidth = display->getStringWidth(layerIndicator.c_str());
display->drawString(screenWidth - indicatorWidth - 2, offsetY, layerIndicator.c_str());

const int boxX = offsetX;
const int boxWidth = screenWidth;
int boxY;
Expand Down Expand Up @@ -353,8 +371,6 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;

Expand Down Expand Up @@ -417,18 +433,17 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
const int fontH = FONT_HEIGHT_SMALL;
// Build label and metrics first
std::string keyText;
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC || key.type == VK_SHIFT) {
// Keep literal text labels for the action keys on the rightmost column
keyText = (key.type == VK_BACKSPACE) ? "BACK"
: (key.type == VK_ENTER) ? "ENTER"
: (key.type == VK_SPACE) ? "SPACE"
: (key.type == VK_SHIFT) ? "SH"
: (key.type == VK_ESC) ? "ESC"
: "";
} else {
char c = getCharForKey(key, false);
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
// Display characters as they are in the current layer layout
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
}

Expand All @@ -453,7 +468,8 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
int contentH = height;
if (selected) {
display->setColor(WHITE);
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC ||
key.type == VK_SHIFT);

if (display->getHeight() <= 64 && !isAction) {
display->fillRect(x, y, width, height);
Expand Down Expand Up @@ -516,10 +532,12 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)

char c = key.character;

// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
// Long-press functionality: convert lowercase letters to uppercase
// This works alongside the SHIFT key functionality
if (isLongPress && c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
}
// Note: Numbers and symbols from long press are handled by the layer system now

return c;
}
Expand Down Expand Up @@ -611,6 +629,9 @@ void VirtualKeyboard::handlePress()
case VK_SPACE:
insertCharacter(' ');
break;
case VK_SHIFT:
toggleLayer();
break;
case VK_ESC:
if (onTextEntered) {
std::function<void(const std::string &)> callback = onTextEntered;
Expand Down Expand Up @@ -656,6 +677,9 @@ void VirtualKeyboard::handleLongPress()
case VK_SPACE:
insertCharacter(' ');
break;
case VK_SHIFT:
toggleLayer();
break;
case VK_ESC:
if (onTextEntered) {
onTextEntered("");
Expand Down Expand Up @@ -735,4 +759,11 @@ bool VirtualKeyboard::isTimedOut() const
return (millis() - lastActivityTime) > TIMEOUT_MS;
}

} // namespace graphics
void VirtualKeyboard::toggleLayer()
{
resetTimeout();
upperCaseLayer = !upperCaseLayer;
initializeKeyboard(); // Rebuild keyboard with new layer
}

} // namespace graphics
Loading