diff --git a/resources/web/dialog/PluginsDialog/index.html b/resources/web/dialog/PluginsDialog/index.html
index 438f6eb4e9..853d1a8464 100644
--- a/resources/web/dialog/PluginsDialog/index.html
+++ b/resources/web/dialog/PluginsDialog/index.html
@@ -47,7 +47,8 @@
Activate
Name
- Plugin Version
+ Plugin Version
Source
#include
#include
@@ -16,6 +18,7 @@ namespace Slic3r::GUI
Status,
Name,
Source,
+ Version,
// why: neutral "no column selected" state - clearing a header sort returns here and the
// list falls to compare_plugin_base_order only. Header UI reaches it via the asc/desc/clear cycle.
None
@@ -34,6 +37,7 @@ namespace Slic3r::GUI
case PluginSortKey::Status: return "status";
case PluginSortKey::Name: return "name";
case PluginSortKey::Source: return "source";
+ case PluginSortKey::Version: return "version";
case PluginSortKey::None: return "none";
}
@@ -53,6 +57,8 @@ namespace Slic3r::GUI
return PluginSortKey::Name;
if (sort_key == "source")
return PluginSortKey::Source;
+ if (sort_key == "version")
+ return PluginSortKey::Version;
if (sort_key == "none")
return PluginSortKey::None;
return fallback;
@@ -141,6 +147,23 @@ namespace Slic3r::GUI
return lhs.plugin_key.compare(rhs.plugin_key);
}
+ // Compares two version strings returning -1 / 0 / +1. Uses Slic3r::Semver (the same parser the
+ // plugin catalog's update-available check uses); on unparseable input falls back to the natural
+ // compare so the order stays deterministic.
+ // e.g. "1.2.0" < "1.10.0" (numeric), "1.0.0-rc1" < "1.0.0" (semver prerelease rule).
+ inline int compare_plugin_version(const std::string& lhs, const std::string& rhs)
+ {
+ const auto lhs_semver = Semver::parse(lhs);
+ const auto rhs_semver = Semver::parse(rhs);
+ if (lhs_semver && rhs_semver)
+ {
+ if (*lhs_semver < *rhs_semver) return -1;
+ if (*rhs_semver < *lhs_semver) return 1;
+ return 0;
+ }
+ return compare_ascii_case_insensitive_natural(lhs, rhs);
+ }
+
// Compares two items by the chosen primary key, returning -1 / 0 / +1. Status and Source
// rank by enum ordinal (the declared dialog priority); Name uses the natural compare above.
// e.g. Status: an enabled item (lower ordinal) sorts before a disabled one.
@@ -157,6 +180,8 @@ namespace Slic3r::GUI
return compare_ascii_case_insensitive_natural(lhs.display_name, rhs.display_name);
case PluginSortKey::Source:
return static_cast(lhs.source) - static_cast(rhs.source);
+ case PluginSortKey::Version:
+ return compare_plugin_version(lhs.sort_version, rhs.sort_version);
case PluginSortKey::None:
// why: no primary key - every pair ties here so sort_plugin_items_for_dialog falls
// straight to the ascending base order (direction is irrelevant for the baseline).
diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp
index bed37f2ebe..070bc36a14 100644
--- a/src/slic3r/GUI/PluginsDialog.cpp
+++ b/src/slic3r/GUI/PluginsDialog.cpp
@@ -89,6 +89,7 @@ struct PluginDialogItem
std::string version;
std::string installed_version;
std::string latest_version;
+ std::string sort_version; // Version shown in the row (installed if installed, else latest); used by the Version sort.
std::string type_label;
std::string type_key;
std::string sharing_token;
@@ -319,6 +320,9 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
(descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version) :
std::string{};
item.latest_version = descriptor.latest_available_version();
+ // why: sort by the same version the row displays (GetDisplayVersion in index.js) - installed when
+ // installed, otherwise latest - so the Version sort matches what the user sees.
+ item.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
item.type_label = descriptor.type_label();
item.type_key = plugin_capability_type_to_string(descriptor.primary_capability_type());
// "types" is the display-only compatibility list. Cloud plugins show the raw labels the
diff --git a/tests/slic3rutils/test_plugin_sort.cpp b/tests/slic3rutils/test_plugin_sort.cpp
index 98ac71c656..2c1b3b7a2c 100644
--- a/tests/slic3rutils/test_plugin_sort.cpp
+++ b/tests/slic3rutils/test_plugin_sort.cpp
@@ -23,6 +23,7 @@ struct SortFixtureItem
PluginStatus status;
std::string type_key;
std::string display_name;
+ std::string sort_version;
};
std::vector keys(const std::vector& items)
@@ -90,6 +91,25 @@ TEST_CASE("plugin dialog source sort uses enum priority", "[plugin][sort]")
CHECK(keys(items) == desc_expected);
}
+TEST_CASE("plugin dialog version sort is semver-aware with base-order ties", "[plugin][sort]")
+{
+ std::vector items = {
+ {"v_1_2_0", PluginSource::Local, PluginStatus::Activated, "script", "B", "1.2.0"},
+ {"v_1_10_0", PluginSource::Local, PluginStatus::Activated, "script", "A", "1.10.0"},
+ {"v_0_9_3", PluginSource::Local, PluginStatus::Activated, "script", "C", "0.9.3"},
+ };
+
+ sort_plugin_items_for_dialog(items, PluginSortKey::Version, PluginSortOrder::Asc);
+ // why: semver numeric compare - 1.10.0 > 1.2.0 (not lexical "1.10" < "1.2"), so ascending is
+ // 0.9.3 < 1.2.0 < 1.10.0.
+ const std::vector asc_expected = {"v_0_9_3", "v_1_2_0", "v_1_10_0"};
+ CHECK(keys(items) == asc_expected);
+
+ sort_plugin_items_for_dialog(items, PluginSortKey::Version, PluginSortOrder::Desc);
+ const std::vector desc_expected = {"v_1_10_0", "v_1_2_0", "v_0_9_3"};
+ CHECK(keys(items) == desc_expected);
+}
+
TEST_CASE("plugin dialog name sort is case-insensitive and numeric-aware", "[plugin][sort]")
{
std::vector items = {