From 2804e06cf254c543749fe75a95ec8e23385ef75b Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 18 Jul 2026 15:52:28 +0800 Subject: [PATCH] Sort network plug-in versions newest first and mark only the loaded build Discovered builds were inserted positionally behind the whitelist entry they anchored to, so an OTA-installed 02.08.01.53 listed below 02.08.01.52. The list is now built by appending and sorting once, which also drops the per-entry insertion scans. "(installed)" tested whether the library was present on disk, so it marked every version ever selected - switching leaves the previous library in place. The flag is now is_loaded, resolved against the plug-in actually loaded, and the two combo populators share one label helper. --- src/slic3r/GUI/NetworkPluginDialog.cpp | 33 ++--- src/slic3r/GUI/NetworkPluginDialog.hpp | 6 + src/slic3r/GUI/Preferences.cpp | 19 +-- src/slic3r/Utils/BBLNetworkPlugin.cpp | 150 +++++++------------- src/slic3r/Utils/bambu_networking.hpp | 13 +- tests/slic3rutils/test_network_versions.cpp | 54 ++++--- 6 files changed, 119 insertions(+), 156 deletions(-) diff --git a/src/slic3r/GUI/NetworkPluginDialog.cpp b/src/slic3r/GUI/NetworkPluginDialog.cpp index 133ae4f2de..5b124072e7 100644 --- a/src/slic3r/GUI/NetworkPluginDialog.cpp +++ b/src/slic3r/GUI/NetworkPluginDialog.cpp @@ -205,6 +205,19 @@ void NetworkPluginDownloadDialog::create_update_available_ui(const std::string& main_sizer->Add(0, 0, 0, wxBOTTOM, FromDIP(20)); } +wxString network_version_label(const NetworkLibraryVersionInfo& ver) +{ + wxString label = wxString::FromUTF8(ver.display_name); + if (!ver.suffix.empty()) + label = wxString::FromUTF8("\xE2\x94\x94 ") + label; + // Both can apply: the loaded build may also be the highest listed one. + if (ver.is_latest) + label += wxString(" ") + _L("(Latest)"); + if (ver.is_loaded) + label += wxString(" ") + _L("(installed)"); + return label; +} + void NetworkPluginDownloadDialog::setup_version_selector() { m_version_combo = new ComboBox(this, wxID_ANY, wxEmptyString, @@ -212,24 +225,8 @@ void NetworkPluginDownloadDialog::setup_version_selector() m_version_combo->SetFont(::Label::Body_13); m_available_versions = get_all_available_versions(); - for (size_t i = 0; i < m_available_versions.size(); ++i) { - const auto& ver = m_available_versions[i]; - wxString label; - if (!ver.suffix.empty()) { - label = wxString::FromUTF8("\xE2\x94\x94 ") + wxString::FromUTF8(ver.display_name); - } else { - label = wxString::FromUTF8(ver.display_name); - } - // Same labeling as the Preferences selector: "(Latest)" = highest listed - // version, "(installed)" = library already on disk; both can apply. - if (ver.is_latest) { - label += wxString(" ") + _L("(Latest)"); - } - if (ver.is_installed) { - label += wxString(" ") + _L("(installed)"); - } - m_version_combo->Append(label); - } + for (const auto& ver : m_available_versions) + m_version_combo->Append(network_version_label(ver)); m_version_combo->SetSelection(0); } diff --git a/src/slic3r/GUI/NetworkPluginDialog.hpp b/src/slic3r/GUI/NetworkPluginDialog.hpp index d7b392533b..0c2c1376ed 100644 --- a/src/slic3r/GUI/NetworkPluginDialog.hpp +++ b/src/slic3r/GUI/NetworkPluginDialog.hpp @@ -11,6 +11,12 @@ namespace Slic3r { namespace GUI { +// Dropdown label for one network plug-in version: tree glyph for a dev build, then +// "(Latest)" for the highest listed version and "(installed)" for the build currently +// loaded. Shared by the Preferences selector and the download dialog so both stay +// consistent. +wxString network_version_label(const NetworkLibraryVersionInfo& ver); + class NetworkPluginDownloadDialog : public DPIDialog { public: diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index c8d520f2f2..18fab4938c 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -17,6 +17,7 @@ #include "Widgets/RadioGroup.hpp" #include "slic3r/Utils/bambu_networking.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "NetworkPluginDialog.hpp" #include "DownloadProgressDialog.hpp" #ifdef __WINDOWS__ @@ -1262,23 +1263,7 @@ wxBoxSizer *PreferencesDialog::create_item_network_plugin_version(wxString title for (size_t i = 0; i < m_available_versions.size(); i++) { const auto& ver = m_available_versions[i]; - wxString label; - - if (!ver.suffix.empty()) { - label = wxString::FromUTF8("\xE2\x94\x94 ") + wxString::FromUTF8(ver.display_name); - } else { - label = wxString::FromUTF8(ver.display_name); - } - - // "(Latest)" marks the highest listed version; "(installed)" marks versions - // whose library is already on disk. One entry can carry both. - if (ver.is_latest) { - label += " " + _L("(Latest)"); - } - if (ver.is_installed) { - label += " " + _L("(installed)"); - } - m_network_version_combo->Append(label); + m_network_version_combo->Append(network_version_label(ver)); if (current_version == ver.version) { current_selection = i; } diff --git a/src/slic3r/Utils/BBLNetworkPlugin.cpp b/src/slic3r/Utils/BBLNetworkPlugin.cpp index 72e94a8bf0..630ec98086 100644 --- a/src/slic3r/Utils/BBLNetworkPlugin.cpp +++ b/src/slic3r/Utils/BBLNetworkPlugin.cpp @@ -776,6 +776,14 @@ void BBLNetworkPlugin::clear_all_function_pointers() } std::vector get_all_available_versions() +{ + // get_version() reports the "00.00.00.00" sentinel when nothing is loaded; resolve + // that here so the list builder only ever sees a real version or an empty string. + const BBLNetworkPlugin& plugin = BBLNetworkPlugin::instance(); + return get_all_available_versions(plugin.is_loaded() ? plugin.get_version() : std::string()); +} + +std::vector get_all_available_versions(const std::string& loaded_version) { std::vector result; std::set known_base_versions; @@ -789,118 +797,58 @@ std::vector get_all_available_versions() std::vector discovered = BBLNetworkPlugin::scan_plugin_versions(); - std::vector> suffixed_versions; - + // Surface discovered builds that are not whitelisted outright: + // - suffixed dev builds (02.08.01.52-custom) attach to the exact whitelisted base + // version they were built from, so they render nested under it; + // - unsuffixed builds (typically installed by the OTA plug-in update) are accepted + // when is_supported_network_version() recognises their release series - the plugin + // ABI is stable within an AA.BB.CC series and the OTA sync only ever accepts + // same-series updates, so they load with the whitelisted struct layout. That gate + // never matches the legacy entry, which would otherwise be loaded with the modern + // layout and break. + // Only the static whitelist anchors a dev build, never another discovered one, so + // known_base_versions must stay separate from all_known_versions here. for (const auto& version : discovered) { if (all_known_versions.count(version) > 0) continue; - std::string base = extract_base_version(version); - std::string suffix = extract_suffix(version); - - if (suffix.empty()) - continue; - - if (known_base_versions.count(base) == 0) - continue; - - suffixed_versions.emplace_back(base, version); + const std::string suffix = extract_suffix(version); + if (suffix.empty()) { + if (!is_supported_network_version(version)) + continue; + result.push_back(NetworkLibraryVersionInfo::from_discovered(version, version, "")); + } else { + const std::string base = extract_base_version(version); + if (known_base_versions.count(base) == 0) + continue; + result.push_back(NetworkLibraryVersionInfo::from_discovered(version, base, suffix)); + } all_known_versions.insert(version); } - std::sort(suffixed_versions.begin(), suffixed_versions.end(), - [](const auto& a, const auto& b) { - if (a.first != b.first) return a.first > b.first; - return a.second < b.second; + // Newest first. Version components are fixed-width and zero-padded, so a plain + // string compare orders them numerically, and the legacy series sorts last on its + // own. Suffixed dev builds sort directly under the base version they build on. + std::sort(result.begin(), result.end(), + [](const NetworkLibraryVersionInfo& a, const NetworkLibraryVersionInfo& b) { + if (a.base_version != b.base_version) return a.base_version > b.base_version; + return a.suffix < b.suffix; }); - for (const auto& [base, full] : suffixed_versions) { - size_t insert_pos = 0; - for (size_t i = 0; i < result.size(); ++i) { - if (result[i].base_version == base) { - insert_pos = i + 1; - while (insert_pos < result.size() && - result[insert_pos].base_version == base) { - ++insert_pos; - } - break; - } - } - - std::string sfx = extract_suffix(full); - result.insert(result.begin() + insert_pos, - NetworkLibraryVersionInfo::from_discovered(full, base, sfx)); - } - - // Also surface discovered versions WITHOUT a suffix (typically installed by the - // OTA plug-in update) when they belong to a whitelisted release series: the plugin - // ABI is stable within an AA.BB.CC series (the OTA sync only ever accepts same-series - // updates), so these load with the same struct layout as the static entry they follow. - // The legacy entry is excluded as an anchor - is_legacy_version() matches exactly, so - // a different 01.x build would be loaded with the modern layout and break. - std::vector> series_versions; // {anchor whitelist version, discovered version} - for (const auto& version : discovered) { - if (all_known_versions.count(version) > 0) - continue; - if (!extract_suffix(version).empty()) - continue; - if (version.size() < 8) - continue; - - for (const auto& base : known_base_versions) { - if (BBLNetworkPlugin::is_legacy_version(base)) - continue; - if (base.size() >= 8 && base.compare(0, 8, version, 0, 8) == 0) { - series_versions.emplace_back(base, version); - all_known_versions.insert(version); - break; - } - } - } - - // Ascending sort + insertion right behind the anchor leaves the newest build - // closest to its whitelist entry. - std::sort(series_versions.begin(), series_versions.end(), - [](const auto& a, const auto& b) { - if (a.first != b.first) return a.first > b.first; - return a.second < b.second; - }); - - for (const auto& [anchor, full] : series_versions) { - size_t insert_pos = result.size(); - for (size_t i = 0; i < result.size(); ++i) { - if (result[i].version == anchor) { - // Skip past the whole base-version block (the entry itself plus any - // suffixed dev builds shown under it) so the tree glyphs stay adjacent. - insert_pos = i + 1; - while (insert_pos < result.size() && - result[insert_pos].base_version == anchor) { - ++insert_pos; - } - break; - } - } - result.insert(result.begin() + insert_pos, - NetworkLibraryVersionInfo::from_discovered(full, full, "")); - } - - // Final pass: record what is actually present on disk, and put the "(Latest)" - // label on the truly highest full version in the list - an OTA-installed build - // can be newer than the newest whitelisted entry. get_latest_network_version() - // intentionally keeps returning the static whitelist default, which drives the - // download and update-check decisions. - size_t highest = result.size(); - for (size_t i = 0; i < result.size(); ++i) { - auto& info = result[i]; - info.is_installed = info.is_discovered || - BBLNetworkPlugin::versioned_library_exists(info.version); + for (auto& info : result) { + info.is_loaded = !loaded_version.empty() && info.version == loaded_version; info.is_latest = false; - if (info.suffix.empty() && - (highest == result.size() || info.version > result[highest].version)) - highest = i; } - if (highest < result.size()) - result[highest].is_latest = true; + + // "(Latest)" goes on the highest full version in the list, which after the sort is + // simply the first entry without a dev suffix - an OTA-installed build can be newer + // than the newest whitelisted entry. get_latest_network_version() intentionally + // keeps returning the static whitelist default, which drives the download and + // update-check decisions. + auto latest = std::find_if(result.begin(), result.end(), + [](const NetworkLibraryVersionInfo& info) { return info.suffix.empty(); }); + if (latest != result.end()) + latest->is_latest = true; return result; } diff --git a/src/slic3r/Utils/bambu_networking.hpp b/src/slic3r/Utils/bambu_networking.hpp index 9145769e8d..8e25db169e 100644 --- a/src/slic3r/Utils/bambu_networking.hpp +++ b/src/slic3r/Utils/bambu_networking.hpp @@ -406,8 +406,10 @@ struct NetworkLibraryVersionInfo { bool is_latest; std::string warning; bool is_discovered; - // Whether the versioned library file for this entry is present on disk. - bool is_installed = false; + // Whether this is the build currently loaded in this session. Deliberately not + // "present on disk": switching versions leaves the previous library in place, so + // an on-disk test marks every version ever selected. + bool is_loaded = false; static NetworkLibraryVersionInfo from_static(const NetworkLibraryVersion& v) { return { @@ -416,7 +418,7 @@ struct NetworkLibraryVersionInfo { "", v.display_name, v.url_override ? v.url_override : "", - v.is_latest, + false, // assigned by get_all_available_versions() once the list is sorted v.warning ? v.warning : "", false }; @@ -439,7 +441,12 @@ inline std::string extract_suffix(const std::string& full_version) { return (pos == std::string::npos) ? "" : full_version.substr(pos + 1); } +// Selectable versions, newest first. Marks the entry matching the plug-in currently +// loaded in this session. std::vector get_all_available_versions(); +// Same list, resolving is_loaded against an explicitly supplied version rather than the +// live plug-in. Pass an empty string for "nothing loaded". +std::vector get_all_available_versions(const std::string& loaded_version); struct NetworkLibraryLoadError { bool has_error = false; diff --git a/tests/slic3rutils/test_network_versions.cpp b/tests/slic3rutils/test_network_versions.cpp index f35041a469..a26725fc4c 100644 --- a/tests/slic3rutils/test_network_versions.cpp +++ b/tests/slic3rutils/test_network_versions.cpp @@ -75,33 +75,53 @@ TEST_CASE_METHOD(PluginFolderFixture, "Same-series OTA plugin versions are surfa REQUIRE(count_version(versions, "02.08.01.52-custom") == 1); REQUIRE(count_version(versions, "02.03.00.62") == 0); - size_t latest_pos = versions.size(), ota_pos = versions.size(); - for (size_t i = 0; i < versions.size(); ++i) { - if (versions[i].version == "02.08.01.52") latest_pos = i; - if (versions[i].version == "02.08.01.55") ota_pos = i; - } - REQUIRE(latest_pos < versions.size()); - // The discovered build lands right after the whole 02.08.01.52 block - // (the whitelist entry plus its suffixed dev build). - REQUIRE(ota_pos == latest_pos + 2); - REQUIRE(versions[ota_pos - 1].base_version == "02.08.01.52"); - REQUIRE(versions[ota_pos - 1].version == "02.08.01.52-custom"); + // Newest first, regardless of whether a version came from the whitelist or from + // disk: the OTA build outranks the older whitelist entry it was discovered under. + REQUIRE(versions[0].version == "02.08.01.55"); + REQUIRE(versions[1].version == "02.08.01.52"); + // Suffixed dev builds stay nested directly under the base version they build on. + REQUIRE(versions[2].version == "02.08.01.52-custom"); + REQUIRE(versions[2].base_version == "02.08.01.52"); + // The legacy series is the oldest, so it sorts last. + REQUIRE(versions.back().version == BAMBU_NETWORK_AGENT_VERSION_LEGACY); - const auto& ota = versions[ota_pos]; + const auto& ota = versions[0]; REQUIRE(ota.is_discovered); REQUIRE(ota.suffix.empty()); // "(Latest)" is dynamic: the OTA build is the highest listed version, so it takes - // the label from the static whitelist entry - and it is also marked installed. + // the label from the static whitelist entry. REQUIRE(ota.is_latest); - REQUIRE(ota.is_installed); - REQUIRE_FALSE(versions[latest_pos].is_latest); - REQUIRE_FALSE(versions[latest_pos].is_installed); + REQUIRE_FALSE(versions[1].is_latest); // The static default used for download and update-check decisions is unchanged. REQUIRE(std::string(get_latest_network_version()) == "02.08.01.52"); } +TEST_CASE_METHOD(PluginFolderFixture, "Only the loaded build is marked installed", "[NetworkVersions]") +{ + // Switching versions leaves the previous library on disk, so presence on disk is + // not what "(installed)" reports - the build actually loaded in this session is. + add_plugin("02.08.01.55"); + add_plugin("02.08.01.52"); + + auto versions = get_all_available_versions("02.08.01.52"); + + int marked = 0; + for (const auto& info : versions) { + if (info.is_loaded) { + ++marked; + REQUIRE(info.version == "02.08.01.52"); + } + } + REQUIRE(marked == 1); + + // Nothing loaded (plug-in disabled or failed to load) marks nothing, even though + // both libraries are on disk. + for (const auto& info : get_all_available_versions("")) + REQUIRE_FALSE(info.is_loaded); +} + TEST_CASE("Only whitelisted series pass the load gate", "[NetworkVersions]") { // Exact whitelist entries. @@ -144,7 +164,7 @@ TEST_CASE_METHOD(PluginFolderFixture, "Legacy series never adopts discovered bui for (const auto& info : versions) { if (info.version == "02.08.01.52") { REQUIRE(info.is_latest); - REQUIRE_FALSE(info.is_installed); + REQUIRE_FALSE(info.is_loaded); } } }