diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4bc448f29..7e5528b9d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -175,12 +175,14 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif + nodeDB->pause_sort(true); // Store the message and set the expiration timestamp strncpy(NotificationRenderer::alertBannerMessage, message, 255); NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; NotificationRenderer::alertBannerCallback = bannerCallback; NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 07f2e5cde..9f2422748 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -343,4 +343,30 @@ const int *getTextPositions(OLEDDisplay *display) return textPositions; } +bool isAllowedPunctuation(char c) +{ + const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; + return allowed.find(c) != std::string::npos; +} + +std::string sanitizeString(const std::string &input) +{ + std::string output; + bool inReplacement = false; + + for (char c : input) { + if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { + output += c; + inReplacement = false; + } else { + if (!inReplacement) { + output += 0xbf; // ISO-8859-1 for inverted question mark + inReplacement = true; + } + } + } + + return output; +} + } // namespace graphics diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 2e97052a8..b8d82795e 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace graphics { @@ -52,4 +53,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti const int *getTextPositions(OLEDDisplay *display); +bool isAllowedPunctuation(char c); + +std::string sanitizeString(const std::string &input); + } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 65c259cf5..cb4ebeb59 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -8,6 +8,7 @@ #include "NodeDB.h" #include "buzz.h" #include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" #include "main.h" #include "modules/AdminModule.h" @@ -384,13 +385,13 @@ void menuHandler::favoriteBaseMenu() static const char **optionsArrayPtr; if (kb_found) { - static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"}; + static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg", "Remove Favorite"}; + optionsArrayPtr = optionsArray; + options = 4; + } else { + static const char *optionsArray[] = {"Back", "New Preset Msg", "Remove Favorite"}; optionsArrayPtr = optionsArray; options = 3; - } else { - static const char *optionsArray[] = {"Back", "New Preset Msg"}; - optionsArrayPtr = optionsArray; - options = 2; } BannerOverlayOptions bannerOptions; bannerOptions.message = "Favorites Action"; @@ -399,8 +400,12 @@ void menuHandler::favoriteBaseMenu() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } else if (selected == 2) { + } else if (selected == 2 && kb_found) { cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if ((!kb_found && selected == 2) || (selected == 3 && kb_found)) { + menuHandler::menuQueue = menuHandler::remove_favorite; + screen->setInterval(0); + runASAP = true; } }; screen->showOverlayBanner(bannerOptions); @@ -438,13 +443,15 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - static const char *optionsArray[] = {"Back", "Reset NodeDB"}; + static const char *optionsArray[] = {"Back", "Add Favorite", "Reset NodeDB"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { + menuQueue = add_favorite; + } else if (selected == 2) { menuQueue = reset_node_db_menu; } }; @@ -618,13 +625,13 @@ void menuHandler::TFTColorPickerMenu() void menuHandler::rebootMenu() { - static const char *optionsArray[] = {"Yes", "No"}; + static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Reboot Device?"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { + if (selected == 1) { IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; @@ -633,6 +640,37 @@ void menuHandler::rebootMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::addFavoriteMenu() +{ + screen->showNodePicker("Node To Favorite", 30000, [](int nodenum) -> void { + LOG_WARN("Nodenum: %u", nodenum); + nodeDB->set_favorite(true, nodenum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); +} + +void menuHandler::removeFavoriteMenu() +{ + + static const char *optionsArray[] = {"Back", "Yes"}; + BannerOverlayOptions bannerOptions; + std::string message = "Unfavorite This Node?\n"; + auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); + if (node && node->has_user) { + message += sanitizeString(node->user.long_name).substr(0, 15); + } + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::handleMenuSwitch() { switch (menuQueue) { @@ -677,6 +715,12 @@ void menuHandler::handleMenuSwitch() case reboot_menu: rebootMenu(); break; + case add_favorite: + addFavoriteMenu(); + break; + case remove_favorite: + removeFavoriteMenu(); + break; } menuQueue = menu_none; } diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 6b00848d2..5e217b45c 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -19,7 +19,9 @@ class menuHandler buzzermodemenupicker, mui_picker, tftcolormenupicker, - reboot_menu + reboot_menu, + add_favorite, + remove_favorite }; static screenMenus menuQueue; @@ -42,6 +44,8 @@ class menuHandler static void nodeListMenu(); static void resetNodeDBMenu(); static void rebootMenu(); + static void addFavoriteMenu(); + static void removeFavoriteMenu(); }; } // namespace graphics \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index d5acb431b..6c68826a0 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -60,11 +60,125 @@ void NotificationRenderer::resetBanner() { alertBannerMessage[0] = '\0'; current_notification_type = notificationTypeEnum::none; + nodeDB->pause_sort(false); } void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - drawAlertBannerOverlay(display, state); + switch (current_notification_type) { + case notificationTypeEnum::text_banner: + case notificationTypeEnum::selection_picker: + drawAlertBannerOverlay(display, state); + break; + case notificationTypeEnum::node_picker: + drawNodePicker(display, state); + break; + } +} + +void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + static uint32_t selectedNodenum = 0; + + if (!isOverlayBannerShowing() || pauseBanner) + return; + + // === Layout Configuration === + constexpr uint16_t vPadding = 2; + alertBannerOptions = nodeDB->getNumMeshNodes() - 1; + + // let the box drawing function calculate the widths? + + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + + // Handle input + if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) { + curSelected--; + } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { + curSelected++; + } else if (inEvent == INPUT_BROKER_SELECT) { + resetBanner(); + alertBannerCallback(selectedNodenum); + + } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + resetBanner(); + } + + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + + inEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + char scratchLineBuffer[visibleTotalLines - lineCount][40]; + LOG_WARN("Requestion %u buffers", visibleTotalLines - lineCount); + + uint8_t firstOptionToShow = 0; + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + int scratchLineNum = 0; + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + char temp_name[16] = {0}; + if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { + std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); + strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + + } else { + snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + } + // make temp buffer for name + // fi + if (i == curSelected) { + selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + if (isHighResolution) { + strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + } else { + strncpy(scratchLineBuffer[scratchLineNum], ">", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + } + scratchLineBuffer[scratchLineNum][39] = '\0'; + } else { + strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36); + } + linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; + LOG_WARN("Using buffer %u", scratchLineNum); + } + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); } void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) @@ -75,10 +189,6 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // === Layout Configuration === constexpr uint16_t vPadding = 2; - // Setup font and alignment - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - uint16_t optionWidths[alertBannerOptions] = {0}; uint16_t maxWidth = 0; uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); @@ -191,6 +301,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp void NotificationRenderer::drawNotificationBox(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 === @@ -202,7 +313,10 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay if (maxWidth != 0) is_picker = true; - // seelction box + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); while (lines[lineCount] != nullptr) { auto newlinePointer = strchr(lines[lineCount], '\n'); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 885300a51..93e408165 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -24,6 +24,7 @@ class NotificationRenderer static void resetBanner(); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNodePicker(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); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 1d33d5d0c..4ce073c9b 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -18,32 +18,6 @@ #include #include -bool isAllowedPunctuation(char c) -{ - const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; - return allowed.find(c) != std::string::npos; -} - -std::string sanitizeString(const std::string &input) -{ - std::string output; - bool inReplacement = false; - - for (char c : input) { - if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { - output += c; - inReplacement = false; - } else { - if (!inReplacement) { - output += 0xbf; // ISO-8859-1 for inverted question mark - inReplacement = true; - } - } - } - - return output; -} - #if !MESHTASTIC_EXCLUDE_GPS // External variables diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 2deca079d..b1d3fad28 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1694,9 +1694,24 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) } } +void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) +{ + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + if (lite && lite->is_favorite != is_favorite) { + lite->is_favorite = is_favorite; + sortMeshDB(); + saveNodeDatabaseToDisk(); + } +} + +void NodeDB::pause_sort(bool paused) +{ + sortingIsPaused = paused; +} + void NodeDB::sortMeshDB() { - if (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) { + if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { lastSort = millis(); bool changed = true; while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index f4fc2e2f4..8cf0c78b3 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -191,6 +191,16 @@ class NodeDB */ bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); + /* + * Sets a node either favorite or unfavorite + */ + void set_favorite(bool is_favorite, uint32_t nodeId); + + /** + * Other functions like the node picker can request a pause in the node sorting + */ + void pause_sort(bool paused); + /// @return our node number NodeNum getNodeNum() { return myNodeInfo.my_node_num; } @@ -283,6 +293,11 @@ class NodeDB /// Find a node in our DB, create an empty NodeInfoLite if missing meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); + /* + * Internal boolean to track sorting paused + */ + bool sortingIsPaused = false; + /// pick a provisional nodenum we hope no one is using void pickNewNodeNum();