#pragma once #include "PluginSource.hpp" #include "PluginStatus.hpp" #include "libslic3r/Semver.hpp" #include #include #include #include #include namespace Slic3r::GUI { enum class PluginSortKey { 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 }; enum class PluginSortOrder { Asc, Desc }; inline std::string to_string(PluginSortKey sort_key) { switch (sort_key) { 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"; } return "status"; } inline std::string to_string(PluginSortOrder sort_order) { return sort_order == PluginSortOrder::Desc ? "desc" : "asc"; } inline PluginSortKey plugin_sort_key_from_string(const std::string& sort_key, PluginSortKey fallback) { if (sort_key == "status") return PluginSortKey::Status; if (sort_key == "name") 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; } inline PluginSortOrder plugin_sort_order_from_string(const std::string& sort_order, PluginSortOrder fallback) { if (sort_order == "asc") return PluginSortOrder::Asc; if (sort_order == "desc") return PluginSortOrder::Desc; return fallback; } // Natural, case-insensitive ASCII compare returning -1 / 0 / +1. Digit runs compare by // numeric value; other chars compare lowercased; on a prefix tie the shorter string is less. // e.g. "item2" < "item10" (2 < 10, not '2' > '1') // "Camera" == "camera" (case ignored) // "app" < "apple" (prefix is shorter) // "1" < "01" (equal value, fewer leading zeros wins the tie) // note: ASCII only - no locale/Unicode; accented or non-Latin names fall back to byte order. inline int compare_ascii_case_insensitive_natural(const std::string& lhs, const std::string& rhs) { std::size_t li = 0; std::size_t ri = 0; while (li < lhs.size() && ri < rhs.size()) { const unsigned char lc = static_cast(lhs[li]); const unsigned char rc = static_cast(rhs[ri]); if (std::isdigit(lc) && std::isdigit(rc)) { const std::size_t lhs_digit_begin = li; const std::size_t rhs_digit_begin = ri; while (li < lhs.size() && std::isdigit(static_cast(lhs[li]))) ++li; while (ri < rhs.size() && std::isdigit(static_cast(rhs[ri]))) ++ri; const std::string_view lhs_run(lhs.data() + lhs_digit_begin, li - lhs_digit_begin); const std::string_view rhs_run(rhs.data() + rhs_digit_begin, ri - rhs_digit_begin); // why: digit runs compare numerically; leading zeros only break exact ties ("1" < "01"). const std::string_view lhs_num = lhs_run.substr(std::min(lhs_run.find_first_not_of('0'), lhs_run.size())); const std::string_view rhs_num = rhs_run.substr(std::min(rhs_run.find_first_not_of('0'), rhs_run.size())); if (lhs_num.size() != rhs_num.size()) return lhs_num.size() < rhs_num.size() ? -1 : 1; if (const int cmp = lhs_num.compare(rhs_num); cmp != 0) return cmp; // note: fewer-leading-zeros-first is our convention, not an industry standard (impls // diverge here); it only matters as a deterministic total order for unstable std::sort. if (lhs_run.size() != rhs_run.size()) return lhs_run.size() < rhs_run.size() ? -1 : 1; continue; } const int lower_lhs = std::tolower(lc); const int lower_rhs = std::tolower(rc); if (lower_lhs != lower_rhs) return lower_lhs < lower_rhs ? -1 : 1; ++li; ++ri; } if (li == lhs.size() && ri == rhs.size()) return 0; return li == lhs.size() ? -1 : 1; } // Neutral baseline order: the whole order when no column is sorted, and the tie-breaker under // every primary sort key. Name-first so the default view is intuitively alphabetical: // name, then source, then status, then type, with plugin_key as the final deterministic tie. // e.g. with no column sorted the list reads A..Z by name. template int compare_plugin_base_order(const PluginItem& lhs, const PluginItem& rhs) { if (const int cmp = compare_ascii_case_insensitive_natural(lhs.display_name, rhs.display_name); cmp != 0) return cmp; if (const int cmp = static_cast(lhs.source) - static_cast(rhs.source); cmp != 0) return cmp; if (const int cmp = static_cast(lhs.status) - static_cast(rhs.status); cmp != 0) return cmp; if (const int cmp = lhs.type_key.compare(rhs.type_key); cmp != 0) return cmp; 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. // Name: "Plugin 2" sorts before "Plugin 10". template int compare_plugin_sort_key(const PluginItem& lhs, const PluginItem& rhs, PluginSortKey sort_key) { switch (sort_key) { case PluginSortKey::Status: // why: PluginStatus/PluginSource declare the dialog sort priority as their ordinal order. return static_cast(lhs.status) - static_cast(rhs.status); case PluginSortKey::Name: 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). return 0; } return 0; } // Sorts the dialog list in place by primary key + direction. Ties always fall back to the // ascending base order, so the result is deterministic regardless of the primary direction. // e.g. sort_key=Name, order=Desc -> names Z..A, but equal names keep the stable base order. template void sort_plugin_items_for_dialog(std::vector& items, PluginSortKey sort_key, PluginSortOrder sort_order) { std::sort(items.begin(), items.end(), [sort_key, sort_order](const PluginItem& lhs, const PluginItem& rhs) { if (const int cmp = compare_plugin_sort_key(lhs, rhs, sort_key); cmp != 0) return sort_order == PluginSortOrder::Asc ? cmp < 0 : cmp > 0; // why: ties fall back to ascending base order regardless of the primary direction. return compare_plugin_base_order(lhs, rhs) < 0; }); } } // namespace Slic3r::GUI