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.
This commit is contained in:
SoftFever
2026-07-18 15:52:28 +08:00
parent 8b60291ed9
commit 2804e06cf2
6 changed files with 119 additions and 156 deletions

View File

@@ -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);
}

View File

@@ -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:

View File

@@ -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;
}

View File

@@ -776,6 +776,14 @@ void BBLNetworkPlugin::clear_all_function_pointers()
}
std::vector<NetworkLibraryVersionInfo> 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<NetworkLibraryVersionInfo> get_all_available_versions(const std::string& loaded_version)
{
std::vector<NetworkLibraryVersionInfo> result;
std::set<std::string> known_base_versions;
@@ -789,118 +797,58 @@ std::vector<NetworkLibraryVersionInfo> get_all_available_versions()
std::vector<std::string> discovered = BBLNetworkPlugin::scan_plugin_versions();
std::vector<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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;
}

View File

@@ -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<NetworkLibraryVersionInfo> 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<NetworkLibraryVersionInfo> get_all_available_versions(const std::string& loaded_version);
struct NetworkLibraryLoadError {
bool has_error = false;

View File

@@ -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);
}
}
}