diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index 827686393e..9954d85d3e 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -309,6 +309,7 @@ src/slic3r/GUI/BBLStatusBarPrint.cpp src/slic3r/GUI/BBLStatusBarSend.cpp src/slic3r/GUI/BaseTransparentDPIFrame.cpp src/slic3r/GUI/CloneDialog.cpp +src/slic3r/GUI/LegacyPrinterMigrationDialog.cpp src/slic3r/GUI/ColorDecomposeDialog.cpp src/slic3r/GUI/ColorDecomposeSupport.cpp src/slic3r/GUI/ConfigWizard.cpp diff --git a/scripts/orca_profile_tool.py b/scripts/orca_profile_tool.py index 405a594906..627507f892 100755 --- a/scripts/orca_profile_tool.py +++ b/scripts/orca_profile_tool.py @@ -11,7 +11,7 @@ commands: normalize rewrite profile files into their canonical shape trim delete profile files no .json list references update-index regenerate the *_list sections of .json - legacy-compat write the printers user presets keep from an older release + legacy-compat write the printers user presets can be migrated back to options shared by several commands: --vendor VENDOR act on one vendor bundle only; repeatable, empty means all @@ -27,7 +27,7 @@ normalize supplies missing types; update-index registers presets before id generation. After renaming filament presets or narrowing their compatible_printers, also rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2 -keep their printers. +can be migrated back to their printers. Use trim only for deliberate cleanup, previewed with --dry-run: it judges against the current index and can delete newly added, unindexed presets. @@ -3136,12 +3136,12 @@ def legacy_compatible_printers(base_dir, profiles_dir=PROFILES_DIR): User presets store only their diff, so they inherit their parent's compatible_printers; when a profile change narrows that list, a user preset made against the base release disappears from printers it was used on. The - loader adds these printers back to such user presets. Requiring a same-id + app offers to write these printers into such user presets. Requiring a same-id successor leaves out printers a filament was deliberately taken off, and requiring a shared extruder variant keeps a printer from picking up another variant's values. - Keyed by the current preset name: the loader resolves a renamed parent + Keyed by the current preset name: the app resolves a renamed parent before the map is consulted. Printer names are translated through the printers' renamed_from. Returns {preset name: sorted printer names}. """ @@ -3232,7 +3232,7 @@ normalize supplies missing types; update-index registers presets before id generation. After renaming filament presets or narrowing their compatible_printers, also rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2 -keep their printers. +can be migrated back to their printers. Use trim only for deliberate cleanup, previewed with --dry-run: it judges against the current index and can delete newly added, unindexed presets. """ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index ef34da08f9..fbbc02765d 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -81,6 +81,7 @@ #include "LifecycleEvents.hpp" #include "Utils.hpp" #include "InstanceLock.hpp" +#include "nlohmann/json.hpp" #include #include "PlaceholderParser.hpp" @@ -932,14 +933,10 @@ bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const Pre return true; } } - const std::vector &legacy = preset.preset.legacy_compatible_printers; - auto is_legacy_printer = [&legacy](const std::string &name) { return std::find(legacy.begin(), legacy.end(), name) != legacy.end(); }; return preset.preset.is_default || active_printer.preset.name.empty() || !has_compatible_printers || std::find(compatible_printers->values.begin(), compatible_printers->values.end(), active_printer.preset.name) != compatible_printers->values.end() || - (!active_printer.preset.is_system && is_compatible_with_parent_printer(preset, active_printer)) || - is_legacy_printer(active_printer.preset.name) || - (!active_printer.preset.is_system && is_legacy_printer(active_printer.preset.inherits())); + (!active_printer.preset.is_system && is_compatible_with_parent_printer(preset, active_printer)); } bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer) @@ -2745,27 +2742,80 @@ const Preset *PresetCollection::system_parent_with_inherited_printers(const Pres return own != nullptr && inherited != nullptr && own->values == inherited->values ? parent : nullptr; } -void PresetCollection::set_legacy_compatible_printers(const std::map> &printers_by_parent) +std::vector PresetCollection::legacy_printer_migrations(const std::map> &printers_by_parent) const { - lock(); - for (Preset &preset : m_presets) { - preset.legacy_compatible_printers.clear(); + std::vector migrations; + for (const Preset &preset : m_presets) { const Preset *parent = system_parent_with_inherited_printers(preset); if (parent == nullptr) continue; + // An empty list already covers every printer. + const std::vector &own = preset.config.option("compatible_printers")->values; + if (own.empty()) + continue; // The map names the parent as it was when the map was written; a later rename keeps that name in renamed_from. auto it = printers_by_parent.find(parent->name); for (auto old_name = parent->renamed_from.begin(); it == printers_by_parent.end() && old_name != parent->renamed_from.end(); ++old_name) it = printers_by_parent.find(*old_name); if (it == printers_by_parent.end()) continue; - preset.legacy_compatible_printers = it->second; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << preset.name << " keeps " << it->second.size() - << " printer(s) its parent " << parent->name << " listed in an older release"; + LegacyPrinterMigration migration{preset.name, parent->name, {}}; + for (const std::string &printer : it->second) + if (std::find(own.begin(), own.end(), printer) == own.end()) + migration.printers.push_back(printer); + if (!migration.printers.empty()) + migrations.push_back(std::move(migration)); } - if (m_idx_selected < m_presets.size() && m_edited_preset.name == m_presets[m_idx_selected].name) - m_edited_preset.legacy_compatible_printers = m_presets[m_idx_selected].legacy_compatible_printers; + return migrations; +} + +bool PresetCollection::apply_legacy_printer_migration(const LegacyPrinterMigration &migration) +{ + lock(); + auto it = this->find_preset_internal(migration.preset); + if (it == m_presets.end() || it->name != migration.preset || system_parent_with_inherited_printers(*it) == nullptr) { + unlock(); + return false; + } + Preset &preset = *it; + std::vector printers = preset.config.option("compatible_printers")->values; + for (const std::string &printer : migration.printers) + if (std::find(printers.begin(), printers.end(), printer) == printers.end()) + printers.push_back(printer); + const bool selected = m_idx_selected == static_cast(it - m_presets.begin()); unlock(); + + // Only compatible_printers changes in the file. Saving the whole preset would also rewrite an inherits that + // names a renamed parent by its old name, the only name older releases know it by. + { + InstanceLock instance_lock(user_presets_lock_path()); + try { + boost::nowide::ifstream in(preset.file); + json j = json::parse(in); + j["compatible_printers"] = printers; + if (const std::error_code ec = write_file_atomically(preset.file, j.dump(1, '\t'))) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << preset.file << ": " << ec.message(); + return false; + } + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not migrate " << preset.file << ": " << err.what(); + return false; + } + } + + lock(); + preset.config.option("compatible_printers")->values = printers; + if (selected) + m_edited_preset.config.option("compatible_printers", true)->values = printers; + unlock(); + // Push the change to the cloud copy, as an edit in the settings tab would. + if (!preset.setting_id.empty()) { + preset.sync_info = "update"; + preset.save_info(); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << preset.name << " gets " << migration.printers.size() + << " printer(s) its parent listed in an older release"; + return true; } //BBS: validate_preset @@ -3121,7 +3171,6 @@ bool PresetCollection::clone_presets(std::vector const &presets, preset.vendor = nullptr; preset.renamed_from.clear(); preset.m_excluded_from.clear(); - preset.legacy_compatible_printers.clear(); preset.setting_id.clear(); preset.inherits().clear(); preset.is_default = false; @@ -3259,9 +3308,6 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det preset.m_excluded_from.clear(); BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": save preset %1% , with detach")%new_name; } - // Legacy printers belong only to a preset that still inherits its system parent's printer list. - if (system_parent_with_inherited_printers(preset) == nullptr) - preset.legacy_compatible_printers.clear(); //BBS: add lock logic for sync preset in background if (m_type == Preset::TYPE_PRINT) @@ -3314,8 +3360,6 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det } else preset.is_project_embedded = false; - if (system_parent_with_inherited_printers(preset) == nullptr) - preset.legacy_compatible_printers.clear(); if (m_type == Preset::TYPE_PRINT) preset.config.option("print_settings_id", true)->value = new_name; else if (m_type == Preset::TYPE_FILAMENT) diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 58d0a4514a..80882ea596 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -311,10 +311,6 @@ public: // certain printer models defined in the vendor profile as well, in this case we want to hide this generic preset for these printer models. std::set m_excluded_from; - // Printers a user filament preset is also compatible with because its system parent listed them in - // an older release; see PresetCollection::set_legacy_compatible_printers(). - std::vector legacy_compatible_printers; - // Orca: flag to indicate if this preset is from Orca Filament Library bool m_from_orca_filament_lib = false; @@ -555,6 +551,14 @@ struct PresetConfigSubstitutions { // PrusaSlicer and reading the user Print / Filament / Printer profiles. using PresetsConfigSubstitutions = std::vector; +// A user filament preset that no longer shows on printers its system parent listed in an older release, +// and those printers. Migrating it writes them into the preset's own compatible_printers. +struct LegacyPrinterMigration { + std::string preset; + std::string parent; + std::vector printers; +}; + // Collections of presets of the same type (one of the Print, Filament or Printer type). class PresetCollection { @@ -606,10 +610,13 @@ public: bool load_user_preset(std::string name, std::map preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin = PresetOrigin(PresetOrigin::Kind::User)); void update_after_user_presets_loaded(); // User presets store only their diff, so a user preset inherits its system parent's compatible_printers - // and disappears from a printer once a profile update narrows that list. Give each user preset that - // inherits the list the printers its parent covered in an older release (printers_by_parent, keyed by the - // parent's current name), so it keeps showing on the printers it was made for. - void set_legacy_compatible_printers(const std::map> &printers_by_parent); + // and disappears from a printer once a profile update narrows that list. Lists the user presets that + // inherit the list and the printers their parent covered in an older release (printers_by_parent, keyed + // by the parent's name then) but no longer lists. + std::vector legacy_printer_migrations(const std::map> &printers_by_parent) const; + // Adds the migration's printers to the preset's own compatible_printers and saves the preset to disk. + // False if the preset no longer inherits its parent's list or could not be saved. + bool apply_legacy_printer_migration(const LegacyPrinterMigration &migration); //BBS: get user presets int get_user_presets(PresetBundle *preset_bundle, std::vector &result_presets); void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id); @@ -903,7 +910,7 @@ public: protected: // The system parent of a user preset whose compatible_printers is the parent's, or nullptr: the presets - // set_legacy_compatible_printers() gives legacy printers to. + // legacy_printer_migrations() considers. const Preset* system_parent_with_inherited_printers(const Preset &preset) const; PresetCollection() = default; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index e183134954..d5d594b2ae 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ #include "ParallelResolve.hpp" #include "Preset.hpp" #include "PresetCacheFormat.hpp" +#include "Time.hpp" +#include "Zipper.hpp" #include "PrintConfig.hpp" #include "PublishSettings.hpp" #include "FilamentMixer.hpp" @@ -918,7 +921,6 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward // raw vendor-JSON references instead of the silently-repaired ones. if (!validation_mode) this->normalize_compatible_presets(); - filaments.set_legacy_compatible_printers(legacy_compatible_printers()); this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); @@ -1075,6 +1077,55 @@ bool PresetBundle::backup_user_folder() const } } +bool PresetBundle::backup_user_folder_to_zip(const std::string &zip_path) const +{ + const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR; + BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << zip_path; + try { + Zipper zipper(zip_path); + // Directories get their own entries, which extract_archive_confined() needs to recreate them. + zipper.add_entry(std::string(PRESET_USER_DIR) + "/", nullptr, 0); + for (const fs::directory_entry &entry : fs::recursive_directory_iterator(user_dir)) { + const std::string name = (fs::path(PRESET_USER_DIR) / entry.path().lexically_relative(user_dir)).generic_string(); + if (fs::is_directory(entry.status())) { + zipper.add_entry(name + "/", nullptr, 0); + continue; + } + if (!fs::is_regular_file(entry.status())) + continue; + boost::nowide::ifstream in(entry.path().string(), std::ios::binary); + const std::string data((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + if (!in.good() && !in.eof()) + throw Slic3r::RuntimeError("can not read " + entry.path().string()); + zipper.add_entry(name, data.data(), data.size()); + } + zipper.finalize(); + return true; + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what(); + boost::system::error_code ec; + fs::remove(zip_path, ec); + return false; + } +} + +std::vector PresetBundle::legacy_printer_migrations() const +{ + return filaments.legacy_printer_migrations(legacy_compatible_printers()); +} + +std::string PresetBundle::migrate_legacy_printers(const std::vector &migrations, std::vector &failed) +{ + const std::string zip_path = data_dir() + "/filament_preset_migration_" + + Utils::time2str(Utils::TimeZone::utc, Utils::TimeFormat::iso8601Z) + "_backup.zip"; + if (!backup_user_folder_to_zip(zip_path)) + return {}; + for (const LegacyPrinterMigration &migration : migrations) + if (!filaments.apply_legacy_printer_migration(migration)) + failed.push_back(migration.preset); + return zip_path; +} + std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const { if (filament_id.empty()) @@ -1547,7 +1598,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & // Rewrite renamed compatible references in synced user presets before compatibility is evaluated. this->normalize_compatible_presets(); - filaments.set_legacy_compatible_printers(legacy_compatible_printers()); this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); @@ -2554,7 +2604,6 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // Rewrite renamed compatible references in synced user presets before compatibility is evaluated. this->normalize_compatible_presets(); - filaments.set_legacy_compatible_printers(legacy_compatible_printers()); this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index c6f79b741e..16c1052629 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -376,6 +376,16 @@ public: bool use_bbl_device_tab(); bool backup_user_folder() const; + // Writes the user preset folder into a zip archive at zip_path, its entries under "user/". + bool backup_user_folder_to_zip(const std::string &zip_path) const; + + // User filament presets that no longer show on printers their parent listed in an older release + // (resources/info/legacy_compatible_printers.json). + std::vector legacy_printer_migrations() const; + // Backs up the user preset folder into a zip in data_dir(), then writes each migration's printers into its + // preset file. Returns the zip's path, or an empty string when the backup failed and nothing was migrated. + // The presets that could not be migrated are added to failed. + std::string migrate_legacy_printers(const std::vector &migrations, std::vector &failed); //BBS: project embedded preset logic PresetsConfigSubstitutions load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 65c8384ef2..61bd39884f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -362,6 +362,8 @@ set(SLIC3R_GUI_SOURCES GUI/Lazy.hpp GUI/LazyPage.cpp GUI/LazyPage.hpp + GUI/LegacyPrinterMigrationDialog.cpp + GUI/LegacyPrinterMigrationDialog.hpp GUI/LibVGCode/LibVGCodeWrapper.hpp GUI/LibVGCode/LibVGCodeWrapper.cpp GUI/LinuxDisplayBackend.cpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 636ba985f5..0d312b7c6c 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -12,6 +12,7 @@ #include "DeviceManager.hpp" #include "HMS.hpp" #include "PresetBundleDialog.hpp" +#include "LegacyPrinterMigrationDialog.hpp" #include "WebUserLoginDialog.hpp" #include "WebViewDialog.hpp" #include "slic3r/Utils/BBLCloudServiceAgent.hpp" @@ -1109,6 +1110,8 @@ void GUI_App::post_init() } this->check_new_version_sf(); + if (!cw_showed) + this->check_legacy_printer_migration(false); const auto cloud_provider = get_printer_cloud_provider(); if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) { // this->check_privacy_version(0); @@ -6822,6 +6825,50 @@ void GUI_App::reload_settings() } } +void GUI_App::check_legacy_printer_migration(bool by_user) +{ + const std::vector migrations = preset_bundle->legacy_printer_migrations(); + if (migrations.empty()) { + if (by_user) + MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."), + _L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal(); + return; + } + if (!by_user && app_config->get_bool("legacy_printer_migration_dont_remind")) + return; + + LegacyPrinterMigrationDialog dlg(mainframe, migrations, !by_user); + const bool migrate = dlg.ShowModal() == wxID_YES; + if (dlg.dont_remind()) + app_config->set_bool("legacy_printer_migration_dont_remind", true); + if (!migrate) + return; + + std::vector failed; + const std::string backup = preset_bundle->migrate_legacy_printers(migrations, failed); + if (backup.empty()) { + show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated.")); + return; + } + preset_bundle->update_compatible(PresetSelectCompatibleType::Never); + mainframe->update_side_preset_ui(); + for (auto tab : tabs_list) { + tab->reload_config(); + tab->update_changed_ui(); + } + if (plater_) + plater_->sidebar().update_all_preset_comboboxes(); + + wxString message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n" + "A backup of your presets was saved to:\n%1%"), backup); + if (!failed.empty()) { + message += "\n\n" + _L("These presets could not be migrated:"); + for (const std::string &name : failed) + message += "\n" + from_u8(name); + } + MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal(); +} + //BBS reload when logout void GUI_App::remove_user_presets() { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 0a2ddc5f17..8864c99ca3 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -568,6 +568,10 @@ public: void check_update(bool show_tips, int by_user); void check_new_version(bool show_tips = false, int by_user = 0); void check_new_version_sf(bool show_tips = false, int by_user = 0); + // Offers to migrate user filament presets that lost printers their parent listed in an older release. + // At startup it asks once per release unless the user chose not to be reminded; by_user also reports + // when there is nothing to migrate. + void check_legacy_printer_migration(bool by_user); bool process_network_msg(std::string dev_id, std::string msg); void request_new_version(int by_user); void enter_force_upgrade(); diff --git a/src/slic3r/GUI/LegacyPrinterMigrationDialog.cpp b/src/slic3r/GUI/LegacyPrinterMigrationDialog.cpp new file mode 100644 index 0000000000..e2cc65056a --- /dev/null +++ b/src/slic3r/GUI/LegacyPrinterMigrationDialog.cpp @@ -0,0 +1,96 @@ +#include "LegacyPrinterMigrationDialog.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GUI_Utils.hpp" +#include "I18N.hpp" +#include "MainFrame.hpp" +#include "format.hpp" +#include "libslic3r/Preset.hpp" +#include "Widgets/Button.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/DialogButtons.hpp" +#include "Widgets/Label.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace Slic3r { namespace GUI { + +LegacyPrinterMigrationDialog::LegacyPrinterMigrationDialog(wxWindow *parent, const std::vector &migrations, + bool offer_dont_remind) + : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, _L("Migrate Filament Presets"), + wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) +{ + SetBackgroundColour(*wxWHITE); + SetFont(Label::Body_14); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *message = new Label(this, + _L("Since OrcaSlicer 2.5.0, filament presets have been restructured to support more features. Some of your " + "presets need to be migrated to the new structure, or they will no longer be available for some of your " + "printers.\n\n" + "Would you like to migrate them now? Your presets will be backed up first, and this only needs to be done " + "once. You can also do it later from Help > Migrate Filament Presets."), + 0); + message->Wrap(FromDIP(520)); + sizer->Add(message, 0, wxEXPAND | wxALL, FromDIP(15)); + + sizer->Add(new Label(this, _L("Affected presets:")), 0, wxLEFT | wxRIGHT, FromDIP(15)); + sizer->AddSpacer(FromDIP(5)); + + wxString details; + for (const LegacyPrinterMigration &migration : migrations) { + details += format_wxstr(_L("%1% (based on %2%)"), migration.preset, migration.parent) + "\n"; + for (const std::string &printer : migration.printers) + details += " " + from_u8(printer) + "\n"; + } + auto *list = new wxTextCtrl(this, wxID_ANY, details, wxDefaultPosition, wxSize(-1, FromDIP(220)), + wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP); + sizer->Add(list, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(15)); + + if (offer_dont_remind) { + auto *dont_remind_sizer = new wxBoxSizer(wxHORIZONTAL); + m_dont_remind = new ::CheckBox(this); + auto *dont_remind_label = new Label(this, _L("Don't remind me again")); + dont_remind_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &e) { + m_dont_remind->SetValue(!m_dont_remind->GetValue()); + e.Skip(); + }); + dont_remind_sizer->Add(m_dont_remind, 0, wxALIGN_CENTER_VERTICAL); + dont_remind_sizer->Add(dont_remind_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + sizer->Add(dont_remind_sizer, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(15)); + } + + auto *buttons = new DialogButtons(this, {"Yes", "No"}, "Yes"); + buttons->GetYES()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_YES); }); + buttons->GetNO()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_NO); }); + sizer->Add(buttons, 0, wxEXPAND); + + SetSizerAndFit(sizer); + CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); +} + +bool LegacyPrinterMigrationDialog::dont_remind() const +{ + return m_dont_remind != nullptr && m_dont_remind->GetValue(); +} + +void LegacyPrinterMigrationDialog::on_dpi_changed(const wxRect &) +{ + if (m_dont_remind != nullptr) + m_dont_remind->Rescale(); + GetSizer()->SetSizeHints(this); + Refresh(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/LegacyPrinterMigrationDialog.hpp b/src/slic3r/GUI/LegacyPrinterMigrationDialog.hpp new file mode 100644 index 0000000000..5d5cf1b2f6 --- /dev/null +++ b/src/slic3r/GUI/LegacyPrinterMigrationDialog.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "GUI_Utils.hpp" + +#include + +class CheckBox; +class wxWindow; +class wxRect; + +namespace Slic3r { + +struct LegacyPrinterMigration; + +namespace GUI { + +// Lists the user filament presets that no longer show on printers their parent listed in an older +// release, and asks whether to migrate them. ShowModal() returns wxID_YES to migrate. With +// offer_dont_remind it also offers to stop the startup reminder for good. +class LegacyPrinterMigrationDialog : public DPIDialog +{ +public: + LegacyPrinterMigrationDialog(wxWindow *parent, const std::vector &migrations, bool offer_dont_remind); + + bool dont_remind() const; + +protected: + void on_dpi_changed(const wxRect &suggested_rect) override; + +private: + ::CheckBox *m_dont_remind{nullptr}; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index db21915477..110b7c7bac 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2827,6 +2827,10 @@ wxMenu* MainFrame::generate_help_menu() append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"), [](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); }); + append_menu_item(helpMenu, wxID_ANY, _L("Migrate Filament Presets"), + _L("Bring back filament presets that a profile update removed from printers they were made for"), + [](wxCommandEvent&) { wxGetApp().check_legacy_printer_migration(true); }); + helpMenu->AppendSeparator(); // Troubleshoot center diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 334f2aad9b..2c9fba17bc 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -589,6 +589,10 @@ std::vector build_command_catalog() Slic3r::GUI::desktop_open_datadir_folder(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); + add("help_migrate_filament_presets", L("Migrate Filament Presets"), L("Help"), [](const std::string&) { + wxGetApp().check_legacy_printer_migration(true); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) { wxGetApp().troubleshoot(); return AppActionRunResult{AppActionRunResult::Level::Success}; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 55be8f7be0..a142355240 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -37,6 +37,7 @@ #include #include "libslic3r/Config.hpp" #include +#include #include "libslic3r/libslic3r.h" #include "libslic3r/PublishSettings.hpp" #include @@ -7162,7 +7163,7 @@ TEST_CASE("A user filament whose derived compatible printer cannot be written ba } #endif -TEST_CASE("A user filament keeps the printers its narrowed system parent listed in an older release", "[Preset][Bundle]") +TEST_CASE("User filaments that lost printers their narrowed system parent listed in an older release are found", "[Preset][Bundle]") { PresetBundle bundle; PresetCollection &filaments = bundle.filaments; @@ -7179,25 +7180,14 @@ TEST_CASE("A user filament keeps the printers its narrowed system parent listed set_printers("My PETG", { "Creality K1C 0.6 nozzle" }); set_printers("My PETG own list", { "Creality K1C 0.6 nozzle", "Creality K1C 0.8 nozzle" }); - filaments.set_legacy_compatible_printers({ { parent, { "Creality K1C 0.4 nozzle" } } }); - - auto compatible = [&filaments](const std::string &filament, const Preset &printer) { - return is_compatible_with_printer(PresetWithVendorProfile(*filaments.find_preset(filament), nullptr), - PresetWithVendorProfile(printer, nullptr)); - }; - Preset k1c_04(Preset::TYPE_PRINTER, "Creality K1C 0.4 nozzle"); - k1c_04.is_system = true; - Preset k1_max(Preset::TYPE_PRINTER, "Creality K1 Max 0.4 nozzle"); - k1_max.is_system = true; - Preset my_k1c(Preset::TYPE_PRINTER, "My K1C"); - my_k1c.config.option(BBL_JSON_KEY_INHERITS, true)->value = "Creality K1C 0.4 nozzle"; - - CHECK(compatible("My PETG", k1c_04)); - CHECK(compatible("My PETG", my_k1c)); - CHECK_FALSE(compatible("My PETG", k1_max)); - // The system preset itself and a user preset with its own list keep exactly their lists. - CHECK_FALSE(compatible(parent, k1c_04)); - CHECK_FALSE(compatible("My PETG own list", k1c_04)); + // A printer the parent still lists is not reported again. The system preset and a user preset + // with its own list are left alone. + std::vector migrations = + filaments.legacy_printer_migrations({ { parent, { "Creality K1C 0.4 nozzle", "Creality K1C 0.6 nozzle" } } }); + REQUIRE(migrations.size() == 1); + CHECK(migrations[0].preset == "My PETG"); + CHECK(migrations[0].parent == parent); + CHECK(migrations[0].printers == std::vector{ "Creality K1C 0.4 nozzle" }); // A parent renamed after the map was written is still found under its old name. add_inmemory_preset(filaments, "Generic PETG @Creality K1 renamed").is_system = true; @@ -7205,45 +7195,93 @@ TEST_CASE("A user filament keeps the printers its narrowed system parent listed set_renamed_from(filaments, "Generic PETG @Creality K1 renamed", { parent }); set_printers("Generic PETG @Creality K1 renamed", { "Creality K1C 0.6 nozzle" }); set_printers("My renamed PETG", { "Creality K1C 0.6 nozzle" }); - filaments.set_legacy_compatible_printers({ { parent, { "Creality K1C 0.4 nozzle" } } }); - CHECK(compatible("My renamed PETG", k1c_04)); + migrations = filaments.legacy_printer_migrations({ { parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(migrations.size() == 2); + CHECK(migrations[1].preset == "My renamed PETG"); + CHECK(migrations[1].parent == "Generic PETG @Creality K1 renamed"); } -TEST_CASE("A saved user filament keeps its legacy printers only while it inherits its parent's printer list", "[Preset][Bundle]") +TEST_CASE("Migrating a user filament backs up the user folder to a zip and writes the legacy printers to its file", "[Preset][Bundle]") { ScopedTemporaryDir temp_dir; - PresetBundle bundle; - PresetCollection &filaments = bundle.filaments; + ScopedDataDir data(temp_dir.path()); const std::string parent = "Generic PETG @Creality K1-all"; + auto with_parent = [&parent](PresetBundle &bundle) { + add_inmemory_preset(bundle.filaments, parent).is_system = true; + bundle.filaments.find_preset(parent)->config.option("compatible_printers", true)->values = { "Creality K1C 0.6 nozzle" }; + }; + // Like a real user preset, the file states only its diff, so the printer list comes from the parent. + const fs::path user_dir = temp_dir.path() / PRESET_USER_DIR / "default"; + const fs::path file = user_dir / PRESET_FILAMENT_NAME / "My PETG.json"; + fs::create_directories(file.parent_path()); + const std::string original = std::string(R"({"type":"filament","name":"My PETG","from":"User","version":"1.0.0","inherits":")") + parent + R"("})"; + std::ofstream(file.string()) << original; + auto load = [&user_dir](PresetCollection &filaments) { + PresetsConfigSubstitutions substitutions; + filaments.load_presets(user_dir.string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::EnableSilent); + }; + PresetBundle bundle; + PresetCollection &filaments = bundle.filaments; + with_parent(bundle); + load(filaments); + const std::map> map{ { parent, { "Creality K1C 0.4 nozzle" } } }; + const std::vector migrations = filaments.legacy_printer_migrations(map); + REQUIRE(migrations.size() == 1); + + std::vector failed; + const std::string backup = bundle.migrate_legacy_printers(migrations, failed); + REQUIRE_FALSE(backup.empty()); + CHECK(std::regex_match(fs::path(backup).filename().string(), std::regex("filament_preset_migration_\\d{8}T\\d{6}Z_backup\\.zip"))); + CHECK(failed.empty()); + const fs::path extracted = temp_dir.path() / "extracted"; + REQUIRE(extract_archive_confined(backup, extracted.string())); + CHECK(read_file(extracted / PRESET_USER_DIR / "default" / PRESET_FILAMENT_NAME / "My PETG.json") == original); + + // Reloaded from disk, the preset carries both printers itself and needs no further migration. + PresetBundle reloaded; + with_parent(reloaded); + load(reloaded.filaments); + const std::vector expected{ "Creality K1C 0.6 nozzle", "Creality K1C 0.4 nozzle" }; + CHECK(reloaded.filaments.find_preset("My PETG")->config.option("compatible_printers")->values == expected); + CHECK(reloaded.filaments.legacy_printer_migrations(map).empty()); + CHECK_FALSE(reloaded.filaments.apply_legacy_printer_migration(migrations[0])); +} + +namespace { + +// A filament collection that exposes the rename map update, which PresetBundle runs after loading system presets. +struct RenameTestFilaments : public PresetCollection +{ + RenameTestFilaments() + : PresetCollection(Preset::TYPE_FILAMENT, Preset::filament_options(), + static_cast(FullPrintConfig::defaults())) + {} + using PresetCollection::update_map_system_profile_renamed; +}; + +} // namespace + +TEST_CASE("Migrating a user filament keeps the old name of its renamed parent", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestFilaments filaments; + const std::string parent = "Generic PETG @Creality K1-all"; + const std::string old_parent = "Creality Generic PETG @K1-all"; add_inmemory_preset(filaments, parent).is_system = true; filaments.find_preset(parent)->config.option("compatible_printers", true)->values = { "Creality K1C 0.6 nozzle" }; - // Like a real user preset, the file states only its diff, so the printer list comes from the parent. - fs::create_directories(temp_dir.path() / PRESET_FILAMENT_NAME); - std::ofstream((temp_dir.path() / PRESET_FILAMENT_NAME / "My PETG.json").string()) - << R"({"type":"filament","name":"My PETG","from":"User","version":"1.0.0","inherits":")" << parent << R"("})"; + set_renamed_from(filaments, parent, { old_parent }); + filaments.update_map_system_profile_renamed(); + const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PETG.json"; + fs::create_directories(file.parent_path()); + std::ofstream(file.string()) << R"({"type":"filament","name":"My PETG","from":"User","version":"1.0.0","inherits":")" << old_parent << R"("})"; PresetsConfigSubstitutions substitutions; - filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions, - ForwardCompatibilitySubstitutionRule::EnableSilent); - filaments.set_legacy_compatible_printers({ { parent, { "Creality K1C 0.4 nozzle" } } }); - const std::vector legacy { "Creality K1C 0.4 nozzle" }; - REQUIRE(filaments.find_preset("My PETG")->legacy_compatible_printers == legacy); + filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::EnableSilent); - auto save_from_my_petg = [&filaments](const std::string &name, bool detach) { - filaments.select_preset_by_name("My PETG", true); - filaments.save_current_preset(name, detach); - return filaments.find_preset(name)->legacy_compatible_printers; - }; - CHECK(save_from_my_petg("My PETG copy", false) == legacy); - CHECK(save_from_my_petg("My PETG detached", true).empty()); - - // A clone is detached from the parent, as "Create filament" makes for another printer. - std::vector failures; - REQUIRE(filaments.clone_presets({ filaments.find_preset("My PETG") }, failures, - [](Preset &preset, Preset::Type &) { preset.name = "My PETG clone"; })); - CHECK(filaments.find_preset("My PETG clone")->legacy_compatible_printers.empty()); - - filaments.select_preset_by_name("My PETG", true); - filaments.get_edited_preset().config.option("compatible_printers")->values = { "Creality K1C 0.8 nozzle" }; - filaments.save_current_preset("My PETG", false); - CHECK(filaments.find_preset("My PETG")->legacy_compatible_printers.empty()); + const std::vector migrations = filaments.legacy_printer_migrations({ { parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(migrations.size() == 1); + REQUIRE(filaments.apply_legacy_printer_migration(migrations[0])); + // An older release knows the parent only by its old name. + const std::string saved = read_file(file); + CHECK(saved.find("\"inherits\": \"" + old_parent + "\"") != std::string::npos); + CHECK(saved.find("Creality K1C 0.4 nozzle") != std::string::npos); } diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 520c8118fc..b58898a698 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -255,7 +255,7 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands" const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts"); REQUIRE(first != nullptr); - for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test", + for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_migrate_filament_presets", "help_troubleshoot", "help_network_test", "help_benchmark_3d_scene", "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki", "open_youtube"}) { const Slic3r::GUI::NativeCommand* c = find(key);