diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 622b31a17..521a7c531 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -393,9 +393,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: // Read the active config bundle, parse the config version. PresetBundle bundle; bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); - for (const VendorProfile &vp : bundle.vendors) - if (vp.id == cfg.name) - cfg.version.config_version = vp.config_version; + for (const auto &vp : bundle.vendors) + if (vp.second.id == cfg.name) + cfg.version.config_version = vp.second.config_version; // Fill-in the min/max slic3r version from the config index, if possible. try { // Load the config index for the vendor. diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index d368d4fbd..8a850b7a0 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,7 @@ #include #include "libslic3r/Utils.hpp" -#include "PresetBundle.hpp" +// #include "PresetBundle.hpp" #include "GUI.hpp" #include "GUI_Utils.hpp" #include "slic3r/Config/Snapshot.hpp" @@ -40,6 +41,92 @@ using Config::Snapshot; using Config::SnapshotDB; +// Configuration data structures extensions needed for the wizard + +Bundle::Bundle(fs::path source_path, bool is_in_resources, bool is_prusa_bundle) + : source_path(std::move(source_path)) + , preset_bundle(new PresetBundle) + , vendor_profile(nullptr) + , is_in_resources(is_in_resources) + , is_prusa_bundle(is_prusa_bundle) +{ + // XXX: consider removing path <-> string juggling + preset_bundle->load_configbundle(this->source_path.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + auto first_vendor = preset_bundle->vendors.begin(); + wxCHECK_RET(first_vendor != preset_bundle->vendors.end(), "Failed to load preset bundle"); + vendor_profile = &first_vendor->second; +} + +Bundle::Bundle(Bundle &&other) + : source_path(std::move(source_path)) + , preset_bundle(std::move(other.preset_bundle)) + , vendor_profile(other.vendor_profile) + , is_in_resources(other.is_in_resources) + , is_prusa_bundle(other.is_prusa_bundle) +{ + other.vendor_profile = nullptr; +} + +BundleMap BundleMap::load() +{ + BundleMap res; + + // XXX: Keep Prusa bundle separate? (Probably no - keep same codepaths) + + const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); + const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + // XXX + // const auto prusa_bundle_vendor = (vendor_dir / PRUSA_BUNDLE).replace_extension(".ini"); + // const auto prusa_bundle = boost::filesystem::exists(prusa_bundle_vendor) ? prusa_bundle_vendor + // : (rsrc_vendor_dir / PRUSA_BUNDLE).replace_extension(".ini"); + auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + auto prusa_bundle_rsrc = false; + if (! boost::filesystem::exists(prusa_bundle_path)) { + prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + prusa_bundle_rsrc = true; + } + Bundle prusa_bundle(std::move(prusa_bundle_path), prusa_bundle_rsrc, true); + res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); + + // Load the other bundles in the datadir/vendor directory + // and then additionally from resources/profiles. + bool is_in_resources = false; + for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { + for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { + if (Slic3r::is_ini_file(dir_entry)) { + std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part + + // Don't load this bundle if we've already loaded it. + if (res.find(id) != res.end()) { continue; } + + Bundle bundle(dir_entry.path(), is_in_resources); + res.emplace(std::move(id), std::move(bundle)); + } + } + + is_in_resources = true; + } + + return res; +} + +Bundle& BundleMap::prusa_bundle() +{ + auto it = find(PresetBundle::PRUSA_BUNDLE); + if (it == end()) { + throw std::runtime_error("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); + } + + return it->second; +} + +const Bundle& BundleMap::prusa_bundle() const +{ + return const_cast(this)->prusa_bundle(); +} + + // Printer model picker GUI control struct PrinterPickerEvent : public wxEvent @@ -65,6 +152,8 @@ struct PrinterPickerEvent : public wxEvent wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); +const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; + PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) : wxPanel(parent) , vendor_id(vendor.id) @@ -96,6 +185,17 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt if (wxFileExists(bitmap_file)) { bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); bitmap_width = bitmap.GetWidth(); + } else { + BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") + % bitmap_file + % vendor.id + % model.id; + + const wxString placeholder_file = GUI::from_u8(Slic3r::var(PRINTER_PLACEHOLDER)); + if (wxFileExists(placeholder_file)) { + bitmap.LoadFile(placeholder_file, wxBITMAP_TYPE_PNG); + bitmap_width = bitmap.GetWidth(); + } } auto *title = new wxStaticText(this, wxID_ANY, model.name, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); @@ -135,7 +235,7 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); - bool enabled = appconfig.get_variant("PrusaResearch", model_id, variant.name); + bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); cbox->SetValue(enabled); variants_sizer->Add(cbox, 0, wxBOTTOM, 3); @@ -295,12 +395,13 @@ ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxStrin ConfigWizardPage::~ConfigWizardPage() {} -void ConfigWizardPage::append_text(wxString text) +wxStaticText* ConfigWizardPage::append_text(wxString text) { auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); widget->Wrap(WRAP_WIDTH); widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); append(widget); + return widget; } void ConfigWizardPage::append_spacer(int space) @@ -320,34 +421,42 @@ PageWelcome::PageWelcome(ConfigWizard *parent) _(L("Welcome to the %s Configuration Wizard")) #endif , SLIC3R_APP_NAME), _(L("Welcome"))) - , cbox_reset(nullptr) + , welcome_text(append_text(wxString::Format( + _(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print.")), + SLIC3R_APP_NAME, + ConfigWizard::name()) + )) + , cbox_reset(append( + new wxCheckBox(this, wxID_ANY, _(L("Remove user profiles - install from scratch (a snapshot will be taken beforehand)"))) + )) { - if (wizard_p()->run_reason == ConfigWizard::RR_DATA_EMPTY) { - wxString::Format(_(L("Run %s")), ConfigWizard::name()); - append_text(wxString::Format( - _(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print.")), - SLIC3R_APP_NAME, - ConfigWizard::name()) - ); - } else { - cbox_reset = new wxCheckBox(this, wxID_ANY, _(L("Remove user profiles - install from scratch (a snapshot will be taken beforehand)"))); - append(cbox_reset); - } + welcome_text->Hide(); + cbox_reset->Hide(); +} - Show(); +void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) +{ + const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; + welcome_text->Show(data_empty); + cbox_reset->Show(!data_empty); } -PagePrinters::PagePrinters(ConfigWizard *parent, wxString title, wxString shortname, const VendorProfile &vendor, unsigned indent, Technology technology) +PagePrinters::PagePrinters(ConfigWizard *parent, + wxString title, + wxString shortname, + const VendorProfile &vendor, + unsigned indent, + Technology technology) : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) + , technology(technology) + , install(false) // only used for 3rd party vendors { enum { COL_SIZE = 200, }; - bool check_first_variant = technology == T_FFF && wizard_p()->check_first_variant(); - - AppConfig &appconfig = this->wizard_p()->appconfig_new; + AppConfig *appconfig = &this->wizard_p()->appconfig_new; const auto families = vendor.families(); for (const auto &family : families) { @@ -362,16 +471,10 @@ PagePrinters::PagePrinters(ConfigWizard *parent, wxString title, wxString shortn } const auto picker_title = family.empty() ? wxString() : wxString::Format(_(L("%s Family")), family); - auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, appconfig, filter); + auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); - if (check_first_variant) { - // Select the default (first) model/variant on the Prusa vendor - picker->select_one(0, true); - check_first_variant = false; - } - - picker->Bind(EVT_PRINTER_PICK, [this, &appconfig](const PrinterPickerEvent &evt) { - appconfig.set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { + appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); wizard_p()->on_printer_pick(this); }); @@ -404,6 +507,15 @@ bool PagePrinters::any_selected() const return false; } +void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) +{ + if (technology == T_FFF + && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) + && printer_pickers.size() > 0) { + printer_pickers[0]->select_one(0, true); + } +} + const std::string PageMaterials::EMPTY; @@ -619,48 +731,26 @@ PageUpdate::PageUpdate(ConfigWizard *parent) PageVendors::PageVendors(ConfigWizard *parent) : ConfigWizardPage(parent, _(L("Other Vendors")), _(L("Other Vendors"))) { - append_text(wxString::Format(_(L("Pick another vendor supported by %s:")), SLIC3R_APP_NAME)); + + + // FIXME: persistence: this doesn't reload choices + + + append_text(wxString::Format(_(L("Pick another vendor supported by %s: (FIXME: this text)")), SLIC3R_APP_NAME)); auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); boldfont.SetWeight(wxFONTWEIGHT_BOLD); - AppConfig &appconfig = this->wizard_p()->appconfig_new; - wxArrayString choices_vendors; + for (const auto &pair : wizard_p()->bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - for (const auto vendor_pair : wizard_p()->vendors) { - const auto &vendor = vendor_pair.second; - if (vendor.id == "PrusaResearch") { continue; } - - auto *picker = new PrinterPicker(this, vendor, "", MAX_COLS, appconfig); - picker->Hide(); - pickers.push_back(picker); - choices_vendors.Add(vendor.name); - - picker->Bind(EVT_PRINTER_PICK, [this, &appconfig](const PrinterPickerEvent &evt) { - appconfig.set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); }); - } - auto *vendor_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices_vendors); - if (choices_vendors.GetCount() > 0) { - vendor_picker->SetSelection(0); - on_vendor_pick(0); - } - - vendor_picker->Bind(wxEVT_CHOICE, [this](wxCommandEvent &evt) { - this->on_vendor_pick(evt.GetInt()); - }); - - append(vendor_picker); - for (PrinterPicker *picker : pickers) { this->append(picker); } -} - -void PageVendors::on_vendor_pick(size_t i) -{ - for (PrinterPicker *picker : pickers) { picker->Hide(); } - if (i < pickers.size()) { - pickers[i]->Show(); - parent->Layout(); + append(cbox); } } @@ -954,12 +1044,15 @@ void ConfigWizardIndex::go_to(size_t i) } } -void ConfigWizardIndex::go_to(ConfigWizardPage *page) +void ConfigWizardIndex::go_to(const ConfigWizardPage *page) { if (page == nullptr) { return; } for (size_t i = 0; i < items.size(); i++) { - if (items[i].page == page) { go_to(i); } + if (items[i].page == page) { + go_to(i); + return; + } } } @@ -1121,21 +1214,25 @@ static const std::unordered_map void ConfigWizard::priv::load_pages() { - const auto former_active = index->active_item(); + wxWindowUpdateLocker freeze_guard(q); + (void)freeze_guard; + + const ConfigWizardPage *former_active = index->active_page(); index->clear(); index->add_page(page_welcome); + + // Printers index->add_page(page_fff); index->add_page(page_msla); - - index->add_page(page_filaments); - if (any_sla_selected) { - index->add_page(page_sla_materials); + index->add_page(page_vendors); + for (const auto &pair : pages_3rdparty) { + PagePrinters *page = pair.second; + if (page->install) { index->add_page(page); } } index->add_page(page_custom); - if (page_custom->custom_wanted()) { index->add_page(page_firmware); index->add_page(page_bed); @@ -1143,6 +1240,10 @@ void ConfigWizard::priv::load_pages() index->add_page(page_temps); } + // Filaments & Materials + if (any_fff_selected) { index->add_page(page_filaments); } + if (any_sla_selected) { index->add_page(page_sla_materials); } + index->add_page(page_update); index->go_to(former_active); // Will restore the active item/page if possible @@ -1173,66 +1274,75 @@ void ConfigWizard::priv::init_dialog_size() q->SetSize(window_rect); } -bool ConfigWizard::priv::check_first_variant() const -{ - return run_reason == RR_DATA_EMPTY || run_reason == RR_DATA_LEGACY; -} - void ConfigWizard::priv::load_vendors() { - const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; - const auto rsrc_vendor_dir = fs::path(resources_dir()) / "profiles"; + // const auto vendor_dir = fs::path(Slic3r::data_dir()) / "vendor"; + // const auto rsrc_vendor_dir = fs::path(resources_dir()) / "profiles"; - PresetBundle bundle; + // PresetBundle bundle; + // bundle.load_available_system_presets(); + bundles = BundleMap::load(); - // Load vendors from the "vendors" directory in datadir - // XXX: The VendorProfile is loaded twice here, ditto below - for (auto &dir_entry : boost::filesystem::directory_iterator(vendor_dir)) { - if (Slic3r::is_ini_file(dir_entry)) { - try { - bundle.load_configbundle(dir_entry.path().string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + // // Load vendors from the "vendors" directory in datadir + // // XXX: The VendorProfile is loaded twice here, ditto below + // for (auto &dir_entry : boost::filesystem::directory_iterator(vendor_dir)) { + // if (Slic3r::is_ini_file(dir_entry)) { + // try { + // bundle.load_configbundle(dir_entry.path().string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); - auto vp = VendorProfile::from_ini(dir_entry.path()); - vendors[vp.id] = std::move(vp); - } - catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % dir_entry.path() % e.what(); - } + // auto vp = VendorProfile::from_ini(dir_entry.path()); + // vendors[vp.id] = std::move(vp); + // } + // catch (const std::exception& e) { + // BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % dir_entry.path() % e.what(); + // } + // } + // } + + // // Additionally load up vendors from the application resources directory, but only those not seen in the datadir + // for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { + // if (Slic3r::is_ini_file(dir_entry)) { + // const auto id = dir_entry.path().stem().string(); + + // if (vendors.find(id) == vendors.end()) { + // try { + // bundle.load_configbundle(dir_entry.path().string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + + // auto vp = VendorProfile::from_ini(dir_entry.path()); + // vendors_rsrc[vp.id] = dir_entry.path().filename().string(); + // vendors[vp.id] = std::move(vp); + // } + // catch (const std::exception& e) { + // BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % dir_entry.path() % e.what(); + // } + // } + // } + // } + + // // Move materials to our Materials container: + // for (auto &&f : bundle.filaments) { + // f.vendor = nullptr; + // filaments.presets.push_back(std::move(f)); + // filaments.types.insert(Materials::get_filament_type(f)); + // } + // for (auto &&m : bundle.sla_materials) { + // m.vendor = nullptr; + // sla_materials.presets.push_back(std::move(m)); + // sla_materials.types.insert(Materials::get_material_type(m)); + // } + for (auto &pair : bundles) { + for (auto &&f : pair.second.preset_bundle->filaments) { + f.vendor = nullptr; + filaments.presets.push_back(std::move(f)); + filaments.types.insert(Materials::get_filament_type(f)); } - } - - // Additionally load up vendors from the application resources directory, but only those not seen in the datadir - for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { - if (Slic3r::is_ini_file(dir_entry)) { - const auto id = dir_entry.path().stem().string(); - - if (vendors.find(id) == vendors.end()) { - try { - bundle.load_configbundle(dir_entry.path().string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); - - auto vp = VendorProfile::from_ini(dir_entry.path()); - vendors_rsrc[vp.id] = dir_entry.path().filename().string(); - vendors[vp.id] = std::move(vp); - } - catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % dir_entry.path() % e.what(); - } - } + for (auto &&m : pair.second.preset_bundle->sla_materials) { + m.vendor = nullptr; + sla_materials.presets.push_back(std::move(m)); + sla_materials.types.insert(Materials::get_material_type(m)); } } - // Move materials to our Materials container: - for (auto &&f : bundle.filaments) { - f.vendor = nullptr; - filaments.presets.push_back(std::move(f)); - filaments.types.insert(Materials::get_filament_type(f)); - } - for (auto &&m : bundle.sla_materials) { - m.vendor = nullptr; - sla_materials.presets.push_back(std::move(m)); - sla_materials.types.insert(Materials::get_material_type(m)); - } - // Load up the set of vendors / models / variants the user has had enabled up till now AppConfig *app_config = wxGetApp().app_config; if (! app_config->legacy_datadir()) { @@ -1251,11 +1361,14 @@ void ConfigWizard::priv::load_vendors() } } +// TODO: This'll be done differently, cf. the design document // Load up the materials enabled till now, // apply defaults from vendor profiles if there are no selections yet. - bundle.init_materials_selection(*app_config); - wxCHECK_RET(app_config->has_section(AppConfig::SECTION_FILAMENTS) && app_config->has_section(AppConfig::SECTION_MATERIALS), - "Failed to initialize default material selections"); + // bundle.init_materials_selection(*app_config); + + // XXX: ? + // wxCHECK_RET(app_config->has_section(AppConfig::SECTION_FILAMENTS) && app_config->has_section(AppConfig::SECTION_MATERIALS), + // "Failed to initialize default material selections"); appconfig_new.set_section(AppConfig::SECTION_FILAMENTS, app_config->get_section(AppConfig::SECTION_FILAMENTS)); appconfig_new.set_section(AppConfig::SECTION_MATERIALS, app_config->get_section(AppConfig::SECTION_MATERIALS)); } @@ -1263,6 +1376,7 @@ void ConfigWizard::priv::load_vendors() void ConfigWizard::priv::add_page(ConfigWizardPage *page) { hscroll_sizer->Add(page, 0, wxEXPAND); + all_pages.push_back(page); } void ConfigWizard::priv::enable_next(bool enable) @@ -1281,6 +1395,27 @@ void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) } } +void ConfigWizard::priv::create_3rdparty_pages() +{ + for (const auto &pair : bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + auto *page = new PagePrinters(q, vendor->name, vendor->name, *vendor, 1, T_ANY); + add_page(page); + + pages_3rdparty.insert({vendor->id, page}); + } +} + +void ConfigWizard::priv::set_run_reason(RunReason run_reason) +{ + this->run_reason = run_reason; + for (auto &page : all_pages) { + page->set_run_reason(run_reason); + } +} + void ConfigWizard::priv::on_custom_setup() { load_pages(); @@ -1288,23 +1423,43 @@ void ConfigWizard::priv::on_custom_setup() void ConfigWizard::priv::on_printer_pick(PagePrinters *page) { - if (page == page_msla) { - const bool any_sla_selected_new = page->any_selected(); - if (any_sla_selected != any_sla_selected_new) { - any_sla_selected = any_sla_selected_new; - load_pages(); - } + if (page_msla->any_selected() != any_sla_selected || + page_fff->any_selected() != any_fff_selected) { + any_fff_selected = page_fff->any_selected(); + any_sla_selected = page_msla->any_selected(); + + load_pages(); } } +void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) +{ + auto it = pages_3rdparty.find(vendor->id); + wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); + PagePrinters *page = it->second; + page->install = install; + page->Layout(); + + load_pages(); +} + void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater) { const auto enabled_vendors = appconfig_new.vendors(); // Install bundles from resources if needed: std::vector install_bundles; - for (const auto &vendor_rsrc : vendors_rsrc) { - const auto vendor = enabled_vendors.find(vendor_rsrc.first); + for (const auto &pair : bundles) { + if (! pair.second.is_in_resources) { continue; } + + if (pair.second.is_prusa_bundle) { + // Always install Prusa bundle, because it has a lot of filaments/materials + // likely to be referenced by other profiles. + install_bundles.emplace_back(pair.first); + continue; + } + + const auto vendor = enabled_vendors.find(pair.first); if (vendor == enabled_vendors.end()) { continue; } size_t size_sum = 0; @@ -1312,7 +1467,7 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese if (size_sum > 0) { // This vendor needs to be installed - install_bundles.emplace_back(vendor_rsrc.second); + install_bundles.emplace_back(pair.first); } } @@ -1370,17 +1525,18 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese // The default is the first selected printer model (one with at least 1 variant selected). // The default is only applied by load_presets() if the user doesn't have a (visible) printer // selected already. - const auto vendor_prusa = vendors.find("PrusaResearch"); - const auto config_prusa = enabled_vendors.find("PrusaResearch"); - if (vendor_prusa != vendors.end() && config_prusa != enabled_vendors.end()) { - for (const auto &model : vendor_prusa->second.models) { - const auto model_it = config_prusa->second.find(model.id); - if (model_it != config_prusa->second.end() && model_it->second.size() > 0) { - preferred_model = model.id; - break; - } - } - } +// TODO + // const auto vendor_prusa = bundle.vendors.find("PrusaResearch"); + // const auto config_prusa = enabled_vendors.find("PrusaResearch"); + // if (vendor_prusa != bundle.vendors.end() && config_prusa != enabled_vendors.end()) { + // for (const auto &model : vendor_prusa->second.models) { + // const auto model_it = config_prusa->second.find(model.id); + // if (model_it != config_prusa->second.end() && model_it->second.size() > 0) { + // preferred_model = model.id; + // break; + // } + // } + // } preset_bundle->load_presets(*app_config, preferred_model); @@ -1398,10 +1554,11 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese preset_bundle->export_selections(*app_config); } + // Public -ConfigWizard::ConfigWizard() - : DPIDialog(nullptr, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + name(), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +ConfigWizard::ConfigWizard(wxWindow *parent) + : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + name(), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , p(new priv(this)) { this->SetFont(wxGetApp().normal_font()); @@ -1441,17 +1598,16 @@ ConfigWizard::ConfigWizard() p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); - const auto &vendors = p->vendors; - const auto vendor_prusa_it = vendors.find("PrusaResearch"); - wxCHECK_RET(vendor_prusa_it != vendors.cend(), "Vendor PrusaResearch not found"); - const VendorProfile &vendor_prusa = vendor_prusa_it->second; + const auto prusa_it = p->bundles.find("PrusaResearch"); + wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); + const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; p->add_page(p->page_welcome = new PageWelcome(this)); - p->page_fff = new PagePrinters(this, _(L("Prusa FFF Technology Printers")), "Prusa FFF", vendor_prusa, 0, T_FFF); + p->page_fff = new PagePrinters(this, _(L("Prusa FFF Technology Printers")), "Prusa FFF", *vendor_prusa, 0, T_FFF); p->add_page(p->page_fff); - p->page_msla = new PagePrinters(this, _(L("Prusa MSLA Technology Printers")), "Prusa MSLA", vendor_prusa, 0, T_SLA); + p->page_msla = new PagePrinters(this, _(L("Prusa MSLA Technology Printers")), "Prusa MSLA", *vendor_prusa, 0, T_SLA); p->add_page(p->page_msla); p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, @@ -1467,7 +1623,10 @@ ConfigWizard::ConfigWizard() p->add_page(p->page_diams = new PageDiameters(this)); p->add_page(p->page_temps = new PageTemperatures(this)); + p->create_3rdparty_pages(); + p->any_sla_selected = p->page_msla->any_selected(); + p->any_fff_selected = p->page_fff->any_selected(); p->load_pages(); vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); @@ -1514,7 +1673,7 @@ bool ConfigWizard::run(RunReason reason, StartPage start_page) GUI_App &app = wxGetApp(); - p->run_reason = reason; + p->set_run_reason(reason); p->set_start_page(start_page); if (ShowModal() == wxID_OK) { diff --git a/src/slic3r/GUI/ConfigWizard.hpp b/src/slic3r/GUI/ConfigWizard.hpp index 3c85b7343..942f4b4ce 100644 --- a/src/slic3r/GUI/ConfigWizard.hpp +++ b/src/slic3r/GUI/ConfigWizard.hpp @@ -34,7 +34,7 @@ public: SP_MATERIALS, }; - ConfigWizard(); + ConfigWizard(wxWindow *parent); ConfigWizard(ConfigWizard &&) = delete; ConfigWizard(const ConfigWizard &) = delete; ConfigWizard &operator=(ConfigWizard &&) = delete; diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 82708cc59..e6f389785 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -21,7 +21,8 @@ #include "libslic3r/PrintConfig.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "AppConfig.hpp" -#include "Preset.hpp" +// #include "Preset.hpp" +#include "PresetBundle.hpp" #include "BedShapeDialog.hpp" namespace fs = boost::filesystem; @@ -43,13 +44,70 @@ enum { ROW_SPACING = 75, }; + + +// Configuration data structures extensions needed for the wizard + enum Technology { // Bitflag equivalent of PrinterTechnology T_FFF = 0x1, T_SLA = 0x2, - T_Any = ~0, + T_ANY = ~0, }; +struct Materials +{ + Technology technology; + std::vector presets; + std::set types; + + Materials(Technology technology) : technology(technology) {} + + const std::string& appconfig_section() const; + const std::string& get_type(Preset &preset) const; + const std::string& get_vendor(Preset &preset) const; + + template void filter_presets(const std::string &type, const std::string &vendor, F cb) { + for (Preset &preset : presets) { + if ((type.empty() || get_type(preset) == type) && (vendor.empty() || get_vendor(preset) == vendor)) { + cb(preset); + } + } + } + + static const std::string UNKNOWN; + static const std::string& get_filament_type(const Preset &preset); + static const std::string& get_filament_vendor(const Preset &preset); + static const std::string& get_material_type(Preset &preset); + static const std::string& get_material_vendor(const Preset &preset); +}; + +struct Bundle +{ + fs::path source_path; // XXX: not needed? + std::unique_ptr preset_bundle; + VendorProfile *vendor_profile; + const bool is_in_resources; + const bool is_prusa_bundle; + + Bundle(fs::path source_path, bool is_in_resources, bool is_prusa_bundle = false); + Bundle(Bundle &&other); + + const std::string& vendor_id() const { return vendor_profile->id; } +}; + +struct BundleMap: std::unordered_map +{ + static BundleMap load(); + + Bundle& prusa_bundle(); + const Bundle& prusa_bundle() const; +}; + + + +// GUI elements + typedef std::function ModelFilter; struct PrinterPicker: wxPanel @@ -79,6 +137,8 @@ struct PrinterPicker: wxPanel int get_width() const { return width; } const std::vector& get_button_indexes() { return m_button_indexes; } + + static const std::string PRINTER_PLACEHOLDER; private: int width; std::vector m_button_indexes; @@ -97,65 +157,50 @@ struct ConfigWizardPage: wxPanel virtual ~ConfigWizardPage(); template - void append(T *thing, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10) + T* append(T *thing, int proportion = 0, int flag = wxEXPAND|wxTOP|wxBOTTOM, int border = 10) { content->Add(thing, proportion, flag, border); + return thing; } - void append_text(wxString text); + wxStaticText* append_text(wxString text); void append_spacer(int space); ConfigWizard::priv *wizard_p() const { return parent->p.get(); } virtual void apply_custom_config(DynamicPrintConfig &config) {} + virtual void set_run_reason(ConfigWizard::RunReason run_reason) {} }; struct PageWelcome: ConfigWizardPage { + wxStaticText *welcome_text; wxCheckBox *cbox_reset; PageWelcome(ConfigWizard *parent); bool reset_user_profile() const { return cbox_reset != nullptr ? cbox_reset->GetValue() : false; } + + virtual void set_run_reason(ConfigWizard::RunReason run_reason) override; }; struct PagePrinters: ConfigWizardPage { std::vector printer_pickers; + Technology technology; + bool install; - PagePrinters(ConfigWizard *parent, wxString title, wxString shortname, const VendorProfile &vendor, unsigned indent, Technology technology); + PagePrinters(ConfigWizard *parent, + wxString title, + wxString shortname, + const VendorProfile &vendor, + unsigned indent, Technology technology); void select_all(bool select, bool alternates = false); int get_width() const; bool any_selected() const; -}; - -struct Materials -{ - Technology technology; - std::vector presets; - std::set types; - - Materials(Technology technology) : technology(technology) {} - - const std::string& appconfig_section() const; - const std::string& get_type(Preset &preset) const; - const std::string& get_vendor(Preset &preset) const; - - template void filter_presets(const std::string &type, const std::string &vendor, F cb) { - for (Preset &preset : presets) { - if ((type.empty() || get_type(preset) == type) && (vendor.empty() || get_vendor(preset) == vendor)) { - cb(preset); - } - } - } - - static const std::string UNKNOWN; - static const std::string& get_filament_type(const Preset &preset); - static const std::string& get_filament_vendor(const Preset &preset); - static const std::string& get_material_type(Preset &preset); - static const std::string& get_material_vendor(const Preset &preset); + virtual void set_run_reason(ConfigWizard::RunReason run_reason) override; }; // Here we extend wxListBox and wxCheckListBox @@ -232,11 +277,11 @@ struct PageUpdate: ConfigWizardPage struct PageVendors: ConfigWizardPage { - std::vector pickers; + // std::vector pickers; PageVendors(ConfigWizard *parent); - void on_vendor_pick(size_t i); + // void on_vendor_pick(size_t i); }; struct PageFirmware: ConfigWizardPage @@ -290,7 +335,7 @@ public: void go_prev(); void go_next(); void go_to(size_t i); - void go_to(ConfigWizardPage *page); + void go_to(const ConfigWizardPage *page); void clear(); void msw_rescale(); @@ -328,16 +373,24 @@ private: wxDEFINE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); + +// ConfigWizard private data + struct ConfigWizard::priv { ConfigWizard *q; ConfigWizard::RunReason run_reason = RR_USER; AppConfig appconfig_new; // Backing for vendor/model/variant and material selections in the GUI - std::unordered_map vendors; + // std::unordered_map vendors; + // PresetBundle bundle; // XXX: comment + BundleMap bundles; // XXX: comment Materials filaments; // Holds available filament presets and their types & vendors Materials sla_materials; // Ditto for SLA materials - std::unordered_map vendors_rsrc; // List of bundles to install from resources + // std::set install_3rdparty; + // XXX: rm: (?) + // std::unordered_map vendors_rsrc; // List of bundles to install from resources std::unique_ptr custom_config; // Backing for custom printer definition + bool any_fff_selected; // Used to decide whether to display Filaments page bool any_sla_selected; // Used to decide whether to display SLA Materials page wxScrolledWindow *hscroll = nullptr; @@ -359,6 +412,7 @@ struct ConfigWizard::priv PageCustom *page_custom = nullptr; PageUpdate *page_update = nullptr; PageVendors *page_vendors = nullptr; // XXX: ? + std::map pages_3rdparty; // Custom setup pages PageFirmware *page_firmware = nullptr; @@ -366,6 +420,9 @@ struct ConfigWizard::priv PageDiameters *page_diams = nullptr; PageTemperatures *page_temps = nullptr; + // Pointers to all pages (regardless or whether currently part of the ConfigWizardIndex) + std::vector all_pages; + priv(ConfigWizard *q) : q(q) , filaments(T_FFF) @@ -376,14 +433,16 @@ struct ConfigWizard::priv void load_pages(); void init_dialog_size(); - bool check_first_variant() const; void load_vendors(); void add_page(ConfigWizardPage *page); void enable_next(bool enable); void set_start_page(ConfigWizard::StartPage start_page); + void create_3rdparty_pages(); + void set_run_reason(RunReason run_reason); void on_custom_setup(); void on_printer_pick(PagePrinters *page); + void on_3rdparty_install(const VendorProfile *vendor, bool install); // XXX: ? void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 028886915..b5d9c3d61 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1059,8 +1059,10 @@ void GUI_App::open_web_page_localized(const std::string &http_address) bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) { + wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + if (! m_wizard) { - m_wizard.reset(new ConfigWizard()); + m_wizard = new ConfigWizard(mainframe); } const bool res = m_wizard->run(reason, start_page); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8c5f0c30c..c5ddc0152 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -98,7 +98,7 @@ class GUI_App : public wxApp std::unique_ptr m_imgui; std::unique_ptr m_printhost_job_queue; - std::unique_ptr m_wizard; + ConfigWizard* m_wizard; // Managed by wxWindow tree public: bool OnInit() override; diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp index bda096d66..be53267f9 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/slic3r/GUI/Preset.cpp @@ -1318,7 +1318,7 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name) } // Merge one vendor's presets with the other vendor's presets, report duplicates. -std::vector PresetCollection::merge_presets(PresetCollection &&other, const std::set &new_vendors) +std::vector PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors) { std::vector duplicates; for (Preset &preset : other.m_presets) { @@ -1329,9 +1329,9 @@ std::vector PresetCollection::merge_presets(PresetCollection &&othe if (it == m_presets.end() || it->name != preset.name) { if (preset.vendor != nullptr) { // Re-assign a pointer to the vendor structure in the new PresetBundle. - auto it = new_vendors.find(*preset.vendor); + auto it = new_vendors.find(preset.vendor->id); assert(it != new_vendors.end()); - preset.vendor = &(*it); + preset.vendor = &it->second; } this->m_presets.emplace(it, std::move(preset)); } else diff --git a/src/slic3r/GUI/Preset.hpp b/src/slic3r/GUI/Preset.hpp index dfb72d495..8be1388f0 100644 --- a/src/slic3r/GUI/Preset.hpp +++ b/src/slic3r/GUI/Preset.hpp @@ -2,6 +2,8 @@ #define slic3r_Preset_hpp_ #include +#include +#include #include #include @@ -89,6 +91,12 @@ public: bool operator==(const VendorProfile &rhs) const { return this->id == rhs.id; } }; +// Note: it is imporant that map is used here rather than unordered_map, +// because we need iterators to not be invalidated, +// because Preset and the ConfigWizard hold pointers to VendorProfiles. +// XXX: maybe set is enough (cf. changes in Wizard) +typedef std::map VendorMap; + class Preset { public: @@ -435,7 +443,7 @@ protected: bool select_preset_by_name_strict(const std::string &name); // Merge one vendor's presets with the other vendor's presets, report duplicates. - std::vector merge_presets(PresetCollection &&other, const std::set &new_vendors); + std::vector merge_presets(PresetCollection &&other, const VendorMap &new_vendors); private: PresetCollection(); diff --git a/src/slic3r/GUI/PresetBundle.cpp b/src/slic3r/GUI/PresetBundle.cpp index 5f9111111..579c74dca 100644 --- a/src/slic3r/GUI/PresetBundle.cpp +++ b/src/slic3r/GUI/PresetBundle.cpp @@ -41,6 +41,8 @@ static std::vector s_project_options { "wiping_volumes_matrix" }; +const char *PresetBundle::PRUSA_BUNDLE = "PrusaResearch"; + PresetBundle::PresetBundle() : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())), filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast(FullPrintConfig::defaults())), @@ -244,6 +246,48 @@ void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_ this->load_selections(config, preferred_model_id); } +// FIXME: Comment +// XXX: rm +void PresetBundle::load_available_system_presets() +{ + const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); + const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + const auto prusa_bundle_vendor = (vendor_dir / PRUSA_BUNDLE).replace_extension(".ini"); + const auto prusa_bundle = boost::filesystem::exists(prusa_bundle_vendor) ? prusa_bundle_vendor + : (rsrc_vendor_dir / PRUSA_BUNDLE).replace_extension(".ini"); + + // Reset this PresetBundle and load the Prusa bundle first. + this->load_configbundle(prusa_bundle.string(), LOAD_CFGBNDLE_SYSTEM); + + // Load the other bundles in the datadir/vendor directory + // and then additionally from resources/profiles. + for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { + for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { + if (Slic3r::is_ini_file(dir_entry)) { + std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part + + // Don't load this bundle if we've already loaded it. + // Note that this takes care of not loading the PRUSA_BUNDLE which was loaded upfront + // as well as bundles with the same name (id) in rsrc_vendor_dir as in vendor_dir. + if (vendors.find(id) != vendors.end()) { continue; } + + PresetBundle other; + other.load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + + std::vector duplicates = this->merge_presets(std::move(other)); + if (! duplicates.empty()) { + std::string msg = "Vendor configuration file " + id + " contains the following presets with names used by other vendors: "; + for (size_t i = 0; i < duplicates.size(); ++ i) { + if (i > 0) { msg += ", "; } + msg += duplicates[i]; + } + } + } + } + } +} + // Load system presets into this PresetBundle. // For each vendor, there will be a single PresetBundle loaded. std::string PresetBundle::load_system_presets() @@ -414,14 +458,14 @@ void PresetBundle::export_selections(AppConfig &config) void PresetBundle::init_materials_selection(AppConfig &config) const { if (! config.has_section(AppConfig::SECTION_FILAMENTS)) { for (const auto &vendor : this->vendors) { - for (const auto &profile : vendor.default_filaments) { + for (const auto &profile : vendor.second.default_filaments) { config.set(AppConfig::SECTION_FILAMENTS, profile, "1"); } } } if (! config.has_section(AppConfig::SECTION_MATERIALS)) { for (const auto &vendor : this->vendors) { - for (const auto &profile : vendor.default_sla_materials) { + for (const auto &profile : vendor.second.default_sla_materials) { config.set(AppConfig::SECTION_MATERIALS, profile, "1"); } } @@ -1061,9 +1105,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla auto vp = VendorProfile::from_ini(tree, path); if (vp.num_variants() == 0) return 0; - vendor_profile = &(*this->vendors.insert(vp).first); + vendor_profile = &this->vendors.insert({vp.id, vp}).first->second; } - + if (flags & LOAD_CFGBUNDLE_VENDOR_ONLY) { return 0; } diff --git a/src/slic3r/GUI/PresetBundle.hpp b/src/slic3r/GUI/PresetBundle.hpp index 847b37018..d7ec5e3f0 100644 --- a/src/slic3r/GUI/PresetBundle.hpp +++ b/src/slic3r/GUI/PresetBundle.hpp @@ -4,7 +4,9 @@ #include "AppConfig.hpp" #include "Preset.hpp" +#include #include +#include #include class wxWindow; @@ -56,7 +58,8 @@ public: // There will be an entry for each system profile loaded, // and the system profiles will point to the VendorProfile instances owned by PresetBundle::vendors. - std::set vendors; + // std::set vendors; + VendorMap vendors; struct ObsoletePresets { std::vector prints; @@ -135,6 +138,9 @@ public: void load_default_preset_bitmaps(wxWindow *window); + void load_available_system_presets(); // XXX: name XXX: retval (VendorMap stored internally) + + static const char *PRUSA_BUNDLE; private: std::string load_system_presets(); // Merge one vendor's presets with the other vendor's presets, report duplicates. diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index cafdbb14e..3cebf2f89 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -153,7 +153,7 @@ struct PresetUpdater::priv bool get_file(const std::string &url, const fs::path &target_path) const; void prune_tmps() const; void sync_version() const; - void sync_config(const std::set vendors); + void sync_config(const VendorMap vendors); void check_install_indices() const; Updates get_config_updates() const; @@ -266,7 +266,7 @@ void PresetUpdater::priv::sync_version() const // Download vendor indices. Also download new bundles if an index indicates there's a new one available. // Both are saved in cache. -void PresetUpdater::priv::sync_config(const std::set vendors) +void PresetUpdater::priv::sync_config(const VendorMap vendors) { BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache"; @@ -276,13 +276,13 @@ void PresetUpdater::priv::sync_config(const std::set vendors) for (auto &index : index_db) { if (cancel) { return; } - const auto vendor_it = vendors.find(VendorProfile(index.vendor())); + const auto vendor_it = vendors.find(index.vendor()); if (vendor_it == vendors.end()) { BOOST_LOG_TRIVIAL(warning) << "No such vendor: " << index.vendor(); continue; } - const VendorProfile &vendor = *vendor_it; + const VendorProfile &vendor = vendor_it->second; if (vendor.config_update_url.empty()) { BOOST_LOG_TRIVIAL(info) << "Vendor has no config_update_url: " << vendor.name; continue; @@ -574,7 +574,7 @@ void PresetUpdater::sync(PresetBundle *preset_bundle) // Copy the whole vendors data for use in the background thread // Unfortunatelly as of C++11, it needs to be copied again // into the closure (but perhaps the compiler can elide this). - std::set vendors = preset_bundle->vendors; + VendorMap vendors = preset_bundle->vendors; p->thread = std::move(std::thread([this, vendors]() { this->p->prune_tmps(); @@ -691,8 +691,8 @@ void PresetUpdater::install_bundles_rsrc(std::vector bundles, bool BOOST_LOG_TRIVIAL(info) << boost::format("Installing %1% bundles from resources ...") % bundles.size(); for (const auto &bundle : bundles) { - auto path_in_rsrc = p->rsrc_path / bundle; - auto path_in_vendors = p->vendor_path / bundle; + auto path_in_rsrc = (p->rsrc_path / bundle).replace_extension(".ini"); + auto path_in_vendors = (p->vendor_path / bundle).replace_extension(".ini"); updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); }