From 60b4a6185442dc1fa1a6120b0e4eba387aed21ca Mon Sep 17 00:00:00 2001 From: Valerii Bokhan <80919135+valerii-bokhan@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:56:17 +0200 Subject: [PATCH] Fix: Show indexed coFloatsOrPercents options in unsaved changes dialog (#15472) --- src/slic3r/GUI/ConfigValueFormatter.cpp | 11 +++++ src/slic3r/GUI/Search.cpp | 50 +++++++++++++++-------- src/slic3r/GUI/UnsavedChangesDialog.cpp | 54 +++++++++++++++++-------- tests/libslic3r/test_preset_diff.cpp | 17 ++++++++ 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/src/slic3r/GUI/ConfigValueFormatter.cpp b/src/slic3r/GUI/ConfigValueFormatter.cpp index 6f9128841b..17cf902217 100644 --- a/src/slic3r/GUI/ConfigValueFormatter.cpp +++ b/src/slic3r/GUI/ConfigValueFormatter.cpp @@ -188,6 +188,17 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& out = double_to_string(opt->value) + (opt->percent ? "%" : ""); return out; } + case coFloatsOrPercents: { + const auto* values = static_cast*>(option); + // Orca: Preset comparison may request the entire vector instead of an indexed entry. + if (orig_opt_idx < 0) + return from_u8(option->serialize()); + if (opt_idx < values->size()) { + const FloatOrPercent& value = values->get_at(opt_idx); + return double_to_string(value.value) + (value.percent ? "%" : ""); + } + return _L("Undefined"); + } case coEnum: { return get_string_from_enum(pure_key, config, pure_key == "top_surface_pattern" || diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index f8fc51ed02..eec03ebf62 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -62,7 +62,7 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt) } } -std::string Option::opt_key() const { return into_u8(key).substr(2); } +std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); } void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const { @@ -116,6 +116,7 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty case coFloats: change_opt_key(opt_key, config, cnt); break; case coStrings: change_opt_key(opt_key, config, cnt); break; case coPercents: change_opt_key(opt_key, config, cnt); break; + case coFloatsOrPercents: change_opt_key>(opt_key, config, cnt); break; case coPoints: change_opt_key(opt_key, config, cnt); break; // BBS case coEnums: change_opt_key(opt_key, config, cnt); break; @@ -334,29 +335,46 @@ const Option &OptionsSearcher::get_option(size_t pos_in_filter) const const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const { + auto not_found = [&variant_index]() -> const Option& { + static const Option empty_option; + variant_index = -2; + return empty_option; + }; + + variant_index = -1; std::string opt_key2 = opt_key; if (auto n = opt_key.find('#'); n != std::string::npos) { variant_index = std::atoi(opt_key.c_str() + n + 1); opt_key2 = opt_key.substr(0, n); } - auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))})); - // BBS: return the 0th option when not found in searcher caused by mode difference - // assert(it != options.end()); - if (it == options.end()) { variant_index = -2 ; return options[0]; } - if (it->opt_key() == opt_key2) { + const std::wstring key = boost::nowide::widen(get_key(opt_key2, type)); + auto it = std::lower_bound(options.begin(), options.end(), Option({key})); + if (it == options.end()) return not_found(); + if (it->key == key) { variant_index = -1; } else { - const std::string opt_key3 = opt_key2 + "#"; - it = std::lower_bound(it, options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))})); - if (it == options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) { - variant_index = -2; // Not found - return options[0]; + const std::wstring prefix = key + L"#"; + it = std::lower_bound(it, options.end(), Option({prefix})); + if (it == options.end() || it->key.compare(0, prefix.length(), prefix) != 0) + return not_found(); + // Orca: Copy-parameters dialogs request the base key, without a vector index. + if (variant_index < 0) return *it; + + const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0; + const bool has_variant = + (type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) || + (type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) || + (type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode; + if (!has_variant || has_mode) { + // Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1. + const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) : + boost::nowide::widen(get_key(opt_key, type)); + it = std::lower_bound(it, options.end(), Option({indexed_key})); + if (it == options.end() || it->key != indexed_key) + return not_found(); + if (!has_variant) + variant_index = -1; } - auto it2 = it; - ++it2; - if (it2 != options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0 - && printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end()) - variant_index = -2; } return options[it - options.begin()]; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 65b678953d..acf50e6641 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -1490,7 +1490,15 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config for (const std::string &opt_key : config->keys()) { int variant_index = -2; - const Search::Option &option = searcher.get_option(opt_key, type, variant_index); + Search::Option option = searcher.get_option(opt_key, type, variant_index); + if (variant_index == -2) { + // Orca: Every transferred setting must remain visible even when it is absent from the search index. + const ConfigOptionDef* def = print_config_def.get(opt_key); + const std::string label = def ? (def->full_label.empty() ? def->label : def->full_label) : std::string(); + option.label_local = (label.empty() ? from_u8(opt_key) : _L(label)).ToStdWstring(); + option.category_local = (def && !def->category.empty() ? + Tab::translate_category(from_u8(def->category), type) : _L("Other")).ToStdWstring(); + } auto category = option.category_local; auto opt = dynamic_cast(config->option(opt_key)); std::string value_from = opt->vserialize()[from]; @@ -1518,6 +1526,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres else presets_list.emplace_back(presets_); + const bool multiple_extruders = wxGetApp().preset_bundle->get_printer_extruder_count() > 1; + // Display a dialog showing the dirty options in a human readable form. for (PresetCollection* presets : presets_list) { @@ -1553,29 +1563,41 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres auto variant_key = Preset::get_iot_type_string(type) + "_extruder_variant"; auto id_key = Preset::get_iot_type_string(type) + "_extruder_id"; - auto extruder_variant = dynamic_cast(old_config.option(variant_key)); - auto extruder_id = dynamic_cast(old_config.option(id_key)); + // Orca: Dirty indices belong to the edited config, which may contain newly added variants. + auto extruder_variant = dynamic_cast(new_config.option(variant_key)); + auto extruder_id = dynamic_cast(new_config.option(id_key)); for (const std::string& opt_key : dirty_options) { int variant_index = -2; const Search::Option &option = searcher.get_option(opt_key, type, variant_index); - if (option.opt_key() != opt_key && variant_index < -1) { + if (variant_index == -2) { // When founded option isn't the correct one. // It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id", // because of they don't exist in searcher continue; } - auto category = option.category_local; - if (variant_index >= 0) { - if (printer_options_with_variant_2.count(opt_key.substr(0, opt_key.find_last_of('#'))) > 0) - variant_index /= 2; - if (boost::nowide::narrow(category).find("Extruder ") == 0) - category = category.substr(0, 8); - if (extruder_id) - category = category + (wxString(" {") + (extruder_id->values[variant_index] == 1 ? _L("Left: ") : _L("Right: ")) - + L(extruder_variant->values[variant_index]) + "}"); - else - category = category + (wxString(" {") + L(extruder_variant->values[variant_index]) + "}"); + wxString category = option.category_local; + wxString label = option.label_local; + if (type == Preset::TYPE_PRINTER && variant_index >= 0 && + printer_options_with_variant_2.count(get_pure_opt_key(opt_key)) > 0) { + // Orca: silent_mode is obsolete on import, but its option and two-column UI still exist. + // Keep mode labels for configs that explicitly enable it; omit them in the default single-mode UI. + if (new_config.opt_bool("silent_mode")) + label += " (" + (variant_index % 2 == 0 ? _L("Normal") : _L("Silent")) + ")"; + variant_index /= 2; + } + if (variant_index >= 0 && extruder_variant && variant_index < extruder_variant->size()) { + // Orca: Match the untranslated category and use the same extruder names as the printer tabs. + if (option.category.compare(0, 9, L"Extruder ") == 0) + category = _L("Extruder"); + wxString variant_label = L(extruder_variant->values[variant_index]); + // Orca: An extruder name only disambiguates variants on printers with multiple extruders. + if (multiple_extruders && extruder_id && variant_index < extruder_id->size() && extruder_id->values[variant_index] > 0) { + const wxString extruder_name = Tab::translate_category( + wxString::Format("Extruder %d", extruder_id->values[variant_index]), Preset::TYPE_PRINTER); + variant_label = extruder_name + " (" + variant_label + ")"; + } + category = variant_label + ": " + category; } /*m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local, @@ -1584,7 +1606,7 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres //PresetItem pi = {opt_key, type, 1983}; //m_presetitems.push_back() - PresetItem pi = {type, opt_key, category, option.group_local, option.label_local, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)}; + PresetItem pi = {type, opt_key, category, option.group_local, label, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)}; m_presetitems.push_back(pi); } diff --git a/tests/libslic3r/test_preset_diff.cpp b/tests/libslic3r/test_preset_diff.cpp index 399fdabae1..22255c049e 100644 --- a/tests/libslic3r/test_preset_diff.cpp +++ b/tests/libslic3r/test_preset_diff.cpp @@ -33,3 +33,20 @@ TEST_CASE("deep_diff flags new vector entries that duplicate values[0]", "[Prese // specific to new indices rather than flagging the whole vector. REQUIRE(std::find(diff.begin(), diff.end(), "nozzle_diameter#0") == diff.end()); } + +TEST_CASE("deep_diff distinguishes absolute and percentage speeds for each variant", "[PresetDiff][Config]") +{ + const size_t changed_index = GENERATE(size_t(0), size_t(1)); + Preset reference(Preset::TYPE_PRINT, "ref"); + reference.config.set_key_value("small_perimeter_speed", new ConfigOptionFloatsOrPercents{{50., false}, {50., false}}); + + Preset edited = reference; + edited.config.option("small_perimeter_speed")->values[changed_index].percent = true; + + const auto diff = PresetCollection::dirty_options(&edited, &reference, /*deep_compare=*/true); + REQUIRE(diff == std::vector{"small_perimeter_speed#" + std::to_string(changed_index)}); + + DynamicPrintConfig transferred = reference.config; + transferred.apply_only(edited.config, diff); + REQUIRE(*transferred.option("small_perimeter_speed") == *edited.config.option("small_perimeter_speed")); +}