diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index a89a7c8b0..934d1b978 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -81,6 +81,8 @@ #define ENABLE_USED_FILAMENT_POST_PROCESS (1 && ENABLE_2_5_0_ALPHA1) // Enable gizmo grabbers to share common models #define ENABLE_GIZMO_GRABBER_REFACTOR (1 && ENABLE_2_5_0_ALPHA1) +// Disable association to 3mf and stl files if the application is run on Windows 8 or later +#define ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER (1 && ENABLE_2_5_0_ALPHA1) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 86768dfc5..b9dcc1a4f 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1,3075 +1,3093 @@ -// FIXME: extract absolute units -> em - -#include "ConfigWizard_private.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE - -#include "libslic3r/Platform.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Config.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/Model.hpp" -#include "libslic3r/Color.hpp" -#include "GUI.hpp" -#include "GUI_App.hpp" -#include "GUI_Utils.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "Field.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" -#include "format.hpp" -#include "MsgDialog.hpp" -#include "UnsavedChangesDialog.hpp" - -#if defined(__linux__) && defined(__WXGTK3__) -#define wxLinux_gtk3 true -#else -#define wxLinux_gtk3 false -#endif //defined(__linux__) && defined(__WXGTK3__) - -namespace Slic3r { -namespace GUI { - - -using Config::Snapshot; -using Config::SnapshotDB; - - -// Configuration data structures extensions needed for the wizard - -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) -{ - this->preset_bundle = std::make_unique(); - this->is_in_resources = ais_in_resources; - this->is_prusa_bundle = ais_prusa_bundle; - - std::string path_string = source_path.string(); - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( - path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); - UNUSED(config_substitutions); - // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. - assert(config_substitutions.empty()); - auto first_vendor = preset_bundle->vendors.begin(); - if (first_vendor == preset_bundle->vendors.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; - return false; - } - if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; - return false; - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; - this->vendor_profile = &first_vendor->second; - return true; -} - -Bundle::Bundle(Bundle &&other) - : 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; - - 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(); - - 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; - if (prusa_bundle.load(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; - if (bundle.load(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 Slic3r::RuntimeError("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 -{ - std::string vendor_id; - std::string model_id; - std::string variant_name; - bool enable; - - PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) - : wxEvent(winid, eventType) - , vendor_id(std::move(vendor_id)) - , model_id(std::move(model_id)) - , variant_name(std::move(variant_name)) - , enable(enable) - {} - - virtual wxEvent *Clone() const - { - return new PrinterPickerEvent(*this); - } -}; - -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) - , width(0) -{ - wxGetApp().UpdateDarkUI(this); - const auto &models = vendor.models; - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - const auto font_title = GetFont().MakeBold().Scaled(1.3f); - const auto font_name = GetFont().MakeBold(); - const auto font_alt_nozzle = GetFont().Scaled(0.9f); - - // wxGrid appends widgets by rows, but we need to construct them in columns. - // These vectors are used to hold the elements so that they can be appended in the right order. - std::vector titles; - std::vector bitmaps; - std::vector variants_panels; - - int max_row_width = 0; - int current_row_width = 0; - - bool is_variants = false; - - for (const auto &model : models) { - if (! filter(model)) { continue; } - - wxBitmap bitmap; - int bitmap_width = 0; - auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { - if (wxFileExists(bitmap_file)) { - bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); - bitmap_width = bitmap.GetWidth(); - return true; - } - return false; - }; - if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") - % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") - % vendor.id - % model.id; - load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); - } - } - auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - title->SetFont(font_name); - const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); - title->Wrap(wrap_width); - - current_row_width += wrap_width; - if (titles.size() % max_cols == max_cols - 1) { - max_row_width = std::max(max_row_width, current_row_width); - current_row_width = 0; - } - - titles.push_back(title); - - auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); - bitmaps.push_back(bitmap_widget); - - auto *variants_panel = new wxPanel(this); - wxGetApp().UpdateDarkUI(variants_panel); - auto *variants_sizer = new wxBoxSizer(wxVERTICAL); - variants_panel->SetSizer(variants_sizer); - const auto model_id = model.id; - - for (size_t i = 0; i < model.variants.size(); i++) { - const auto &variant = model.variants[i]; - - const auto label = model.technology == ptFFF - ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) - : from_u8(model.name); - - if (i == 1) { - auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); - alt_label->SetFont(font_alt_nozzle); - variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); - is_variants = true; - } - - auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); - i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); - - const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); - cbox->SetValue(enabled); - - variants_sizer->Add(cbox, 0, wxBOTTOM, 3); - - cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { - on_checkbox(cbox, event.IsChecked()); - }); - } - - variants_panels.push_back(variants_panel); - } - - width = std::max(max_row_width, current_row_width); - - const size_t cols = std::min(max_cols, titles.size()); - - auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); - printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); - - if (titles.size() > 0) { - const size_t odd_items = titles.size() % cols; - - for (size_t i = 0; i < titles.size() - odd_items; i += cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } - - // Add separator space to multiliners - if (titles.size() > cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } - } - } - if (odd_items > 0) { - const size_t rem = titles.size() - odd_items; - - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } - } - } - - auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); - if (! title.IsEmpty()) { - auto *title_widget = new wxStaticText(this, wxID_ANY, title); - title_widget->SetFont(font_title); - title_sizer->Add(title_widget); - } - title_sizer->AddStretchSpacer(); - - if (titles.size() > 1 || is_variants) { - // It only makes sense to add the All / None buttons if there's multiple printers - // All Standard button is added when there are more variants for at least one printer - auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - if (is_variants) - sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); - sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); - sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); - if (is_variants) - title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(sel_all_std); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - // fill button indexes used later for buttons rescaling - if (is_variants) - m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; - else { - sel_all_std->Destroy(); - m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; - } - } - - sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); - sizer->Add(printer_grid); - - SetSizer(sizer); -} - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) - : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) -{} - -void PrinterPicker::select_all(bool select, bool alternates) -{ - for (const auto &cb : cboxes) { - if (cb->GetValue() != select) { - cb->SetValue(select); - on_checkbox(cb, select); - } - } - - if (! select) { alternates = false; } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue() != alternates) { - cb->SetValue(alternates); - on_checkbox(cb, alternates); - } - } -} - -void PrinterPicker::select_one(size_t i, bool select) -{ - if (i < cboxes.size() && cboxes[i]->GetValue() != select) { - cboxes[i]->SetValue(select); - on_checkbox(cboxes[i], select); - } -} - -bool PrinterPicker::any_selected() const -{ - for (const auto &cb : cboxes) { - if (cb->GetValue()) { return true; } - } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue()) { return true; } - } - - return false; -} - -std::set PrinterPicker::get_selected_models() const -{ - std::set ret_set; - - for (const auto& cb : cboxes) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - for (const auto& cb : cboxes_alt) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - return ret_set; -} - -void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) -{ - PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); - AddPendingEvent(evt); -} - - -// Wizard page base - -ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) - : wxPanel(parent->p->hscroll) - , parent(parent) - , shortname(std::move(shortname)) - , indent(indent) -{ - wxGetApp().UpdateDarkUI(this); - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - const auto font = GetFont().MakeBold().Scaled(1.5); - text->SetFont(font); - sizer->Add(text, 0, wxALIGN_LEFT, 0); - sizer->AddSpacer(10); - - content = new wxBoxSizer(wxVERTICAL); - sizer->Add(content, 1, wxEXPAND); - - SetSizer(sizer); - - // There is strange layout on Linux with GTK3, - // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 - // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages - if (!wxLinux_gtk3) - this->Hide(); - - Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { - this->Layout(); - event.Skip(); - }); -} - -ConfigWizardPage::~ConfigWizardPage() {} - -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) -{ - // FIXME: scaling - content->AddSpacer(space); -} - -// Wizard pages - -PageWelcome::PageWelcome(ConfigWizard *parent) - : ConfigWizardPage(parent, from_u8((boost::format( -#ifdef __APPLE__ - _utf8(L("Welcome to the %s Configuration Assistant")) -#else - _utf8(L("Welcome to the %s Configuration Wizard")) -#endif - ) % SLIC3R_APP_NAME).str()), _L("Welcome")) - , welcome_text(append_text(from_u8((boost::format( - _utf8(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 - % _utf8(ConfigWizard::name())).str()) - )) - , cbox_reset(append( - new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) - )) - , cbox_integrate(append( - new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) - )) -{ - welcome_text->Hide(); - cbox_reset->Hide(); - cbox_integrate->Hide(); -} - -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); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - if (!DesktopIntegrationDialog::is_integrated()) - cbox_integrate->Show(true); - else - cbox_integrate->Hide(); -#else - cbox_integrate->Hide(); -#endif -} - - -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, - }; - - AppConfig *appconfig = &this->wizard_p()->appconfig_new; - - const auto families = vendor.families(); - for (const auto &family : families) { - const auto filter = [&](const VendorProfile::PrinterModel &model) { - return ((model.technology == ptFFF && technology & T_FFF) - || (model.technology == ptSLA && technology & T_SLA)) - && model.family == family; - }; - - if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { - continue; - } - - const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); - auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); - - 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, evt); - }); - - append(new StaticLine(this)); - - append(picker); - printer_pickers.push_back(picker); - has_printers = true; - } - -} - -void PagePrinters::select_all(bool select, bool alternates) -{ - for (auto picker : printer_pickers) { - picker->select_all(select, alternates); - } -} - -int PagePrinters::get_width() const -{ - return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, - [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); -} - -bool PagePrinters::any_selected() const -{ - for (const auto *picker : printer_pickers) { - if (picker->any_selected()) { return true; } - } - - return false; -} - -std::set PagePrinters::get_selected_models() -{ - std::set ret_set; - - for (const auto *picker : printer_pickers) - { - std::set tmp_models = picker->get_selected_models(); - ret_set.insert(tmp_models.begin(), tmp_models.end()); - } - - return ret_set; -} - -void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) -{ - if (is_primary_printer_page - && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) - && printer_pickers.size() > 0 - && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { - printer_pickers[0]->select_one(0, true); - } -} - - -const std::string PageMaterials::EMPTY; - -PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) - : ConfigWizardPage(parent, std::move(title), std::move(shortname)) - , materials(materials) - , list_printer(new StringList(this, wxLB_MULTIPLE)) - , list_type(new StringList(this)) - , list_vendor(new StringList(this)) - , list_profile(new PresetList(this)) -{ - append_spacer(VERTICAL_SPACING); - - const int em = parent->em_unit(); - const int list_h = 30*em; - - - list_printer->SetMinSize(wxSize(23*em, list_h)); - list_type->SetMinSize(wxSize(13*em, list_h)); - list_vendor->SetMinSize(wxSize(13*em, list_h)); - list_profile->SetMinSize(wxSize(23*em, list_h)); - - - - grid = new wxFlexGridSizer(4, em/2, em); - grid->AddGrowableCol(3, 1); - grid->AddGrowableRow(1, 1); - - grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); - grid->Add(new wxStaticText(this, wxID_ANY, list1name)); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); - - grid->Add(list_printer, 0, wxEXPAND); - grid->Add(list_type, 0, wxEXPAND); - grid->Add(list_vendor, 0, wxEXPAND); - grid->Add(list_profile, 1, wxEXPAND); - - auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); - btn_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(list_printer); - wxGetApp().UpdateDarkUI(list_type); - wxGetApp().UpdateDarkUI(list_vendor); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(btn_sizer, 0, wxALIGN_RIGHT); - - append(grid, 1, wxEXPAND); - - append_spacer(VERTICAL_SPACING); - - html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, - wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); - append(html_window, 0, wxEXPAND); - - list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); - }); - list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - - list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); - list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); - - sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); - sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); - /* - Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); - - list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); - list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); - list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); - */ - reload_presets(); - set_compatible_printers_html_window(std::vector(), false); -} -void PageMaterials::on_paint() -{ -} -void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) -{ - const wxClientDC dc(list_profile); - const wxPoint pos = evt.GetLogicalPosition(dc); - int item = list_profile->HitTest(pos); - on_material_hovered(item); -} -void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) -{} -void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) -{ - on_material_hovered(-1); -} -void PageMaterials::reload_presets() -{ - clear(); - - list_printer->append(_L("(All)"), &EMPTY); - //list_printer->SetLabelMarkup("bald"); - for (const Preset* printer : materials->printers) { - list_printer->append(printer->name, &printer->name); - } - sort_list_data(list_printer, true, false); - if (list_printer->GetCount() > 0) { - list_printer->SetSelection(0); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - update_lists(0, 0, 0); - } - - presets_loaded = true; -} - -void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) -{ - const auto bgr_clr = -#if defined(__APPLE__) - html_window->GetParent()->GetBackgroundColour(); -#else -#if defined(_WIN32) - wxGetApp().get_window_default_clr(); -#else - wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); -#endif -#endif - const auto text_clr = wxGetApp().get_label_clr_default(); - const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); - const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); - wxString text; - if (all_printers) { - wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "
" - "
" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line - ); - } else { - wxString second_line; - if (!printer_names.empty()) - second_line = (materials->technology == T_FFF ? - _L("Only the following installed printers are compatible with the selected filaments") : - _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line); - for (size_t i = 0; i < printer_names.size(); ++i) - { - text += wxString::Format("", boost::nowide::widen(printer_names[i])); - if (i % 3 == 2) { - text += wxString::Format( - "" - ""); - } - } - text += wxString::Format( - "" - "
%s
" - "
" - "
" - "" - "" - ); - } - - wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); - const int fs = font.GetPointSize(); - int size[] = { fs,fs,fs,fs,fs,fs,fs }; - html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); - html_window->SetPage(text); -} - -void PageMaterials::clear_compatible_printers_label() -{ - set_compatible_printers_html_window(std::vector(), false); -} - -void PageMaterials::on_material_hovered(int sel_material) -{ - -} - -void PageMaterials::on_material_highlighted(int sel_material) -{ - if (sel_material == last_hovered_item) - return; - if (sel_material == -1) { - clear_compatible_printers_label(); - return; - } - last_hovered_item = sel_material; - std::vector tabs; - tabs.push_back(std::string()); - tabs.push_back(std::string()); - tabs.push_back(std::string()); - //selected material string - std::string material_name = list_profile->get_data(sel_material); - // get material preset - const std::vector matching_materials = materials->get_presets_by_alias(material_name); - if (matching_materials.empty()) - { - clear_compatible_printers_label(); - return; - } - //find matching printers - std::vector names; - for (const Preset* printer : materials->printers) { - for (const Preset* material : matching_materials) { - if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { - names.push_back(printer->name); - break; - } - } - } - set_compatible_printers_html_window(names, names.size() == materials->printers.size()); -} - -void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - wxArrayInt sel_printers; - int sel_printers_count = list_printer->GetSelections(sel_printers); - - // Does our wxWidgets version support operator== for wxArrayInt ? - // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 -#if wxCHECK_VERSION(3, 1, 1) - if (sel_printers != sel_printers_prev) { -#else - auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { - if (arr_first.GetCount() != arr_second.GetCount()) - return false; - for (size_t i = 0; i < arr_first.GetCount(); i++) - if (arr_first[i] != arr_second[i]) - return false; - return true; - }; - if (!are_equal(sel_printers, sel_printers_prev)) { -#endif - - // Refresh type list - list_type->Clear(); - list_type->append(_L("(All)"), &EMPTY); - if (sel_printers_count > 0) { - // If all is selected with other printers - // unselect "all" or all printers depending on last value - if (sel_printers[0] == 0 && sel_printers_count > 1) { - if (last_selected_printer == 0) { - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - } else { - list_printer->SetSelection(0, false); - sel_printers_count = list_printer->GetSelections(sel_printers); - } - } - if (sel_printers[0] != 0) { - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - } else { - //clear selection except "ALL" - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - sel_printers_count = list_printer->GetSelections(sel_printers); - - materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - sort_list_data(list_type, true, true); - } - - sel_printers_prev = sel_printers; - sel_type = 0; - sel_type_prev = wxNOT_FOUND; - list_type->SetSelection(sel_type); - list_profile->Clear(); - } - - if (sel_type != sel_type_prev) { - // Refresh vendor list - - // XXX: The vendor list is created with quadratic complexity here, - // but the number of vendors is going to be very small this shouldn't be a problem. - - list_vendor->Clear(); - list_vendor->append(_L("(All)"), &EMPTY); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - // find printer preset - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { - const std::string& vendor = this->materials->get_vendor(p); - if (list_vendor->find(vendor) == wxNOT_FOUND) { - list_vendor->append(vendor, &vendor); - } - }); - } - sort_list_data(list_vendor, true, false); - } - - sel_type_prev = sel_type; - sel_vendor = 0; - sel_vendor_prev = wxNOT_FOUND; - list_vendor->SetSelection(sel_vendor); - list_profile->Clear(); - } - - if (sel_vendor != sel_vendor_prev) { - // Refresh material list - - list_profile->Clear(); - clear_compatible_printers_label(); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - const std::string& vendor = list_vendor->get_data(sel_vendor); - // finst printer preset - std::vector to_list; - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - - materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { - const std::string& section = materials->appconfig_section(); - bool checked = wizard_p()->appconfig_new.has(section, p->name); - bool was_checked = false; - - int cur_i = list_profile->find(p->alias); - if (cur_i == wxNOT_FOUND) { - cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); - to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); - } - else { - was_checked = list_profile->IsChecked(cur_i); - to_list[cur_i].checked = checked || was_checked; - } - list_profile->Check(cur_i, checked || was_checked); - - /* Update preset selection in config. - * If one preset from aliases bundle is selected, - * than mark all presets with this aliases as selected - * */ - if (checked && !was_checked) - wizard_p()->update_presets_in_config(section, p->alias, true); - else if (!checked && was_checked) - wizard_p()->appconfig_new.set(section, p->name, "1"); - }); - } - sort_list_data(list_profile, to_list); - } - - sel_vendor_prev = sel_vendor; - } - wxGetApp().UpdateDarkUI(list_profile); -} - -void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) -{ -// get data from list -// sort data -// first should be -// then prusa profiles -// then the rest -// in alphabetical order - - std::vector> prusa_profiles; - std::vector> other_profiles; - for (int i = 0 ; i < list->size(); ++i) { - const std::string& data = list->get_data(i); - if (data == EMPTY) // do not sort item - continue; - if (!material_type_ordering && data.find("Prusa") != std::string::npos) - prusa_profiles.push_back(data); - else - other_profiles.push_back(data); - } - if(material_type_ordering) { - - const ConfigOptionDef* def = print_config_def.get("filament_type"); - std::vectorenum_values = def->enum_values; - size_t end_of_sorted = 0; - for (size_t vals = 0; vals < enum_values.size(); vals++) { - for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) - { - // find instead compare because PET vs PETG - if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { - //swap - if(profs != end_of_sorted) { - std::reference_wrapper aux = other_profiles[end_of_sorted]; - other_profiles[end_of_sorted] = other_profiles[profs]; - other_profiles[profs] = aux; - } - end_of_sorted++; - break; - } - } - } - } else { - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - } - - list->Clear(); - if (add_All_item) - list->append(_L("(All)"), &EMPTY); - for (const auto& item : prusa_profiles) - list->append(item, &const_cast(item.get())); - for (const auto& item : other_profiles) - list->append(item, &const_cast(item.get())); -} - -void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) -{ - // sort data - // then prusa profiles - // then the rest - // in alphabetical order - std::vector prusa_profiles; - std::vector other_profiles; - //for (int i = 0; i < data.size(); ++i) { - for (const auto& item : data) { - const std::string& name = item.name; - if (name.find("Prusa") != std::string::npos) - prusa_profiles.emplace_back(item); - else - other_profiles.emplace_back(item); - } - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - list->Clear(); - for (size_t i = 0; i < prusa_profiles.size(); ++i) { - list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); - list->Check(i, prusa_profiles[i].checked); - } - for (size_t i = 0; i < other_profiles.size(); ++i) { - list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); - list->Check(i + prusa_profiles.size(), other_profiles[i].checked); - } -} - -void PageMaterials::select_material(int i) -{ - const bool checked = list_profile->IsChecked(i); - - const std::string& alias_key = list_profile->get_data(i); - wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); -} - -void PageMaterials::select_all(bool select) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - for (unsigned i = 0; i < list_profile->GetCount(); i++) { - const bool current = list_profile->IsChecked(i); - if (current != select) { - list_profile->Check(i, select); - select_material(i); - } - } -} - -void PageMaterials::clear() -{ - list_printer->Clear(); - list_type->Clear(); - list_vendor->Clear(); - list_profile->Clear(); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - presets_loaded = false; -} - -void PageMaterials::on_activate() -{ - if (! presets_loaded) { - wizard_p()->update_materials(materials->technology); - reload_presets(); - } - first_paint = true; -} - - -const char *PageCustom::default_profile_name = "My Settings"; - -PageCustom::PageCustom(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) -{ - cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); - tc_profile_name = new wxTextCtrl(this, wxID_ANY, default_profile_name); - auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); - - wxGetApp().UpdateDarkUI(tc_profile_name); - - tc_profile_name->Enable(false); - tc_profile_name->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent &evt) { - if (tc_profile_name->GetValue().IsEmpty()) { - if (profile_name_prev.IsEmpty()) { tc_profile_name->SetValue(default_profile_name); } - else { tc_profile_name->SetValue(profile_name_prev); } - } else { - profile_name_prev = tc_profile_name->GetValue(); - } - evt.Skip(); - }); - - cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { - tc_profile_name->Enable(custom_wanted()); - wizard_p()->on_custom_setup(custom_wanted()); - - }); - - append(cb_custom); - append(label); - append(tc_profile_name); -} - -PageUpdate::PageUpdate(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) - , version_check(true) - , preset_update(true) -{ - const AppConfig *app_config = wxGetApp().app_config; - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); - box_slic3r->SetValue(app_config->get("notify_release") != "none"); - append(box_slic3r); - append_text(wxString::Format(_L( - "If enabled, %s checks for new application versions online. When a new version becomes available, " - "a notification is displayed at the next application startup (never during program usage). " - "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); - - append_spacer(VERTICAL_SPACING); - - auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); - box_presets->SetValue(app_config->get("preset_update") == "1"); - append(box_presets); - append_text(wxString::Format(_L( - "If enabled, %s downloads updates of built-in system presets in the background." - "These updates are downloaded into a separate temporary location." - "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); - const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); - auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); - label_bold->SetFont(boldfont); - label_bold->Wrap(WRAP_WIDTH); - append(label_bold); - append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); - - box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); - box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); -} - -PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) - , full_pathnames(false) -{ - auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); - box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); - append(box_pathnames); - append_text(_L( - "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" - "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." - )); - - box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); -} - -#ifdef _WIN32 -PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) -{ - cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); - cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); -// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); - - append(cb_3mf); - append(cb_stl); -// append(cb_gcode); -} -#endif // _WIN32 - -PageMode::PageMode(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) -{ - append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" - "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " - "The other two offer progressively more sophisticated fine-tuning, " - "they are suitable for advanced and expert users, respectively.")); - - radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); - radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); - radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); - - std::string mode { "simple" }; - wxGetApp().app_config->get("", "view_mode", mode); - - if (mode == "advanced") { radio_advanced->SetValue(true); } - else if (mode == "expert") { radio_expert->SetValue(true); } - else { radio_simple->SetValue(true); } - - append(radio_simple); - append(radio_advanced); - append(radio_expert); - - append_text("\n" + _L("The size of the object can be specified in inches")); - check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); - check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); - append(check_inch); - - on_activate(); -} - -void PageMode::serialize_mode(AppConfig *app_config) const -{ - std::string mode = ""; - - if (radio_simple->GetValue()) { mode = "simple"; } - if (radio_advanced->GetValue()) { mode = "advanced"; } - if (radio_expert->GetValue()) { mode = "expert"; } - - app_config->set("view_mode", mode); - app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); -} - -PageVendors::PageVendors(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) -{ - const AppConfig &appconfig = this->wizard_p()->appconfig_new; - - append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); - - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - for (const auto &pair : wizard_p()->bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); - cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { - wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); - }); - - const auto &vendors = appconfig.vendors(); - const bool enabled = vendors.find(pair.first) != vendors.end(); - if (enabled) { - cbox->SetValue(true); - - auto pages = wizard_p()->pages_3rdparty.find(vendor->id); - wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); - - for (PagePrinters* page : { pages->second.first, pages->second.second }) - if (page) page->install = true; - } - - append(cbox); - } -} - -PageFirmware::PageFirmware(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) - , gcode_opt(*print_config_def.get("gcode_flavor")) - , gcode_picker(nullptr) -{ - append_text(_L("Choose the type of firmware used by your printer.")); - append_text(_(gcode_opt.tooltip)); - - wxArrayString choices; - choices.Alloc(gcode_opt.enum_labels.size()); - for (const auto &label : gcode_opt.enum_labels) { - choices.Add(label); - } - - gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); - wxGetApp().UpdateDarkUI(gcode_picker); - const auto &enum_values = gcode_opt.enum_values; - auto needle = enum_values.cend(); - if (gcode_opt.default_value) { - needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); - } - if (needle != enum_values.cend()) { - gcode_picker->SetSelection(needle - enum_values.cbegin()); - } else { - gcode_picker->SetSelection(0); - } - - append(gcode_picker); -} - -void PageFirmware::apply_custom_config(DynamicPrintConfig &config) -{ - auto sel = gcode_picker->GetSelection(); - if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { - auto *opt = new ConfigOptionEnum(static_cast(sel)); - config.set_key_value("gcode_flavor", opt); - } -} - -PageBedShape::PageBedShape(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) - , shape_panel(new BedShapePanel(this)) -{ - append_text(_L("Set the shape of your printer's bed.")); - - shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), - *wizard_p()->custom_config->option("bed_custom_texture"), - *wizard_p()->custom_config->option("bed_custom_model")); - - append(shape_panel); -} - -void PageBedShape::apply_custom_config(DynamicPrintConfig &config) -{ - const std::vector& points = shape_panel->get_shape(); - const std::string& custom_texture = shape_panel->get_custom_texture(); - const std::string& custom_model = shape_panel->get_custom_model(); - config.set_key_value("bed_shape", new ConfigOptionPoints(points)); - config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); - config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); -} - -static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) -{ - e.Skip(); - wxString str = ctrl->GetValue(); - - const char dec_sep = is_decimal_separator_point() ? '.' : ','; - const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; - - double val = 0.0; - if (!str.ToDouble(&val)) { - if (val == 0.0) - val = def_value; - ctrl->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - ctrl->SetFocus(); - } - else if (was_replaced) - ctrl->SetValue(double_to_string(val)); -} - -class DiamTextCtrl : public wxTextCtrl -{ -public: - DiamTextCtrl(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxBORDER_SIMPLE; -#else - long style = 0; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); - wxGetApp().UpdateDarkUI(this); - } - ~DiamTextCtrl() {} -}; - -PageDiameters::PageDiameters(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) - , diam_nozzle(new DiamTextCtrl(this)) - , diam_filam (new DiamTextCtrl(this)) -{ - auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); - wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); - diam_nozzle->SetValue(value); - - auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); - value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); - diam_filam->SetValue(value); - - diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); - diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); - - append_text(_L("Enter the diameter of your printer's hot end nozzle.")); - - auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); - auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); - auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_nozzle->AddGrowableCol(0, 1); - sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - sizer_nozzle->Add(diam_nozzle); - sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_nozzle); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the diameter of your filament.")); - append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); - - auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); - auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); - auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_filam->AddGrowableCol(0, 1); - sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); - sizer_filam->Add(diam_filam); - sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_filam); -} - -void PageDiameters::apply_custom_config(DynamicPrintConfig &config) -{ - double val = 0.0; - diam_nozzle->GetValue().ToDouble(&val); - auto *opt_nozzle = new ConfigOptionFloats(1, val); - config.set_key_value("nozzle_diameter", opt_nozzle); - - val = 0.0; - diam_filam->GetValue().ToDouble(&val); - auto * opt_filam = new ConfigOptionFloats(1, val); - config.set_key_value("filament_diameter", opt_filam); - - auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { - char buf[64]; // locales don't matter here (sprintf/atof) - sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); - config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); - }; - - set_extrusion_width("support_material_extrusion_width", 0.35); - set_extrusion_width("top_infill_extrusion_width", 0.40); - set_extrusion_width("first_layer_extrusion_width", 0.42); - - set_extrusion_width("extrusion_width", 0.45); - set_extrusion_width("perimeter_extrusion_width", 0.45); - set_extrusion_width("external_perimeter_extrusion_width", 0.45); - set_extrusion_width("infill_extrusion_width", 0.45); - set_extrusion_width("solid_infill_extrusion_width", 0.45); -} - -class SpinCtrlDouble: public wxSpinCtrlDouble -{ -public: - SpinCtrlDouble(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; -#else - long style = wxSP_ARROW_KEYS; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this->GetText()); -#endif - this->Refresh(); - } - ~SpinCtrlDouble() {} -}; - -PageTemperatures::PageTemperatures(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) - , spin_extr(new SpinCtrlDouble(this)) - , spin_bed (new SpinCtrlDouble(this)) -{ - spin_extr->SetIncrement(5.0); - const auto &def_extr = *print_config_def.get("temperature"); - spin_extr->SetRange(def_extr.min, def_extr.max); - auto *default_extr = def_extr.get_default_value(); - spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); - - spin_bed->SetIncrement(5.0); - const auto &def_bed = *print_config_def.get("bed_temperature"); - spin_bed->SetRange(def_bed.min, def_bed.max); - auto *default_bed = def_bed.get_default_value(); - spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); - - append_text(_L("Enter the temperature needed for extruding your filament.")); - append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); - - auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); - auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); - auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_extr->AddGrowableCol(0, 1); - sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); - sizer_extr->Add(spin_extr); - sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_extr); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); - append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); - - auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); - auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); - auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_bed->AddGrowableCol(0, 1); - sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); - sizer_bed->Add(spin_bed); - sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_bed); -} - -void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) -{ - auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("temperature", opt_extr); - auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("first_layer_temperature", opt_extr1st); - auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("bed_temperature", opt_bed); - auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("first_layer_bed_temperature", opt_bed1st); -} - - -// Index - -ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) - : wxPanel(parent) - , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) - , bullet_black(ScalableBitmap(parent, "bullet_black.png")) - , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) - , bullet_white(ScalableBitmap(parent, "bullet_white.png")) - , item_active(NO_ITEM) - , item_hover(NO_ITEM) - , last_page((size_t)-1) -{ -#ifndef __WXOSX__ - SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX -#endif //__WXOSX__ - SetMinSize(bg.bmp().GetSize()); - - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); - Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); - Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); - - Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { - if (item_hover != -1) { - item_hover = -1; - Refresh(); - } - evt.Skip(); - }); - - Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { - if (item_hover >= 0) { go_to(item_hover); } - }); -} - -wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); - -void ConfigWizardIndex::add_page(ConfigWizardPage *page) -{ - last_page = items.size(); - items.emplace_back(Item { page->shortname, page->indent, page }); - Refresh(); -} - -void ConfigWizardIndex::add_label(wxString label, unsigned indent) -{ - items.emplace_back(Item { std::move(label), indent, nullptr }); - Refresh(); -} - -ConfigWizardPage* ConfigWizardIndex::active_page() const -{ - if (item_active >= items.size()) { return nullptr; } - - return items[item_active].page; -} - -void ConfigWizardIndex::go_prev() -{ - // Search for a preceiding item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active; i > 0; i--) { - if (items[i - 1].page != nullptr) { - go_to(i - 1); - return; - } - } -} - -void ConfigWizardIndex::go_next() -{ - // Search for a next item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active + 1; i < items.size(); i++) { - if (items[i].page != nullptr) { - go_to(i); - return; - } - } -} - -// This one actually performs the go-to op -void ConfigWizardIndex::go_to(size_t i) -{ - if (i != item_active - && i < items.size() - && items[i].page != nullptr) { - auto *new_active = items[i].page; - auto *former_active = active_page(); - if (former_active != nullptr) { - former_active->Hide(); - } - - item_active = i; - new_active->Show(); - - wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); - AddPendingEvent(evt); - - Refresh(); - - new_active->on_activate(); - } -} - -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); - return; - } - } -} - -void ConfigWizardIndex::clear() -{ - auto *former_active = active_page(); - if (former_active != nullptr) { former_active->Hide(); } - - items.clear(); - item_active = NO_ITEM; -} - -void ConfigWizardIndex::on_paint(wxPaintEvent & evt) -{ - const auto size = GetClientSize(); - if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } - - wxPaintDC dc(this); - - const auto bullet_w = bullet_black.bmp().GetSize().GetWidth(); - const auto bullet_h = bullet_black.bmp().GetSize().GetHeight(); - const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; - const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; - const int yinc = item_height(); - - int index_width = 0; - - unsigned y = 0; - for (size_t i = 0; i < items.size(); i++) { - const Item& item = items[i]; - unsigned x = em_w/2 + item.indent * em_w; - - if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { - dc.DrawBitmap(bullet_blue.bmp(), x, y + yoff_icon, false); - } - else if (i < item_active) { dc.DrawBitmap(bullet_black.bmp(), x, y + yoff_icon, false); } - else if (i > item_active) { dc.DrawBitmap(bullet_white.bmp(), x, y + yoff_icon, false); } - - x += + bullet_w + em_w/2; - const auto text_size = dc.GetTextExtent(item.label); - dc.SetTextForeground(wxGetApp().get_label_clr_default()); - dc.DrawText(item.label, x, y + yoff_text); - - y += yinc; - index_width = std::max(index_width, (int)x + text_size.x); - } - - //draw logo - if (int y = size.y - bg.GetBmpHeight(); y>=0) { - dc.DrawBitmap(bg.bmp(), 0, y, false); - index_width = std::max(index_width, bg.GetBmpWidth() + em_w / 2); - } - - if (GetMinSize().x < index_width) { - CallAfter([this, index_width]() { - SetMinSize(wxSize(index_width, GetMinSize().y)); - Refresh(); - }); - } -} - -void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) -{ - const wxClientDC dc(this); - const wxPoint pos = evt.GetLogicalPosition(dc); - - const ssize_t item_hover_new = pos.y / item_height(); - - if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { - item_hover = item_hover_new; - Refresh(); - } - - evt.Skip(); -} - -void ConfigWizardIndex::msw_rescale() -{ - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - bg.msw_rescale(); - SetMinSize(bg.bmp().GetSize()); - - bullet_black.msw_rescale(); - bullet_blue.msw_rescale(); - bullet_white.msw_rescale(); - Refresh(); -} - - -// Materials - -const std::string Materials::UNKNOWN = "(Unknown)"; - -void Materials::push(const Preset *preset) -{ - presets.emplace_back(preset); - types.insert(technology & T_FFF - ? Materials::get_filament_type(preset) - : Materials::get_material_type(preset)); -} - -void Materials::add_printer(const Preset* preset) -{ - printers.insert(preset); -} - -void Materials::clear() -{ - presets.clear(); - types.clear(); - printers.clear(); - compatibility_counter.clear(); -} - -const std::string& Materials::appconfig_section() const -{ - return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; -} - -const std::string& Materials::get_type(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); -} - -const std::string& Materials::get_vendor(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); -} - -const std::string& Materials::get_filament_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_type"); - if (opt != nullptr && opt->values.size() > 0) { - return opt->values[0]; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_filament_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -const std::string& Materials::get_material_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_type"); - if (opt != nullptr) { - return opt->value; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_material_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -// priv - -static const std::unordered_map> legacy_preset_map {{ - { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, - { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, - { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, -}}; - -void ConfigWizard::priv::load_pages() -{ - wxWindowUpdateLocker freeze_guard(q); - (void)freeze_guard; - - const ConfigWizardPage *former_active = index->active_page(); - - index->clear(); - - index->add_page(page_welcome); - - // Printers - if (!only_sla_mode) - index->add_page(page_fff); - index->add_page(page_msla); - if (!only_sla_mode) { - index->add_page(page_vendors); - for (const auto &pages : pages_3rdparty) { - for ( PagePrinters* page : { pages.second.first, pages.second.second }) - if (page && 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); - index->add_page(page_diams); - 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); } - - // there should to be selected at least one printer - btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); - - index->add_page(page_update); - index->add_page(page_reload_from_disk); -#ifdef _WIN32 - index->add_page(page_files_association); -#endif // _WIN32 - index->add_page(page_mode); - - index->go_to(former_active); // Will restore the active item/page if possible - - q->Layout(); -// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig - q->Refresh(); -} - -void ConfigWizard::priv::init_dialog_size() -{ - // Clamp the Wizard size based on screen dimensions - - const auto idx = wxDisplay::GetFromWindow(q); - wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); - - const auto disp_rect = display.GetClientArea(); - wxRect window_rect( - disp_rect.x + disp_rect.width / 20, - disp_rect.y + disp_rect.height / 20, - 9*disp_rect.width / 10, - 9*disp_rect.height / 10); - - const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution - if (width_hint < window_rect.width) { - window_rect.x += (window_rect.width - width_hint) / 2; - window_rect.width = width_hint; - } - - q->SetSize(window_rect); -} - -void ConfigWizard::priv::load_vendors() -{ - bundles = BundleMap::load(); - - // 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()) { - appconfig_new.set_vendors(*app_config); - } else { - // In case of legacy datadir, try to guess the preference based on the printer preset files that are present - const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; - for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) - if (Slic3r::is_ini_file(dir_entry)) { - auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); - if (needle == legacy_preset_map.end()) { continue; } - - const auto &model = needle->second.first; - const auto &variant = needle->second.second; - appconfig_new.set_variant("PrusaResearch", model, variant, true); - } - } - - // Initialize the is_visible flag in printer Presets - for (auto &pair : bundles) { - pair.second.preset_bundle->load_installed_printers(appconfig_new); - } - - // Copy installed filaments and SLA material names from app_config to appconfig_new - // while resolving current names of profiles, which were renamed in the meantime. - for (PrinterTechnology technology : { ptFFF, ptSLA }) { - const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; - std::map section_new; - if (app_config->has_section(section_name)) { - const std::map §ion_old = app_config->get_section(section_name); - for (const auto& material_name_and_installed : section_old) - if (material_name_and_installed.second == "1") { - // Material is installed. Resolve it in bundles. - size_t num_found = 0; - const std::string &material_name = material_name_and_installed.first; - for (auto &bundle : bundles) { - const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); - const Preset *preset = materials.find_preset(material_name); - if (preset == nullptr) { - // Not found. Maybe the material preset is there, bu it was was renamed? - const std::string *new_name = materials.get_preset_name_renamed(material_name); - if (new_name != nullptr) - preset = materials.find_preset(*new_name); - } - if (preset != nullptr) { - // Materal preset was found, mark it as installed. - section_new[preset->name] = "1"; - ++ num_found; - } - } - if (num_found == 0) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; - else if (num_found > 1) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; - } - } - appconfig_new.set_section(section_name, section_new); - }; -} - -void ConfigWizard::priv::add_page(ConfigWizardPage *page) -{ - const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; - hscroll_sizer->Add(page, proportion, wxEXPAND); - all_pages.push_back(page); -} - -void ConfigWizard::priv::enable_next(bool enable) -{ - btn_next->Enable(enable); - btn_finish->Enable(enable); -} - -void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) -{ - switch (start_page) { - case ConfigWizard::SP_PRINTERS: - index->go_to(page_fff); - btn_next->SetFocus(); - break; - case ConfigWizard::SP_FILAMENTS: - index->go_to(page_filaments); - btn_finish->SetFocus(); - break; - case ConfigWizard::SP_MATERIALS: - index->go_to(page_sla_materials); - btn_finish->SetFocus(); - break; - default: - index->go_to(page_welcome); - btn_next->SetFocus(); - break; - } -} - -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; } - - bool is_fff_technology = false; - bool is_sla_technology = false; - - for (auto& model: vendor->models) - { - if (!is_fff_technology && model.technology == ptFFF) - is_fff_technology = true; - if (!is_sla_technology && model.technology == ptSLA) - is_sla_technology = true; - } - - PagePrinters* pageFFF = nullptr; - PagePrinters* pageSLA = nullptr; - - if (is_fff_technology) { - pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); - add_page(pageFFF); - } - - if (is_sla_technology) { - pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); - add_page(pageSLA); - } - - pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); - } -} - -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::update_materials(Technology technology) -{ - if (any_fff_selected && (technology & T_FFF)) { - filaments.clear(); - aliases_fff.clear(); - // Iterate filaments in all bundles - for (const auto &pair : bundles) { - for (const auto &filament : pair.second.preset_bundle->filaments) { - // Check if filament is already added - if (filaments.containts(&filament)) - continue; - // Iterate printers in all bundles - for (const auto &printer : pair.second.preset_bundle->printers) { - if (!printer.is_visible || printer.printer_technology() != ptFFF) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { - if (!filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } - filaments.add_printer(&printer); - } - } - - } - } - // count compatible printers - for (const auto& preset : filaments.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < filaments.presets.size(); ++i) { - if (preset->alias == filaments.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : filaments.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - filaments.compatibility_counter.emplace_back(preset->alias, counter); - } - } - - if (any_sla_selected && (technology & T_SLA)) { - sla_materials.clear(); - aliases_sla.clear(); - - // Iterate SLA materials in all bundles - for (const auto &pair : bundles) { - for (const auto &material : pair.second.preset_bundle->sla_materials) { - // Check if material is already added - if (sla_materials.containts(&material)) - continue; - // Iterate printers in all bundles - // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. - for (const auto& printer : pair.second.preset_bundle->printers) { - if(!printer.is_visible || printer.printer_technology() != ptSLA) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - // Check if material is already added - if(!sla_materials.containts(&material)) { - sla_materials.push(&material); - if (!material.alias.empty()) - aliases_sla[material.alias].insert(material.name); - } - sla_materials.add_printer(&printer); - } - } - } - } - // count compatible printers - for (const auto& preset : sla_materials.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < sla_materials.presets.size(); ++i) { - if(preset->alias == sla_materials.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : sla_materials.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - sla_materials.compatibility_counter.emplace_back(preset->alias, counter); - } - } -} - -void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) -{ - custom_printer_selected = custom_wanted; - load_pages(); -} - -void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) -{ - if (check_sla_selected() != any_sla_selected || - check_fff_selected() != any_fff_selected) { - any_fff_selected = check_fff_selected(); - any_sla_selected = check_sla_selected(); - - load_pages(); - } - - // Update the is_visible flag on relevant printer profiles - for (auto &pair : bundles) { - if (pair.first != evt.vendor_id) { continue; } - - for (auto &preset : pair.second.preset_bundle->printers) { - if (preset.config.opt_string("printer_model") == evt.model_id - && preset.config.opt_string("printer_variant") == evt.variant_name) { - preset.is_visible = evt.enable; - } - } - - // When a printer model is picked, but there is no material installed compatible with this printer model, - // install default materials for selected printer model silently. - check_and_install_missing_materials(page->technology, evt.model_id); - } - - if (page->technology & T_FFF) { - page_filaments->clear(); - } else if (page->technology & T_SLA) { - page_sla_materials->clear(); - } -} - -void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) -{ - PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - for (const std::string& material : printer_model.default_materials) - appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); -} - -void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) -{ - PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - const std::string &appconfig_section = page_materials->materials->appconfig_section(); - - // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. - // Filament is selected on same page for all printers of same technology. - /* - auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) - { - const std::string vendor_id = page_printers->get_vendor_id(); - for (auto& pair : bundles) - if (pair.first == vendor_id) - for (const VendorProfile::PrinterModel *printer_model : printer_models) - for (const std::string &material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - }; - - PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; - select_default_materials_for_printer_page(page_printers, technology); - - for (const auto& printer : pages_3rdparty) - { - page_printers = technology & T_FFF ? printer.second.first : printer.second.second; - if (page_printers) - select_default_materials_for_printer_page(page_printers, technology); - } - */ - - // Iterate printer_models and select default materials. If none available -> msg to user. - std::vector models_without_default; - for (const VendorProfile::PrinterModel* printer_model : printer_models) { - if (printer_model->default_materials.empty()) { - models_without_default.emplace_back(printer_model); - } else { - for (const std::string& material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - } - } - - if (!models_without_default.empty()) { - std::string printer_names = "\n\n"; - for (const VendorProfile::PrinterModel* printer_model : models_without_default) { - printer_names += printer_model->name + "\n"; - } - printer_names += "\n\n"; - std::string message = (technology & T_FFF ? - GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : - GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); - MessageDialog msg(q, message, _L("Notice"), wxOK); - msg.ShowModal(); - } - - update_materials(technology); - ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); -} - -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"); - - for (PagePrinters* page : { it->second.first, it->second.second }) - if (page) { - if (page->install && !install) - page->select_all(false); - page->install = install; - // if some 3rd vendor is selected, select first printer for them - if (install) - page->printer_pickers[0]->select_one(0, true); - page->Layout(); - } - - load_pages(); -} - -bool ConfigWizard::priv::on_bnt_finish() -{ - wxBusyCursor wait; - /* When Filaments or Sla Materials pages are activated, - * materials for this pages are automaticaly updated and presets are reloaded. - * - * But, if _Finish_ button was clicked without activation of those pages - * (for example, just some printers were added/deleted), - * than last changes wouldn't be updated for filaments/materials. - * SO, do that before close of Wizard - */ - update_materials(T_ANY); - if (any_fff_selected) - page_filaments->reload_presets(); - if (any_sla_selected) - page_sla_materials->reload_presets(); - - // theres no need to check that filament is selected if we have only custom printer - if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; - // check, that there is selected at least one filament/material - return check_and_install_missing_materials(T_ANY); -} - -// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed -// for each Printer preset of each Printer Model installed. -// -// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. -// Otherwise the user is quieried whether to install the missing default materials or not. -// -// Return true if the tested Printer Models already had materials installed. -// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these -// respective Printer Models or not. -bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) -{ - // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, - // which is compatible with it. - const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) - { - const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); - std::set printer_models_without_material; - for (const auto &pair : bundles) { - const PresetCollection &materials = pair.second.preset_bundle->materials(technology); - for (const auto &printer : pair.second.preset_bundle->printers) { - if (printer.is_visible && printer.printer_technology() == technology) { - const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); - assert(printer_model != nullptr); - if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && - printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { - bool has_material = false; - for (const auto& preset : appconfig_presets) { - if (preset.second == "1") { - const Preset *material = materials.find_preset(preset.first, false); - if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - has_material = true; - break; - } - } - } - if (! has_material) - printer_models_without_material.insert(printer_model); - } - } - } - } - assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); - return printer_models_without_material; - }; - - const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) - { - //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); - MessageDialog msg(q, message, _L("Notice"), wxYES_NO); - if (msg.ShowModal() == wxID_YES) - select_default_materials_for_printer_models(technology, printer_models); - }; - - const auto printer_model_list = [](const std::set &printer_models) -> wxString { - wxString out; - for (const VendorProfile::PrinterModel *printer_model : printer_models) { - wxString name = from_u8(printer_model->name); - out += "\t\t"; - out += name; - out += "\n"; - } - return out; - }; - - if (any_fff_selected && (technology & T_FFF)) { - std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following FFF printer models have no filament selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default filaments for these FFF printer models?"), - printer_models_without_material, - T_FFF); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); - return false; - } - } - - if (any_sla_selected && (technology & T_SLA)) { - std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following SLA printer models have no materials selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default SLA materials for these printer models?"), - printer_models_without_material, - T_SLA); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); - return false; - } - } - - return true; -} - -static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) -{ - auto get_aliases = [](const std::map& data) { - std::set old_aliases; - for (auto item : data) { - const std::string& name = item.first; - size_t pos = name.find("@"); - old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); - } - return old_aliases; - }; - - std::set old_aliases = get_aliases(old_data); - std::set new_aliases = get_aliases(new_data); - std::set diff; - std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); - - return diff; -} - -static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) -{ - std::set diff = get_new_added_presets(old_data, new_data); - if (diff.empty()) - return std::string(); - return *diff.begin(); -} - -bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) -{ - wxString header, caption = _L("Configuration is edited in ConfigWizard"); - const auto enabled_vendors = appconfig_new.vendors(); - const auto enabled_vendors_old = app_config->vendors(); - - bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); - PrinterTechnology preferred_pt = ptAny; - auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { - const auto config = enabled_vendors.find(bundle_name); - PrinterTechnology pt = ptAny; - if (config != enabled_vendors.end()) { - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0) { - pt = model.technology; - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - - if (const auto model_it_old = config_old->second.find(model.id); - model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - } - } - } - return pt; - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); - preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) - continue; - else if (preferred_pt == ptAny) - preferred_pt = pt; - if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) - break; - } - } - - if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) - return false; - - bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); - if (check_unsaved_preset_changes) - header = _L("All user presets will be deleted."); - int act_btns = UnsavedChangesDialog::ActionButtons::KEEP; - if (!check_unsaved_preset_changes) - act_btns |= UnsavedChangesDialog::ActionButtons::SAVE; - - // Install bundles from resources if needed: - std::vector install_bundles; - 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; - for (const auto &model : vendor->second) { size_sum += model.second.size(); } - - if (size_sum > 0) { - // This vendor needs to be installed - install_bundles.emplace_back(pair.first); - } - } - if (!check_unsaved_preset_changes) - if ((check_unsaved_preset_changes = install_bundles.size() > 0)) - header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); - -#ifdef __linux__ - // Desktop integration on Linux - if (page_welcome->integrate_desktop()) - DesktopIntegrationDialog::perform_desktop_integration(); -#endif - - // Decide whether to create snapshot based on run_reason and the reset profile checkbox - bool snapshot = true; - Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; - switch (run_reason) { - case ConfigWizard::RR_DATA_EMPTY: - snapshot = false; - break; - case ConfigWizard::RR_DATA_LEGACY: - snapshot = true; - break; - case ConfigWizard::RR_DATA_INCOMPAT: - // In this case snapshot has already been taken by - // PresetUpdater with the appropriate reason - snapshot = false; - break; - case ConfigWizard::RR_USER: - snapshot = page_welcome->reset_user_profile(); - snapshot_reason = Snapshot::SNAPSHOT_USER; - break; - } - - if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) - return false; - - if (check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - - if (install_bundles.size() > 0) { - // Install bundles from resources. - // Don't create snapshot - we've already done that above if applicable. - if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) - return false; - } else { - BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; - } - - if (page_welcome->reset_user_profile()) { - BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; - preset_bundle->reset(true); - } - - std::string preferred_model; - std::string preferred_variant; - auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { - const auto config = enabled_vendors.find(bundle_name); - if (config == enabled_vendors.end()) - return std::string(); - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0 && - preferred_pt == model.technology) { - variant = *model_it->second.begin(); - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end()) - return model.id; - const auto model_it_old = config_old->second.find(model.id); - if (model_it_old == config_old->second.end()) - return model.id; - else if (model_it_old->second != model_it->second) { - for (const auto& var : model_it->second) - if (model_it_old->second.find(var) == model_it_old->second.end()) { - variant = var; - return model.id; - } - } - } - } - if (!variant.empty()) - variant.clear(); - return std::string(); - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); - preferred_model.empty()) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); - !preferred_model.empty()) - break; - } - } - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !preferred_model.empty())) { - header = _L("A new Printer was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { - header = _L("Some Printers were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - - std::string first_added_filament, first_added_sla_material; - auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { - if (appconfig_new.has_section(section_name)) { - // get first of new added preset names - const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); - first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); - } - }; - get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); - get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { - header = !first_added_filament.empty() ? - _L("A new filament was installed and it will be activated.") : - _L("A new SLA material was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else { - auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { - return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); - }; - bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); - bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); - if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { - header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - } - - // apply materials in app_config - for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) - app_config->set_section(section_name, appconfig_new.get_section(section_name)); - - app_config->set_vendors(appconfig_new); - - app_config->set("notify_release", page_update->version_check ? "all" : "none"); - app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); - -#ifdef _WIN32 - app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); - app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); -// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); - - if (wxGetApp().is_editor()) { - if (page_files_association->associate_3mf()) - wxGetApp().associate_3mf_files(); - if (page_files_association->associate_stl()) - wxGetApp().associate_stl_files(); - } -// else { -// if (page_files_association->associate_gcode()) -// wxGetApp().associate_gcode_files(); -// } - -#endif // _WIN32 - - page_mode->serialize_mode(app_config); - - if (check_unsaved_preset_changes) - preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, - {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); - - if (!only_sla_mode && page_custom->custom_wanted()) { - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) - return false; - - page_firmware->apply_custom_config(*custom_config); - page_bed->apply_custom_config(*custom_config); - page_diams->apply_custom_config(*custom_config); - page_temps->apply_custom_config(*custom_config); - - const std::string profile_name = page_custom->profile_name(); - preset_bundle->load_config_from_wizard(profile_name, *custom_config); - } - - // Update the selections from the compatibilty. - preset_bundle->export_selections(*app_config); - - return true; -} -void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) -{ - const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; - - auto update = [this, add](const std::string& s, const std::string& key) { - assert(! s.empty()); - if (add) - appconfig_new.set(s, key, "1"); - else - appconfig_new.erase(s, key); - }; - - // add or delete presets had a same alias - auto it = aliases.find(alias_key); - if (it != aliases.end()) - for (const std::string& name : it->second) - update(section, name); -} - -bool ConfigWizard::priv::check_fff_selected() -{ - bool ret = page_fff->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.first) // FFF page - ret |= printer.second.first->any_selected(); - return ret; -} - -bool ConfigWizard::priv::check_sla_selected() -{ - bool ret = page_msla->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.second) // SLA page - ret |= printer.second.second->any_selected(); - return ret; -} - - -// Public - -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()); - - p->load_vendors(); - p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ - "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", - })); - - p->index = new ConfigWizardIndex(this); - - auto *vsizer = new wxBoxSizer(wxVERTICAL); - auto *topsizer = new wxBoxSizer(wxHORIZONTAL); - auto* hline = new StaticLine(this); - p->btnsizer = new wxBoxSizer(wxHORIZONTAL); - - // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. - // Later, we compare that to the size of the current screen and set minimum width based on that (see below). - p->hscroll = new wxScrolledWindow(this); - p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); - p->hscroll->SetSizer(p->hscroll_sizer); - - topsizer->Add(p->index, 0, wxEXPAND); - topsizer->AddSpacer(INDEX_MARGIN); - topsizer->Add(p->hscroll, 1, wxEXPAND); - - p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); - p->btnsizer->Add(p->btn_sel_all); - - p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); - p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); - p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); - p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac - p->btnsizer->AddStretchSpacer(); - p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); - - wxGetApp().UpdateDarkUI(p->btn_sel_all); - wxGetApp().UpdateDarkUI(p->btn_prev); - wxGetApp().UpdateDarkUI(p->btn_next); - wxGetApp().UpdateDarkUI(p->btn_finish); - wxGetApp().UpdateDarkUI(p->btn_cancel); - - 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->only_sla_mode = !p->page_fff->has_printers; - if (!p->only_sla_mode) { - p->add_page(p->page_fff); - p->page_fff->is_primary_printer_page = true; - } - - - p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); - p->add_page(p->page_msla); - if (p->only_sla_mode) { - p->page_msla->is_primary_printer_page = true; - } - - if (!p->only_sla_mode) { - // Pages for 3rd party vendors - p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors - p->add_page(p->page_vendors = new PageVendors(this)); - p->add_page(p->page_custom = new PageCustom(this)); - p->custom_printer_selected = p->page_custom->custom_wanted(); - } - - p->any_sla_selected = p->check_sla_selected(); - p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); - - p->update_materials(T_ANY); - if (!p->only_sla_mode) - p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, - _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); - - p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, - _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); - - - p->add_page(p->page_update = new PageUpdate(this)); - p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); -#ifdef _WIN32 - p->add_page(p->page_files_association = new PageFilesAssociation(this)); -#endif // _WIN32 - p->add_page(p->page_mode = new PageMode(this)); - p->add_page(p->page_firmware = new PageFirmware(this)); - p->add_page(p->page_bed = new PageBedShape(this)); - p->add_page(p->page_diams = new PageDiameters(this)); - p->add_page(p->page_temps = new PageTemperatures(this)); - - p->load_pages(); - p->index->go_to(size_t{0}); - - vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); - vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); - vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); - - SetSizer(vsizer); - SetSizerAndFit(vsizer); - - // We can now enable scrolling on hscroll - p->hscroll->SetScrollRate(30, 30); - - on_window_geometry(this, [this]() { - p->init_dialog_size(); - }); - - p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); - - p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - // check, that there is selected at least one filament/material - ConfigWizardPage* active_page = this->p->index->active_page(); - if (// Leaving the filaments or SLA materials page and - (active_page == p->page_filaments || active_page == p->page_sla_materials) && - // some Printer models had no filament or SLA material selected. - ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) - // In that case don't leave the page and the function above queried the user whether to install default materials. - return; - this->p->index->go_next(); - }); - - p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - if (p->on_bnt_finish()) - this->EndModal(wxID_OK); - }); - - p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { - p->any_sla_selected = true; - p->load_pages(); - p->page_fff->select_all(true, false); - p->page_msla->select_all(true, false); - p->index->go_to(p->page_mode); - }); - - p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { - const bool is_last = p->index->active_is_last(); - p->btn_next->Show(! is_last); - if (is_last) - p->btn_finish->SetFocus(); - - Layout(); - }); - - if (wxLinux_gtk3) - this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { - ConfigWizardPage* active_page = p->index->active_page(); - if (!active_page) - return; - for (auto page : p->all_pages) - if (page != active_page) - page->Hide(); - // update best size for the dialog after hiding of the non-active pages - vsizer->SetSizeHints(this); - // set initial dialog size - p->init_dialog_size(); - }); -} - -ConfigWizard::~ConfigWizard() {} - -bool ConfigWizard::run(RunReason reason, StartPage start_page) -{ - BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; - - GUI_App &app = wxGetApp(); - - p->set_run_reason(reason); - p->set_start_page(start_page); - - if (ShowModal() == wxID_OK) { - bool apply_keeped_changes = false; - if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) - return false; - - if (apply_keeped_changes) - app.apply_keeped_preset_modifications(); - - app.app_config->set_legacy_datadir(false); - app.update_mode(); - app.obj_manipul()->update_ui_from_settings(); - BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; - return true; - } else { - BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; - return false; - } -} - -const wxString& ConfigWizard::name(const bool from_menu/* = false*/) -{ - // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. - // Note: Don't call _() macro here. - // This function just return the current name according to the OS. - // Translation is implemented inside GUI_App::add_config_menu() -#if __APPLE__ - static const wxString config_wizard_name = L("Configuration Assistant"); - static const wxString config_wizard_name_menu = L("Configuration &Assistant"); -#else - static const wxString config_wizard_name = L("Configuration Wizard"); - static const wxString config_wizard_name_menu = L("Configuration &Wizard"); -#endif - return from_menu ? config_wizard_name_menu : config_wizard_name; -} - -void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) -{ - p->index->msw_rescale(); - - const int em = em_unit(); - - msw_buttons_rescale(this, em, { wxID_APPLY, - wxID_CANCEL, - p->btn_sel_all->GetId(), - p->btn_next->GetId(), - p->btn_prev->GetId() }); - - for (auto printer_picker: p->page_fff->printer_pickers) - msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); - - p->init_dialog_size(); - - Refresh(); -} - -void ConfigWizard::on_sys_color_changed() -{ - wxGetApp().UpdateDlgDarkUI(this); - Refresh(); -} - -} -} +// FIXME: extract absolute units -> em + +#include "ConfigWizard_private.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE + +#include "libslic3r/Platform.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/libslic3r.h" +#include "libslic3r/Model.hpp" +#include "libslic3r/Color.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GUI_Utils.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "Field.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" +#include "format.hpp" +#include "MsgDialog.hpp" +#include "UnsavedChangesDialog.hpp" + +#if defined(__linux__) && defined(__WXGTK3__) +#define wxLinux_gtk3 true +#else +#define wxLinux_gtk3 false +#endif //defined(__linux__) && defined(__WXGTK3__) + +namespace Slic3r { +namespace GUI { + + +using Config::Snapshot; +using Config::SnapshotDB; + + +// Configuration data structures extensions needed for the wizard + +bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) +{ + this->preset_bundle = std::make_unique(); + this->is_in_resources = ais_in_resources; + this->is_prusa_bundle = ais_prusa_bundle; + + std::string path_string = source_path.string(); + // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( + path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + UNUSED(config_substitutions); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); + auto first_vendor = preset_bundle->vendors.begin(); + if (first_vendor == preset_bundle->vendors.end()) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; + return false; + } + if (presets_loaded == 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; + return false; + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; + this->vendor_profile = &first_vendor->second; + return true; +} + +Bundle::Bundle(Bundle &&other) + : 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; + + 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(); + + 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; + if (prusa_bundle.load(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; + if (bundle.load(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 Slic3r::RuntimeError("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 +{ + std::string vendor_id; + std::string model_id; + std::string variant_name; + bool enable; + + PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) + : wxEvent(winid, eventType) + , vendor_id(std::move(vendor_id)) + , model_id(std::move(model_id)) + , variant_name(std::move(variant_name)) + , enable(enable) + {} + + virtual wxEvent *Clone() const + { + return new PrinterPickerEvent(*this); + } +}; + +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) + , width(0) +{ + wxGetApp().UpdateDarkUI(this); + const auto &models = vendor.models; + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + const auto font_title = GetFont().MakeBold().Scaled(1.3f); + const auto font_name = GetFont().MakeBold(); + const auto font_alt_nozzle = GetFont().Scaled(0.9f); + + // wxGrid appends widgets by rows, but we need to construct them in columns. + // These vectors are used to hold the elements so that they can be appended in the right order. + std::vector titles; + std::vector bitmaps; + std::vector variants_panels; + + int max_row_width = 0; + int current_row_width = 0; + + bool is_variants = false; + + for (const auto &model : models) { + if (! filter(model)) { continue; } + + wxBitmap bitmap; + int bitmap_width = 0; + auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { + if (wxFileExists(bitmap_file)) { + bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); + bitmap_width = bitmap.GetWidth(); + return true; + } + return false; + }; + if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { + if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { + BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") + % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") + % vendor.id + % model.id; + load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); + } + } + auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + title->SetFont(font_name); + const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); + title->Wrap(wrap_width); + + current_row_width += wrap_width; + if (titles.size() % max_cols == max_cols - 1) { + max_row_width = std::max(max_row_width, current_row_width); + current_row_width = 0; + } + + titles.push_back(title); + + auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); + bitmaps.push_back(bitmap_widget); + + auto *variants_panel = new wxPanel(this); + wxGetApp().UpdateDarkUI(variants_panel); + auto *variants_sizer = new wxBoxSizer(wxVERTICAL); + variants_panel->SetSizer(variants_sizer); + const auto model_id = model.id; + + for (size_t i = 0; i < model.variants.size(); i++) { + const auto &variant = model.variants[i]; + + const auto label = model.technology == ptFFF + ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) + : from_u8(model.name); + + if (i == 1) { + auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); + alt_label->SetFont(font_alt_nozzle); + variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); + is_variants = true; + } + + auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); + i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); + + const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); + cbox->SetValue(enabled); + + variants_sizer->Add(cbox, 0, wxBOTTOM, 3); + + cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { + on_checkbox(cbox, event.IsChecked()); + }); + } + + variants_panels.push_back(variants_panel); + } + + width = std::max(max_row_width, current_row_width); + + const size_t cols = std::min(max_cols, titles.size()); + + auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); + printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); + + if (titles.size() > 0) { + const size_t odd_items = titles.size() % cols; + + for (size_t i = 0; i < titles.size() - odd_items; i += cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } + + // Add separator space to multiliners + if (titles.size() > cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } + } + } + if (odd_items > 0) { + const size_t rem = titles.size() - odd_items; + + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } + } + } + + auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); + if (! title.IsEmpty()) { + auto *title_widget = new wxStaticText(this, wxID_ANY, title); + title_widget->SetFont(font_title); + title_sizer->Add(title_widget); + } + title_sizer->AddStretchSpacer(); + + if (titles.size() > 1 || is_variants) { + // It only makes sense to add the All / None buttons if there's multiple printers + // All Standard button is added when there are more variants for at least one printer + auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + if (is_variants) + sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); + sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); + sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); + if (is_variants) + title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(sel_all_std); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + // fill button indexes used later for buttons rescaling + if (is_variants) + m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; + else { + sel_all_std->Destroy(); + m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; + } + } + + sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); + sizer->Add(printer_grid); + + SetSizer(sizer); +} + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) + : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) +{} + +void PrinterPicker::select_all(bool select, bool alternates) +{ + for (const auto &cb : cboxes) { + if (cb->GetValue() != select) { + cb->SetValue(select); + on_checkbox(cb, select); + } + } + + if (! select) { alternates = false; } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue() != alternates) { + cb->SetValue(alternates); + on_checkbox(cb, alternates); + } + } +} + +void PrinterPicker::select_one(size_t i, bool select) +{ + if (i < cboxes.size() && cboxes[i]->GetValue() != select) { + cboxes[i]->SetValue(select); + on_checkbox(cboxes[i], select); + } +} + +bool PrinterPicker::any_selected() const +{ + for (const auto &cb : cboxes) { + if (cb->GetValue()) { return true; } + } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue()) { return true; } + } + + return false; +} + +std::set PrinterPicker::get_selected_models() const +{ + std::set ret_set; + + for (const auto& cb : cboxes) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + for (const auto& cb : cboxes_alt) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + return ret_set; +} + +void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) +{ + PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); + AddPendingEvent(evt); +} + + +// Wizard page base + +ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) + : wxPanel(parent->p->hscroll) + , parent(parent) + , shortname(std::move(shortname)) + , indent(indent) +{ + wxGetApp().UpdateDarkUI(this); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + const auto font = GetFont().MakeBold().Scaled(1.5); + text->SetFont(font); + sizer->Add(text, 0, wxALIGN_LEFT, 0); + sizer->AddSpacer(10); + + content = new wxBoxSizer(wxVERTICAL); + sizer->Add(content, 1, wxEXPAND); + + SetSizer(sizer); + + // There is strange layout on Linux with GTK3, + // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 + // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages + if (!wxLinux_gtk3) + this->Hide(); + + Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { + this->Layout(); + event.Skip(); + }); +} + +ConfigWizardPage::~ConfigWizardPage() {} + +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) +{ + // FIXME: scaling + content->AddSpacer(space); +} + +// Wizard pages + +PageWelcome::PageWelcome(ConfigWizard *parent) + : ConfigWizardPage(parent, from_u8((boost::format( +#ifdef __APPLE__ + _utf8(L("Welcome to the %s Configuration Assistant")) +#else + _utf8(L("Welcome to the %s Configuration Wizard")) +#endif + ) % SLIC3R_APP_NAME).str()), _L("Welcome")) + , welcome_text(append_text(from_u8((boost::format( + _utf8(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 + % _utf8(ConfigWizard::name())).str()) + )) + , cbox_reset(append( + new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) + )) + , cbox_integrate(append( + new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) + )) +{ + welcome_text->Hide(); + cbox_reset->Hide(); + cbox_integrate->Hide(); +} + +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); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + if (!DesktopIntegrationDialog::is_integrated()) + cbox_integrate->Show(true); + else + cbox_integrate->Hide(); +#else + cbox_integrate->Hide(); +#endif +} + + +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, + }; + + AppConfig *appconfig = &this->wizard_p()->appconfig_new; + + const auto families = vendor.families(); + for (const auto &family : families) { + const auto filter = [&](const VendorProfile::PrinterModel &model) { + return ((model.technology == ptFFF && technology & T_FFF) + || (model.technology == ptSLA && technology & T_SLA)) + && model.family == family; + }; + + if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { + continue; + } + + const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); + auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); + + 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, evt); + }); + + append(new StaticLine(this)); + + append(picker); + printer_pickers.push_back(picker); + has_printers = true; + } + +} + +void PagePrinters::select_all(bool select, bool alternates) +{ + for (auto picker : printer_pickers) { + picker->select_all(select, alternates); + } +} + +int PagePrinters::get_width() const +{ + return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, + [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); +} + +bool PagePrinters::any_selected() const +{ + for (const auto *picker : printer_pickers) { + if (picker->any_selected()) { return true; } + } + + return false; +} + +std::set PagePrinters::get_selected_models() +{ + std::set ret_set; + + for (const auto *picker : printer_pickers) + { + std::set tmp_models = picker->get_selected_models(); + ret_set.insert(tmp_models.begin(), tmp_models.end()); + } + + return ret_set; +} + +void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) +{ + if (is_primary_printer_page + && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) + && printer_pickers.size() > 0 + && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { + printer_pickers[0]->select_one(0, true); + } +} + + +const std::string PageMaterials::EMPTY; + +PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) + : ConfigWizardPage(parent, std::move(title), std::move(shortname)) + , materials(materials) + , list_printer(new StringList(this, wxLB_MULTIPLE)) + , list_type(new StringList(this)) + , list_vendor(new StringList(this)) + , list_profile(new PresetList(this)) +{ + append_spacer(VERTICAL_SPACING); + + const int em = parent->em_unit(); + const int list_h = 30*em; + + + list_printer->SetMinSize(wxSize(23*em, list_h)); + list_type->SetMinSize(wxSize(13*em, list_h)); + list_vendor->SetMinSize(wxSize(13*em, list_h)); + list_profile->SetMinSize(wxSize(23*em, list_h)); + + + + grid = new wxFlexGridSizer(4, em/2, em); + grid->AddGrowableCol(3, 1); + grid->AddGrowableRow(1, 1); + + grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); + grid->Add(new wxStaticText(this, wxID_ANY, list1name)); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); + + grid->Add(list_printer, 0, wxEXPAND); + grid->Add(list_type, 0, wxEXPAND); + grid->Add(list_vendor, 0, wxEXPAND); + grid->Add(list_profile, 1, wxEXPAND); + + auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); + btn_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(list_printer); + wxGetApp().UpdateDarkUI(list_type); + wxGetApp().UpdateDarkUI(list_vendor); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(btn_sizer, 0, wxALIGN_RIGHT); + + append(grid, 1, wxEXPAND); + + append_spacer(VERTICAL_SPACING); + + html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, + wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); + append(html_window, 0, wxEXPAND); + + list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); + }); + list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + + list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); + list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); + + sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); + sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); + /* + Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); + + list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); + list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); + list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); + */ + reload_presets(); + set_compatible_printers_html_window(std::vector(), false); +} +void PageMaterials::on_paint() +{ +} +void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) +{ + const wxClientDC dc(list_profile); + const wxPoint pos = evt.GetLogicalPosition(dc); + int item = list_profile->HitTest(pos); + on_material_hovered(item); +} +void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) +{} +void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) +{ + on_material_hovered(-1); +} +void PageMaterials::reload_presets() +{ + clear(); + + list_printer->append(_L("(All)"), &EMPTY); + //list_printer->SetLabelMarkup("bald"); + for (const Preset* printer : materials->printers) { + list_printer->append(printer->name, &printer->name); + } + sort_list_data(list_printer, true, false); + if (list_printer->GetCount() > 0) { + list_printer->SetSelection(0); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + update_lists(0, 0, 0); + } + + presets_loaded = true; +} + +void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) +{ + const auto bgr_clr = +#if defined(__APPLE__) + html_window->GetParent()->GetBackgroundColour(); +#else +#if defined(_WIN32) + wxGetApp().get_window_default_clr(); +#else + wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); +#endif +#endif + const auto text_clr = wxGetApp().get_label_clr_default(); + const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); + const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); + wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); + wxString text; + if (all_printers) { + wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "
" + "
" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line + ); + } else { + wxString second_line; + if (!printer_names.empty()) + second_line = (materials->technology == T_FFF ? + _L("Only the following installed printers are compatible with the selected filaments") : + _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line); + for (size_t i = 0; i < printer_names.size(); ++i) + { + text += wxString::Format("", boost::nowide::widen(printer_names[i])); + if (i % 3 == 2) { + text += wxString::Format( + "" + ""); + } + } + text += wxString::Format( + "" + "
%s
" + "
" + "
" + "" + "" + ); + } + + wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); + const int fs = font.GetPointSize(); + int size[] = { fs,fs,fs,fs,fs,fs,fs }; + html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html_window->SetPage(text); +} + +void PageMaterials::clear_compatible_printers_label() +{ + set_compatible_printers_html_window(std::vector(), false); +} + +void PageMaterials::on_material_hovered(int sel_material) +{ + +} + +void PageMaterials::on_material_highlighted(int sel_material) +{ + if (sel_material == last_hovered_item) + return; + if (sel_material == -1) { + clear_compatible_printers_label(); + return; + } + last_hovered_item = sel_material; + std::vector tabs; + tabs.push_back(std::string()); + tabs.push_back(std::string()); + tabs.push_back(std::string()); + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + { + clear_compatible_printers_label(); + return; + } + //find matching printers + std::vector names; + for (const Preset* printer : materials->printers) { + for (const Preset* material : matching_materials) { + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + names.push_back(printer->name); + break; + } + } + } + set_compatible_printers_html_window(names, names.size() == materials->printers.size()); +} + +void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + wxArrayInt sel_printers; + int sel_printers_count = list_printer->GetSelections(sel_printers); + + // Does our wxWidgets version support operator== for wxArrayInt ? + // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 +#if wxCHECK_VERSION(3, 1, 1) + if (sel_printers != sel_printers_prev) { +#else + auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { + if (arr_first.GetCount() != arr_second.GetCount()) + return false; + for (size_t i = 0; i < arr_first.GetCount(); i++) + if (arr_first[i] != arr_second[i]) + return false; + return true; + }; + if (!are_equal(sel_printers, sel_printers_prev)) { +#endif + + // Refresh type list + list_type->Clear(); + list_type->append(_L("(All)"), &EMPTY); + if (sel_printers_count > 0) { + // If all is selected with other printers + // unselect "all" or all printers depending on last value + if (sel_printers[0] == 0 && sel_printers_count > 1) { + if (last_selected_printer == 0) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + } else { + list_printer->SetSelection(0, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + if (sel_printers[0] != 0) { + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + } else { + //clear selection except "ALL" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + sel_printers_count = list_printer->GetSelections(sel_printers); + + materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + sort_list_data(list_type, true, true); + } + + sel_printers_prev = sel_printers; + sel_type = 0; + sel_type_prev = wxNOT_FOUND; + list_type->SetSelection(sel_type); + list_profile->Clear(); + } + + if (sel_type != sel_type_prev) { + // Refresh vendor list + + // XXX: The vendor list is created with quadratic complexity here, + // but the number of vendors is going to be very small this shouldn't be a problem. + + list_vendor->Clear(); + list_vendor->append(_L("(All)"), &EMPTY); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + // find printer preset + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { + const std::string& vendor = this->materials->get_vendor(p); + if (list_vendor->find(vendor) == wxNOT_FOUND) { + list_vendor->append(vendor, &vendor); + } + }); + } + sort_list_data(list_vendor, true, false); + } + + sel_type_prev = sel_type; + sel_vendor = 0; + sel_vendor_prev = wxNOT_FOUND; + list_vendor->SetSelection(sel_vendor); + list_profile->Clear(); + } + + if (sel_vendor != sel_vendor_prev) { + // Refresh material list + + list_profile->Clear(); + clear_compatible_printers_label(); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + const std::string& vendor = list_vendor->get_data(sel_vendor); + // finst printer preset + std::vector to_list; + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + + materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { + const std::string& section = materials->appconfig_section(); + bool checked = wizard_p()->appconfig_new.has(section, p->name); + bool was_checked = false; + + int cur_i = list_profile->find(p->alias); + if (cur_i == wxNOT_FOUND) { + cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); + to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); + } + else { + was_checked = list_profile->IsChecked(cur_i); + to_list[cur_i].checked = checked || was_checked; + } + list_profile->Check(cur_i, checked || was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + }); + } + sort_list_data(list_profile, to_list); + } + + sel_vendor_prev = sel_vendor; + } + wxGetApp().UpdateDarkUI(list_profile); +} + +void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) +{ +// get data from list +// sort data +// first should be +// then prusa profiles +// then the rest +// in alphabetical order + + std::vector> prusa_profiles; + std::vector> other_profiles; + for (int i = 0 ; i < list->size(); ++i) { + const std::string& data = list->get_data(i); + if (data == EMPTY) // do not sort item + continue; + if (!material_type_ordering && data.find("Prusa") != std::string::npos) + prusa_profiles.push_back(data); + else + other_profiles.push_back(data); + } + if(material_type_ordering) { + + const ConfigOptionDef* def = print_config_def.get("filament_type"); + std::vectorenum_values = def->enum_values; + size_t end_of_sorted = 0; + for (size_t vals = 0; vals < enum_values.size(); vals++) { + for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) + { + // find instead compare because PET vs PETG + if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { + //swap + if(profs != end_of_sorted) { + std::reference_wrapper aux = other_profiles[end_of_sorted]; + other_profiles[end_of_sorted] = other_profiles[profs]; + other_profiles[profs] = aux; + } + end_of_sorted++; + break; + } + } + } + } else { + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + } + + list->Clear(); + if (add_All_item) + list->append(_L("(All)"), &EMPTY); + for (const auto& item : prusa_profiles) + list->append(item, &const_cast(item.get())); + for (const auto& item : other_profiles) + list->append(item, &const_cast(item.get())); +} + +void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) +{ + // sort data + // then prusa profiles + // then the rest + // in alphabetical order + std::vector prusa_profiles; + std::vector other_profiles; + //for (int i = 0; i < data.size(); ++i) { + for (const auto& item : data) { + const std::string& name = item.name; + if (name.find("Prusa") != std::string::npos) + prusa_profiles.emplace_back(item); + else + other_profiles.emplace_back(item); + } + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + list->Clear(); + for (size_t i = 0; i < prusa_profiles.size(); ++i) { + list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); + list->Check(i, prusa_profiles[i].checked); + } + for (size_t i = 0; i < other_profiles.size(); ++i) { + list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); + list->Check(i + prusa_profiles.size(), other_profiles[i].checked); + } +} + +void PageMaterials::select_material(int i) +{ + const bool checked = list_profile->IsChecked(i); + + const std::string& alias_key = list_profile->get_data(i); + wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); +} + +void PageMaterials::select_all(bool select) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + for (unsigned i = 0; i < list_profile->GetCount(); i++) { + const bool current = list_profile->IsChecked(i); + if (current != select) { + list_profile->Check(i, select); + select_material(i); + } + } +} + +void PageMaterials::clear() +{ + list_printer->Clear(); + list_type->Clear(); + list_vendor->Clear(); + list_profile->Clear(); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + presets_loaded = false; +} + +void PageMaterials::on_activate() +{ + if (! presets_loaded) { + wizard_p()->update_materials(materials->technology); + reload_presets(); + } + first_paint = true; +} + + +const char *PageCustom::default_profile_name = "My Settings"; + +PageCustom::PageCustom(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) +{ + cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); + tc_profile_name = new wxTextCtrl(this, wxID_ANY, default_profile_name); + auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); + + wxGetApp().UpdateDarkUI(tc_profile_name); + + tc_profile_name->Enable(false); + tc_profile_name->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent &evt) { + if (tc_profile_name->GetValue().IsEmpty()) { + if (profile_name_prev.IsEmpty()) { tc_profile_name->SetValue(default_profile_name); } + else { tc_profile_name->SetValue(profile_name_prev); } + } else { + profile_name_prev = tc_profile_name->GetValue(); + } + evt.Skip(); + }); + + cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { + tc_profile_name->Enable(custom_wanted()); + wizard_p()->on_custom_setup(custom_wanted()); + + }); + + append(cb_custom); + append(label); + append(tc_profile_name); +} + +PageUpdate::PageUpdate(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) + , version_check(true) + , preset_update(true) +{ + const AppConfig *app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); + box_slic3r->SetValue(app_config->get("notify_release") != "none"); + append(box_slic3r); + append_text(wxString::Format(_L( + "If enabled, %s checks for new application versions online. When a new version becomes available, " + "a notification is displayed at the next application startup (never during program usage). " + "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); + + append_spacer(VERTICAL_SPACING); + + auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); + box_presets->SetValue(app_config->get("preset_update") == "1"); + append(box_presets); + append_text(wxString::Format(_L( + "If enabled, %s downloads updates of built-in system presets in the background." + "These updates are downloaded into a separate temporary location." + "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); + const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); + auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); + label_bold->SetFont(boldfont); + label_bold->Wrap(WRAP_WIDTH); + append(label_bold); + append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); + + box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); + box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); +} + +PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) + , full_pathnames(false) +{ + auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); + box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); + append(box_pathnames); + append_text(_L( + "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" + "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." + )); + + box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); +} + +#ifdef _WIN32 +PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) +{ + cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); + cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); +// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); + + append(cb_3mf); + append(cb_stl); +// append(cb_gcode); +} +#endif // _WIN32 + +PageMode::PageMode(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) +{ + append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" + "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " + "The other two offer progressively more sophisticated fine-tuning, " + "they are suitable for advanced and expert users, respectively.")); + + radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); + radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); + radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); + + std::string mode { "simple" }; + wxGetApp().app_config->get("", "view_mode", mode); + + if (mode == "advanced") { radio_advanced->SetValue(true); } + else if (mode == "expert") { radio_expert->SetValue(true); } + else { radio_simple->SetValue(true); } + + append(radio_simple); + append(radio_advanced); + append(radio_expert); + + append_text("\n" + _L("The size of the object can be specified in inches")); + check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); + check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); + append(check_inch); + + on_activate(); +} + +void PageMode::serialize_mode(AppConfig *app_config) const +{ + std::string mode = ""; + + if (radio_simple->GetValue()) { mode = "simple"; } + if (radio_advanced->GetValue()) { mode = "advanced"; } + if (radio_expert->GetValue()) { mode = "expert"; } + + app_config->set("view_mode", mode); + app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); +} + +PageVendors::PageVendors(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) +{ + const AppConfig &appconfig = this->wizard_p()->appconfig_new; + + append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); + + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + for (const auto &pair : wizard_p()->bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); + }); + + const auto &vendors = appconfig.vendors(); + const bool enabled = vendors.find(pair.first) != vendors.end(); + if (enabled) { + cbox->SetValue(true); + + auto pages = wizard_p()->pages_3rdparty.find(vendor->id); + wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); + + for (PagePrinters* page : { pages->second.first, pages->second.second }) + if (page) page->install = true; + } + + append(cbox); + } +} + +PageFirmware::PageFirmware(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) + , gcode_opt(*print_config_def.get("gcode_flavor")) + , gcode_picker(nullptr) +{ + append_text(_L("Choose the type of firmware used by your printer.")); + append_text(_(gcode_opt.tooltip)); + + wxArrayString choices; + choices.Alloc(gcode_opt.enum_labels.size()); + for (const auto &label : gcode_opt.enum_labels) { + choices.Add(label); + } + + gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + wxGetApp().UpdateDarkUI(gcode_picker); + const auto &enum_values = gcode_opt.enum_values; + auto needle = enum_values.cend(); + if (gcode_opt.default_value) { + needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); + } + if (needle != enum_values.cend()) { + gcode_picker->SetSelection(needle - enum_values.cbegin()); + } else { + gcode_picker->SetSelection(0); + } + + append(gcode_picker); +} + +void PageFirmware::apply_custom_config(DynamicPrintConfig &config) +{ + auto sel = gcode_picker->GetSelection(); + if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { + auto *opt = new ConfigOptionEnum(static_cast(sel)); + config.set_key_value("gcode_flavor", opt); + } +} + +PageBedShape::PageBedShape(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) + , shape_panel(new BedShapePanel(this)) +{ + append_text(_L("Set the shape of your printer's bed.")); + + shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), + *wizard_p()->custom_config->option("bed_custom_texture"), + *wizard_p()->custom_config->option("bed_custom_model")); + + append(shape_panel); +} + +void PageBedShape::apply_custom_config(DynamicPrintConfig &config) +{ + const std::vector& points = shape_panel->get_shape(); + const std::string& custom_texture = shape_panel->get_custom_texture(); + const std::string& custom_model = shape_panel->get_custom_model(); + config.set_key_value("bed_shape", new ConfigOptionPoints(points)); + config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); + config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); +} + +static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) +{ + e.Skip(); + wxString str = ctrl->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + if (val == 0.0) + val = def_value; + ctrl->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + ctrl->SetFocus(); + } + else if (was_replaced) + ctrl->SetValue(double_to_string(val)); +} + +class DiamTextCtrl : public wxTextCtrl +{ +public: + DiamTextCtrl(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxBORDER_SIMPLE; +#else + long style = 0; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); + wxGetApp().UpdateDarkUI(this); + } + ~DiamTextCtrl() {} +}; + +PageDiameters::PageDiameters(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) + , diam_nozzle(new DiamTextCtrl(this)) + , diam_filam (new DiamTextCtrl(this)) +{ + auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); + wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); + diam_nozzle->SetValue(value); + + auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); + value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); + diam_filam->SetValue(value); + + diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); + diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); + + append_text(_L("Enter the diameter of your printer's hot end nozzle.")); + + auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); + auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); + auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_nozzle->AddGrowableCol(0, 1); + sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + sizer_nozzle->Add(diam_nozzle); + sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_nozzle); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the diameter of your filament.")); + append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); + + auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); + auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); + auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_filam->AddGrowableCol(0, 1); + sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(diam_filam); + sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_filam); +} + +void PageDiameters::apply_custom_config(DynamicPrintConfig &config) +{ + double val = 0.0; + diam_nozzle->GetValue().ToDouble(&val); + auto *opt_nozzle = new ConfigOptionFloats(1, val); + config.set_key_value("nozzle_diameter", opt_nozzle); + + val = 0.0; + diam_filam->GetValue().ToDouble(&val); + auto * opt_filam = new ConfigOptionFloats(1, val); + config.set_key_value("filament_diameter", opt_filam); + + auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { + char buf[64]; // locales don't matter here (sprintf/atof) + sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); + config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); + }; + + set_extrusion_width("support_material_extrusion_width", 0.35); + set_extrusion_width("top_infill_extrusion_width", 0.40); + set_extrusion_width("first_layer_extrusion_width", 0.42); + + set_extrusion_width("extrusion_width", 0.45); + set_extrusion_width("perimeter_extrusion_width", 0.45); + set_extrusion_width("external_perimeter_extrusion_width", 0.45); + set_extrusion_width("infill_extrusion_width", 0.45); + set_extrusion_width("solid_infill_extrusion_width", 0.45); +} + +class SpinCtrlDouble: public wxSpinCtrlDouble +{ +public: + SpinCtrlDouble(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; +#else + long style = wxSP_ARROW_KEYS; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this->GetText()); +#endif + this->Refresh(); + } + ~SpinCtrlDouble() {} +}; + +PageTemperatures::PageTemperatures(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) + , spin_extr(new SpinCtrlDouble(this)) + , spin_bed (new SpinCtrlDouble(this)) +{ + spin_extr->SetIncrement(5.0); + const auto &def_extr = *print_config_def.get("temperature"); + spin_extr->SetRange(def_extr.min, def_extr.max); + auto *default_extr = def_extr.get_default_value(); + spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); + + spin_bed->SetIncrement(5.0); + const auto &def_bed = *print_config_def.get("bed_temperature"); + spin_bed->SetRange(def_bed.min, def_bed.max); + auto *default_bed = def_bed.get_default_value(); + spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); + + append_text(_L("Enter the temperature needed for extruding your filament.")); + append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); + + auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); + auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); + auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_extr->AddGrowableCol(0, 1); + sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); + sizer_extr->Add(spin_extr); + sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_extr); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); + append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); + + auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); + auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); + auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_bed->AddGrowableCol(0, 1); + sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); + sizer_bed->Add(spin_bed); + sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_bed); +} + +void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) +{ + auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("temperature", opt_extr); + auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("first_layer_temperature", opt_extr1st); + auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("bed_temperature", opt_bed); + auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("first_layer_bed_temperature", opt_bed1st); +} + + +// Index + +ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) + : wxPanel(parent) + , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) + , bullet_black(ScalableBitmap(parent, "bullet_black.png")) + , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) + , bullet_white(ScalableBitmap(parent, "bullet_white.png")) + , item_active(NO_ITEM) + , item_hover(NO_ITEM) + , last_page((size_t)-1) +{ +#ifndef __WXOSX__ + SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#endif //__WXOSX__ + SetMinSize(bg.bmp().GetSize()); + + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); + Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); + Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); + + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { + if (item_hover != -1) { + item_hover = -1; + Refresh(); + } + evt.Skip(); + }); + + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { + if (item_hover >= 0) { go_to(item_hover); } + }); +} + +wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); + +void ConfigWizardIndex::add_page(ConfigWizardPage *page) +{ + last_page = items.size(); + items.emplace_back(Item { page->shortname, page->indent, page }); + Refresh(); +} + +void ConfigWizardIndex::add_label(wxString label, unsigned indent) +{ + items.emplace_back(Item { std::move(label), indent, nullptr }); + Refresh(); +} + +ConfigWizardPage* ConfigWizardIndex::active_page() const +{ + if (item_active >= items.size()) { return nullptr; } + + return items[item_active].page; +} + +void ConfigWizardIndex::go_prev() +{ + // Search for a preceiding item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active; i > 0; i--) { + if (items[i - 1].page != nullptr) { + go_to(i - 1); + return; + } + } +} + +void ConfigWizardIndex::go_next() +{ + // Search for a next item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active + 1; i < items.size(); i++) { + if (items[i].page != nullptr) { + go_to(i); + return; + } + } +} + +// This one actually performs the go-to op +void ConfigWizardIndex::go_to(size_t i) +{ + if (i != item_active + && i < items.size() + && items[i].page != nullptr) { + auto *new_active = items[i].page; + auto *former_active = active_page(); + if (former_active != nullptr) { + former_active->Hide(); + } + + item_active = i; + new_active->Show(); + + wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); + AddPendingEvent(evt); + + Refresh(); + + new_active->on_activate(); + } +} + +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); + return; + } + } +} + +void ConfigWizardIndex::clear() +{ + auto *former_active = active_page(); + if (former_active != nullptr) { former_active->Hide(); } + + items.clear(); + item_active = NO_ITEM; +} + +void ConfigWizardIndex::on_paint(wxPaintEvent & evt) +{ + const auto size = GetClientSize(); + if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } + + wxPaintDC dc(this); + + const auto bullet_w = bullet_black.bmp().GetSize().GetWidth(); + const auto bullet_h = bullet_black.bmp().GetSize().GetHeight(); + const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; + const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; + const int yinc = item_height(); + + int index_width = 0; + + unsigned y = 0; + for (size_t i = 0; i < items.size(); i++) { + const Item& item = items[i]; + unsigned x = em_w/2 + item.indent * em_w; + + if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { + dc.DrawBitmap(bullet_blue.bmp(), x, y + yoff_icon, false); + } + else if (i < item_active) { dc.DrawBitmap(bullet_black.bmp(), x, y + yoff_icon, false); } + else if (i > item_active) { dc.DrawBitmap(bullet_white.bmp(), x, y + yoff_icon, false); } + + x += + bullet_w + em_w/2; + const auto text_size = dc.GetTextExtent(item.label); + dc.SetTextForeground(wxGetApp().get_label_clr_default()); + dc.DrawText(item.label, x, y + yoff_text); + + y += yinc; + index_width = std::max(index_width, (int)x + text_size.x); + } + + //draw logo + if (int y = size.y - bg.GetBmpHeight(); y>=0) { + dc.DrawBitmap(bg.bmp(), 0, y, false); + index_width = std::max(index_width, bg.GetBmpWidth() + em_w / 2); + } + + if (GetMinSize().x < index_width) { + CallAfter([this, index_width]() { + SetMinSize(wxSize(index_width, GetMinSize().y)); + Refresh(); + }); + } +} + +void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) +{ + const wxClientDC dc(this); + const wxPoint pos = evt.GetLogicalPosition(dc); + + const ssize_t item_hover_new = pos.y / item_height(); + + if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { + item_hover = item_hover_new; + Refresh(); + } + + evt.Skip(); +} + +void ConfigWizardIndex::msw_rescale() +{ + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + bg.msw_rescale(); + SetMinSize(bg.bmp().GetSize()); + + bullet_black.msw_rescale(); + bullet_blue.msw_rescale(); + bullet_white.msw_rescale(); + Refresh(); +} + + +// Materials + +const std::string Materials::UNKNOWN = "(Unknown)"; + +void Materials::push(const Preset *preset) +{ + presets.emplace_back(preset); + types.insert(technology & T_FFF + ? Materials::get_filament_type(preset) + : Materials::get_material_type(preset)); +} + +void Materials::add_printer(const Preset* preset) +{ + printers.insert(preset); +} + +void Materials::clear() +{ + presets.clear(); + types.clear(); + printers.clear(); + compatibility_counter.clear(); +} + +const std::string& Materials::appconfig_section() const +{ + return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; +} + +const std::string& Materials::get_type(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); +} + +const std::string& Materials::get_vendor(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); +} + +const std::string& Materials::get_filament_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_type"); + if (opt != nullptr && opt->values.size() > 0) { + return opt->values[0]; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_filament_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +const std::string& Materials::get_material_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_type"); + if (opt != nullptr) { + return opt->value; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_material_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +// priv + +static const std::unordered_map> legacy_preset_map {{ + { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, + { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, +}}; + +void ConfigWizard::priv::load_pages() +{ + wxWindowUpdateLocker freeze_guard(q); + (void)freeze_guard; + + const ConfigWizardPage *former_active = index->active_page(); + + index->clear(); + + index->add_page(page_welcome); + + // Printers + if (!only_sla_mode) + index->add_page(page_fff); + index->add_page(page_msla); + if (!only_sla_mode) { + index->add_page(page_vendors); + for (const auto &pages : pages_3rdparty) { + for ( PagePrinters* page : { pages.second.first, pages.second.second }) + if (page && 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); + index->add_page(page_diams); + 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); } + + // there should to be selected at least one printer + btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); + + index->add_page(page_update); + index->add_page(page_reload_from_disk); +#ifdef _WIN32 +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + if (page_files_association != nullptr) +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + index->add_page(page_files_association); +#endif // _WIN32 + index->add_page(page_mode); + + index->go_to(former_active); // Will restore the active item/page if possible + + q->Layout(); +// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig + q->Refresh(); +} + +void ConfigWizard::priv::init_dialog_size() +{ + // Clamp the Wizard size based on screen dimensions + + const auto idx = wxDisplay::GetFromWindow(q); + wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); + + const auto disp_rect = display.GetClientArea(); + wxRect window_rect( + disp_rect.x + disp_rect.width / 20, + disp_rect.y + disp_rect.height / 20, + 9*disp_rect.width / 10, + 9*disp_rect.height / 10); + + const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution + if (width_hint < window_rect.width) { + window_rect.x += (window_rect.width - width_hint) / 2; + window_rect.width = width_hint; + } + + q->SetSize(window_rect); +} + +void ConfigWizard::priv::load_vendors() +{ + bundles = BundleMap::load(); + + // 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()) { + appconfig_new.set_vendors(*app_config); + } else { + // In case of legacy datadir, try to guess the preference based on the printer preset files that are present + const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; + for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } + + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_new.set_variant("PrusaResearch", model, variant, true); + } + } + + // Initialize the is_visible flag in printer Presets + for (auto &pair : bundles) { + pair.second.preset_bundle->load_installed_printers(appconfig_new); + } + + // Copy installed filaments and SLA material names from app_config to appconfig_new + // while resolving current names of profiles, which were renamed in the meantime. + for (PrinterTechnology technology : { ptFFF, ptSLA }) { + const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; + std::map section_new; + if (app_config->has_section(section_name)) { + const std::map §ion_old = app_config->get_section(section_name); + for (const auto& material_name_and_installed : section_old) + if (material_name_and_installed.second == "1") { + // Material is installed. Resolve it in bundles. + size_t num_found = 0; + const std::string &material_name = material_name_and_installed.first; + for (auto &bundle : bundles) { + const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); + const Preset *preset = materials.find_preset(material_name); + if (preset == nullptr) { + // Not found. Maybe the material preset is there, bu it was was renamed? + const std::string *new_name = materials.get_preset_name_renamed(material_name); + if (new_name != nullptr) + preset = materials.find_preset(*new_name); + } + if (preset != nullptr) { + // Materal preset was found, mark it as installed. + section_new[preset->name] = "1"; + ++ num_found; + } + } + if (num_found == 0) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; + else if (num_found > 1) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; + } + } + appconfig_new.set_section(section_name, section_new); + }; +} + +void ConfigWizard::priv::add_page(ConfigWizardPage *page) +{ + const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; + hscroll_sizer->Add(page, proportion, wxEXPAND); + all_pages.push_back(page); +} + +void ConfigWizard::priv::enable_next(bool enable) +{ + btn_next->Enable(enable); + btn_finish->Enable(enable); +} + +void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) +{ + switch (start_page) { + case ConfigWizard::SP_PRINTERS: + index->go_to(page_fff); + btn_next->SetFocus(); + break; + case ConfigWizard::SP_FILAMENTS: + index->go_to(page_filaments); + btn_finish->SetFocus(); + break; + case ConfigWizard::SP_MATERIALS: + index->go_to(page_sla_materials); + btn_finish->SetFocus(); + break; + default: + index->go_to(page_welcome); + btn_next->SetFocus(); + break; + } +} + +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; } + + bool is_fff_technology = false; + bool is_sla_technology = false; + + for (auto& model: vendor->models) + { + if (!is_fff_technology && model.technology == ptFFF) + is_fff_technology = true; + if (!is_sla_technology && model.technology == ptSLA) + is_sla_technology = true; + } + + PagePrinters* pageFFF = nullptr; + PagePrinters* pageSLA = nullptr; + + if (is_fff_technology) { + pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); + add_page(pageFFF); + } + + if (is_sla_technology) { + pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); + add_page(pageSLA); + } + + pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); + } +} + +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::update_materials(Technology technology) +{ + if (any_fff_selected && (technology & T_FFF)) { + filaments.clear(); + aliases_fff.clear(); + // Iterate filaments in all bundles + for (const auto &pair : bundles) { + for (const auto &filament : pair.second.preset_bundle->filaments) { + // Check if filament is already added + if (filaments.containts(&filament)) + continue; + // Iterate printers in all bundles + for (const auto &printer : pair.second.preset_bundle->printers) { + if (!printer.is_visible || printer.printer_technology() != ptFFF) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + filaments.add_printer(&printer); + } + } + + } + } + // count compatible printers + for (const auto& preset : filaments.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < filaments.presets.size(); ++i) { + if (preset->alias == filaments.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : filaments.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + filaments.compatibility_counter.emplace_back(preset->alias, counter); + } + } + + if (any_sla_selected && (technology & T_SLA)) { + sla_materials.clear(); + aliases_sla.clear(); + + // Iterate SLA materials in all bundles + for (const auto &pair : bundles) { + for (const auto &material : pair.second.preset_bundle->sla_materials) { + // Check if material is already added + if (sla_materials.containts(&material)) + continue; + // Iterate printers in all bundles + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. + for (const auto& printer : pair.second.preset_bundle->printers) { + if(!printer.is_visible || printer.printer_technology() != ptSLA) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + // Check if material is already added + if(!sla_materials.containts(&material)) { + sla_materials.push(&material); + if (!material.alias.empty()) + aliases_sla[material.alias].insert(material.name); + } + sla_materials.add_printer(&printer); + } + } + } + } + // count compatible printers + for (const auto& preset : sla_materials.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < sla_materials.presets.size(); ++i) { + if(preset->alias == sla_materials.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : sla_materials.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + sla_materials.compatibility_counter.emplace_back(preset->alias, counter); + } + } +} + +void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) +{ + custom_printer_selected = custom_wanted; + load_pages(); +} + +void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) +{ + if (check_sla_selected() != any_sla_selected || + check_fff_selected() != any_fff_selected) { + any_fff_selected = check_fff_selected(); + any_sla_selected = check_sla_selected(); + + load_pages(); + } + + // Update the is_visible flag on relevant printer profiles + for (auto &pair : bundles) { + if (pair.first != evt.vendor_id) { continue; } + + for (auto &preset : pair.second.preset_bundle->printers) { + if (preset.config.opt_string("printer_model") == evt.model_id + && preset.config.opt_string("printer_variant") == evt.variant_name) { + preset.is_visible = evt.enable; + } + } + + // When a printer model is picked, but there is no material installed compatible with this printer model, + // install default materials for selected printer model silently. + check_and_install_missing_materials(page->technology, evt.model_id); + } + + if (page->technology & T_FFF) { + page_filaments->clear(); + } else if (page->technology & T_SLA) { + page_sla_materials->clear(); + } +} + +void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) +{ + PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + for (const std::string& material : printer_model.default_materials) + appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); +} + +void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) +{ + PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + const std::string &appconfig_section = page_materials->materials->appconfig_section(); + + // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. + // Filament is selected on same page for all printers of same technology. + /* + auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) + { + const std::string vendor_id = page_printers->get_vendor_id(); + for (auto& pair : bundles) + if (pair.first == vendor_id) + for (const VendorProfile::PrinterModel *printer_model : printer_models) + for (const std::string &material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + }; + + PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; + select_default_materials_for_printer_page(page_printers, technology); + + for (const auto& printer : pages_3rdparty) + { + page_printers = technology & T_FFF ? printer.second.first : printer.second.second; + if (page_printers) + select_default_materials_for_printer_page(page_printers, technology); + } + */ + + // Iterate printer_models and select default materials. If none available -> msg to user. + std::vector models_without_default; + for (const VendorProfile::PrinterModel* printer_model : printer_models) { + if (printer_model->default_materials.empty()) { + models_without_default.emplace_back(printer_model); + } else { + for (const std::string& material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + } + } + + if (!models_without_default.empty()) { + std::string printer_names = "\n\n"; + for (const VendorProfile::PrinterModel* printer_model : models_without_default) { + printer_names += printer_model->name + "\n"; + } + printer_names += "\n\n"; + std::string message = (technology & T_FFF ? + GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : + GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); + MessageDialog msg(q, message, _L("Notice"), wxOK); + msg.ShowModal(); + } + + update_materials(technology); + ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); +} + +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"); + + for (PagePrinters* page : { it->second.first, it->second.second }) + if (page) { + if (page->install && !install) + page->select_all(false); + page->install = install; + // if some 3rd vendor is selected, select first printer for them + if (install) + page->printer_pickers[0]->select_one(0, true); + page->Layout(); + } + + load_pages(); +} + +bool ConfigWizard::priv::on_bnt_finish() +{ + wxBusyCursor wait; + /* When Filaments or Sla Materials pages are activated, + * materials for this pages are automaticaly updated and presets are reloaded. + * + * But, if _Finish_ button was clicked without activation of those pages + * (for example, just some printers were added/deleted), + * than last changes wouldn't be updated for filaments/materials. + * SO, do that before close of Wizard + */ + update_materials(T_ANY); + if (any_fff_selected) + page_filaments->reload_presets(); + if (any_sla_selected) + page_sla_materials->reload_presets(); + + // theres no need to check that filament is selected if we have only custom printer + if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; + // check, that there is selected at least one filament/material + return check_and_install_missing_materials(T_ANY); +} + +// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed +// for each Printer preset of each Printer Model installed. +// +// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. +// Otherwise the user is quieried whether to install the missing default materials or not. +// +// Return true if the tested Printer Models already had materials installed. +// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these +// respective Printer Models or not. +bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) +{ + // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, + // which is compatible with it. + const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) + { + const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); + std::set printer_models_without_material; + for (const auto &pair : bundles) { + const PresetCollection &materials = pair.second.preset_bundle->materials(technology); + for (const auto &printer : pair.second.preset_bundle->printers) { + if (printer.is_visible && printer.printer_technology() == technology) { + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + assert(printer_model != nullptr); + if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && + printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { + bool has_material = false; + for (const auto& preset : appconfig_presets) { + if (preset.second == "1") { + const Preset *material = materials.find_preset(preset.first, false); + if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + } + } + if (! has_material) + printer_models_without_material.insert(printer_model); + } + } + } + } + assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); + return printer_models_without_material; + }; + + const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) + { + //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); + MessageDialog msg(q, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_YES) + select_default_materials_for_printer_models(technology, printer_models); + }; + + const auto printer_model_list = [](const std::set &printer_models) -> wxString { + wxString out; + for (const VendorProfile::PrinterModel *printer_model : printer_models) { + wxString name = from_u8(printer_model->name); + out += "\t\t"; + out += name; + out += "\n"; + } + return out; + }; + + if (any_fff_selected && (technology & T_FFF)) { + std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following FFF printer models have no filament selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default filaments for these FFF printer models?"), + printer_models_without_material, + T_FFF); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); + return false; + } + } + + if (any_sla_selected && (technology & T_SLA)) { + std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following SLA printer models have no materials selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default SLA materials for these printer models?"), + printer_models_without_material, + T_SLA); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); + return false; + } + } + + return true; +} + +static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) +{ + auto get_aliases = [](const std::map& data) { + std::set old_aliases; + for (auto item : data) { + const std::string& name = item.first; + size_t pos = name.find("@"); + old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); + } + return old_aliases; + }; + + std::set old_aliases = get_aliases(old_data); + std::set new_aliases = get_aliases(new_data); + std::set diff; + std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); + + return diff; +} + +static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) +{ + std::set diff = get_new_added_presets(old_data, new_data); + if (diff.empty()) + return std::string(); + return *diff.begin(); +} + +bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) +{ + wxString header, caption = _L("Configuration is edited in ConfigWizard"); + const auto enabled_vendors = appconfig_new.vendors(); + const auto enabled_vendors_old = app_config->vendors(); + + bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); + PrinterTechnology preferred_pt = ptAny; + auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { + const auto config = enabled_vendors.find(bundle_name); + PrinterTechnology pt = ptAny; + if (config != enabled_vendors.end()) { + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0) { + pt = model.technology; + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + + if (const auto model_it_old = config_old->second.find(model.id); + model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + } + } + } + return pt; + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); + preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) + continue; + else if (preferred_pt == ptAny) + preferred_pt = pt; + if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) + break; + } + } + + if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) + return false; + + bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); + if (check_unsaved_preset_changes) + header = _L("All user presets will be deleted."); + int act_btns = UnsavedChangesDialog::ActionButtons::KEEP; + if (!check_unsaved_preset_changes) + act_btns |= UnsavedChangesDialog::ActionButtons::SAVE; + + // Install bundles from resources if needed: + std::vector install_bundles; + 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; + for (const auto &model : vendor->second) { size_sum += model.second.size(); } + + if (size_sum > 0) { + // This vendor needs to be installed + install_bundles.emplace_back(pair.first); + } + } + if (!check_unsaved_preset_changes) + if ((check_unsaved_preset_changes = install_bundles.size() > 0)) + header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); + +#ifdef __linux__ + // Desktop integration on Linux + if (page_welcome->integrate_desktop()) + DesktopIntegrationDialog::perform_desktop_integration(); +#endif + + // Decide whether to create snapshot based on run_reason and the reset profile checkbox + bool snapshot = true; + Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; + switch (run_reason) { + case ConfigWizard::RR_DATA_EMPTY: + snapshot = false; + break; + case ConfigWizard::RR_DATA_LEGACY: + snapshot = true; + break; + case ConfigWizard::RR_DATA_INCOMPAT: + // In this case snapshot has already been taken by + // PresetUpdater with the appropriate reason + snapshot = false; + break; + case ConfigWizard::RR_USER: + snapshot = page_welcome->reset_user_profile(); + snapshot_reason = Snapshot::SNAPSHOT_USER; + break; + } + + if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) + return false; + + if (check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + + if (install_bundles.size() > 0) { + // Install bundles from resources. + // Don't create snapshot - we've already done that above if applicable. + if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) + return false; + } else { + BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; + } + + if (page_welcome->reset_user_profile()) { + BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; + preset_bundle->reset(true); + } + + std::string preferred_model; + std::string preferred_variant; + auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { + const auto config = enabled_vendors.find(bundle_name); + if (config == enabled_vendors.end()) + return std::string(); + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0 && + preferred_pt == model.technology) { + variant = *model_it->second.begin(); + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end()) + return model.id; + const auto model_it_old = config_old->second.find(model.id); + if (model_it_old == config_old->second.end()) + return model.id; + else if (model_it_old->second != model_it->second) { + for (const auto& var : model_it->second) + if (model_it_old->second.find(var) == model_it_old->second.end()) { + variant = var; + return model.id; + } + } + } + } + if (!variant.empty()) + variant.clear(); + return std::string(); + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); + preferred_model.empty()) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); + !preferred_model.empty()) + break; + } + } + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !preferred_model.empty())) { + header = _L("A new Printer was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { + header = _L("Some Printers were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + + std::string first_added_filament, first_added_sla_material; + auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { + if (appconfig_new.has_section(section_name)) { + // get first of new added preset names + const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); + first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); + } + }; + get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); + get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { + header = !first_added_filament.empty() ? + _L("A new filament was installed and it will be activated.") : + _L("A new SLA material was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else { + auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { + return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); + }; + bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); + bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); + if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { + header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + } + + // apply materials in app_config + for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) + app_config->set_section(section_name, appconfig_new.get_section(section_name)); + + app_config->set_vendors(appconfig_new); + + app_config->set("notify_release", page_update->version_check ? "all" : "none"); + app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); + +#ifdef _WIN32 +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + if (page_files_association != nullptr) { +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); + app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); +// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); + + if (wxGetApp().is_editor()) { + if (page_files_association->associate_3mf()) + wxGetApp().associate_3mf_files(); + if (page_files_association->associate_stl()) + wxGetApp().associate_stl_files(); + } +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + } + else { + app_config->set("associate_3mf", "0"); + app_config->set("associate_stl", "0"); +// app_config->set("associate_gcode", "0"); + } +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + +#endif // _WIN32 + + page_mode->serialize_mode(app_config); + + if (check_unsaved_preset_changes) + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, + {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); + + if (!only_sla_mode && page_custom->custom_wanted()) { + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) + return false; + + page_firmware->apply_custom_config(*custom_config); + page_bed->apply_custom_config(*custom_config); + page_diams->apply_custom_config(*custom_config); + page_temps->apply_custom_config(*custom_config); + + const std::string profile_name = page_custom->profile_name(); + preset_bundle->load_config_from_wizard(profile_name, *custom_config); + } + + // Update the selections from the compatibilty. + preset_bundle->export_selections(*app_config); + + return true; +} +void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) +{ + const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; + + auto update = [this, add](const std::string& s, const std::string& key) { + assert(! s.empty()); + if (add) + appconfig_new.set(s, key, "1"); + else + appconfig_new.erase(s, key); + }; + + // add or delete presets had a same alias + auto it = aliases.find(alias_key); + if (it != aliases.end()) + for (const std::string& name : it->second) + update(section, name); +} + +bool ConfigWizard::priv::check_fff_selected() +{ + bool ret = page_fff->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.first) // FFF page + ret |= printer.second.first->any_selected(); + return ret; +} + +bool ConfigWizard::priv::check_sla_selected() +{ + bool ret = page_msla->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.second) // SLA page + ret |= printer.second.second->any_selected(); + return ret; +} + + +// Public + +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()); + + p->load_vendors(); + p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ + "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", + })); + + p->index = new ConfigWizardIndex(this); + + auto *vsizer = new wxBoxSizer(wxVERTICAL); + auto *topsizer = new wxBoxSizer(wxHORIZONTAL); + auto* hline = new StaticLine(this); + p->btnsizer = new wxBoxSizer(wxHORIZONTAL); + + // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. + // Later, we compare that to the size of the current screen and set minimum width based on that (see below). + p->hscroll = new wxScrolledWindow(this); + p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); + p->hscroll->SetSizer(p->hscroll_sizer); + + topsizer->Add(p->index, 0, wxEXPAND); + topsizer->AddSpacer(INDEX_MARGIN); + topsizer->Add(p->hscroll, 1, wxEXPAND); + + p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); + p->btnsizer->Add(p->btn_sel_all); + + p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); + p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); + p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); + p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac + p->btnsizer->AddStretchSpacer(); + p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); + + wxGetApp().UpdateDarkUI(p->btn_sel_all); + wxGetApp().UpdateDarkUI(p->btn_prev); + wxGetApp().UpdateDarkUI(p->btn_next); + wxGetApp().UpdateDarkUI(p->btn_finish); + wxGetApp().UpdateDarkUI(p->btn_cancel); + + 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->only_sla_mode = !p->page_fff->has_printers; + if (!p->only_sla_mode) { + p->add_page(p->page_fff); + p->page_fff->is_primary_printer_page = true; + } + + + p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); + p->add_page(p->page_msla); + if (p->only_sla_mode) { + p->page_msla->is_primary_printer_page = true; + } + + if (!p->only_sla_mode) { + // Pages for 3rd party vendors + p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors + p->add_page(p->page_vendors = new PageVendors(this)); + p->add_page(p->page_custom = new PageCustom(this)); + p->custom_printer_selected = p->page_custom->custom_wanted(); + } + + p->any_sla_selected = p->check_sla_selected(); + p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); + + p->update_materials(T_ANY); + if (!p->only_sla_mode) + p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, + _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); + + p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, + _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); + + + p->add_page(p->page_update = new PageUpdate(this)); + p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); +#ifdef _WIN32 +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + // file association is not possible anymore starting with Win 8 + if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + p->add_page(p->page_files_association = new PageFilesAssociation(this)); +#endif // _WIN32 + p->add_page(p->page_mode = new PageMode(this)); + p->add_page(p->page_firmware = new PageFirmware(this)); + p->add_page(p->page_bed = new PageBedShape(this)); + p->add_page(p->page_diams = new PageDiameters(this)); + p->add_page(p->page_temps = new PageTemperatures(this)); + + p->load_pages(); + p->index->go_to(size_t{0}); + + vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); + vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); + vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); + + SetSizer(vsizer); + SetSizerAndFit(vsizer); + + // We can now enable scrolling on hscroll + p->hscroll->SetScrollRate(30, 30); + + on_window_geometry(this, [this]() { + p->init_dialog_size(); + }); + + p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); + + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + // check, that there is selected at least one filament/material + ConfigWizardPage* active_page = this->p->index->active_page(); + if (// Leaving the filaments or SLA materials page and + (active_page == p->page_filaments || active_page == p->page_sla_materials) && + // some Printer models had no filament or SLA material selected. + ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) + // In that case don't leave the page and the function above queried the user whether to install default materials. + return; + this->p->index->go_next(); + }); + + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + if (p->on_bnt_finish()) + this->EndModal(wxID_OK); + }); + + p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { + p->any_sla_selected = true; + p->load_pages(); + p->page_fff->select_all(true, false); + p->page_msla->select_all(true, false); + p->index->go_to(p->page_mode); + }); + + p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { + const bool is_last = p->index->active_is_last(); + p->btn_next->Show(! is_last); + if (is_last) + p->btn_finish->SetFocus(); + + Layout(); + }); + + if (wxLinux_gtk3) + this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { + ConfigWizardPage* active_page = p->index->active_page(); + if (!active_page) + return; + for (auto page : p->all_pages) + if (page != active_page) + page->Hide(); + // update best size for the dialog after hiding of the non-active pages + vsizer->SetSizeHints(this); + // set initial dialog size + p->init_dialog_size(); + }); +} + +ConfigWizard::~ConfigWizard() {} + +bool ConfigWizard::run(RunReason reason, StartPage start_page) +{ + BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; + + GUI_App &app = wxGetApp(); + + p->set_run_reason(reason); + p->set_start_page(start_page); + + if (ShowModal() == wxID_OK) { + bool apply_keeped_changes = false; + if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) + return false; + + if (apply_keeped_changes) + app.apply_keeped_preset_modifications(); + + app.app_config->set_legacy_datadir(false); + app.update_mode(); + app.obj_manipul()->update_ui_from_settings(); + BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; + return true; + } else { + BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; + return false; + } +} + +const wxString& ConfigWizard::name(const bool from_menu/* = false*/) +{ + // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. + // Note: Don't call _() macro here. + // This function just return the current name according to the OS. + // Translation is implemented inside GUI_App::add_config_menu() +#if __APPLE__ + static const wxString config_wizard_name = L("Configuration Assistant"); + static const wxString config_wizard_name_menu = L("Configuration &Assistant"); +#else + static const wxString config_wizard_name = L("Configuration Wizard"); + static const wxString config_wizard_name_menu = L("Configuration &Wizard"); +#endif + return from_menu ? config_wizard_name_menu : config_wizard_name; +} + +void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) +{ + p->index->msw_rescale(); + + const int em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_APPLY, + wxID_CANCEL, + p->btn_sel_all->GetId(), + p->btn_next->GetId(), + p->btn_prev->GetId() }); + + for (auto printer_picker: p->page_fff->printer_pickers) + msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); + + p->init_dialog_size(); + + Refresh(); +} + +void ConfigWizard::on_sys_color_changed() +{ + wxGetApp().UpdateDlgDarkUI(this); + Refresh(); +} + +} +} diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 4435f836b..af9b63acd 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1,3333 +1,3354 @@ -#include "libslic3r/Technologies.hpp" -#include "GUI_App.hpp" -#include "GUI_Init.hpp" -#include "GUI_ObjectList.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "GUI_Factories.hpp" -#include "format.hpp" -#include "I18N.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Utils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/I18N.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Color.hpp" - -#include "GUI.hpp" -#include "GUI_Utils.hpp" -#include "3DScene.hpp" -#include "MainFrame.hpp" -#include "Plater.hpp" -#include "GLCanvas3D.hpp" - -#include "../Utils/PresetUpdater.hpp" -#include "../Utils/PrintHost.hpp" -#include "../Utils/Process.hpp" -#include "../Utils/MacDarkMode.hpp" -#include "../Utils/AppUpdater.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "ConfigSnapshotDialog.hpp" -#include "FirmwareDialog.hpp" -#include "Preferences.hpp" -#include "Tab.hpp" -#include "SysInfoDialog.hpp" -#include "KBShortcutsDialog.hpp" -#include "UpdateDialogs.hpp" -#include "Mouse3DController.hpp" -#include "RemovableDriveManager.hpp" -#include "InstanceCheck.hpp" -#include "NotificationManager.hpp" -#include "UnsavedChangesDialog.hpp" -#include "SavePresetDialog.hpp" -#include "PrintHostDialogs.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "SendSystemInfoDialog.hpp" - -#include "BitmapCache.hpp" -#include "Notebook.hpp" - -#ifdef __WXMSW__ -#include -#include -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE -#endif -#ifdef _WIN32 -#include -#endif - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -#include -#include -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -// Needed for forcing menu icons back under gtk2 and gtk3 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - #include -#endif - -using namespace std::literals; - -namespace Slic3r { -namespace GUI { - -class MainFrame; - -class SplashScreen : public wxSplashScreen -{ -public: - SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) - : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, -#ifdef __APPLE__ - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP -#else - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR -#endif // !__APPLE__ - ) - { - wxASSERT(bitmap.IsOk()); - - int init_dpi = get_dpi_for_window(this); - this->SetPosition(pos); - this->CenterOnScreen(); - int new_dpi = get_dpi_for_window(this); - - m_scale = (float)(new_dpi) / (float)(init_dpi); - m_main_bitmap = bitmap; - - scale_bitmap(m_main_bitmap, m_scale); - - // init constant texts and scale fonts - init_constant_text(); - - // this font will be used for the action string - m_action_font = m_constant_text.credits_font.Bold(); - - // draw logo and constant info text - Decorate(m_main_bitmap); - } - - void SetText(const wxString& text) - { - set_bitmap(m_main_bitmap); - if (!text.empty()) { - wxBitmap bitmap(m_main_bitmap); - - wxMemoryDC memDC; - memDC.SelectObject(bitmap); - - memDC.SetFont(m_action_font); - memDC.SetTextForeground(wxColour(237, 107, 33)); - memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); - - memDC.SelectObject(wxNullBitmap); - set_bitmap(bitmap); -#ifdef __WXOSX__ - // without this code splash screen wouldn't be updated under OSX - wxYield(); -#endif - } - } - - static wxBitmap MakeBitmap(wxBitmap bmp) - { - if (!bmp.IsOk()) - return wxNullBitmap; - - // create dark grey background for the splashscreen - // It will be 5/3 of the weight of the bitmap - int width = lround((double)5 / 3 * bmp.GetWidth()); - int height = bmp.GetHeight(); - - wxImage image(width, height); - unsigned char* imgdata_ = image.GetData(); - for (int i = 0; i < width * height; ++i) { - *imgdata_++ = 51; - *imgdata_++ = 51; - *imgdata_++ = 51; - } - - wxBitmap new_bmp(image); - - wxMemoryDC memDC; - memDC.SelectObject(new_bmp); - memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); - - return new_bmp; - } - - void Decorate(wxBitmap& bmp) - { - if (!bmp.IsOk()) - return; - - // draw text to the box at the left of the splashscreen. - // this box will be 2/5 of the weight of the bitmap, and be at the left. - int width = lround(bmp.GetWidth() * 0.4); - - // load bitmap for logo - BitmapCache bmp_cache; - int logo_size = lround(width * 0.25); - wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); - - wxCoord margin = int(m_scale * 20); - - wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); - banner_rect.Deflate(margin, 2 * margin); - - // use a memory DC to draw directly onto the bitmap - wxMemoryDC memDc(bmp); - - // draw logo - memDc.DrawBitmap(logo_bmp, margin, margin, true); - - // draw the (white) labels inside of our black box (at the left of the splashscreen) - memDc.SetTextForeground(wxColour(255, 255, 255)); - - memDc.SetFont(m_constant_text.title_font); - memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - - int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); - banner_rect.SetTop(banner_rect.GetTop() + title_height); - banner_rect.SetHeight(banner_rect.GetHeight() - title_height); - - memDc.SetFont(m_constant_text.version_font); - memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); - - memDc.SetFont(m_constant_text.credits_font); - memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); - int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); - int text_height = memDc.GetTextExtent("text").GetY(); - - // calculate position for the dynamic text - int logo_and_header_height = margin + logo_size + title_height + version_height; - m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); - } - -private: - wxBitmap m_main_bitmap; - wxFont m_action_font; - int m_action_line_y_position; - float m_scale {1.0}; - - struct ConstantText - { - wxString title; - wxString version; - wxString credits; - - wxFont title_font; - wxFont version_font; - wxFont credits_font; - - void init(wxFont init_font) - { - // title - title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; - - // dynamically get the version to display - version = _L("Version") + " " + std::string(SLIC3R_VERSION); - - // credits infornation - credits = title + " " + - _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + - _L("Developed by Prusa Research.")+ "\n\n" + - title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + "\n\n" + - _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + - _L("Artwork model by M Boyer"); - - title_font = version_font = credits_font = init_font; - } - } - m_constant_text; - - void init_constant_text() - { - m_constant_text.init(get_default_font(this)); - - // As default we use a system font for current display. - // Scale fonts in respect to banner width - - int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins - - float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); - scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); - - float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); - scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); - - // The width of the credits information string doesn't respect to the banner width some times. - // So, scale credits_font in the respect to the longest string width - int longest_string_width = word_wrap_string(m_constant_text.credits); - float font_scale = (float)text_banner_width / longest_string_width; - scale_font(m_constant_text.credits_font, font_scale); - } - - void set_bitmap(wxBitmap& bmp) - { - m_window->SetBitmap(bmp); - m_window->Refresh(); - m_window->Update(); - } - - void scale_bitmap(wxBitmap& bmp, float scale) - { - if (scale == 1.0) - return; - - wxImage image = bmp.ConvertToImage(); - if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) - return; - - int width = int(scale * image.GetWidth()); - int height = int(scale * image.GetHeight()); - image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); - - bmp = wxBitmap(std::move(image)); - } - - void scale_font(wxFont& font, float scale) - { -#ifdef __WXMSW__ - // Workaround for the font scaling in respect to the current active display, - // not for the primary display, as it's implemented in Font.cpp - // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp - // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) - wxNativeFontInfo nfi= *font.GetNativeFontInfo(); - float pointSizeNew = scale * font.GetPointSize(); - nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); - nfi.pointSize = pointSizeNew; - font = wxFont(nfi); -#else - font.Scale(scale); -#endif //__WXMSW__ - } - - // wrap a string for the strings no longer then 55 symbols - // return extent of the longest string - int word_wrap_string(wxString& input) - { - size_t line_len = 55;// count of symbols in one line - int idx = -1; - size_t cur_len = 0; - - wxString longest_sub_string; - auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { - if (cur_len > longest_sub_str.Len()) - longest_sub_str = input.SubString(i - cur_len + 1, i); - }; - - for (size_t i = 0; i < input.Len(); i++) - { - cur_len++; - if (input[i] == ' ') - idx = i; - if (input[i] == '\n') - { - get_longest_sub_string(longest_sub_string, cur_len, i); - idx = -1; - cur_len = 0; - } - if (cur_len >= line_len && idx >= 0) - { - get_longest_sub_string(longest_sub_string, cur_len, i); - input[idx] = '\n'; - cur_len = i - static_cast(idx); - } - } - - return GetTextExtent(longest_sub_string).GetX(); - } -}; - - -#ifdef __linux__ -bool static check_old_linux_datadir(const wxString& app_name) { - // If we are on Linux and the datadir does not exist yet, look into the old - // location where the datadir was before version 2.3. If we find it there, - // tell the user that he might wanna migrate to the new location. - // (https://github.com/prusa3d/PrusaSlicer/issues/2911) - // To be precise, the datadir should exist, it is created when single instance - // lock happens. Instead of checking for existence, check the contents. - - namespace fs = boost::filesystem; - - std::string new_path = Slic3r::data_dir(); - - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - std::string default_path = (dir + "/" + app_name).ToUTF8().data(); - - if (new_path != default_path) { - // This happens when the user specifies a custom --datadir. - // Do not show anything in that case. - return true; - } - - fs::path data_dir = fs::path(new_path); - if (! fs::is_directory(data_dir)) - return true; // This should not happen. - - int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); - - if (file_count <= 1) { // just cache dir with an instance lock - std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); - - if (fs::is_directory(old_path)) { - wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " - "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" - "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " - "an old %1% configuration directory was detected in \n%3%.\n\n" - "Consider moving the contents of the old directory to the new location in order to access " - "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " - "location again.\n\n" - "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); - wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); - RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); - dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); - if (dlg.ShowModal() != wxID_NO) - return false; - } - } else { - // If the new directory exists, be silent. The user likely already saw the message. - } - return true; -} -#endif - -#ifdef _WIN32 -#if 0 // External Updater is replaced with AppUpdater.cpp -static bool run_updater_win() -{ - // find updater exe - boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; - // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst - std::string msg; - bool res = create_process(path_updater, L"/silent", msg); - if (!res) - BOOST_LOG_TRIVIAL(error) << msg; - return res; -} -#endif // 0 -#endif // _WIN32 - -struct FileWildcards { - std::string_view title; - std::vector file_extensions; -}; - -static const FileWildcards file_wildcards_by_type[FT_SIZE] = { - /* FT_STL */ { "STL files"sv, { ".stl"sv } }, - /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, - /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, - /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, - /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, - /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, - /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, - - /* FT_INI */ { "INI files"sv, { ".ini"sv } }, - /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, - - /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, - - /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, -}; - -#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR -wxString file_wildcards(FileType file_type) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - - // Generate cumulative first item - for (const std::string_view& ext : data.file_extensions) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } - else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); - - // Adds an item for each of the extensions - if (data.file_extensions.size() > 1) { - for (const std::string_view& ext : data.file_extensions) { - title = "*"; - title += ext; - ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); - } - } - - return ret; -} -#else -// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. -// The function accepts a custom extension parameter. If the parameter is provided, the custom extension -// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips -// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). -wxString file_wildcards(FileType file_type, const std::string &custom_extension) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - std::string custom_ext_lower; - - if (! custom_extension.empty()) { - // Generate an extension into the title mask and into the list of extensions. - custom_ext_lower = boost::to_lower_copy(custom_extension); - const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); - if (custom_ext_lower == custom_extension) { - // Add a lower case version. - title = std::string("*") + custom_ext_lower; - mask = title; - // Add an upper case version. - mask += ";*"; - mask += custom_ext_upper; - } else if (custom_ext_upper == custom_extension) { - // Add an upper case version. - title = std::string("*") + custom_ext_upper; - mask = title; - // Add a lower case version. - mask += ";*"; - mask += custom_ext_lower; - } else { - // Add the mixed case version only. - title = std::string("*") + custom_extension; - mask = title; - } - } - - for (const std::string_view &ext : data.file_extensions) - // Only add an extension if it was not added first as the custom extension. - if (ext != custom_ext_lower) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); -} -#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR - -static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) -static void register_win32_dpi_event() -{ - enum { WM_DPICHANGED_ = 0x02e0 }; - - wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { - const int dpi = wParam & 0xffff; - const auto rect = reinterpret_cast(lParam); - const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); - - DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); - win->GetEventHandler()->AddPendingEvent(evt); - - return true; - }); -} -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; - -static void register_win32_device_notification_event() -{ - wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; - switch (wParam) { - case DBT_DEVICEARRIVAL: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { -// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - case DBT_DEVICEREMOVECOMPLETE: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) -// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - default: - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - wchar_t sPath[MAX_PATH]; - if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { - struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); - if (! SHGetPathFromIDList(pidl, sPath)) { - BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; - return false; - } - } - switch (lParam) { - case SHCNE_MEDIAINSERTED: - { - //printf("SHCNE_MEDIAINSERTED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - break; - } - case SHCNE_MEDIAREMOVED: - { - //printf("SHCNE_MEDIAREMOVED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - break; - } - default: -// printf("Unknown\n"); - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); -// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { - if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { - RAWINPUT raw; - UINT rawSize = sizeof(RAWINPUT); - ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); - if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) - return true; - } - return false; - }); - - wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - COPYDATASTRUCT* copy_data_structure = { 0 }; - copy_data_structure = (COPYDATASTRUCT*)lParam; - if (copy_data_structure->dwData == 1) { - LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; - Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); - } - return true; - }); -} -#endif // WIN32 - -static void generic_exception_handle() -{ - // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage - // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 - // - // wxLogError typically goes around exception handling and display an error dialog some time - // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. - // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates - // errors if multiple have been collected and displays just one error message for all of them. - // Otherwise we would get multiple error messages for one missing png, for example. - // - // If a custom error message window (or some other solution) were to be used, it would be necessary - // to turn off wxLogError() usage in wx APIs, most notably in wxImage - // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a - - try { - throw; - } catch (const std::bad_alloc& ex) { - // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) - // and terminate the app so it is at least certain to happen now. - wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " - "If you are sure you have enough RAM on your system, this may also be a bug and we would " - "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); - std::terminate(); - } catch (const boost::io::bad_format_string& ex) { - wxString errmsg = _L("PrusaSlicer has encountered a localization error. " - "Please report to PrusaSlicer team, what language was active and in which scenario " - "this issue happened. Thank you.\n\nThe application will now terminate."); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - std::terminate(); - throw; - } catch (const std::exception& ex) { - wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - throw; - } -} - -void GUI_App::post_init() -{ - assert(initialized()); - if (! this->initialized()) - throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); - - if (this->init_params->start_as_gcodeviewer) { - if (! this->init_params->input_files.empty()) - this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); - } - else { - if (! this->init_params->preset_substitutions.empty()) - show_substitutions_info(this->init_params->preset_substitutions); - -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - this->gui->mainframe->load_config(m_print_config); -#endif - if (! this->init_params->load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - this->mainframe->load_config_file(this->init_params->load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!this->init_params->input_files.empty()) { - const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); - if (!res.empty() && this->init_params->input_files.size() == 1) { - // Update application titlebar when opening a project file - const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) - this->plater()->set_project_filename(from_u8(filename)); - } - } - if (! this->init_params->extra_config.empty()) - this->mainframe->load_config(this->init_params->extra_config); - } - - // show "Did you know" notification - if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) - plater_->get_notification_manager()->push_hint_notification(true); - - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. - if (! this->check_updates(false)) - // Configuration is not compatible and reconfigure was refused by the user. Application is closing. - return; - CallAfter([this] { - bool cw_showed = this->config_wizard_startup(); - this->preset_updater->sync(preset_bundle); - this->app_version_check(false); - if (! cw_showed) { - // The CallAfter is needed as well, without it, GL extensions did not show. - // Also, we only want to show this when the wizard does not, so the new user - // sees something else than "we want something" on the first start. - show_send_system_info_dialog_if_needed(); - } - }); - } - - // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. - app_config->set("version", SLIC3R_VERSION); - app_config->save(); - -#ifdef _WIN32 - // Sets window property to mainframe so other instances can indentify it. - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 -} - -IMPLEMENT_APP(GUI_App) - -GUI_App::GUI_App(EAppMode mode) - : wxApp() - , m_app_mode(mode) - , m_em_unit(10) - , m_imgui(new ImGuiWrapper()) - , m_removable_drive_manager(std::make_unique()) - , m_other_instance_message_handler(std::make_unique()) -{ - //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp - this->init_app_config(); - // init app downloader after path to datadir is set - m_app_updater = std::make_unique(); -} - -GUI_App::~GUI_App() -{ - if (app_config != nullptr) - delete app_config; - - if (preset_bundle != nullptr) - delete preset_bundle; - - if (preset_updater != nullptr) - delete preset_updater; -} - -// If formatted for github, plaintext with OpenGL extensions enclosed into
. -// Otherwise HTML formatted for the system info dialog. -std::string GUI_App::get_gl_info(bool for_github) -{ - return OpenGLManager::get_gl_info().to_string(for_github); -} - -wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) -{ - return m_opengl_mgr.init_glcontext(canvas); -} - -bool GUI_App::init_opengl() -{ -#ifdef __linux__ - bool status = m_opengl_mgr.init_gl(); - m_opengl_initialized = true; - return status; -#else - return m_opengl_mgr.init_gl(); -#endif -} - -// gets path to PrusaSlicer.ini, returns semver from first line comment -static boost::optional parse_semver_from_ini(std::string path) -{ - std::ifstream stream(path); - std::stringstream buffer; - buffer << stream.rdbuf(); - std::string body = buffer.str(); - size_t start = body.find("PrusaSlicer "); - if (start == std::string::npos) - return boost::none; - body = body.substr(start + 12); - size_t end = body.find_first_of(" \n"); - if (end < body.size()) - body.resize(end); - return Semver::parse(body); -} - -void GUI_App::init_app_config() -{ - // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. -// SetAppName(SLIC3R_APP_KEY); - SetAppName(SLIC3R_APP_KEY "-alpha"); -// SetAppName(SLIC3R_APP_KEY "-beta"); - - -// SetAppDisplayName(SLIC3R_APP_NAME); - - // Set the Slic3r data directory at the Slic3r XS module. - // Unix: ~/ .Slic3r - // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" - // Mac : "~/Library/Application Support/Slic3r" - - if (data_dir().empty()) { - #ifndef __linux__ - set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); - #else - // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. - // https://github.com/prusa3d/PrusaSlicer/issues/2911 - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); - #endif - } else { - m_datadir_redefined = true; - } - - if (!app_config) - app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); - - // load settings - m_app_conf_exists = app_config->exists(); - if (m_app_conf_exists) { - std::string error = app_config->load(); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - } -} - -// returns old config path to copy from if such exists, -// returns an empty string if such config path does not exists or if it cannot be loaded. -std::string GUI_App::check_older_app_config(Semver current_version, bool backup) -{ - std::string older_data_dir_path; - - // If the config folder is redefined - do not check - if (m_datadir_redefined) - return {}; - - // find other version app config (alpha / beta / release) - std::string config_path = app_config->config_path(); - boost::filesystem::path parent_file_path(config_path); - std::string filename = parent_file_path.filename().string(); - parent_file_path.remove_filename().remove_filename(); - - std::vector candidates; - - if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); - if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); - if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); - - Semver last_semver = current_version; - for (const auto& candidate : candidates) { - if (boost::filesystem::exists(candidate)) { - // parse - boost::optionalother_semver = parse_semver_from_ini(candidate.string()); - if (other_semver && *other_semver > last_semver) { - last_semver = *other_semver; - older_data_dir_path = candidate.parent_path().string(); - } - } - } - if (older_data_dir_path.empty()) - return {}; - BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; - // ask about using older data folder - - InfoDialog msg(nullptr - , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) - , backup ? - format_wxstr(_L( - "The active configuration was created by %1% %2%," - "\nwhile a newer configuration was found in %3%" - "\ncreated by %1% %4%." - "\n\nShall the newer configuration be imported?" - "\nIf so, your active configuration will be backed up before importing the new configuration." - ) - , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) - : format_wxstr(_L( - "An existing configuration was found in %3%" - "\ncreated by %1% %2%." - "\n\nShall this configuration be imported?" - ) - , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) - , true, wxYES_NO); - - if (backup) { - msg.SetButtonLabel(wxID_YES, _L("Import")); - msg.SetButtonLabel(wxID_NO, _L("Don't import")); - } - - if (msg.ShowModal() == wxID_YES) { - std::string snapshot_id; - if (backup) { - const Config::Snapshot* snapshot{ nullptr }; - if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", - _u8L("Continue and import newer configuration?"), &snapshot)) - return {}; - if (snapshot) { - // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. - snapshot_id = snapshot->id; - assert(! snapshot_id.empty()); - app_config->set("on_snapshot", snapshot_id); - } else - BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; - } - - // load app config from older file - std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - if (!snapshot_id.empty()) - app_config->set("on_snapshot", snapshot_id); - m_app_conf_exists = true; - return older_data_dir_path; - } - return {}; -} - -void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) -{ - BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; - m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); -} - -bool GUI_App::OnInit() -{ - try { - return on_init_inner(); - } catch (const std::exception&) { - generic_exception_handle(); - return false; - } -} - -bool GUI_App::on_init_inner() -{ - // Set initialization of image handlers before any UI actions - See GH issue #7469 - wxInitAllImageHandlers(); - -#if defined(_WIN32) && ! defined(_WIN64) - // Win32 32bit build. - if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { - RichMessageDialog dlg(nullptr, - _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." - "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." - "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." - "\nDo you wish to continue?"), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - if (dlg.ShowModal() != wxID_YES) - return false; - } -#endif // _WIN64 - - // Forcing back menu icons under gtk2 and gtk3. Solution is based on: - // https://docs.gtk.org/gtk3/class.Settings.html - // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb - // TODO: Find workaround for GTK4 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); -#endif - - // Verify resources path - const wxString resources_dir = from_u8(Slic3r::resources_dir()); - wxCHECK_MSG(wxDirExists(resources_dir), false, - wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - -#ifdef __linux__ - if (! check_old_linux_datadir(GetAppName())) { - std::cerr << "Quitting, user chose to move their data to new location." << std::endl; - return false; - } -#endif - - // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. -// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); - // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible - // performance when working on high resolution multi-display setups. -// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); - -// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - - // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. - // Like here, before the show InfoDialog in check_older_app_config() - - // If load_language() fails, the application closes. - load_language(wxString(), true); -#ifdef _MSW_DARK_MODE - bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; - bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); -#endif - // initialize label colors and fonts - init_label_colours(); - init_fonts(); - - std::string older_data_dir_path; - if (m_app_conf_exists) { - if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) - // Only copying configuration if it was saved with a newer slicer than the one currently running. - older_data_dir_path = check_older_app_config(app_config->orig_version(), true); - } else { - // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. - older_data_dir_path = check_older_app_config(Semver(), false); - } - -#ifdef _MSW_DARK_MODE - // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed - if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; - init_dark_color_mode != new_dark_color_mode) { - NppDarkMode::SetDarkMode(new_dark_color_mode); - init_label_colours(); - update_label_colours_from_appconfig(); - } - if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - init_sys_menu_enabled != new_sys_menu_enabled) - NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); -#endif - - if (is_editor()) { - std::string msg = Http::tls_global_init(); - std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); - bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); - - if (!msg.empty() && !ssl_accept) { - RichMessageDialog - dlg(nullptr, - wxString::Format(_L("%s\nDo you want to continue?"), msg), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - dlg.ShowCheckBox(_L("Remember my choice")); - if (dlg.ShowModal() != wxID_YES) return false; - - app_config->set("tls_cert_store_accepted", - dlg.IsCheckBoxChecked() ? "yes" : "no"); - app_config->set("tls_accepted_cert_store_location", - dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); - } - } - - SplashScreen* scrn = nullptr; - if (app_config->get("show_splash_screen") == "1") { - // make a bitmap with dark grey banner on the left side - wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); - - // Detect position (display) to show the splash screen - // Now this position is equal to the mainframe position - wxPoint splashscreen_pos = wxDefaultPosition; - bool default_splashscreen_pos = true; - if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { - auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); - default_splashscreen_pos = metrics == boost::none; - if (!default_splashscreen_pos) - splashscreen_pos = metrics->get_rect().GetPosition(); - } - - if (!default_splashscreen_pos) { - // workaround for crash related to the positioning of the window on secondary monitor - get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); - get_app_config()->save(); - } - - // create splash screen with updated bmp - scrn = new SplashScreen(bmp.IsOk() ? bmp : create_scaled_bitmap("PrusaSlicer", nullptr, 400), - wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); - - if (!default_splashscreen_pos) - // revert "restore_win_position" value if application wasn't crashed - get_app_config()->set("restore_win_position", "1"); -#ifndef __linux__ - wxYield(); -#endif - scrn->SetText(_L("Loading configuration")+ dots); - } - - preset_bundle = new PresetBundle(); - - // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory - // supplied as argument to --datadir; in that case we should still run the wizard - preset_bundle->setup_directories(); - - if (! older_data_dir_path.empty()) { - preset_bundle->import_newer_configs(older_data_dir_path); - app_config->save(); - } - - if (is_editor()) { -#ifdef __WXMSW__ - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); -#endif // __WXMSW__ - - preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); - Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { - std::string evt_string = into_u8(evt.GetString()); - if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { - auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); - this->plater_->get_notification_manager()->push_notification( notif_type - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) - , _u8L("See Releases page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } - ); - } - } - }); - Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { - //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); - }); - - Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); - if(!evt.GetString().IsEmpty()) - show_error(nullptr, evt.GetString()); - }); - - Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { - show_error(nullptr, evt.GetString()); - }); - } - else { -#ifdef __WXMSW__ - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); -#endif // __WXMSW__ - } - - // Suppress the '- default -' presets. - preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); - try { - // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. - // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force - // installation of a compatible system preset, thus nullifying the system preset substitutions. - init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); - } catch (const std::exception &ex) { - show_error(nullptr, ex.what()); - } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - register_win32_dpi_event(); -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - register_win32_device_notification_event(); -#endif // WIN32 - - // Let the libslic3r know the callback, which will translate messages on demand. - Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); - - // application frame - if (scrn && is_editor()) - scrn->SetText(_L("Preparing settings tabs") + dots); - - mainframe = new MainFrame(); - // hide settings tabs after first Layout - if (is_editor()) - mainframe->select_tab(size_t(0)); - - sidebar().obj_list()->init_objects(); // propagate model objects to object list -// update_mode(); // !!! do that later - SetTopWindow(mainframe); - - plater_->init_notification_manager(); - - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - - if (is_gcode_viewer()) { - mainframe->update_layout(); - if (plater_ != nullptr) - // ensure the selected technology is ptFFF - plater_->set_printer_technology(ptFFF); - } - else - load_current_presets(); - - // Save the active profiles as a "saved into project". - update_saved_preset_from_current_preset(); - - if (plater_ != nullptr) { - // Save the names of active presets and project specific config into ProjectDirtyStateManager. - plater_->reset_project_dirty_initial_presets(); - // Update Project dirty state, update application title bar. - plater_->update_project_dirty_from_presets(); - } - - mainframe->Show(true); - - obj_list()->set_min_height(); - - update_mode(); // update view mode after fix of the object_list size - -#ifdef __APPLE__ - other_instance_message_handler()->bring_instance_forward(); -#endif //__APPLE__ - - Bind(wxEVT_IDLE, [this](wxIdleEvent& event) - { - if (! plater_) - return; - - this->obj_manipul()->update_if_dirty(); - - // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT - // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. -#ifdef __linux__ - if (! m_post_initialized && m_opengl_initialized) { -#else - if (! m_post_initialized) { -#endif - m_post_initialized = true; -#ifdef WIN32 - this->mainframe->register_win32_callbacks(); -#endif - this->post_init(); - } - - if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") - app_config->save(); - }); - - m_initialized = true; - - if (const std::string& crash_reason = app_config->get("restore_win_position"); - boost::starts_with(crash_reason,"crashed")) - { - wxString preferences_item = _L("Restore window position on start"); - InfoDialog dialog(nullptr, - _L("PrusaSlicer started after a crash"), - format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" - "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" - "More precise reason for the crash: \"%1%\".\n" - "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" - "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " - "Otherwise, the application will most likely crash again next time."), - "" + from_u8(crash_reason) + "", - "#2939", - "#5573", - "" + preferences_item + ""), - true, wxYES_NO); - - dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); - dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); - - auto answer = dialog.ShowModal(); - if (answer == wxID_YES) - app_config->set("restore_win_position", "0"); - else if (answer == wxID_NO) - app_config->set("restore_win_position", "1"); - app_config->save(); - } - - return true; -} - -unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) -{ - double r = colour.Red(); - double g = colour.Green(); - double b = colour.Blue(); - - return std::round(std::sqrt( - r * r * .241 + - g * g * .691 + - b * b * .068 - )); -} - -bool GUI_App::dark_mode() -{ -#if __APPLE__ - // The check for dark mode returns false positive on 10.12 and 10.13, - // which allowed setting dark menu bar and dock area, which is - // is detected as dark mode. We must run on at least 10.14 where the - // proper dark mode was first introduced. - return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); -#else - return wxGetApp().app_config->get("dark_color_mode") == "1" ? true : check_dark_mode(); - //const unsigned luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - //return luma < 128; -#endif -} - -const wxColour GUI_App::get_label_default_clr_system() -{ - return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); -} - -const wxColour GUI_App::get_label_default_clr_modified() -{ - return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); -} - -void GUI_App::init_label_colours() -{ - m_color_label_modified = get_label_default_clr_modified(); - m_color_label_sys = get_label_default_clr_system(); - - bool is_dark_mode = dark_mode(); -#ifdef _WIN32 - m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); - m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); - m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); - m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); -#else - m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); -#endif - m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); -} - -void GUI_App::update_label_colours_from_appconfig() -{ - if (app_config->has("label_clr_sys")) { - auto str = app_config->get("label_clr_sys"); - if (str != "") - m_color_label_sys = wxColour(str); - } - - if (app_config->has("label_clr_modified")) { - auto str = app_config->get("label_clr_modified"); - if (str != "") - m_color_label_modified = wxColour(str); - } -} - -void GUI_App::update_label_colours() -{ - for (Tab* tab : tabs_list) - tab->update_label_colours(); -} - -#ifdef _WIN32 -static bool is_focused(HWND hWnd) -{ - HWND hFocusedWnd = ::GetFocus(); - return hFocusedWnd && hWnd == hFocusedWnd; -} - -static bool is_default(wxWindow* win) -{ - wxTopLevelWindow* tlw = find_toplevel_parent(win); - if (!tlw) - return false; - - return win == tlw->GetDefaultItem(); -} -#endif - -void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) -{ -#ifdef _WIN32 - bool is_focused_button = false; - bool is_default_button = false; - if (wxButton* btn = dynamic_cast(window)) { - if (!(btn->GetWindowStyle() & wxNO_BORDER)) { - btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); - highlited = true; - } - // button marking - { - auto mark_button = [this, btn, highlited](const bool mark) { - if (btn->GetLabel().IsEmpty()) - btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); - else - btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); - btn->Refresh(); - btn->Update(); - }; - - // hovering - btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); - // focusing - btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); - - is_focused_button = is_focused(btn->GetHWND()); - is_default_button = is_default(btn); - if (is_focused_button || is_default_button) - mark_button(is_focused_button); - } - } - else if (wxTextCtrl* text = dynamic_cast(window)) { - if (text->GetBorder() != wxBORDER_SIMPLE) - text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); - } - else if (wxCheckListBox* list = dynamic_cast(window)) { - list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); - list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - for (size_t i = 0; i < list->GetCount(); i++) - if (wxOwnerDrawn* item = list->GetItem(i)) { - item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - item->SetTextColour(m_color_label_default); - } - return; - } - else if (dynamic_cast(window)) - window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); - - if (!just_font) - window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - if (!is_focused_button && !is_default_button) - window->SetForegroundColour(m_color_label_default); -#endif -} - -// recursive function for scaling fonts for all controls in Window -#ifdef _WIN32 -static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) -{ - bool is_btn = dynamic_cast(window) != nullptr; - if (!(just_buttons_update && !is_btn)) - wxGetApp().UpdateDarkUI(window, is_btn); - - auto children = window->GetChildren(); - for (auto child : children) { - update_dark_children_ui(child); - } -} -#endif - -// Note: Don't use this function for Dialog contains ScalableButtons -void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) -{ -#ifdef _WIN32 - update_dark_children_ui(dlg, just_buttons_update); -#endif -} -void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) -{ -#ifdef _WIN32 - UpdateDarkUI(dvc, highlited ? dark_mode() : false); -#ifdef _MSW_DARK_MODE - dvc->RefreshHeaderDarkMode(&m_normal_font); -#endif //_MSW_DARK_MODE - if (dvc->HasFlag(wxDV_ROW_LINES)) - dvc->SetAlternateRowColour(m_color_highlight_default); - if (dvc->GetBorder() != wxBORDER_SIMPLE) - dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); -#endif -} - -void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(parent); - - auto children = parent->GetChildren(); - for (auto child : children) { - if (dynamic_cast(child)) - child->SetForegroundColour(m_color_label_default); - } -#endif -} - -void GUI_App::init_fonts() -{ - m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); - m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - -#ifdef __WXMAC__ - m_small_font.SetPointSize(11); - m_bold_font.SetPointSize(13); -#endif /*__WXMAC__*/ - - // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as - // DEFAULT in wxGtk. Use the TELETYPE family as a work-around - m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::update_fonts(const MainFrame *main_frame) -{ - /* Only normal and bold fonts are used for an application rescale, - * because of under MSW small and normal fonts are the same. - * To avoid same rescaling twice, just fill this values - * from rescaled MainFrame - */ - if (main_frame == nullptr) - main_frame = this->mainframe; - m_normal_font = main_frame->normal_font(); - m_small_font = m_normal_font; - m_bold_font = main_frame->normal_font().Bold(); - m_link_font = m_bold_font.Underlined(); - m_em_unit = main_frame->em_unit(); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::set_label_clr_modified(const wxColour& clr) -{ - if (m_color_label_modified == clr) - return; - m_color_label_modified = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_modified", str); - app_config->save(); -} - -void GUI_App::set_label_clr_sys(const wxColour& clr) -{ - if (m_color_label_sys == clr) - return; - m_color_label_sys = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_sys", str); - app_config->save(); -} - -bool GUI_App::tabs_as_menu() const -{ - return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); -} - -wxSize GUI_App::get_min_size() const -{ - return wxSize(76*m_em_unit, 49 * m_em_unit); -} - -float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit*0.1f; -#endif // __APPLE__ - - const std::string& use_val = app_config->get("use_custom_toolbar_size"); - const std::string& val = app_config->get("custom_toolbar_size"); - const std::string& auto_val = app_config->get("auto_toolbar_size"); - - if (val.empty() || auto_val.empty() || use_val.empty()) - return icon_sc; - - int int_val = use_val == "0" ? 100 : atoi(val.c_str()); - // correct value in respect to auto_toolbar_size - int_val = std::min(atoi(auto_val.c_str()), int_val); - - if (is_limited && int_val < 50) - int_val = 50; - - return 0.01f * int_val * icon_sc; -} - -void GUI_App::set_auto_toolbar_icon_scale(float scale) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit * 0.1f; -#endif // __APPLE__ - - long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); - std::string val = std::to_string(int_val); - - app_config->set("auto_toolbar_size", val); -} - -// check user printer_presets for the containing information about "Print Host upload" -void GUI_App::check_printer_presets() -{ - std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); - if (preset_names.empty()) - return; - - wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; - for (const std::string& preset_name : preset_names) - msg_text += "\n \"" + from_u8(preset_name) + "\","; - msg_text.RemoveLast(); - msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" - "Settings will be available in physical printers settings.") + "\n\n" + - _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" - "Note: This name can be changed later from the physical printers settings"); - - //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - - preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); -} - -void GUI_App::recreate_GUI(const wxString& msg_name) -{ - m_is_recreating_gui = true; - - mainframe->shutdown(); - - wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); - dlg.Pulse(); - dlg.Update(10, _L("Recreating") + dots); - - MainFrame *old_main_frame = mainframe; - mainframe = new MainFrame(); - if (is_editor()) - // hide settings tabs after first Layout - mainframe->select_tab(size_t(0)); - // Propagate model objects to object list. - sidebar().obj_list()->init_objects(); - SetTopWindow(mainframe); - - dlg.Update(30, _L("Recreating") + dots); - old_main_frame->Destroy(); - - dlg.Update(80, _L("Loading of current presets") + dots); - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - load_current_presets(); - mainframe->Show(true); - - dlg.Update(90, _L("Loading of a mode view") + dots); - - obj_list()->set_min_height(); - update_mode(); - - // #ys_FIXME_delete_after_testing Do we still need this ? -// CallAfter([]() { -// // Run the config wizard, don't offer the "reset user profile" checkbox. -// config_wizard_startup(true); -// }); - - m_is_recreating_gui = false; -} - -void GUI_App::system_info() -{ - SysInfoDialog dlg; - dlg.ShowModal(); -} - -void GUI_App::keyboard_shortcuts() -{ - KBShortcutsDialog dlg; - dlg.ShowModal(); -} - -// static method accepting a wxWindow object as first parameter -bool GUI_App::catch_error(std::function cb, - // wxMessageDialog* message_dialog, - const std::string& err /*= ""*/) -{ - if (!err.empty()) { - if (cb) - cb(); - // if (message_dialog) - // message_dialog->(err, "Error", wxOK | wxICON_ERROR); - show_error(/*this*/nullptr, err); - return true; - } - return false; -} - -// static method accepting a wxWindow object as first parameter -void fatal_error(wxWindow* parent) -{ - show_error(parent, ""); - // exit 1; // #ys_FIXME -} - -#ifdef _WIN32 - -#ifdef _MSW_DARK_MODE -static void update_scrolls(wxWindow* window) -{ - wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); - while (node) - { - wxWindow* win = node->GetData(); - if (dynamic_cast(win) || - dynamic_cast(win) || - dynamic_cast(win)) - NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); - - update_scrolls(win); - node = node->GetNext(); - } -} -#endif //_MSW_DARK_MODE - - -#ifdef _MSW_DARK_MODE -void GUI_App::force_menu_update() -{ - NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); -} -#endif //_MSW_DARK_MODE - -void GUI_App::force_colors_update() -{ -#ifdef _MSW_DARK_MODE - NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); - if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) - NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); - NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); - NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); -#endif //_MSW_DARK_MODE - m_force_colors_update = true; -} -#endif //_WIN32 - -// Called after the Preferences dialog is closed and the program settings are saved. -// Update the UI based on the current preferences. -void GUI_App::update_ui_from_settings() -{ - update_label_colours(); -#ifdef _WIN32 - // Upadte UI colors before Update UI from settings - if (m_force_colors_update) { - m_force_colors_update = false; - mainframe->force_color_changed(); - mainframe->diff_dialog.force_color_changed(); - mainframe->preferences_dialog->force_color_changed(); - mainframe->printhost_queue_dlg()->force_color_changed(); -#ifdef _MSW_DARK_MODE - update_scrolls(mainframe); - if (mainframe->is_dlg_layout()) { - // update for tabs bar - UpdateDarkUI(&mainframe->m_settings_dialog); - mainframe->m_settings_dialog.Fit(); - mainframe->m_settings_dialog.Refresh(); - // update scrollbars - update_scrolls(&mainframe->m_settings_dialog); - } -#endif //_MSW_DARK_MODE - } -#endif - mainframe->update_ui_from_settings(); -} - -void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) -{ - const std::string name = into_u8(window->GetName()); - - window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { - window_pos_save(window, name); - event.Skip(); - }); - - window_pos_restore(window, name, default_maximized); - - on_window_geometry(window, [=]() { - window_pos_sanitize(window); - }); -} - -void GUI_App::load_project(wxWindow *parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (3MF/AMF):"), - app_config->get_last_dir(), "", - file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const -{ - input_files.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one or more files (STL/OBJ/AMF/3MF/PRUSA):"), - from_u8(app_config->get_last_dir()), "", - file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - dialog.GetPaths(input_files); -} - -void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), - app_config->get_last_dir(), "", - file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -bool GUI_App::switch_language() -{ - if (select_language()) { - recreate_GUI(_L("Changing of an application language") + dots); - return true; - } else { - return false; - } -} - -#ifdef __linux__ -static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, - const wxLanguageInfo* system_language) -{ - constexpr size_t max_len = 50; - char path[max_len] = ""; - std::vector locales; - const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); - - // Call locale -a so we can parse the output to get the list of available locales - // We expect lines such as "en_US.utf8". Pick ones starting with the language code - // we are switching to. Lines with different formatting will be removed later. - FILE* fp = popen("locale -a", "r"); - if (fp != NULL) { - while (fgets(path, max_len, fp) != NULL) { - std::string line(path); - line = line.substr(0, line.find('\n')); - if (boost::starts_with(line, lang_prefix)) - locales.push_back(line); - } - pclose(fp); - } - - // locales now contain all candidates for this language. - // Sort them so ones containing anything about UTF-8 are at the end. - std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) - { - auto has_utf8 = [](const std::string & s) { - auto S = boost::to_upper_copy(s); - return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; - }; - return ! has_utf8(a) && has_utf8(b); - }); - - // Remove the suffix behind a dot, if there is one. - for (std::string& s : locales) - s = s.substr(0, s.find(".")); - - // We just hope that dear Linux "locale -a" returns country codes - // in ISO 3166-1 alpha-2 code (two letter) format. - // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes - // To be sure, remove anything not looking as expected - // (any number of lowercase letters, underscore, two uppercase letters). - locales.erase(std::remove_if(locales.begin(), - locales.end(), - [](const std::string& s) { - return ! std::regex_match(s, - std::regex("^[a-z]+_[A-Z]{2}$")); - }), - locales.end()); - - // Is there a candidate matching a country code of a system language? Move it to the end, - // while maintaining the order of matches, so that the best match ends up at the very end. - std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); - int cnt = locales.size(); - for (int i=0; iempty()) { - const std::string &locale = *it; - const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); - if (wxLocale::IsAvailable(lang->Language)) - return lang; - } - return language; -} -#endif - -int GUI_App::GetSingleChoiceIndex(const wxString& message, - const wxString& caption, - const wxArrayString& choices, - int initialSelection) -{ -#ifdef _WIN32 - wxSingleChoiceDialog dialog(nullptr, message, caption, choices); - wxGetApp().UpdateDlgDarkUI(&dialog); - - dialog.SetSelection(initialSelection); - return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; -#else - return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); -#endif -} - -// select language from the list of installed languages -bool GUI_App::select_language() -{ - wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); - std::vector language_infos; - language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - for (size_t i = 0; i < translations.GetCount(); ++ i) { - const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); - if (langinfo != nullptr) - language_infos.emplace_back(langinfo); - } - sort_remove_duplicates(language_infos); - std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); - - wxArrayString names; - names.Alloc(language_infos.size()); - - // Some valid language should be selected since the application start up. - const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); - int init_selection = -1; - int init_selection_alt = -1; - int init_selection_default = -1; - for (size_t i = 0; i < language_infos.size(); ++ i) { - if (wxLanguage(language_infos[i]->Language) == current_language) - // The dictionary matches the active language and country. - init_selection = i; - else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || - // if the active language is Slovak, mark the Czech language as active. - (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) - // The dictionary matches the active language, it does not necessarily match the country. - init_selection_alt = i; - if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") - // This will be the default selection if the active language does not match any dictionary. - init_selection_default = i; - names.Add(language_infos[i]->Description); - } - if (init_selection == -1) - // This is the dictionary matching the active language. - init_selection = init_selection_alt; - if (init_selection != -1) - // This is the language to highlight in the choice dialog initially. - init_selection_default = init_selection; - - const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); - // Try to load a new language. - if (index != -1 && (init_selection == -1 || init_selection != index)) { - const wxLanguageInfo *new_language_info = language_infos[index]; - if (this->load_language(new_language_info->CanonicalName, false)) { - // Save language at application config. - // Which language to save as the selected dictionary language? - // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its - // stability in the future: - // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - // 2) Current locale language may not match the dictionary name, see GH issue #3901 - // m_wxLocale->GetCanonicalName() - // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. - app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); - app_config->save(); - return true; - } - } - - return false; -} - -// Load gettext translation files and activate them at the start of the application, -// based on the "translation_language" key stored in the application config. -bool GUI_App::load_language(wxString language, bool initial) -{ - if (initial) { - // There is a static list of lookup path prefixes in wxWidgets. Add ours. - wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); - // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. - language = app_config->get("translation_language"); - if (! language.empty()) - BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; - - // Get the system language. - { - const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); - if (lang_system != wxLANGUAGE_UNKNOWN) { - m_language_info_system = wxLocale::GetLanguageInfo(lang_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); - } - } - { - // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. - wxLocale temp_locale; - // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). - wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); - // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer - // and try to match them with the system specific "preferred languages". - // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). - // The last parameter gets added to the list of detected dictionaries. This is a workaround - // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. - wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - if (! best_language.IsEmpty()) { - m_language_info_best = wxLocale::FindLanguageInfo(best_language); - BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); - } - #ifdef __linux__ - wxString lc_all; - if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { - // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. - // Disregard the "best" suggestion in case LC_ALL is provided. - m_language_info_best = nullptr; - } - #endif - } - } - - const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); - if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { - // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). - language_info = nullptr; - BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); - } - - if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { - BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); - language_info = nullptr; - } - - if (language_info == nullptr) { - // PrusaSlicer does not support the Right to Left languages yet. - if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_system; - if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_best; - if (language_info == nullptr) - language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); - - // Alternate language code. - wxLanguage language_dict = wxLanguage(language_info->Language); - if (language_info->CanonicalName.BeforeFirst('_') == "sk") { - // Slovaks understand Czech well. Give them the Czech translation. - language_dict = wxLANGUAGE_CZECH; - BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; - } - - // Select language for locales. This language may be different from the language of the dictionary. - if (language_info == m_language_info_best || language_info == m_language_info_system) { - // The current language matches user's default profile exactly. That's great. - } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { - // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. - // This allows a Swiss guy to use a German dictionary without forcing him to German locales. - language_info = m_language_info_best; - } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) - language_info = m_language_info_system; - -#ifdef __linux__ - // If we can't find this locale , try to use different one for the language - // instead of just reporting that it is impossible to switch. - if (! wxLocale::IsAvailable(language_info->Language)) { - std::string original_lang = into_u8(language_info->CanonicalName); - language_info = linux_get_existing_locale_language(language_info, m_language_info_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") - % original_lang % language_info->CanonicalName.ToUTF8().data(); - } -#endif - - if (! wxLocale::IsAvailable(language_info->Language)) { - // Loading the language dictionary failed. - wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; -#if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux system - message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; -#endif - if (initial) - message + "\n\nApplication will close."; - wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); - if (initial) - std::exit(EXIT_FAILURE); - else - return false; - } - - // Release the old locales, create new locales. - //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. - m_wxLocale.release(); - m_wxLocale = Slic3r::make_unique(); - m_wxLocale->Init(language_info->Language); - // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) - // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. - wxTranslations::Get()->SetLanguage(language_dict); - m_wxLocale->AddCatalog(SLIC3R_APP_KEY); - m_imgui->set_language(into_u8(language_info->CanonicalName)); - //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. - //wxSetlocale(LC_NUMERIC, "C"); - Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); - return true; -} - -Tab* GUI_App::get_tab(Preset::Type type) -{ - for (Tab* tab: tabs_list) - if (tab->type() == type) - return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab - return nullptr; -} - -ConfigOptionMode GUI_App::get_mode() -{ - if (!app_config->has("view_mode")) - return comSimple; - - const auto mode = app_config->get("view_mode"); - return mode == "expert" ? comExpert : - mode == "simple" ? comSimple : comAdvanced; -} - -void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) -{ - const std::string mode_str = mode == comExpert ? "expert" : - mode == comSimple ? "simple" : "advanced"; - app_config->set("view_mode", mode_str); - app_config->save(); - update_mode(); -} - -// Update view mode according to selected menu -void GUI_App::update_mode() -{ - sidebar().update_mode(); - -#ifdef _MSW_DARK_MODE - if (!wxGetApp().tabs_as_menu()) - dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); -#endif - - for (auto tab : tabs_list) - tab->update_mode(); - - plater()->update_menus(); - plater()->canvas3D()->update_gizmos_on_off_state(); -} - -void GUI_App::add_config_menu(wxMenuBar *menu) -{ - auto local_menu = new wxMenu(); - wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); - - const auto config_wizard_name = _(ConfigWizard::name(true)); - const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); - // Cmd+, is standard on OS X - what about other operating systems? - if (is_editor()) { - local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); - local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); - local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); - local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - //if (DesktopIntegrationDialog::integration_possible()) - local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); -#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - local_menu->AppendSeparator(); - } - local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + -#ifdef __APPLE__ - "\tCtrl+,", -#else - "\tCtrl+P", -#endif - _L("Application preferences")); - wxMenu* mode_menu = nullptr; - if (is_editor()) { - local_menu->AppendSeparator(); - mode_menu = new wxMenu(); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); - - local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); - } - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); - if (is_editor()) { - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); - // TODO: for when we're able to flash dictionaries - // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); - } - - local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { - switch (event.GetId() - config_id_base) { - case ConfigMenuWizard: - run_wizard(ConfigWizard::RR_USER); - break; - case ConfigMenuUpdateConf: - check_updates(true); - break; - case ConfigMenuUpdateApp: - app_version_check(true); - break; -#ifdef __linux__ - case ConfigMenuDesktopIntegration: - show_desktop_integration_dialog(); - break; -#endif - case ConfigMenuTakeSnapshot: - // Take a configuration snapshot. - if (wxString action_name = _L("Taking a configuration snapshot"); - check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { - wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); - UpdateDlgDarkUI(&dlg); - - // set current normal font for dialog children, - // because of just dlg.SetFont(normal_font()) has no result; - for (auto child : dlg.GetChildren()) - child->SetFont(normal_font()); - - if (dlg.ShowModal() == wxID_OK) - if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( - *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); - snapshot != nullptr) - app_config->set("on_snapshot", snapshot->id); - } - break; - case ConfigMenuSnapshots: - if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { - std::string on_snapshot; - if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) - on_snapshot = app_config->get("on_snapshot"); - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); - dlg.ShowModal(); - if (!dlg.snapshot_to_activate().empty()) { - if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && - ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", - GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) - break; - try { - app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system - // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users - // are known to be creative and mess with the config files in various ways. - if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); - ! all_substitutions.empty()) - show_substitutions_info(all_substitutions); - - // Load the currently selected preset into the GUI, update the preset selection box. - load_current_presets(); - } catch (std::exception &ex) { - GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); - } - } - } - break; - case ConfigMenuPreferences: - { - open_preferences(); - break; - } - case ConfigMenuLanguage: - { - /* Before change application language, let's check unsaved changes on 3D-Scene - * and draw user's attention to the application restarting after a language change - */ - { - // the dialog needs to be destroyed before the call to switch_language() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Language selection"); - //wxMessageDialog dialog(nullptr, - MessageDialog dialog(nullptr, - _L("Switching the language will trigger application restart.\n" - "You will lose content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxOK | wxCANCEL); - if (dialog.ShowModal() == wxID_CANCEL) - return; - } - - switch_language(); - break; - } - case ConfigMenuFlashFirmware: - FirmwareDialog::run(mainframe); - break; - default: - break; - } - }); - - using std::placeholders::_1; - - if (mode_menu != nullptr) { - auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); - } - - menu->Append(local_menu, _L("&Configuration")); -} - -void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - mainframe->preferences_dialog->show(highlight_option, tab_name); - - if (mainframe->preferences_dialog->recreate_GUI()) - recreate_GUI(_L("Restart application") + dots); - -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (mainframe->preferences_dialog->seq_top_layer_only_changed()) -#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - this->plater_->refresh_print(); - -#ifdef _WIN32 - if (is_editor()) { - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); - } - else { - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); - } -#endif // _WIN32 - - if (mainframe->preferences_dialog->settings_layout_changed()) { - // hide full main_sizer for mainFrame - mainframe->GetSizer()->Show(false); - mainframe->update_layout(); - mainframe->select_tab(size_t(0)); - } -} - -bool GUI_App::has_unsaved_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) - return true; - } - return false; -} - -bool GUI_App::has_current_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - return true; - } - return false; -} - -void GUI_App::update_saved_preset_from_current_preset() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->update_saved_preset_from_current_preset(); - } -} - -std::vector GUI_App::get_active_preset_collections() const -{ - std::vector ret; - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) - ret.push_back(tab->get_presets()); - return ret; -} - -// To notify the user whether he is aware that some preset changes will be lost, -// UnsavedChangesDialog: "Discard / Save / Cancel" -// This is called when: -// - Close Application & Current project isn't saved -// - Load Project & Current project isn't saved -// - Undo / Redo with change of print technologie -// - Loading snapshot -// - Loading config_file/bundle -// UnsavedChangesDialog: "Don't save / Save / Cancel" -// This is called when: -// - Exporting config_bundle -// - Taking snapshot -bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) -{ - if (has_current_preset_changes()) { - const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; - int act_buttons = UnsavedChangesDialog::ActionButtons::SAVE; - if (dont_save_insted_of_discard) - act_buttons |= UnsavedChangesDialog::ActionButtons::DONT_SAVE; - UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - if (dlg.save_preset()) // save selected changes - { - for (const std::pair& nt : dlg.get_names_and_types()) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - load_current_presets(false); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - MessageDialog(nullptr, _L_PLURAL("The preset modifications are successfully saved", - "The presets modifications are successfully saved", dlg.get_names_and_types().size())).ShowModal(); - } - } - - return true; -} - -void GUI_App::apply_keeped_preset_modifications() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->apply_config_from_cache(); - } - load_current_presets(false); -} - -// This is called when creating new project or load another project -// OR close ConfigWizard -// to ask the user what should we do with unsaved changes for presets. -// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" -// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed -bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) -{ - if (has_current_preset_changes()) { - bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; - - const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; - UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - auto reset_modifications = [this, is_called_from_configwizard]() { - if (is_called_from_configwizard) - return; // no need to discared changes. It will be done fromConfigWizard closing - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - tab->m_presets->discard_current_changes(); - } - load_current_presets(false); - }; - - if (dlg.discard()) - reset_modifications(); - else // save selected changes - { - const auto& preset_names_and_types = dlg.get_names_and_types(); - if (dlg.save_preset()) { - for (const std::pair& nt : preset_names_and_types) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - wxString text = _L_PLURAL("The preset modifications are successfully saved", - "The presets modifications are successfully saved", preset_names_and_types.size()); - if (!is_called_from_configwizard) - text += "\n\n" + _L("For new project all modifications will be reseted"); - - MessageDialog(nullptr, text).ShowModal(); - reset_modifications(); - } - else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { - // execute this part of code only if not all modifications are keeping to the new project - // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected - for (const std::pair& nt : preset_names_and_types) { - Preset::Type type = nt.second; - Tab* tab = get_tab(type); - std::vector selected_options = dlg.get_selected_options(type); - if (type == Preset::TYPE_PRINTER) { - auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); - if (it != selected_options.end()) { - // erase "extruders_count" option from the list - selected_options.erase(it); - // cache the extruders count - static_cast(tab)->cache_extruder_cnt(); - } - } - tab->cache_config_diff(selected_options); - if (!is_called_from_configwizard) - tab->m_presets->discard_current_changes(); - } - if (is_called_from_configwizard) - *postponed_apply_of_keeped_changes = true; - else - apply_keeped_preset_modifications(); - } - } - } - - return true; -} - -bool GUI_App::can_load_project() -{ - int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); - if (saved_project == wxID_CANCEL || - (plater()->is_project_dirty() && saved_project == wxID_NO && - !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) - return false; - return true; -} - -bool GUI_App::check_print_host_queue() -{ - wxString dirty; - std::vector> jobs; - // Get ongoing jobs from dialog - mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); - if (jobs.empty()) - return true; - // Show dialog - wxString job_string = wxString(); - for (const auto& job : jobs) { - job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); - } - wxString message; - message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); - //wxMessageDialog dialog(mainframe, - MessageDialog dialog(mainframe, - message, - wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), - wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - if (dialog.ShowModal() == wxID_YES) - return true; - - // TODO: If already shown, bring forward - mainframe->m_printhost_queue_dlg->Show(); - return false; -} - -bool GUI_App::checked_tab(Tab* tab) -{ - bool ret = true; - if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) - ret = false; - return ret; -} - -// Update UI / Tabs to reflect changes in the currently loaded presets -void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) -{ - // check printer_presets for the containing information about "Print Host upload" - // and create physical printer from it, if any exists - if (check_printer_presets_) - check_printer_presets(); - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - this->plater()->set_printer_technology(printer_technology); - for (Tab *tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) { - if (tab->type() == Preset::TYPE_PRINTER) { - static_cast(tab)->update_pages(); - // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). - this->plater()->force_print_bed_update(); - } - tab->load_current_preset(); - } -} - -bool GUI_App::OnExceptionInMainLoop() -{ - generic_exception_handle(); - return false; -} - -#ifdef __APPLE__ -// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run -// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App -// to a G-code viewer. -void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) -{ - size_t num_gcodes = 0; - for (const wxString &filename : fileNames) - if (is_gcode_file(into_u8(filename))) - ++ num_gcodes; - if (fileNames.size() == num_gcodes) { - // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, - // just G-codes were passed. Switch to G-code viewer mode. - m_app_mode = EAppMode::GCodeViewer; - unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); - if(app_config != nullptr) - delete app_config; - app_config = nullptr; - init_app_config(); - } - wxApp::OSXStoreOpenFiles(fileNames); -} -// wxWidgets override to get an event on open files. -void GUI_App::MacOpenFiles(const wxArrayString &fileNames) -{ - std::vector files; - std::vector gcode_files; - std::vector non_gcode_files; - for (const auto& filename : fileNames) { - if (is_gcode_file(into_u8(filename))) - gcode_files.emplace_back(filename); - else { - files.emplace_back(into_u8(filename)); - non_gcode_files.emplace_back(filename); - } - } - if (m_app_mode == EAppMode::GCodeViewer) { - // Running in G-code viewer. - // Load the first G-code into the G-code viewer. - // Or if no G-codes, send other files to slicer. - if (! gcode_files.empty()) { - if (m_post_initialized) - this->plater()->load_gcode(gcode_files.front()); - else - this->init_params->input_files = { into_u8(gcode_files.front()) }; - } - if (!non_gcode_files.empty()) - start_new_slicer(non_gcode_files, true); - } else { - if (! files.empty()) { - if (m_post_initialized) { - wxArrayString input_files; - for (size_t i = 0; i < non_gcode_files.size(); ++i) - input_files.push_back(non_gcode_files[i]); - this->plater()->load_files(input_files); - } else { - for (const auto &f : non_gcode_files) - this->init_params->input_files.emplace_back(into_u8(f)); - } - } - for (const wxString &filename : gcode_files) - start_new_gcodeviewer(&filename); - } -} -#endif /* __APPLE */ - -Sidebar& GUI_App::sidebar() -{ - return plater_->sidebar(); -} - -ObjectManipulation* GUI_App::obj_manipul() -{ - // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) - return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; -} - -ObjectSettings* GUI_App::obj_settings() -{ - return sidebar().obj_settings(); -} - -ObjectList* GUI_App::obj_list() -{ - return sidebar().obj_list(); -} - -ObjectLayers* GUI_App::obj_layers() -{ - return sidebar().obj_layers(); -} - -Plater* GUI_App::plater() -{ - return plater_; -} - -const Plater* GUI_App::plater() const -{ - return plater_; -} - -Model& GUI_App::model() -{ - return plater_->model(); -} -wxBookCtrlBase* GUI_App::tab_panel() const -{ - return mainframe->m_tabpanel; -} - -NotificationManager * GUI_App::notification_manager() -{ - return plater_->get_notification_manager(); -} - -// extruders count from selected printer preset -int GUI_App::extruders_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_selected_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -// extruders count from edited printer preset -int GUI_App::extruders_edited_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_edited_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -wxString GUI_App::current_language_code_safe() const -{ - // Translate the language code to a code, for which Prusa Research maintains translations. - const std::map mapping { - { "cs", "cs_CZ", }, - { "sk", "cs_CZ", }, - { "de", "de_DE", }, - { "es", "es_ES", }, - { "fr", "fr_FR", }, - { "it", "it_IT", }, - { "ja", "ja_JP", }, - { "ko", "ko_KR", }, - { "pl", "pl_PL", }, - { "uk", "uk_UA", }, - { "zh", "zh_CN", }, - { "ru", "ru_RU", }, - }; - wxString language_code = this->current_language_code().BeforeFirst('_'); - auto it = mapping.find(language_code); - if (it != mapping.end()) - language_code = it->second; - else - language_code = "en_US"; - return language_code; -} - -void GUI_App::open_web_page_localized(const std::string &http_address) -{ - open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); -} - -// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). -// Because of we can't to print the multi-part objects with SLA technology. -bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) -{ - if (model_has_multi_part_objects(model())) { - show_info(nullptr, - _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - return true; -} - -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 (reason == ConfigWizard::RR_USER) { - if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) - return false; - } - - auto wizard = new ConfigWizard(mainframe); - const bool res = wizard->run(reason, start_page); - - if (res) { - load_current_presets(); - - // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() - if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) - may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); - } - - return res; -} - -void GUI_App::show_desktop_integration_dialog() -{ -#ifdef __linux__ - //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - DesktopIntegrationDialog dialog(mainframe); - dialog.ShowModal(); -#endif //__linux__ -} - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -void GUI_App::gcode_thumbnails_debug() -{ - const std::string BEGIN_MASK = "; thumbnail begin"; - const std::string END_MASK = "; thumbnail end"; - std::string gcode_line; - bool reading_image = false; - unsigned int width = 0; - unsigned int height = 0; - - wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() != wxID_OK) - return; - - std::string in_filename = into_u8(dialog.GetPath()); - std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); - - boost::nowide::ifstream in_file(in_filename.c_str()); - std::vector rows; - std::string row; - if (in_file.good()) - { - while (std::getline(in_file, gcode_line)) - { - if (in_file.good()) - { - if (boost::starts_with(gcode_line, BEGIN_MASK)) - { - reading_image = true; - gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); - std::string::size_type x_pos = gcode_line.find('x'); - std::string width_str = gcode_line.substr(0, x_pos); - width = (unsigned int)::atoi(width_str.c_str()); - std::string height_str = gcode_line.substr(x_pos + 1); - height = (unsigned int)::atoi(height_str.c_str()); - row.clear(); - } - else if (reading_image && boost::starts_with(gcode_line, END_MASK)) - { - std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; - boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); - if (out_file.good()) - { - std::string decoded; - decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); - decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); - - out_file.write(decoded.c_str(), decoded.size()); - out_file.close(); - } - - reading_image = false; - width = 0; - height = 0; - rows.clear(); - } - else if (reading_image) - row += gcode_line.substr(2); - } - } - - in_file.close(); - } -} -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - WindowMetrics metrics = WindowMetrics::from_window(window); - app_config->set(config_key, metrics.serialize()); - app_config->save(); -} - -void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - if (! app_config->has(config_key)) { - window->Maximize(default_maximized); - return; - } - - auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); - if (! metrics) { - window->Maximize(default_maximized); - return; - } - - const wxRect& rect = metrics->get_rect(); - - if (app_config->get("restore_win_position") == "1") { - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); - app_config->save(); - window->SetPosition(rect.GetPosition()); - - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); - app_config->save(); - window->SetSize(rect.GetSize()); - - // revert "restore_win_position" value if application wasn't crashed - app_config->set("restore_win_position", "1"); - app_config->save(); - } - else - window->CenterOnScreen(); - - window->Maximize(metrics->get_maximized()); -} - -void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) -{ - /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); - wxRect display; - if (display_idx == wxNOT_FOUND) { - display = wxDisplay(0u).GetClientArea(); - window->Move(display.GetTopLeft()); - } else { - display = wxDisplay(display_idx).GetClientArea(); - } - - auto metrics = WindowMetrics::from_window(window); - metrics.sanitize_for_display(display); - if (window->GetScreenRect() != metrics.get_rect()) { - window->SetSize(metrics.get_rect()); - } -} - -bool GUI_App::config_wizard_startup() -{ - if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { - run_wizard(ConfigWizard::RR_DATA_EMPTY); - return true; - } else if (get_app_config()->legacy_datadir()) { - // Looks like user has legacy pre-vendorbundle data directory, - // explain what this is and run the wizard - - MsgDataLegacy dlg; - dlg.ShowModal(); - - run_wizard(ConfigWizard::RR_DATA_LEGACY); - return true; - } - return false; -} - -bool GUI_App::check_updates(const bool verbose) -{ - PresetUpdater::UpdateResult updater_result; - try { - updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); - if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { - mainframe->Close(); - // Applicaiton is closing. - return false; - } - else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { - m_app_conf_exists = true; - } - else if (verbose && updater_result == PresetUpdater::R_NOOP) { - MsgNoUpdates dlg; - dlg.ShowModal(); - } - } - catch (const std::exception & ex) { - show_error(nullptr, ex.what()); - } - // Applicaiton will continue. - return true; -} - -bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) -{ - bool launch = true; - - // warning dialog containes a "Remember my choice" checkbox - std::string option_key = "suppress_hyperlinks"; - if (force_remember_choice || app_config->get(option_key).empty()) { - if (app_config->get(option_key).empty()) { - RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - dialog.ShowCheckBox(_L("Remember my choice")); - auto answer = dialog.ShowModal(); - launch = answer == wxID_YES; - if (dialog.IsCheckBoxChecked()) { - wxString preferences_item = _L("Suppress to open hyperlink in browser"); - wxString msg = - _L("PrusaSlicer will remember your choice.") + "\n\n" + - _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + - format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); - - MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); - if (msg_dlg.ShowModal() == wxID_CANCEL) - return false; - app_config->set(option_key, answer == wxID_NO ? "1" : "0"); - } - } - if (launch) - launch = app_config->get(option_key) != "1"; - } - // warning dialog doesn't containe a "Remember my choice" checkbox - // and will be shown only when "Suppress to open hyperlink in browser" is ON. - else if (app_config->get(option_key) == "1") { - MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - launch = dialog.ShowModal() == wxID_YES; - } - - return launch && wxLaunchDefaultBrowser(url, flags); -} - -// static method accepting a wxWindow object as first parameter -// void warning_catcher{ -// my($self, $message_dialog) = @_; -// return sub{ -// my $message = shift; -// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; -// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); -// $message_dialog -// ? $message_dialog->(@params) -// : Wx::MessageDialog->new($self, @params)->ShowModal; -// }; -// } - -// Do we need this function??? -// void GUI_App::notify(message) { -// auto frame = GetTopWindow(); -// // try harder to attract user attention on OS X -// if (!frame->IsActive()) -// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); -// -// // There used to be notifier using a Growl application for OSX, but Growl is dead. -// // The notifier also supported the Linux X D - bus notifications, but that support was broken. -// //TODO use wxNotificationMessage ? -// } - - -#ifdef __WXMSW__ -static bool set_into_win_registry(HKEY hkeyHive, const wchar_t* pszVar, const wchar_t* pszValue) -{ - // see as reference: https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association - wchar_t szValueCurrent[1000]; - DWORD dwType; - DWORD dwSize = sizeof(szValueCurrent); - - int iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); - - bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; - - if ((iRC != ERROR_SUCCESS) && !bDidntExist) - // an error occurred - return false; - - if (!bDidntExist) { - if (dwType != REG_SZ) - // invalid type - return false; - - if (::wcscmp(szValueCurrent, pszValue) == 0) - // value already set - return false; - } - - DWORD dwDisposition; - HKEY hkey; - iRC = ::RegCreateKeyExW(hkeyHive, pszVar, 0, 0, 0, KEY_ALL_ACCESS, nullptr, &hkey, &dwDisposition); - bool ret = false; - if (iRC == ERROR_SUCCESS) { - iRC = ::RegSetValueExW(hkey, L"", 0, REG_SZ, (BYTE*)pszValue, (::wcslen(pszValue) + 1) * sizeof(wchar_t)); - if (iRC == ERROR_SUCCESS) - ret = true; - } - - RegCloseKey(hkey); - return ret; -} - -void GUI_App::associate_3mf_files() -{ - wchar_t app_path[MAX_PATH]; - ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); - - std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; - std::wstring prog_id = L"Prusa.Slicer.1"; - std::wstring prog_desc = L"PrusaSlicer"; - std::wstring prog_command = prog_path + L" \"%1\""; - std::wstring reg_base = L"Software\\Classes"; - std::wstring reg_extension = reg_base + L"\\.3mf"; - std::wstring reg_prog_id = reg_base + L"\\" + prog_id; - std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; - - bool is_new = false; - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); - - if (is_new) - // notify Windows only when any of the values gets changed - ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); -} - -void GUI_App::associate_stl_files() -{ - wchar_t app_path[MAX_PATH]; - ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); - - std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; - std::wstring prog_id = L"Prusa.Slicer.1"; - std::wstring prog_desc = L"PrusaSlicer"; - std::wstring prog_command = prog_path + L" \"%1\""; - std::wstring reg_base = L"Software\\Classes"; - std::wstring reg_extension = reg_base + L"\\.stl"; - std::wstring reg_prog_id = reg_base + L"\\" + prog_id; - std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; - - bool is_new = false; - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); - - if (is_new) - // notify Windows only when any of the values gets changed - ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); -} - -void GUI_App::associate_gcode_files() -{ - wchar_t app_path[MAX_PATH]; - ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); - - std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; - std::wstring prog_id = L"PrusaSlicer.GCodeViewer.1"; - std::wstring prog_desc = L"PrusaSlicerGCodeViewer"; - std::wstring prog_command = prog_path + L" \"%1\""; - std::wstring reg_base = L"Software\\Classes"; - std::wstring reg_extension = reg_base + L"\\.gcode"; - std::wstring reg_prog_id = reg_base + L"\\" + prog_id; - std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; - - bool is_new = false; - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); - - if (is_new) - // notify Windows only when any of the values gets changed - ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); -} -#endif // __WXMSW__ - - -void GUI_App::on_version_read(wxCommandEvent& evt) -{ - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - std::string opt = app_config->get("notify_release"); - if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { - return; - } - if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { - return; - } - // notification - /* - this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) - , _u8L("See Download page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } - ); - */ - // updater - // read triggered_by_user that was set when calling GUI_App::app_version_check - app_updater(m_app_updater->get_triggered_by_user()); -} - -void GUI_App::app_updater(bool from_user) -{ - DownloadAppData app_data = m_app_updater->get_app_data(); - - if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) - { - BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; - MsgNoAppUpdates no_update_dialog; - no_update_dialog.ShowModal(); - return; - - } - - assert(!app_data.url.empty()); - assert(!app_data.target_path.empty()); - - // dialog with new version info - AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); - auto dialog_result = dialog.ShowModal(); - // checkbox "do not show again" - if (dialog.disable_version_check()) { - app_config->set("notify_release", "none"); - } - // Doesn't wish to update - if (dialog_result != wxID_OK) { - return; - } - // dialog with new version download (installer or app dependent on system) including path selection - AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); - dialog_result = dwnld_dlg.ShowModal(); - // Doesn't wish to download - if (dialog_result != wxID_OK) { - return; - } - app_data.target_path =dwnld_dlg.get_download_path(); - - // start download - this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); - app_data.start_after = dwnld_dlg.run_after_download(); - m_app_updater->set_app_data(std::move(app_data)); - m_app_updater->sync_download(); -} - -void GUI_App::app_version_check(bool from_user) -{ - if (from_user) { - if (m_app_updater->get_download_ongoing()) { - MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); - if (msgdlg.ShowModal() != wxID_YES) - return; - } - } - std::string version_check_url = app_config->version_check_url(); - m_app_updater->sync_version(version_check_url, from_user); -} - -} // GUI -} //Slic3r +#include "libslic3r/Technologies.hpp" +#include "GUI_App.hpp" +#include "GUI_Init.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_Factories.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Color.hpp" + +#include "GUI.hpp" +#include "GUI_Utils.hpp" +#include "3DScene.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" + +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/Process.hpp" +#include "../Utils/MacDarkMode.hpp" +#include "../Utils/AppUpdater.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "ConfigSnapshotDialog.hpp" +#include "FirmwareDialog.hpp" +#include "Preferences.hpp" +#include "Tab.hpp" +#include "SysInfoDialog.hpp" +#include "KBShortcutsDialog.hpp" +#include "UpdateDialogs.hpp" +#include "Mouse3DController.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "SavePresetDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "SendSystemInfoDialog.hpp" + +#include "BitmapCache.hpp" +#include "Notebook.hpp" + +#ifdef __WXMSW__ +#include +#include +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE +#endif +#ifdef _WIN32 +#include +#endif + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +#include +#include +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +// Needed for forcing menu icons back under gtk2 and gtk3 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include +#endif + +using namespace std::literals; + +namespace Slic3r { +namespace GUI { + +class MainFrame; + +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + wxASSERT(bitmap.IsOk()); + + int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + this->CenterOnScreen(); + int new_dpi = get_dpi_for_window(this); + + m_scale = (float)(new_dpi) / (float)(init_dpi); + m_main_bitmap = bitmap; + + scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + init_constant_text(); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font.Bold(); + + // draw logo and constant info text + Decorate(m_main_bitmap); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + memDC.SetFont(m_action_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + static wxBitmap MakeBitmap(wxBitmap bmp) + { + if (!bmp.IsOk()) + return wxNullBitmap; + + // create dark grey background for the splashscreen + // It will be 5/3 of the weight of the bitmap + int width = lround((double)5 / 3 * bmp.GetWidth()); + int height = bmp.GetHeight(); + + wxImage image(width, height); + unsigned char* imgdata_ = image.GetData(); + for (int i = 0; i < width * height; ++i) { + *imgdata_++ = 51; + *imgdata_++ = 51; + *imgdata_++ = 51; + } + + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); + + return new_bmp; + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // draw text to the box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int width = lround(bmp.GetWidth() * 0.4); + + // load bitmap for logo + BitmapCache bmp_cache; + int logo_size = lround(width * 0.25); + wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); + + wxCoord margin = int(m_scale * 20); + + wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); + banner_rect.Deflate(margin, 2 * margin); + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw logo + memDc.DrawBitmap(logo_bmp, margin, margin, true); + + // draw the (white) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(255, 255, 255)); + + memDc.SetFont(m_constant_text.title_font); + memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + + int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); + banner_rect.SetTop(banner_rect.GetTop() + title_height); + banner_rect.SetHeight(banner_rect.GetHeight() - title_height); + + memDc.SetFont(m_constant_text.version_font); + memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); + + memDc.SetFont(m_constant_text.credits_font); + memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); + int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); + int text_height = memDc.GetTextExtent("text").GetY(); + + // calculate position for the dynamic text + int logo_and_header_height = margin + logo_size + title_height + version_height; + m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); + } + +private: + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + version = _L("Version") + " " + std::string(SLIC3R_VERSION); + + // credits infornation + credits = title + " " + + _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + + _L("Developed by Prusa Research.")+ "\n\n" + + title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + "\n\n" + + _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + + _L("Artwork model by M Boyer"); + + title_font = version_font = credits_font = init_font; + } + } + m_constant_text; + + void init_constant_text() + { + m_constant_text.init(get_default_font(this)); + + // As default we use a system font for current display. + // Scale fonts in respect to banner width + + int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins + + float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); + scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); + + float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); + scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); + + // The width of the credits information string doesn't respect to the banner width some times. + // So, scale credits_font in the respect to the longest string width + int longest_string_width = word_wrap_string(m_constant_text.credits); + float font_scale = (float)text_banner_width / longest_string_width; + scale_font(m_constant_text.credits_font, font_scale); + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + // wrap a string for the strings no longer then 55 symbols + // return extent of the longest string + int word_wrap_string(wxString& input) + { + size_t line_len = 55;// count of symbols in one line + int idx = -1; + size_t cur_len = 0; + + wxString longest_sub_string; + auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { + if (cur_len > longest_sub_str.Len()) + longest_sub_str = input.SubString(i - cur_len + 1, i); + }; + + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + get_longest_sub_string(longest_sub_string, cur_len, i); + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + get_longest_sub_string(longest_sub_string, cur_len, i); + input[idx] = '\n'; + cur_len = i - static_cast(idx); + } + } + + return GetTextExtent(longest_sub_string).GetX(); + } +}; + + +#ifdef __linux__ +bool static check_old_linux_datadir(const wxString& app_name) { + // If we are on Linux and the datadir does not exist yet, look into the old + // location where the datadir was before version 2.3. If we find it there, + // tell the user that he might wanna migrate to the new location. + // (https://github.com/prusa3d/PrusaSlicer/issues/2911) + // To be precise, the datadir should exist, it is created when single instance + // lock happens. Instead of checking for existence, check the contents. + + namespace fs = boost::filesystem; + + std::string new_path = Slic3r::data_dir(); + + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string default_path = (dir + "/" + app_name).ToUTF8().data(); + + if (new_path != default_path) { + // This happens when the user specifies a custom --datadir. + // Do not show anything in that case. + return true; + } + + fs::path data_dir = fs::path(new_path); + if (! fs::is_directory(data_dir)) + return true; // This should not happen. + + int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); + + if (file_count <= 1) { // just cache dir with an instance lock + std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + + if (fs::is_directory(old_path)) { + wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " + "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" + "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " + "an old %1% configuration directory was detected in \n%3%.\n\n" + "Consider moving the contents of the old directory to the new location in order to access " + "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " + "location again.\n\n" + "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); + wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); + RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); + dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); + if (dlg.ShowModal() != wxID_NO) + return false; + } + } else { + // If the new directory exists, be silent. The user likely already saw the message. + } + return true; +} +#endif + +#ifdef _WIN32 +#if 0 // External Updater is replaced with AppUpdater.cpp +static bool run_updater_win() +{ + // find updater exe + boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; + // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst + std::string msg; + bool res = create_process(path_updater, L"/silent", msg); + if (!res) + BOOST_LOG_TRIVIAL(error) << msg; + return res; +} +#endif // 0 +#endif // _WIN32 + +struct FileWildcards { + std::string_view title; + std::vector file_extensions; +}; + +static const FileWildcards file_wildcards_by_type[FT_SIZE] = { + /* FT_STL */ { "STL files"sv, { ".stl"sv } }, + /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, + /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, + /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, + /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, + /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, + + /* FT_INI */ { "INI files"sv, { ".ini"sv } }, + /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, + + /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, + + /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, +}; + +#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR +wxString file_wildcards(FileType file_type) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + + // Generate cumulative first item + for (const std::string_view& ext : data.file_extensions) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } + else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); + + // Adds an item for each of the extensions + if (data.file_extensions.size() > 1) { + for (const std::string_view& ext : data.file_extensions) { + title = "*"; + title += ext; + ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); + } + } + + return ret; +} +#else +// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. +// The function accepts a custom extension parameter. If the parameter is provided, the custom extension +// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips +// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). +wxString file_wildcards(FileType file_type, const std::string &custom_extension) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + std::string custom_ext_lower; + + if (! custom_extension.empty()) { + // Generate an extension into the title mask and into the list of extensions. + custom_ext_lower = boost::to_lower_copy(custom_extension); + const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); + if (custom_ext_lower == custom_extension) { + // Add a lower case version. + title = std::string("*") + custom_ext_lower; + mask = title; + // Add an upper case version. + mask += ";*"; + mask += custom_ext_upper; + } else if (custom_ext_upper == custom_extension) { + // Add an upper case version. + title = std::string("*") + custom_ext_upper; + mask = title; + // Add a lower case version. + mask += ";*"; + mask += custom_ext_lower; + } else { + // Add the mixed case version only. + title = std::string("*") + custom_extension; + mask = title; + } + } + + for (const std::string_view &ext : data.file_extensions) + // Only add an extension if it was not added first as the custom extension. + if (ext != custom_ext_lower) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); +} +#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR + +static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) +static void register_win32_dpi_event() +{ + enum { WM_DPICHANGED_ = 0x02e0 }; + + wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { + const int dpi = wParam & 0xffff; + const auto rect = reinterpret_cast(lParam); + const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); + + DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); + win->GetEventHandler()->AddPendingEvent(evt); + + return true; + }); +} +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + +static void register_win32_device_notification_event() +{ + wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + switch (wParam) { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { +// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) +// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + default: + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + wchar_t sPath[MAX_PATH]; + if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { + struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); + if (! SHGetPathFromIDList(pidl, sPath)) { + BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; + return false; + } + } + switch (lParam) { + case SHCNE_MEDIAINSERTED: + { + //printf("SHCNE_MEDIAINSERTED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + break; + } + case SHCNE_MEDIAREMOVED: + { + //printf("SHCNE_MEDIAREMOVED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + break; + } + default: +// printf("Unknown\n"); + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); +// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { + if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { + RAWINPUT raw; + UINT rawSize = sizeof(RAWINPUT); + ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); + if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) + return true; + } + return false; + }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); +} +#endif // WIN32 + +static void generic_exception_handle() +{ + // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage + // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 + // + // wxLogError typically goes around exception handling and display an error dialog some time + // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. + // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates + // errors if multiple have been collected and displays just one error message for all of them. + // Otherwise we would get multiple error messages for one missing png, for example. + // + // If a custom error message window (or some other solution) were to be used, it would be necessary + // to turn off wxLogError() usage in wx APIs, most notably in wxImage + // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a + + try { + throw; + } catch (const std::bad_alloc& ex) { + // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) + // and terminate the app so it is at least certain to happen now. + wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " + "If you are sure you have enough RAM on your system, this may also be a bug and we would " + "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); + std::terminate(); + } catch (const boost::io::bad_format_string& ex) { + wxString errmsg = _L("PrusaSlicer has encountered a localization error. " + "Please report to PrusaSlicer team, what language was active and in which scenario " + "this issue happened. Thank you.\n\nThe application will now terminate."); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + std::terminate(); + throw; + } catch (const std::exception& ex) { + wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + throw; + } +} + +void GUI_App::post_init() +{ + assert(initialized()); + if (! this->initialized()) + throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); + + if (this->init_params->start_as_gcodeviewer) { + if (! this->init_params->input_files.empty()) + this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); + } + else { + if (! this->init_params->preset_substitutions.empty()) + show_substitutions_info(this->init_params->preset_substitutions); + +#if 0 + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + this->gui->mainframe->load_config(m_print_config); +#endif + if (! this->init_params->load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + this->mainframe->load_config_file(this->init_params->load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!this->init_params->input_files.empty()) { + const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); + if (!res.empty() && this->init_params->input_files.size() == 1) { + // Update application titlebar when opening a project file + const std::string& filename = this->init_params->input_files.front(); + if (boost::algorithm::iends_with(filename, ".amf") || + boost::algorithm::iends_with(filename, ".amf.xml") || + boost::algorithm::iends_with(filename, ".3mf")) + this->plater()->set_project_filename(from_u8(filename)); + } + } + if (! this->init_params->extra_config.empty()) + this->mainframe->load_config(this->init_params->extra_config); + } + + // show "Did you know" notification + if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) + plater_->get_notification_manager()->push_hint_notification(true); + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. + if (! this->check_updates(false)) + // Configuration is not compatible and reconfigure was refused by the user. Application is closing. + return; + CallAfter([this] { + bool cw_showed = this->config_wizard_startup(); + this->preset_updater->sync(preset_bundle); + this->app_version_check(false); + if (! cw_showed) { + // The CallAfter is needed as well, without it, GL extensions did not show. + // Also, we only want to show this when the wizard does not, so the new user + // sees something else than "we want something" on the first start. + show_send_system_info_dialog_if_needed(); + } + }); + } + + // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. + app_config->set("version", SLIC3R_VERSION); + app_config->save(); + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 +} + +IMPLEMENT_APP(GUI_App) + +GUI_App::GUI_App(EAppMode mode) + : wxApp() + , m_app_mode(mode) + , m_em_unit(10) + , m_imgui(new ImGuiWrapper()) + , m_removable_drive_manager(std::make_unique()) + , m_other_instance_message_handler(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp + this->init_app_config(); + // init app downloader after path to datadir is set + m_app_updater = std::make_unique(); +} + +GUI_App::~GUI_App() +{ + if (app_config != nullptr) + delete app_config; + + if (preset_bundle != nullptr) + delete preset_bundle; + + if (preset_updater != nullptr) + delete preset_updater; +} + +// If formatted for github, plaintext with OpenGL extensions enclosed into
. +// Otherwise HTML formatted for the system info dialog. +std::string GUI_App::get_gl_info(bool for_github) +{ + return OpenGLManager::get_gl_info().to_string(for_github); +} + +wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) +{ + return m_opengl_mgr.init_glcontext(canvas); +} + +bool GUI_App::init_opengl() +{ +#ifdef __linux__ + bool status = m_opengl_mgr.init_gl(); + m_opengl_initialized = true; + return status; +#else + return m_opengl_mgr.init_gl(); +#endif +} + +// gets path to PrusaSlicer.ini, returns semver from first line comment +static boost::optional parse_semver_from_ini(std::string path) +{ + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string body = buffer.str(); + size_t start = body.find("PrusaSlicer "); + if (start == std::string::npos) + return boost::none; + body = body.substr(start + 12); + size_t end = body.find_first_of(" \n"); + if (end < body.size()) + body.resize(end); + return Semver::parse(body); +} + +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. +// SetAppName(SLIC3R_APP_KEY); + SetAppName(SLIC3R_APP_KEY "-alpha"); +// SetAppName(SLIC3R_APP_KEY "-beta"); + + +// SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) { + #ifndef __linux__ + set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); + #else + // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. + // https://github.com/prusa3d/PrusaSlicer/issues/2911 + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); + #endif + } else { + m_datadir_redefined = true; + } + + if (!app_config) + app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); + + // load settings + m_app_conf_exists = app_config->exists(); + if (m_app_conf_exists) { + std::string error = app_config->load(); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + } +} + +// returns old config path to copy from if such exists, +// returns an empty string if such config path does not exists or if it cannot be loaded. +std::string GUI_App::check_older_app_config(Semver current_version, bool backup) +{ + std::string older_data_dir_path; + + // If the config folder is redefined - do not check + if (m_datadir_redefined) + return {}; + + // find other version app config (alpha / beta / release) + std::string config_path = app_config->config_path(); + boost::filesystem::path parent_file_path(config_path); + std::string filename = parent_file_path.filename().string(); + parent_file_path.remove_filename().remove_filename(); + + std::vector candidates; + + if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); + if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); + if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); + + Semver last_semver = current_version; + for (const auto& candidate : candidates) { + if (boost::filesystem::exists(candidate)) { + // parse + boost::optionalother_semver = parse_semver_from_ini(candidate.string()); + if (other_semver && *other_semver > last_semver) { + last_semver = *other_semver; + older_data_dir_path = candidate.parent_path().string(); + } + } + } + if (older_data_dir_path.empty()) + return {}; + BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; + // ask about using older data folder + + InfoDialog msg(nullptr + , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) + , backup ? + format_wxstr(_L( + "The active configuration was created by %1% %2%," + "\nwhile a newer configuration was found in %3%" + "\ncreated by %1% %4%." + "\n\nShall the newer configuration be imported?" + "\nIf so, your active configuration will be backed up before importing the new configuration." + ) + , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) + : format_wxstr(_L( + "An existing configuration was found in %3%" + "\ncreated by %1% %2%." + "\n\nShall this configuration be imported?" + ) + , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) + , true, wxYES_NO); + + if (backup) { + msg.SetButtonLabel(wxID_YES, _L("Import")); + msg.SetButtonLabel(wxID_NO, _L("Don't import")); + } + + if (msg.ShowModal() == wxID_YES) { + std::string snapshot_id; + if (backup) { + const Config::Snapshot* snapshot{ nullptr }; + if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", + _u8L("Continue and import newer configuration?"), &snapshot)) + return {}; + if (snapshot) { + // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. + snapshot_id = snapshot->id; + assert(! snapshot_id.empty()); + app_config->set("on_snapshot", snapshot_id); + } else + BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; + } + + // load app config from older file + std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + if (!snapshot_id.empty()) + app_config->set("on_snapshot", snapshot_id); + m_app_conf_exists = true; + return older_data_dir_path; + } + return {}; +} + +void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) +{ + BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; + m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); +} + +bool GUI_App::OnInit() +{ + try { + return on_init_inner(); + } catch (const std::exception&) { + generic_exception_handle(); + return false; + } +} + +bool GUI_App::on_init_inner() +{ + // Set initialization of image handlers before any UI actions - See GH issue #7469 + wxInitAllImageHandlers(); + +#if defined(_WIN32) && ! defined(_WIN64) + // Win32 32bit build. + if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { + RichMessageDialog dlg(nullptr, + _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." + "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." + "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." + "\nDo you wish to continue?"), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } +#endif // _WIN64 + + // Forcing back menu icons under gtk2 and gtk3. Solution is based on: + // https://docs.gtk.org/gtk3/class.Settings.html + // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb + // TODO: Find workaround for GTK4 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); +#endif + + // Verify resources path + const wxString resources_dir = from_u8(Slic3r::resources_dir()); + wxCHECK_MSG(wxDirExists(resources_dir), false, + wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); + +#ifdef __linux__ + if (! check_old_linux_datadir(GetAppName())) { + std::cerr << "Quitting, user chose to move their data to new location." << std::endl; + return false; + } +#endif + + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. +// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. +// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); + +// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; + + // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. + // Like here, before the show InfoDialog in check_older_app_config() + + // If load_language() fails, the application closes. + load_language(wxString(), true); +#ifdef _MSW_DARK_MODE + bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; + bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); +#endif + // initialize label colors and fonts + init_label_colours(); + init_fonts(); + + std::string older_data_dir_path; + if (m_app_conf_exists) { + if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) + // Only copying configuration if it was saved with a newer slicer than the one currently running. + older_data_dir_path = check_older_app_config(app_config->orig_version(), true); + } else { + // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. + older_data_dir_path = check_older_app_config(Semver(), false); + } + +#ifdef _MSW_DARK_MODE + // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed + if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; + init_dark_color_mode != new_dark_color_mode) { + NppDarkMode::SetDarkMode(new_dark_color_mode); + init_label_colours(); + update_label_colours_from_appconfig(); + } + if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + init_sys_menu_enabled != new_sys_menu_enabled) + NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); +#endif + + if (is_editor()) { + std::string msg = Http::tls_global_init(); + std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); + bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); + + if (!msg.empty() && !ssl_accept) { + RichMessageDialog + dlg(nullptr, + wxString::Format(_L("%s\nDo you want to continue?"), msg), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Remember my choice")); + if (dlg.ShowModal() != wxID_YES) return false; + + app_config->set("tls_cert_store_accepted", + dlg.IsCheckBoxChecked() ? "yes" : "no"); + app_config->set("tls_accepted_cert_store_location", + dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); + } + } + + SplashScreen* scrn = nullptr; + if (app_config->get("show_splash_screen") == "1") { + // make a bitmap with dark grey banner on the left side + wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); + + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + bool default_splashscreen_pos = true; + if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + default_splashscreen_pos = metrics == boost::none; + if (!default_splashscreen_pos) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + if (!default_splashscreen_pos) { + // workaround for crash related to the positioning of the window on secondary monitor + get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); + get_app_config()->save(); + } + + // create splash screen with updated bmp + scrn = new SplashScreen(bmp.IsOk() ? bmp : create_scaled_bitmap("PrusaSlicer", nullptr, 400), + wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); + + if (!default_splashscreen_pos) + // revert "restore_win_position" value if application wasn't crashed + get_app_config()->set("restore_win_position", "1"); +#ifndef __linux__ + wxYield(); +#endif + scrn->SetText(_L("Loading configuration")+ dots); + } + + preset_bundle = new PresetBundle(); + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle->setup_directories(); + + if (! older_data_dir_path.empty()) { + preset_bundle->import_newer_configs(older_data_dir_path); + app_config->save(); + } + + if (is_editor()) { +#ifdef __WXMSW__ +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + // file association is not possible anymore starting with Win 8 + if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + } +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER +#endif // __WXMSW__ + + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); + Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { + std::string evt_string = into_u8(evt.GetString()); + if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { + auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); + this->plater_->get_notification_manager()->push_notification( notif_type + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) + , _u8L("See Releases page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } + ); + } + } + }); + Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { + //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); + }); + + Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); + if(!evt.GetString().IsEmpty()) + show_error(nullptr, evt.GetString()); + }); + + Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { + show_error(nullptr, evt.GetString()); + }); + } + else { +#ifdef __WXMSW__ +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + // file association is not possible anymore starting with Win 8 + if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + } +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER +#endif // __WXMSW__ + } + + // Suppress the '- default -' presets. + preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); + try { + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + } catch (const std::exception &ex) { + show_error(nullptr, ex.what()); + } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + register_win32_dpi_event(); +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + register_win32_device_notification_event(); +#endif // WIN32 + + // Let the libslic3r know the callback, which will translate messages on demand. + Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); + + // application frame + if (scrn && is_editor()) + scrn->SetText(_L("Preparing settings tabs") + dots); + + mainframe = new MainFrame(); + // hide settings tabs after first Layout + if (is_editor()) + mainframe->select_tab(size_t(0)); + + sidebar().obj_list()->init_objects(); // propagate model objects to object list +// update_mode(); // !!! do that later + SetTopWindow(mainframe); + + plater_->init_notification_manager(); + + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else + load_current_presets(); + + // Save the active profiles as a "saved into project". + update_saved_preset_from_current_preset(); + + if (plater_ != nullptr) { + // Save the names of active presets and project specific config into ProjectDirtyStateManager. + plater_->reset_project_dirty_initial_presets(); + // Update Project dirty state, update application title bar. + plater_->update_project_dirty_from_presets(); + } + + mainframe->Show(true); + + obj_list()->set_min_height(); + + update_mode(); // update view mode after fix of the object_list size + +#ifdef __APPLE__ + other_instance_message_handler()->bring_instance_forward(); +#endif //__APPLE__ + + Bind(wxEVT_IDLE, [this](wxIdleEvent& event) + { + if (! plater_) + return; + + this->obj_manipul()->update_if_dirty(); + + // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT + // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. +#ifdef __linux__ + if (! m_post_initialized && m_opengl_initialized) { +#else + if (! m_post_initialized) { +#endif + m_post_initialized = true; +#ifdef WIN32 + this->mainframe->register_win32_callbacks(); +#endif + this->post_init(); + } + + if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") + app_config->save(); + }); + + m_initialized = true; + + if (const std::string& crash_reason = app_config->get("restore_win_position"); + boost::starts_with(crash_reason,"crashed")) + { + wxString preferences_item = _L("Restore window position on start"); + InfoDialog dialog(nullptr, + _L("PrusaSlicer started after a crash"), + format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" + "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" + "More precise reason for the crash: \"%1%\".\n" + "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" + "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " + "Otherwise, the application will most likely crash again next time."), + "" + from_u8(crash_reason) + "", + "#2939", + "#5573", + "" + preferences_item + ""), + true, wxYES_NO); + + dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); + dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); + + auto answer = dialog.ShowModal(); + if (answer == wxID_YES) + app_config->set("restore_win_position", "0"); + else if (answer == wxID_NO) + app_config->set("restore_win_position", "1"); + app_config->save(); + } + + return true; +} + +unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) +{ + double r = colour.Red(); + double g = colour.Green(); + double b = colour.Blue(); + + return std::round(std::sqrt( + r * r * .241 + + g * g * .691 + + b * b * .068 + )); +} + +bool GUI_App::dark_mode() +{ +#if __APPLE__ + // The check for dark mode returns false positive on 10.12 and 10.13, + // which allowed setting dark menu bar and dock area, which is + // is detected as dark mode. We must run on at least 10.14 where the + // proper dark mode was first introduced. + return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); +#else + return wxGetApp().app_config->get("dark_color_mode") == "1" ? true : check_dark_mode(); + //const unsigned luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + //return luma < 128; +#endif +} + +const wxColour GUI_App::get_label_default_clr_system() +{ + return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); +} + +const wxColour GUI_App::get_label_default_clr_modified() +{ + return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); +} + +void GUI_App::init_label_colours() +{ + m_color_label_modified = get_label_default_clr_modified(); + m_color_label_sys = get_label_default_clr_system(); + + bool is_dark_mode = dark_mode(); +#ifdef _WIN32 + m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); + m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); + m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); + m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); +#else + m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +#endif + m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + +void GUI_App::update_label_colours_from_appconfig() +{ + if (app_config->has("label_clr_sys")) { + auto str = app_config->get("label_clr_sys"); + if (str != "") + m_color_label_sys = wxColour(str); + } + + if (app_config->has("label_clr_modified")) { + auto str = app_config->get("label_clr_modified"); + if (str != "") + m_color_label_modified = wxColour(str); + } +} + +void GUI_App::update_label_colours() +{ + for (Tab* tab : tabs_list) + tab->update_label_colours(); +} + +#ifdef _WIN32 +static bool is_focused(HWND hWnd) +{ + HWND hFocusedWnd = ::GetFocus(); + return hFocusedWnd && hWnd == hFocusedWnd; +} + +static bool is_default(wxWindow* win) +{ + wxTopLevelWindow* tlw = find_toplevel_parent(win); + if (!tlw) + return false; + + return win == tlw->GetDefaultItem(); +} +#endif + +void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) +{ +#ifdef _WIN32 + bool is_focused_button = false; + bool is_default_button = false; + if (wxButton* btn = dynamic_cast(window)) { + if (!(btn->GetWindowStyle() & wxNO_BORDER)) { + btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); + highlited = true; + } + // button marking + { + auto mark_button = [this, btn, highlited](const bool mark) { + if (btn->GetLabel().IsEmpty()) + btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); + else + btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); + btn->Refresh(); + btn->Update(); + }; + + // hovering + btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + // focusing + btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); + + is_focused_button = is_focused(btn->GetHWND()); + is_default_button = is_default(btn); + if (is_focused_button || is_default_button) + mark_button(is_focused_button); + } + } + else if (wxTextCtrl* text = dynamic_cast(window)) { + if (text->GetBorder() != wxBORDER_SIMPLE) + text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); + } + else if (wxCheckListBox* list = dynamic_cast(window)) { + list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); + list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + for (size_t i = 0; i < list->GetCount(); i++) + if (wxOwnerDrawn* item = list->GetItem(i)) { + item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + item->SetTextColour(m_color_label_default); + } + return; + } + else if (dynamic_cast(window)) + window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); + + if (!just_font) + window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + if (!is_focused_button && !is_default_button) + window->SetForegroundColour(m_color_label_default); +#endif +} + +// recursive function for scaling fonts for all controls in Window +#ifdef _WIN32 +static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) +{ + bool is_btn = dynamic_cast(window) != nullptr; + if (!(just_buttons_update && !is_btn)) + wxGetApp().UpdateDarkUI(window, is_btn); + + auto children = window->GetChildren(); + for (auto child : children) { + update_dark_children_ui(child); + } +} +#endif + +// Note: Don't use this function for Dialog contains ScalableButtons +void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) +{ +#ifdef _WIN32 + update_dark_children_ui(dlg, just_buttons_update); +#endif +} +void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) +{ +#ifdef _WIN32 + UpdateDarkUI(dvc, highlited ? dark_mode() : false); +#ifdef _MSW_DARK_MODE + dvc->RefreshHeaderDarkMode(&m_normal_font); +#endif //_MSW_DARK_MODE + if (dvc->HasFlag(wxDV_ROW_LINES)) + dvc->SetAlternateRowColour(m_color_highlight_default); + if (dvc->GetBorder() != wxBORDER_SIMPLE) + dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); +#endif +} + +void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(parent); + + auto children = parent->GetChildren(); + for (auto child : children) { + if (dynamic_cast(child)) + child->SetForegroundColour(m_color_label_default); + } +#endif +} + +void GUI_App::init_fonts() +{ + m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); + m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + +#ifdef __WXMAC__ + m_small_font.SetPointSize(11); + m_bold_font.SetPointSize(13); +#endif /*__WXMAC__*/ + + // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as + // DEFAULT in wxGtk. Use the TELETYPE family as a work-around + m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::update_fonts(const MainFrame *main_frame) +{ + /* Only normal and bold fonts are used for an application rescale, + * because of under MSW small and normal fonts are the same. + * To avoid same rescaling twice, just fill this values + * from rescaled MainFrame + */ + if (main_frame == nullptr) + main_frame = this->mainframe; + m_normal_font = main_frame->normal_font(); + m_small_font = m_normal_font; + m_bold_font = main_frame->normal_font().Bold(); + m_link_font = m_bold_font.Underlined(); + m_em_unit = main_frame->em_unit(); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::set_label_clr_modified(const wxColour& clr) +{ + if (m_color_label_modified == clr) + return; + m_color_label_modified = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_modified", str); + app_config->save(); +} + +void GUI_App::set_label_clr_sys(const wxColour& clr) +{ + if (m_color_label_sys == clr) + return; + m_color_label_sys = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_sys", str); + app_config->save(); +} + +bool GUI_App::tabs_as_menu() const +{ + return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); +} + +wxSize GUI_App::get_min_size() const +{ + return wxSize(76*m_em_unit, 49 * m_em_unit); +} + +float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit*0.1f; +#endif // __APPLE__ + + const std::string& use_val = app_config->get("use_custom_toolbar_size"); + const std::string& val = app_config->get("custom_toolbar_size"); + const std::string& auto_val = app_config->get("auto_toolbar_size"); + + if (val.empty() || auto_val.empty() || use_val.empty()) + return icon_sc; + + int int_val = use_val == "0" ? 100 : atoi(val.c_str()); + // correct value in respect to auto_toolbar_size + int_val = std::min(atoi(auto_val.c_str()), int_val); + + if (is_limited && int_val < 50) + int_val = 50; + + return 0.01f * int_val * icon_sc; +} + +void GUI_App::set_auto_toolbar_icon_scale(float scale) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); + std::string val = std::to_string(int_val); + + app_config->set("auto_toolbar_size", val); +} + +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; + for (const std::string& preset_name : preset_names) + msg_text += "\n \"" + from_u8(preset_name) + "\","; + msg_text.RemoveLast(); + msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" + "Settings will be available in physical printers settings.") + "\n\n" + + _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" + "Note: This name can be changed later from the physical printers settings"); + + //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +} + +void GUI_App::recreate_GUI(const wxString& msg_name) +{ + m_is_recreating_gui = true; + + mainframe->shutdown(); + + wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); + dlg.Pulse(); + dlg.Update(10, _L("Recreating") + dots); + + MainFrame *old_main_frame = mainframe; + mainframe = new MainFrame(); + if (is_editor()) + // hide settings tabs after first Layout + mainframe->select_tab(size_t(0)); + // Propagate model objects to object list. + sidebar().obj_list()->init_objects(); + SetTopWindow(mainframe); + + dlg.Update(30, _L("Recreating") + dots); + old_main_frame->Destroy(); + + dlg.Update(80, _L("Loading of current presets") + dots); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + load_current_presets(); + mainframe->Show(true); + + dlg.Update(90, _L("Loading of a mode view") + dots); + + obj_list()->set_min_height(); + update_mode(); + + // #ys_FIXME_delete_after_testing Do we still need this ? +// CallAfter([]() { +// // Run the config wizard, don't offer the "reset user profile" checkbox. +// config_wizard_startup(true); +// }); + + m_is_recreating_gui = false; +} + +void GUI_App::system_info() +{ + SysInfoDialog dlg; + dlg.ShowModal(); +} + +void GUI_App::keyboard_shortcuts() +{ + KBShortcutsDialog dlg; + dlg.ShowModal(); +} + +// static method accepting a wxWindow object as first parameter +bool GUI_App::catch_error(std::function cb, + // wxMessageDialog* message_dialog, + const std::string& err /*= ""*/) +{ + if (!err.empty()) { + if (cb) + cb(); + // if (message_dialog) + // message_dialog->(err, "Error", wxOK | wxICON_ERROR); + show_error(/*this*/nullptr, err); + return true; + } + return false; +} + +// static method accepting a wxWindow object as first parameter +void fatal_error(wxWindow* parent) +{ + show_error(parent, ""); + // exit 1; // #ys_FIXME +} + +#ifdef _WIN32 + +#ifdef _MSW_DARK_MODE +static void update_scrolls(wxWindow* window) +{ + wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); + while (node) + { + wxWindow* win = node->GetData(); + if (dynamic_cast(win) || + dynamic_cast(win) || + dynamic_cast(win)) + NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); + + update_scrolls(win); + node = node->GetNext(); + } +} +#endif //_MSW_DARK_MODE + + +#ifdef _MSW_DARK_MODE +void GUI_App::force_menu_update() +{ + NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); +} +#endif //_MSW_DARK_MODE + +void GUI_App::force_colors_update() +{ +#ifdef _MSW_DARK_MODE + NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); + if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) + NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); + NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); + NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); +#endif //_MSW_DARK_MODE + m_force_colors_update = true; +} +#endif //_WIN32 + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void GUI_App::update_ui_from_settings() +{ + update_label_colours(); +#ifdef _WIN32 + // Upadte UI colors before Update UI from settings + if (m_force_colors_update) { + m_force_colors_update = false; + mainframe->force_color_changed(); + mainframe->diff_dialog.force_color_changed(); + mainframe->preferences_dialog->force_color_changed(); + mainframe->printhost_queue_dlg()->force_color_changed(); +#ifdef _MSW_DARK_MODE + update_scrolls(mainframe); + if (mainframe->is_dlg_layout()) { + // update for tabs bar + UpdateDarkUI(&mainframe->m_settings_dialog); + mainframe->m_settings_dialog.Fit(); + mainframe->m_settings_dialog.Refresh(); + // update scrollbars + update_scrolls(&mainframe->m_settings_dialog); + } +#endif //_MSW_DARK_MODE + } +#endif + mainframe->update_ui_from_settings(); +} + +void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) +{ + const std::string name = into_u8(window->GetName()); + + window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { + window_pos_save(window, name); + event.Skip(); + }); + + window_pos_restore(window, name, default_maximized); + + on_window_geometry(window, [=]() { + window_pos_sanitize(window); + }); +} + +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (3MF/AMF):"), + app_config->get_last_dir(), "", + file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const +{ + input_files.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one or more files (STL/OBJ/AMF/3MF/PRUSA):"), + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + dialog.GetPaths(input_files); +} + +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +bool GUI_App::switch_language() +{ + if (select_language()) { + recreate_GUI(_L("Changing of an application language") + dots); + return true; + } else { + return false; + } +} + +#ifdef __linux__ +static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, + const wxLanguageInfo* system_language) +{ + constexpr size_t max_len = 50; + char path[max_len] = ""; + std::vector locales; + const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); + + // Call locale -a so we can parse the output to get the list of available locales + // We expect lines such as "en_US.utf8". Pick ones starting with the language code + // we are switching to. Lines with different formatting will be removed later. + FILE* fp = popen("locale -a", "r"); + if (fp != NULL) { + while (fgets(path, max_len, fp) != NULL) { + std::string line(path); + line = line.substr(0, line.find('\n')); + if (boost::starts_with(line, lang_prefix)) + locales.push_back(line); + } + pclose(fp); + } + + // locales now contain all candidates for this language. + // Sort them so ones containing anything about UTF-8 are at the end. + std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) + { + auto has_utf8 = [](const std::string & s) { + auto S = boost::to_upper_copy(s); + return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; + }; + return ! has_utf8(a) && has_utf8(b); + }); + + // Remove the suffix behind a dot, if there is one. + for (std::string& s : locales) + s = s.substr(0, s.find(".")); + + // We just hope that dear Linux "locale -a" returns country codes + // in ISO 3166-1 alpha-2 code (two letter) format. + // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + // To be sure, remove anything not looking as expected + // (any number of lowercase letters, underscore, two uppercase letters). + locales.erase(std::remove_if(locales.begin(), + locales.end(), + [](const std::string& s) { + return ! std::regex_match(s, + std::regex("^[a-z]+_[A-Z]{2}$")); + }), + locales.end()); + + // Is there a candidate matching a country code of a system language? Move it to the end, + // while maintaining the order of matches, so that the best match ends up at the very end. + std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); + int cnt = locales.size(); + for (int i=0; iempty()) { + const std::string &locale = *it; + const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); + if (wxLocale::IsAvailable(lang->Language)) + return lang; + } + return language; +} +#endif + +int GUI_App::GetSingleChoiceIndex(const wxString& message, + const wxString& caption, + const wxArrayString& choices, + int initialSelection) +{ +#ifdef _WIN32 + wxSingleChoiceDialog dialog(nullptr, message, caption, choices); + wxGetApp().UpdateDlgDarkUI(&dialog); + + dialog.SetSelection(initialSelection); + return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; +#else + return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); +#endif +} + +// select language from the list of installed languages +bool GUI_App::select_language() +{ + wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); + std::vector language_infos; + language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); + for (size_t i = 0; i < translations.GetCount(); ++ i) { + const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); + if (langinfo != nullptr) + language_infos.emplace_back(langinfo); + } + sort_remove_duplicates(language_infos); + std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); + + wxArrayString names; + names.Alloc(language_infos.size()); + + // Some valid language should be selected since the application start up. + const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); + int init_selection = -1; + int init_selection_alt = -1; + int init_selection_default = -1; + for (size_t i = 0; i < language_infos.size(); ++ i) { + if (wxLanguage(language_infos[i]->Language) == current_language) + // The dictionary matches the active language and country. + init_selection = i; + else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || + // if the active language is Slovak, mark the Czech language as active. + (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) + // The dictionary matches the active language, it does not necessarily match the country. + init_selection_alt = i; + if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") + // This will be the default selection if the active language does not match any dictionary. + init_selection_default = i; + names.Add(language_infos[i]->Description); + } + if (init_selection == -1) + // This is the dictionary matching the active language. + init_selection = init_selection_alt; + if (init_selection != -1) + // This is the language to highlight in the choice dialog initially. + init_selection_default = init_selection; + + const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); + // Try to load a new language. + if (index != -1 && (init_selection == -1 || init_selection != index)) { + const wxLanguageInfo *new_language_info = language_infos[index]; + if (this->load_language(new_language_info->CanonicalName, false)) { + // Save language at application config. + // Which language to save as the selected dictionary language? + // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its + // stability in the future: + // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + // 2) Current locale language may not match the dictionary name, see GH issue #3901 + // m_wxLocale->GetCanonicalName() + // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. + app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); + app_config->save(); + return true; + } + } + + return false; +} + +// Load gettext translation files and activate them at the start of the application, +// based on the "translation_language" key stored in the application config. +bool GUI_App::load_language(wxString language, bool initial) +{ + if (initial) { + // There is a static list of lookup path prefixes in wxWidgets. Add ours. + wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); + // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. + language = app_config->get("translation_language"); + if (! language.empty()) + BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; + + // Get the system language. + { + const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); + if (lang_system != wxLANGUAGE_UNKNOWN) { + m_language_info_system = wxLocale::GetLanguageInfo(lang_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); + } + } + { + // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. + wxLocale temp_locale; + // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). + wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); + // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer + // and try to match them with the system specific "preferred languages". + // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). + // The last parameter gets added to the list of detected dictionaries. This is a workaround + // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. + wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + if (! best_language.IsEmpty()) { + m_language_info_best = wxLocale::FindLanguageInfo(best_language); + BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); + } + #ifdef __linux__ + wxString lc_all; + if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { + // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. + // Disregard the "best" suggestion in case LC_ALL is provided. + m_language_info_best = nullptr; + } + #endif + } + } + + const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); + if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { + // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). + language_info = nullptr; + BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); + } + + if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { + BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); + language_info = nullptr; + } + + if (language_info == nullptr) { + // PrusaSlicer does not support the Right to Left languages yet. + if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_system; + if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_best; + if (language_info == nullptr) + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); + + // Alternate language code. + wxLanguage language_dict = wxLanguage(language_info->Language); + if (language_info->CanonicalName.BeforeFirst('_') == "sk") { + // Slovaks understand Czech well. Give them the Czech translation. + language_dict = wxLANGUAGE_CZECH; + BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; + } + + // Select language for locales. This language may be different from the language of the dictionary. + if (language_info == m_language_info_best || language_info == m_language_info_system) { + // The current language matches user's default profile exactly. That's great. + } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { + // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. + // This allows a Swiss guy to use a German dictionary without forcing him to German locales. + language_info = m_language_info_best; + } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) + language_info = m_language_info_system; + +#ifdef __linux__ + // If we can't find this locale , try to use different one for the language + // instead of just reporting that it is impossible to switch. + if (! wxLocale::IsAvailable(language_info->Language)) { + std::string original_lang = into_u8(language_info->CanonicalName); + language_info = linux_get_existing_locale_language(language_info, m_language_info_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") + % original_lang % language_info->CanonicalName.ToUTF8().data(); + } +#endif + + if (! wxLocale::IsAvailable(language_info->Language)) { + // Loading the language dictionary failed. + wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; +#endif + if (initial) + message + "\n\nApplication will close."; + wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); + if (initial) + std::exit(EXIT_FAILURE); + else + return false; + } + + // Release the old locales, create new locales. + //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. + m_wxLocale.release(); + m_wxLocale = Slic3r::make_unique(); + m_wxLocale->Init(language_info->Language); + // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) + // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. + wxTranslations::Get()->SetLanguage(language_dict); + m_wxLocale->AddCatalog(SLIC3R_APP_KEY); + m_imgui->set_language(into_u8(language_info->CanonicalName)); + //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. + //wxSetlocale(LC_NUMERIC, "C"); + Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); + return true; +} + +Tab* GUI_App::get_tab(Preset::Type type) +{ + for (Tab* tab: tabs_list) + if (tab->type() == type) + return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab + return nullptr; +} + +ConfigOptionMode GUI_App::get_mode() +{ + if (!app_config->has("view_mode")) + return comSimple; + + const auto mode = app_config->get("view_mode"); + return mode == "expert" ? comExpert : + mode == "simple" ? comSimple : comAdvanced; +} + +void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) +{ + const std::string mode_str = mode == comExpert ? "expert" : + mode == comSimple ? "simple" : "advanced"; + app_config->set("view_mode", mode_str); + app_config->save(); + update_mode(); +} + +// Update view mode according to selected menu +void GUI_App::update_mode() +{ + sidebar().update_mode(); + +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + + for (auto tab : tabs_list) + tab->update_mode(); + + plater()->update_menus(); + plater()->canvas3D()->update_gizmos_on_off_state(); +} + +void GUI_App::add_config_menu(wxMenuBar *menu) +{ + auto local_menu = new wxMenu(); + wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); + + const auto config_wizard_name = _(ConfigWizard::name(true)); + const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); + // Cmd+, is standard on OS X - what about other operating systems? + if (is_editor()) { + local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); + local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); + local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); + local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + //if (DesktopIntegrationDialog::integration_possible()) + local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); +#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + local_menu->AppendSeparator(); + } + local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + +#ifdef __APPLE__ + "\tCtrl+,", +#else + "\tCtrl+P", +#endif + _L("Application preferences")); + wxMenu* mode_menu = nullptr; + if (is_editor()) { + local_menu->AppendSeparator(); + mode_menu = new wxMenu(); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); + + local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); + } + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); + if (is_editor()) { + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); + // TODO: for when we're able to flash dictionaries + // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); + } + + local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { + switch (event.GetId() - config_id_base) { + case ConfigMenuWizard: + run_wizard(ConfigWizard::RR_USER); + break; + case ConfigMenuUpdateConf: + check_updates(true); + break; + case ConfigMenuUpdateApp: + app_version_check(true); + break; +#ifdef __linux__ + case ConfigMenuDesktopIntegration: + show_desktop_integration_dialog(); + break; +#endif + case ConfigMenuTakeSnapshot: + // Take a configuration snapshot. + if (wxString action_name = _L("Taking a configuration snapshot"); + check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { + wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); + UpdateDlgDarkUI(&dlg); + + // set current normal font for dialog children, + // because of just dlg.SetFont(normal_font()) has no result; + for (auto child : dlg.GetChildren()) + child->SetFont(normal_font()); + + if (dlg.ShowModal() == wxID_OK) + if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( + *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + snapshot != nullptr) + app_config->set("on_snapshot", snapshot->id); + } + break; + case ConfigMenuSnapshots: + if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { + std::string on_snapshot; + if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) + on_snapshot = app_config->get("on_snapshot"); + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); + dlg.ShowModal(); + if (!dlg.snapshot_to_activate().empty()) { + if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && + ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", + GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) + break; + try { + app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); + // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system + // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users + // are known to be creative and mess with the config files in various ways. + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) + show_substitutions_info(all_substitutions); + + // Load the currently selected preset into the GUI, update the preset selection box. + load_current_presets(); + } catch (std::exception &ex) { + GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); + } + } + } + break; + case ConfigMenuPreferences: + { + open_preferences(); + break; + } + case ConfigMenuLanguage: + { + /* Before change application language, let's check unsaved changes on 3D-Scene + * and draw user's attention to the application restarting after a language change + */ + { + // the dialog needs to be destroyed before the call to switch_language() + // or sometimes the application crashes into wxDialogBase() destructor + // so we put it into an inner scope + wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Language selection"); + //wxMessageDialog dialog(nullptr, + MessageDialog dialog(nullptr, + _L("Switching the language will trigger application restart.\n" + "You will lose content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxOK | wxCANCEL); + if (dialog.ShowModal() == wxID_CANCEL) + return; + } + + switch_language(); + break; + } + case ConfigMenuFlashFirmware: + FirmwareDialog::run(mainframe); + break; + default: + break; + } + }); + + using std::placeholders::_1; + + if (mode_menu != nullptr) { + auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); + } + + menu->Append(local_menu, _L("&Configuration")); +} + +void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + mainframe->preferences_dialog->show(highlight_option, tab_name); + + if (mainframe->preferences_dialog->recreate_GUI()) + recreate_GUI(_L("Restart application") + dots); + +#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER + if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) +#else + if (mainframe->preferences_dialog->seq_top_layer_only_changed()) +#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER + this->plater_->refresh_print(); + +#ifdef _WIN32 +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + // file association is not possible anymore starting with Win 8 + if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + if (is_editor()) { + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); + } + else { + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); + } +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + } +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER +#endif // _WIN32 + + if (mainframe->preferences_dialog->settings_layout_changed()) { + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); + mainframe->update_layout(); + mainframe->select_tab(size_t(0)); + } +} + +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector GUI_App::get_active_preset_collections() const +{ + std::vector ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) + ret.push_back(tab->get_presets()); + return ret; +} + +// To notify the user whether he is aware that some preset changes will be lost, +// UnsavedChangesDialog: "Discard / Save / Cancel" +// This is called when: +// - Close Application & Current project isn't saved +// - Load Project & Current project isn't saved +// - Undo / Redo with change of print technologie +// - Loading snapshot +// - Loading config_file/bundle +// UnsavedChangesDialog: "Don't save / Save / Cancel" +// This is called when: +// - Exporting config_bundle +// - Taking snapshot +bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) +{ + if (has_current_preset_changes()) { + const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; + int act_buttons = UnsavedChangesDialog::ActionButtons::SAVE; + if (dont_save_insted_of_discard) + act_buttons |= UnsavedChangesDialog::ActionButtons::DONT_SAVE; + UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + for (const std::pair& nt : dlg.get_names_and_types()) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + load_current_presets(false); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + MessageDialog(nullptr, _L_PLURAL("The preset modifications are successfully saved", + "The presets modifications are successfully saved", dlg.get_names_and_types().size())).ShowModal(); + } + } + + return true; +} + +void GUI_App::apply_keeped_preset_modifications() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->apply_config_from_cache(); + } + load_current_presets(false); +} + +// This is called when creating new project or load another project +// OR close ConfigWizard +// to ask the user what should we do with unsaved changes for presets. +// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" +// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed +bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) +{ + if (has_current_preset_changes()) { + bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; + + const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; + UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + auto reset_modifications = [this, is_called_from_configwizard]() { + if (is_called_from_configwizard) + return; // no need to discared changes. It will be done fromConfigWizard closing + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + tab->m_presets->discard_current_changes(); + } + load_current_presets(false); + }; + + if (dlg.discard()) + reset_modifications(); + else // save selected changes + { + const auto& preset_names_and_types = dlg.get_names_and_types(); + if (dlg.save_preset()) { + for (const std::pair& nt : preset_names_and_types) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + wxString text = _L_PLURAL("The preset modifications are successfully saved", + "The presets modifications are successfully saved", preset_names_and_types.size()); + if (!is_called_from_configwizard) + text += "\n\n" + _L("For new project all modifications will be reseted"); + + MessageDialog(nullptr, text).ShowModal(); + reset_modifications(); + } + else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { + // execute this part of code only if not all modifications are keeping to the new project + // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected + for (const std::pair& nt : preset_names_and_types) { + Preset::Type type = nt.second; + Tab* tab = get_tab(type); + std::vector selected_options = dlg.get_selected_options(type); + if (type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(tab)->cache_extruder_cnt(); + } + } + tab->cache_config_diff(selected_options); + if (!is_called_from_configwizard) + tab->m_presets->discard_current_changes(); + } + if (is_called_from_configwizard) + *postponed_apply_of_keeped_changes = true; + else + apply_keeped_preset_modifications(); + } + } + } + + return true; +} + +bool GUI_App::can_load_project() +{ + int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); + if (saved_project == wxID_CANCEL || + (plater()->is_project_dirty() && saved_project == wxID_NO && + !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) + return false; + return true; +} + +bool GUI_App::check_print_host_queue() +{ + wxString dirty; + std::vector> jobs; + // Get ongoing jobs from dialog + mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); + if (jobs.empty()) + return true; + // Show dialog + wxString job_string = wxString(); + for (const auto& job : jobs) { + job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); + } + wxString message; + message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); + //wxMessageDialog dialog(mainframe, + MessageDialog dialog(mainframe, + message, + wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + if (dialog.ShowModal() == wxID_YES) + return true; + + // TODO: If already shown, bring forward + mainframe->m_printhost_queue_dlg->Show(); + return false; +} + +bool GUI_App::checked_tab(Tab* tab) +{ + bool ret = true; + if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) + ret = false; + return ret; +} + +// Update UI / Tabs to reflect changes in the currently loaded presets +void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) +{ + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + if (check_printer_presets_) + check_printer_presets(); + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + this->plater()->set_printer_technology(printer_technology); + for (Tab *tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + if (tab->type() == Preset::TYPE_PRINTER) { + static_cast(tab)->update_pages(); + // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). + this->plater()->force_print_bed_update(); + } + tab->load_current_preset(); + } +} + +bool GUI_App::OnExceptionInMainLoop() +{ + generic_exception_handle(); + return false; +} + +#ifdef __APPLE__ +// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run +// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App +// to a G-code viewer. +void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) +{ + size_t num_gcodes = 0; + for (const wxString &filename : fileNames) + if (is_gcode_file(into_u8(filename))) + ++ num_gcodes; + if (fileNames.size() == num_gcodes) { + // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, + // just G-codes were passed. Switch to G-code viewer mode. + m_app_mode = EAppMode::GCodeViewer; + unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); + if(app_config != nullptr) + delete app_config; + app_config = nullptr; + init_app_config(); + } + wxApp::OSXStoreOpenFiles(fileNames); +} +// wxWidgets override to get an event on open files. +void GUI_App::MacOpenFiles(const wxArrayString &fileNames) +{ + std::vector files; + std::vector gcode_files; + std::vector non_gcode_files; + for (const auto& filename : fileNames) { + if (is_gcode_file(into_u8(filename))) + gcode_files.emplace_back(filename); + else { + files.emplace_back(into_u8(filename)); + non_gcode_files.emplace_back(filename); + } + } + if (m_app_mode == EAppMode::GCodeViewer) { + // Running in G-code viewer. + // Load the first G-code into the G-code viewer. + // Or if no G-codes, send other files to slicer. + if (! gcode_files.empty()) { + if (m_post_initialized) + this->plater()->load_gcode(gcode_files.front()); + else + this->init_params->input_files = { into_u8(gcode_files.front()) }; + } + if (!non_gcode_files.empty()) + start_new_slicer(non_gcode_files, true); + } else { + if (! files.empty()) { + if (m_post_initialized) { + wxArrayString input_files; + for (size_t i = 0; i < non_gcode_files.size(); ++i) + input_files.push_back(non_gcode_files[i]); + this->plater()->load_files(input_files); + } else { + for (const auto &f : non_gcode_files) + this->init_params->input_files.emplace_back(into_u8(f)); + } + } + for (const wxString &filename : gcode_files) + start_new_gcodeviewer(&filename); + } +} +#endif /* __APPLE */ + +Sidebar& GUI_App::sidebar() +{ + return plater_->sidebar(); +} + +ObjectManipulation* GUI_App::obj_manipul() +{ + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; +} + +ObjectSettings* GUI_App::obj_settings() +{ + return sidebar().obj_settings(); +} + +ObjectList* GUI_App::obj_list() +{ + return sidebar().obj_list(); +} + +ObjectLayers* GUI_App::obj_layers() +{ + return sidebar().obj_layers(); +} + +Plater* GUI_App::plater() +{ + return plater_; +} + +const Plater* GUI_App::plater() const +{ + return plater_; +} + +Model& GUI_App::model() +{ + return plater_->model(); +} +wxBookCtrlBase* GUI_App::tab_panel() const +{ + return mainframe->m_tabpanel; +} + +NotificationManager * GUI_App::notification_manager() +{ + return plater_->get_notification_manager(); +} + +// extruders count from selected printer preset +int GUI_App::extruders_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_selected_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// extruders count from edited printer preset +int GUI_App::extruders_edited_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_edited_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +wxString GUI_App::current_language_code_safe() const +{ + // Translate the language code to a code, for which Prusa Research maintains translations. + const std::map mapping { + { "cs", "cs_CZ", }, + { "sk", "cs_CZ", }, + { "de", "de_DE", }, + { "es", "es_ES", }, + { "fr", "fr_FR", }, + { "it", "it_IT", }, + { "ja", "ja_JP", }, + { "ko", "ko_KR", }, + { "pl", "pl_PL", }, + { "uk", "uk_UA", }, + { "zh", "zh_CN", }, + { "ru", "ru_RU", }, + }; + wxString language_code = this->current_language_code().BeforeFirst('_'); + auto it = mapping.find(language_code); + if (it != mapping.end()) + language_code = it->second; + else + language_code = "en_US"; + return language_code; +} + +void GUI_App::open_web_page_localized(const std::string &http_address) +{ + open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); +} + +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). +// Because of we can't to print the multi-part objects with SLA technology. +bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) +{ + if (model_has_multi_part_objects(model())) { + show_info(nullptr, + _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + return true; +} + +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 (reason == ConfigWizard::RR_USER) { + if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) + return false; + } + + auto wizard = new ConfigWizard(mainframe); + const bool res = wizard->run(reason, start_page); + + if (res) { + load_current_presets(); + + // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() + if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) + may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); + } + + return res; +} + +void GUI_App::show_desktop_integration_dialog() +{ +#ifdef __linux__ + //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + DesktopIntegrationDialog dialog(mainframe); + dialog.ShowModal(); +#endif //__linux__ +} + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +void GUI_App::gcode_thumbnails_debug() +{ + const std::string BEGIN_MASK = "; thumbnail begin"; + const std::string END_MASK = "; thumbnail end"; + std::string gcode_line; + bool reading_image = false; + unsigned int width = 0; + unsigned int height = 0; + + wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string in_filename = into_u8(dialog.GetPath()); + std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); + + boost::nowide::ifstream in_file(in_filename.c_str()); + std::vector rows; + std::string row; + if (in_file.good()) + { + while (std::getline(in_file, gcode_line)) + { + if (in_file.good()) + { + if (boost::starts_with(gcode_line, BEGIN_MASK)) + { + reading_image = true; + gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); + std::string::size_type x_pos = gcode_line.find('x'); + std::string width_str = gcode_line.substr(0, x_pos); + width = (unsigned int)::atoi(width_str.c_str()); + std::string height_str = gcode_line.substr(x_pos + 1); + height = (unsigned int)::atoi(height_str.c_str()); + row.clear(); + } + else if (reading_image && boost::starts_with(gcode_line, END_MASK)) + { + std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; + boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); + if (out_file.good()) + { + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); + decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); + + out_file.write(decoded.c_str(), decoded.size()); + out_file.close(); + } + + reading_image = false; + width = 0; + height = 0; + rows.clear(); + } + else if (reading_image) + row += gcode_line.substr(2); + } + } + + in_file.close(); + } +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + WindowMetrics metrics = WindowMetrics::from_window(window); + app_config->set(config_key, metrics.serialize()); + app_config->save(); +} + +void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + if (! app_config->has(config_key)) { + window->Maximize(default_maximized); + return; + } + + auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); + if (! metrics) { + window->Maximize(default_maximized); + return; + } + + const wxRect& rect = metrics->get_rect(); + + if (app_config->get("restore_win_position") == "1") { + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); + app_config->save(); + window->SetPosition(rect.GetPosition()); + + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); + app_config->save(); + window->SetSize(rect.GetSize()); + + // revert "restore_win_position" value if application wasn't crashed + app_config->set("restore_win_position", "1"); + app_config->save(); + } + else + window->CenterOnScreen(); + + window->Maximize(metrics->get_maximized()); +} + +void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.sanitize_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +bool GUI_App::config_wizard_startup() +{ + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + run_wizard(ConfigWizard::RR_DATA_EMPTY); + return true; + } else if (get_app_config()->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + MsgDataLegacy dlg; + dlg.ShowModal(); + + run_wizard(ConfigWizard::RR_DATA_LEGACY); + return true; + } + return false; +} + +bool GUI_App::check_updates(const bool verbose) +{ + PresetUpdater::UpdateResult updater_result; + try { + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + // Applicaiton is closing. + return false; + } + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; + } + else if (verbose && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } + } + catch (const std::exception & ex) { + show_error(nullptr, ex.what()); + } + // Applicaiton will continue. + return true; +} + +bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) +{ + bool launch = true; + + // warning dialog containes a "Remember my choice" checkbox + std::string option_key = "suppress_hyperlinks"; + if (force_remember_choice || app_config->get(option_key).empty()) { + if (app_config->get(option_key).empty()) { + RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + dialog.ShowCheckBox(_L("Remember my choice")); + auto answer = dialog.ShowModal(); + launch = answer == wxID_YES; + if (dialog.IsCheckBoxChecked()) { + wxString preferences_item = _L("Suppress to open hyperlink in browser"); + wxString msg = + _L("PrusaSlicer will remember your choice.") + "\n\n" + + _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + + format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); + + MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); + if (msg_dlg.ShowModal() == wxID_CANCEL) + return false; + app_config->set(option_key, answer == wxID_NO ? "1" : "0"); + } + } + if (launch) + launch = app_config->get(option_key) != "1"; + } + // warning dialog doesn't containe a "Remember my choice" checkbox + // and will be shown only when "Suppress to open hyperlink in browser" is ON. + else if (app_config->get(option_key) == "1") { + MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + launch = dialog.ShowModal() == wxID_YES; + } + + return launch && wxLaunchDefaultBrowser(url, flags); +} + +// static method accepting a wxWindow object as first parameter +// void warning_catcher{ +// my($self, $message_dialog) = @_; +// return sub{ +// my $message = shift; +// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; +// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); +// $message_dialog +// ? $message_dialog->(@params) +// : Wx::MessageDialog->new($self, @params)->ShowModal; +// }; +// } + +// Do we need this function??? +// void GUI_App::notify(message) { +// auto frame = GetTopWindow(); +// // try harder to attract user attention on OS X +// if (!frame->IsActive()) +// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); +// +// // There used to be notifier using a Growl application for OSX, but Growl is dead. +// // The notifier also supported the Linux X D - bus notifications, but that support was broken. +// //TODO use wxNotificationMessage ? +// } + + +#ifdef __WXMSW__ +static bool set_into_win_registry(HKEY hkeyHive, const wchar_t* pszVar, const wchar_t* pszValue) +{ + // see as reference: https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association + wchar_t szValueCurrent[1000]; + DWORD dwType; + DWORD dwSize = sizeof(szValueCurrent); + + int iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); + + bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; + + if ((iRC != ERROR_SUCCESS) && !bDidntExist) + // an error occurred + return false; + + if (!bDidntExist) { + if (dwType != REG_SZ) + // invalid type + return false; + + if (::wcscmp(szValueCurrent, pszValue) == 0) + // value already set + return false; + } + + DWORD dwDisposition; + HKEY hkey; + iRC = ::RegCreateKeyExW(hkeyHive, pszVar, 0, 0, 0, KEY_ALL_ACCESS, nullptr, &hkey, &dwDisposition); + bool ret = false; + if (iRC == ERROR_SUCCESS) { + iRC = ::RegSetValueExW(hkey, L"", 0, REG_SZ, (BYTE*)pszValue, (::wcslen(pszValue) + 1) * sizeof(wchar_t)); + if (iRC == ERROR_SUCCESS) + ret = true; + } + + RegCloseKey(hkey); + return ret; +} + +void GUI_App::associate_3mf_files() +{ + wchar_t app_path[MAX_PATH]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + + std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; + std::wstring prog_id = L"Prusa.Slicer.1"; + std::wstring prog_desc = L"PrusaSlicer"; + std::wstring prog_command = prog_path + L" \"%1\""; + std::wstring reg_base = L"Software\\Classes"; + std::wstring reg_extension = reg_base + L"\\.3mf"; + std::wstring reg_prog_id = reg_base + L"\\" + prog_id; + std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; + + bool is_new = false; + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + + if (is_new) + // notify Windows only when any of the values gets changed + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} + +void GUI_App::associate_stl_files() +{ + wchar_t app_path[MAX_PATH]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + + std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; + std::wstring prog_id = L"Prusa.Slicer.1"; + std::wstring prog_desc = L"PrusaSlicer"; + std::wstring prog_command = prog_path + L" \"%1\""; + std::wstring reg_base = L"Software\\Classes"; + std::wstring reg_extension = reg_base + L"\\.stl"; + std::wstring reg_prog_id = reg_base + L"\\" + prog_id; + std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; + + bool is_new = false; + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + + if (is_new) + // notify Windows only when any of the values gets changed + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} + +void GUI_App::associate_gcode_files() +{ + wchar_t app_path[MAX_PATH]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + + std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; + std::wstring prog_id = L"PrusaSlicer.GCodeViewer.1"; + std::wstring prog_desc = L"PrusaSlicerGCodeViewer"; + std::wstring prog_command = prog_path + L" \"%1\""; + std::wstring reg_base = L"Software\\Classes"; + std::wstring reg_extension = reg_base + L"\\.gcode"; + std::wstring reg_prog_id = reg_base + L"\\" + prog_id; + std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; + + bool is_new = false; + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + + if (is_new) + // notify Windows only when any of the values gets changed + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} +#endif // __WXMSW__ + + +void GUI_App::on_version_read(wxCommandEvent& evt) +{ + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + std::string opt = app_config->get("notify_release"); + if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { + return; + } + if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + return; + } + // notification + /* + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) + , _u8L("See Download page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } + ); + */ + // updater + // read triggered_by_user that was set when calling GUI_App::app_version_check + app_updater(m_app_updater->get_triggered_by_user()); +} + +void GUI_App::app_updater(bool from_user) +{ + DownloadAppData app_data = m_app_updater->get_app_data(); + + if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) + { + BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; + MsgNoAppUpdates no_update_dialog; + no_update_dialog.ShowModal(); + return; + + } + + assert(!app_data.url.empty()); + assert(!app_data.target_path.empty()); + + // dialog with new version info + AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); + auto dialog_result = dialog.ShowModal(); + // checkbox "do not show again" + if (dialog.disable_version_check()) { + app_config->set("notify_release", "none"); + } + // Doesn't wish to update + if (dialog_result != wxID_OK) { + return; + } + // dialog with new version download (installer or app dependent on system) including path selection + AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); + dialog_result = dwnld_dlg.ShowModal(); + // Doesn't wish to download + if (dialog_result != wxID_OK) { + return; + } + app_data.target_path =dwnld_dlg.get_download_path(); + + // start download + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); + app_data.start_after = dwnld_dlg.run_after_download(); + m_app_updater->set_app_data(std::move(app_data)); + m_app_updater->sync_download(); +} + +void GUI_App::app_version_check(bool from_user) +{ + if (from_user) { + if (m_app_updater->get_download_ongoing()) { + MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + } + } + std::string version_check_url = app_config->version_check_url(); + m_app_updater->sync_version(version_check_url, from_user); +} + +} // GUI +} //Slic3r diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 660cfc7f0..3da843f89 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1,960 +1,967 @@ -#include "Preferences.hpp" -#include "OptionsGroup.hpp" -#include "GUI_App.hpp" -#include "Plater.hpp" -#include "MsgDialog.hpp" -#include "I18N.hpp" -#include "libslic3r/AppConfig.hpp" -#include -#include "Notebook.hpp" -#include "ButtonsDescription.hpp" -#include "OG_CustomCtrl.hpp" -#include "GLCanvas3D.hpp" - -namespace Slic3r { - - static t_config_enum_names enum_names_from_keys_map(const t_config_enum_values& enum_keys_map) - { - t_config_enum_names names; - int cnt = 0; - for (const auto& kvp : enum_keys_map) - cnt = std::max(cnt, kvp.second); - cnt += 1; - names.assign(cnt, ""); - for (const auto& kvp : enum_keys_map) - names[kvp.second] = kvp.first; - return names; - } - -#define CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NAME) \ - static t_config_enum_names s_keys_names_##NAME = enum_names_from_keys_map(s_keys_map_##NAME); \ - template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values() { return s_keys_map_##NAME; } \ - template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names() { return s_keys_names_##NAME; } - - - - static const t_config_enum_values s_keys_map_NotifyReleaseMode = { - {"all", NotifyReleaseAll}, - {"release", NotifyReleaseOnly}, - {"none", NotifyReleaseNone}, - }; - - CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NotifyReleaseMode) - -namespace GUI { - -PreferencesDialog::PreferencesDialog(wxWindow* parent) : - DPIDialog(parent, wxID_ANY, _L("Preferences"), wxDefaultPosition, - wxDefaultSize, wxDEFAULT_DIALOG_STYLE) -{ -#ifdef __WXOSX__ - isOSX = true; -#endif - build(); - - m_highlighter.set_timer_owner(this, 0); -} - -static void update_color(wxColourPickerCtrl* color_pckr, const wxColour& color) -{ - if (color_pckr->GetColour() != color) { - color_pckr->SetColour(color); - wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED)); - } -} - -void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - int selected_tab = 0; - for ( ; selected_tab < int(tabs->GetPageCount()); selected_tab++) - if (tabs->GetPageText(selected_tab) == _(tab_name)) - break; - if (selected_tab < int(tabs->GetPageCount())) - tabs->SetSelection(selected_tab); - - if (!highlight_opt_key.empty()) - init_highlighter(highlight_opt_key); - - // cache input values for custom toolbar size - m_custom_toolbar_size = atoi(get_app_config()->get("custom_toolbar_size").c_str()); - m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1"; - - // update colors for color pickers - update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); - update_color(m_mod_colour, wxGetApp().get_label_clr_modified()); - - this->ShowModal(); -} - -static std::shared_ptrcreate_options_tab(const wxString& title, wxBookCtrlBase* tabs) -{ - wxPanel* tab = new wxPanel(tabs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); - tabs->AddPage(tab, _(title)); - tab->SetFont(wxGetApp().normal_font()); - - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - sizer->SetSizeHints(tab); - tab->SetSizer(sizer); - - std::shared_ptr optgroup = std::make_shared(tab); - optgroup->label_width = 40; - optgroup->set_config_category_and_type(title, int(Preset::TYPE_PREFERENCES)); - return optgroup; -} - -static void activate_options_tab(std::shared_ptr optgroup) -{ - optgroup->activate([](){}, wxALIGN_RIGHT); - optgroup->update_visibility(comSimple); - wxBoxSizer* sizer = static_cast(static_cast(optgroup->parent())->GetSizer()); - sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10); - - // apply sercher - wxGetApp().sidebar().get_searcher().append_preferences_options(optgroup->get_lines()); -} - -static void append_bool_option( std::shared_ptr optgroup, - const std::string& opt_key, - const std::string& label, - const std::string& tooltip, - bool def_val, - ConfigOptionMode mode = comSimple) -{ - ConfigOptionDef def = {opt_key, coBool}; - def.label = label; - def.tooltip = tooltip; - def.mode = mode; - def.set_default_value(new ConfigOptionBool{ def_val }); - Option option(def, opt_key); - optgroup->append_single_option_line(option); - - // fill data to the Search Dialog - wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); -} - -static void append_enum_option( std::shared_ptr optgroup, - const std::string& opt_key, - const std::string& label, - const std::string& tooltip, - const ConfigOption* def_val, - const t_config_enum_values *enum_keys_map, - std::initializer_list enum_values, - std::initializer_list enum_labels, - ConfigOptionMode mode = comSimple) -{ - ConfigOptionDef def = {opt_key, coEnum }; - def.label = label; - def.tooltip = tooltip; - def.mode = mode; - def.enum_keys_map = enum_keys_map; - def.enum_values = std::vector(enum_values); - def.enum_labels = std::vector(enum_labels); - - def.set_default_value(def_val); - Option option(def, opt_key); - optgroup->append_single_option_line(option); - - // fill data to the Search Dialog - wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); -} - -static void append_preferences_option_to_searcer(std::shared_ptr optgroup, - const std::string& opt_key, - const wxString& label) -{ - // fill data to the Search Dialog - wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); - // apply sercher - wxGetApp().sidebar().get_searcher().append_preferences_option(Line(opt_key, label, "")); -} - -void PreferencesDialog::build() -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this); -#else - SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -#endif - const wxFont& font = wxGetApp().normal_font(); - SetFont(font); - - auto app_config = get_app_config(); - -#ifdef _MSW_DARK_MODE - tabs = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); -#else - tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL |wxNB_NOPAGETHEME | wxNB_DEFAULT ); - tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -#endif - - // Add "General" tab - m_optgroup_general = create_options_tab(L("General"), tabs); - m_optgroup_general->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - if (opt_key == "default_action_on_close_application" || opt_key == "default_action_on_select_preset" || opt_key == "default_action_on_new_project") - m_values[opt_key] = boost::any_cast(value) ? "none" : "discard"; - else if (opt_key == "default_action_on_dirty_project") - m_values[opt_key] = boost::any_cast(value) ? "" : "0"; - else - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - bool is_editor = wxGetApp().is_editor(); - - if (is_editor) { - append_bool_option(m_optgroup_general, "remember_output_path", - L("Remember output directory"), - L("If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files."), - app_config->has("remember_output_path") ? app_config->get("remember_output_path") == "1" : true); - - append_bool_option(m_optgroup_general, "autocenter", - L("Auto-center parts"), - L("If this is enabled, Slic3r will auto-center objects around the print bed center."), - app_config->get("autocenter") == "1"); - - append_bool_option(m_optgroup_general, "background_processing", - L("Background processing"), - L("If this is enabled, Slic3r will pre-process objects as soon " - "as they\'re loaded in order to save time when exporting G-code."), - app_config->get("background_processing") == "1"); - - m_optgroup_general->append_separator(); - - // Please keep in sync with ConfigWizard - append_bool_option(m_optgroup_general, "export_sources_full_pathnames", - L("Export sources full pathnames to 3mf and amf"), - L("If enabled, allows the Reload from disk command to automatically find and load the files when invoked."), - app_config->get("export_sources_full_pathnames") == "1"); - -#ifdef _WIN32 - // Please keep in sync with ConfigWizard - append_bool_option(m_optgroup_general, "associate_3mf", - L("Associate .3mf files to PrusaSlicer"), - L("If enabled, sets PrusaSlicer as default application to open .3mf files."), - app_config->get("associate_3mf") == "1"); - - append_bool_option(m_optgroup_general, "associate_stl", - L("Associate .stl files to PrusaSlicer"), - L("If enabled, sets PrusaSlicer as default application to open .stl files."), - app_config->get("associate_stl") == "1"); -#endif // _WIN32 - - m_optgroup_general->append_separator(); - - // Please keep in sync with ConfigWizard - append_bool_option(m_optgroup_general, "preset_update", - L("Update built-in Presets automatically"), - L("If enabled, Slic3r downloads updates of built-in system presets in the background. These updates are downloaded " - "into a separate temporary location. When a new preset version becomes available it is offered at application startup."), - app_config->get("preset_update") == "1"); - - append_bool_option(m_optgroup_general, "no_defaults", - L("Suppress \" - default - \" presets"), - L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."), - app_config->get("no_defaults") == "1"); - - append_bool_option(m_optgroup_general, "show_incompatible_presets", - L("Show incompatible print and filament presets"), - L("When checked, the print and filament presets are shown in the preset editor " - "even if they are marked as incompatible with the active printer"), - app_config->get("show_incompatible_presets") == "1"); - - m_optgroup_general->append_separator(); - - append_bool_option(m_optgroup_general, "show_drop_project_dialog", - L("Show drop project dialog"), - L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load."), - app_config->get("show_drop_project_dialog") == "1"); - - append_bool_option(m_optgroup_general, "single_instance", -#if __APPLE__ - L("Allow just a single PrusaSlicer instance"), - L("On OSX there is always only one instance of app running by default. However it is allowed to run multiple instances " - "of same app from the command line. In such case this settings will allow only one instance."), -#else - L("Allow just a single PrusaSlicer instance"), - L("If this is enabled, when starting PrusaSlicer and another instance of the same PrusaSlicer is already running, that instance will be reactivated instead."), -#endif - app_config->has("single_instance") ? app_config->get("single_instance") == "1" : false ); - - m_optgroup_general->append_separator(); - - append_bool_option(m_optgroup_general, "default_action_on_dirty_project", - L("Ask for unsaved changes in project"), - L("Always ask for unsaved changes in project, when: \n" - "- Closing PrusaSlicer,\n" - "- Loading or creating a new project"), - app_config->get("default_action_on_dirty_project").empty()); - - m_optgroup_general->append_separator(); - - append_bool_option(m_optgroup_general, "default_action_on_close_application", - L("Ask to save unsaved changes in presets when closing the application or when loading a new project"), - L("Always ask for unsaved changes in presets, when: \n" - "- Closing PrusaSlicer while some presets are modified,\n" - "- Loading a new project while some presets are modified"), - app_config->get("default_action_on_close_application") == "none"); - - append_bool_option(m_optgroup_general, "default_action_on_select_preset", - L("Ask for unsaved changes in presets when selecting new preset"), - L("Always ask for unsaved changes in presets when selecting new preset or resetting a preset"), - app_config->get("default_action_on_select_preset") == "none"); - - append_bool_option(m_optgroup_general, "default_action_on_new_project", - L("Ask for unsaved changes in presets when creating new project"), - L("Always ask for unsaved changes in presets when creating new project"), - app_config->get("default_action_on_new_project") == "none"); - } -#ifdef _WIN32 - else { - append_bool_option(m_optgroup_general, "associate_gcode", - L("Associate .gcode files to PrusaSlicer G-code Viewer"), - L("If enabled, sets PrusaSlicer G-code Viewer as default application to open .gcode files."), - app_config->get("associate_gcode") == "1"); - } -#endif // _WIN32 - -#if __APPLE__ - append_bool_option(m_optgroup_general, "use_retina_opengl", - L("Use Retina resolution for the 3D scene"), - L("If enabled, the 3D scene will be rendered in Retina resolution. " - "If you are experiencing 3D performance problems, disabling this option may help."), - app_config->get("use_retina_opengl") == "1"); -#endif - - m_optgroup_general->append_separator(); - - // Show/Hide splash screen - append_bool_option(m_optgroup_general, "show_splash_screen", - L("Show splash screen"), - L("Show splash screen"), - app_config->get("show_splash_screen") == "1"); - - append_bool_option(m_optgroup_general, "restore_win_position", - L("Restore window position on start"), - L("If enabled, PrusaSlicer will be open at the position it was closed"), - app_config->get("restore_win_position") == "1"); - - // Clear Undo / Redo stack on new project - append_bool_option(m_optgroup_general, "clear_undo_redo_stack_on_new_project", - L("Clear Undo / Redo stack on new project"), - L("Clear Undo / Redo stack on new project or when an existing project is loaded."), - app_config->get("clear_undo_redo_stack_on_new_project") == "1"); - -#if defined(_WIN32) || defined(__APPLE__) - append_bool_option(m_optgroup_general, "use_legacy_3DConnexion", - L("Enable support for legacy 3DConnexion devices"), - L("If enabled, the legacy 3DConnexion devices settings dialog is available by pressing CTRL+M"), - app_config->get("use_legacy_3DConnexion") == "1"); -#endif // _WIN32 || __APPLE__ - - activate_options_tab(m_optgroup_general); - - // Add "Camera" tab - m_optgroup_camera = create_options_tab(L("Camera"), tabs); - m_optgroup_camera->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key);it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_camera, "use_perspective_camera", - L("Use perspective camera"), - L("If enabled, use perspective camera. If not enabled, use orthographic camera."), - app_config->get("use_perspective_camera") == "1"); - - append_bool_option(m_optgroup_camera, "use_free_camera", - L("Use free camera"), - L("If enabled, use free camera. If not enabled, use constrained camera."), - app_config->get("use_free_camera") == "1"); - - append_bool_option(m_optgroup_camera, "reverse_mouse_wheel_zoom", - L("Reverse direction of zoom with mouse wheel"), - L("If enabled, reverses the direction of zoom with mouse wheel"), - app_config->get("reverse_mouse_wheel_zoom") == "1"); - - activate_options_tab(m_optgroup_camera); - - // Add "GUI" tab - m_optgroup_gui = create_options_tab(L("GUI"), tabs); - m_optgroup_gui->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (opt_key == "notify_release") { - int val_int = boost::any_cast(value); - for (const auto& item : s_keys_map_NotifyReleaseMode) { - if (item.second == val_int) { - m_values[opt_key] = item.first; - return; - } - } - } - if (opt_key == "use_custom_toolbar_size") { - m_icon_size_sizer->ShowItems(boost::any_cast(value)); - refresh_og(m_optgroup_gui); - get_app_config()->set("use_custom_toolbar_size", boost::any_cast(value) ? "1" : "0"); - get_app_config()->save(); - wxGetApp().plater()->get_current_canvas3D()->render(); - return; - } - if (opt_key == "tabs_as_menu") { - bool disable_new_layout = boost::any_cast(value); - m_rb_new_settings_layout_mode->Show(!disable_new_layout); - if (disable_new_layout && m_rb_new_settings_layout_mode->GetValue()) { - m_rb_new_settings_layout_mode->SetValue(false); - m_rb_old_settings_layout_mode->SetValue(true); - } - refresh_og(m_optgroup_gui); - } - - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - - if (opt_key == "suppress_hyperlinks") - m_values[opt_key] = boost::any_cast(value) ? "1" : ""; - else - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_gui, "seq_top_layer_only", - L("Sequential slider applied only to top layer"), - L("If enabled, changes made using the sequential slider, in preview, apply only to gcode top layer." - "If disabled, changes made using the sequential slider, in preview, apply to the whole gcode."), - app_config->get("seq_top_layer_only") == "1"); - - if (is_editor) { - append_bool_option(m_optgroup_gui, "show_collapse_button", - L("Show sidebar collapse/expand button"), - L("If enabled, the button for the collapse sidebar will be appeared in top right corner of the 3D Scene"), - app_config->get("show_collapse_button") == "1"); - - append_bool_option(m_optgroup_gui, "suppress_hyperlinks", - L("Suppress to open hyperlink in browser"), - L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."), - //L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. " - // "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."), - app_config->get("suppress_hyperlinks") == "1"); - - append_bool_option(m_optgroup_gui, "color_mapinulation_panel", - L("Use colors for axes values in Manipulation panel"), - L("If enabled, the axes names and axes values will be colorized according to the axes colors. " - "If disabled, old UI will be used."), - app_config->get("color_mapinulation_panel") == "1"); - - append_bool_option(m_optgroup_gui, "order_volumes", - L("Order object volumes by types"), - L("If enabled, volumes will be always ordered inside the object. Correct order is Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. " - "If disabled, you can reorder Model Parts, Negative Volumes and Modifiers. But one of the model parts have to be on the first place."), - app_config->get("order_volumes") == "1"); - -#if ENABLE_SHOW_NON_MANIFOLD_EDGES - append_bool_option(m_optgroup_gui, "non_manifold_edges", - L("Show non-manifold edges"), - L("If enabled, shows non-manifold edges."), - app_config->get("non_manifold_edges") == "1"); -#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES - -#ifdef _MSW_DARK_MODE - append_bool_option(m_optgroup_gui, "tabs_as_menu", - L("Set settings tabs as menu items (experimental)"), - L("If enabled, Settings Tabs will be placed as menu items. If disabled, old UI will be used."), - app_config->get("tabs_as_menu") == "1"); -#endif - - m_optgroup_gui->append_separator(); - - append_bool_option(m_optgroup_gui, "show_hints", - L("Show \"Tip of the day\" notification after start"), - L("If enabled, useful hints are displayed at startup."), - app_config->get("show_hints") == "1"); - - append_enum_option(m_optgroup_gui, "notify_release", - L("Notify about new releases"), - L("You will be notified about new release after startup acordingly: All = Regular release and alpha / beta releases. Release only = regular release."), - new ConfigOptionEnum(static_cast(s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")))), - &ConfigOptionEnum::get_enum_values(), - {"all", "release", "none"}, - {L("All"), L("Release only"), L("None")}); - - m_optgroup_gui->append_separator(); - - append_bool_option(m_optgroup_gui, "use_custom_toolbar_size", - L("Use custom size for toolbar icons"), - L("If enabled, you can change size of toolbar icons manually."), - app_config->get("use_custom_toolbar_size") == "1"); - } - - activate_options_tab(m_optgroup_gui); - - if (is_editor) { - // set Field for notify_release to its value to activate the object - boost::any val = s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")); - m_optgroup_gui->get_field("notify_release")->set_value(val, false); - - create_icon_size_slider(); - m_icon_size_sizer->ShowItems(app_config->get("use_custom_toolbar_size") == "1"); - - create_settings_mode_widget(); - create_settings_text_color_widget(); - -#if ENABLE_ENVIRONMENT_MAP - // Add "Render" tab - m_optgroup_render = create_options_tab(L("Render"), tabs); - m_optgroup_render->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_render, "use_environment_map", - L("Use environment map"), - L("If enabled, renders object using the environment map."), - app_config->get("use_environment_map") == "1"); - - activate_options_tab(m_optgroup_render); -#endif // ENABLE_ENVIRONMENT_MAP - -#ifdef _WIN32 - // Add "Dark Mode" tab - m_optgroup_dark_mode = create_options_tab(_L("Dark mode (experimental)"), tabs); - m_optgroup_dark_mode->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_dark_mode, "dark_color_mode", - L("Enable dark mode"), - L("If enabled, UI will use Dark mode colors. If disabled, old UI will be used."), - app_config->get("dark_color_mode") == "1"); - - if (wxPlatformInfo::Get().GetOSMajorVersion() >= 10) // Use system menu just for Window newer then Windows 10 - // Use menu with ownerdrawn items by default on systems older then Windows 10 - { - append_bool_option(m_optgroup_dark_mode, "sys_menu_enabled", - L("Use system menu for application"), - L("If enabled, application will use the standart Windows system menu,\n" - "but on some combination od display scales it can look ugly. If disabled, old UI will be used."), - app_config->get("sys_menu_enabled") == "1"); - } - - activate_options_tab(m_optgroup_dark_mode); -#endif //_WIN32 - } - - // update alignment of the controls for all tabs - update_ctrls_alignment(); - - auto sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(tabs, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); - - auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); - this->Bind(wxEVT_BUTTON, &PreferencesDialog::accept, this, wxID_OK); - this->Bind(wxEVT_BUTTON, &PreferencesDialog::revert, this, wxID_CANCEL); - - for (int id : {wxID_OK, wxID_CANCEL}) - wxGetApp().UpdateDarkUI(static_cast(FindWindowById(id, this))); - - sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM | wxTOP, 10); - - SetSizer(sizer); - sizer->SetSizeHints(this); - this->CenterOnParent(); -} - -std::vector PreferencesDialog::optgroups() -{ - std::vector out; - out.reserve(4); - for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get() -#ifdef _WIN32 - , m_optgroup_dark_mode.get() -#endif // _WIN32 -#if ENABLE_ENVIRONMENT_MAP - , m_optgroup_render.get() -#endif // ENABLE_ENVIRONMENT_MAP - }) - if (opt) - out.emplace_back(opt); - return out; -} - -void PreferencesDialog::update_ctrls_alignment() -{ - int max_ctrl_width{ 0 }; - for (ConfigOptionsGroup* og : this->optgroups()) - if (int max = og->custom_ctrl->get_max_win_width(); - max_ctrl_width < max) - max_ctrl_width = max; - if (max_ctrl_width) - for (ConfigOptionsGroup* og : this->optgroups()) - og->custom_ctrl->set_max_win_width(max_ctrl_width); -} - -void PreferencesDialog::accept(wxEvent&) -{ - std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; - - for (const std::string& option : options_to_recreate_GUI) { - if (m_values.find(option) != m_values.end()) { - wxString title = wxGetApp().is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Changes for the critical options"); - MessageDialog dialog(nullptr, - _L("Changing some options will trigger application restart.\n" - "You will lose the content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxYES | wxNO); - if (dialog.ShowModal() == wxID_YES) { - m_recreate_GUI = true; - } - else { - for (const std::string& option : options_to_recreate_GUI) - m_values.erase(option); - } - break; - } - } - - auto app_config = get_app_config(); - - m_seq_top_layer_only_changed = false; - if (auto it = m_values.find("seq_top_layer_only"); it != m_values.end()) - m_seq_top_layer_only_changed = app_config->get("seq_top_layer_only") != it->second; - - m_settings_layout_changed = false; - for (const std::string& key : { "old_settings_layout_mode", - "new_settings_layout_mode", - "dlg_settings_layout_mode" }) - { - auto it = m_values.find(key); - if (it != m_values.end() && app_config->get(key) != it->second) { - m_settings_layout_changed = true; - break; - } - } - -#if 0 //#ifdef _WIN32 // #ysDarkMSW - Allow it when we deside to support the sustem colors for application - if (m_values.find("always_dark_color_mode") != m_values.end()) - wxGetApp().force_sys_colors_update(); -#endif - - for (std::map::iterator it = m_values.begin(); it != m_values.end(); ++it) - app_config->set(it->first, it->second); - - app_config->save(); - if (wxGetApp().is_editor()) { - wxGetApp().set_label_clr_sys(m_sys_colour->GetColour()); - wxGetApp().set_label_clr_modified(m_mod_colour->GetColour()); - } - - EndModal(wxID_OK); - -#ifdef _WIN32 - if (m_values.find("dark_color_mode") != m_values.end()) - wxGetApp().force_colors_update(); -#ifdef _MSW_DARK_MODE - if (m_values.find("sys_menu_enabled") != m_values.end()) - wxGetApp().force_menu_update(); -#endif //_MSW_DARK_MODE -#endif // _WIN32 - - wxGetApp().update_ui_from_settings(); - clear_cache(); -} - -void PreferencesDialog::revert(wxEvent&) -{ - auto app_config = get_app_config(); - - bool save_app_config = false; - if (m_custom_toolbar_size != atoi(app_config->get("custom_toolbar_size").c_str())) { - app_config->set("custom_toolbar_size", (boost::format("%d") % m_custom_toolbar_size).str()); - m_icon_size_slider->SetValue(m_custom_toolbar_size); - save_app_config |= true; - } - if (m_use_custom_toolbar_size != (get_app_config()->get("use_custom_toolbar_size") == "1")) { - app_config->set("use_custom_toolbar_size", m_use_custom_toolbar_size ? "1" : "0"); - save_app_config |= true; - - m_optgroup_gui->set_value("use_custom_toolbar_size", m_use_custom_toolbar_size); - m_icon_size_sizer->ShowItems(m_use_custom_toolbar_size); - refresh_og(m_optgroup_gui); - } - if (save_app_config) - app_config->save(); - - - for (auto value : m_values) { - bool reverted = false; - const std::string& key = value.first; - - if (key == "default_action_on_dirty_project") { - m_optgroup_general->set_value(key, app_config->get(key).empty()); - continue; - } - if (key == "default_action_on_close_application" || key == "default_action_on_select_preset" || key == "default_action_on_new_project") { - m_optgroup_general->set_value(key, app_config->get(key) == "none"); - continue; - } - if (key == "notify_release") { - m_optgroup_gui->set_value(key, s_keys_map_NotifyReleaseMode.at(app_config->get(key))); - continue; - } - if (key == "old_settings_layout_mode") { - m_rb_old_settings_layout_mode->SetValue(app_config->get(key) == "1"); - continue; - } - if (key == "new_settings_layout_mode") { - m_rb_new_settings_layout_mode->SetValue(app_config->get(key) == "1"); - continue; - } - if (key == "dlg_settings_layout_mode") { - m_rb_dlg_settings_layout_mode->SetValue(app_config->get(key) == "1"); - continue; - } - - for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui -#ifdef _WIN32 - , m_optgroup_dark_mode -#endif // _WIN32 -#if ENABLE_ENVIRONMENT_MAP - , m_optgroup_render -#endif // ENABLE_ENVIRONMENT_MAP - }) { - if (opt_group->set_value(key, app_config->get(key) == "1")) - break; - } - if (key == "tabs_as_menu") { - m_rb_new_settings_layout_mode->Show(app_config->get(key) != "1"); - refresh_og(m_optgroup_gui); - continue; - } - } - - clear_cache(); - EndModal(wxID_CANCEL); -} - -void PreferencesDialog::msw_rescale() -{ - for (ConfigOptionsGroup* og : this->optgroups()) - og->msw_rescale(); -#ifdef _WIN32 - m_optgroup_dark_mode->msw_rescale(); -#endif //_WIN32 -#if ENABLE_ENVIRONMENT_MAP - m_optgroup_render->msw_rescale(); -#endif // ENABLE_ENVIRONMENT_MAP - - msw_buttons_rescale(this, em_unit(), { wxID_OK, wxID_CANCEL }); - - layout(); -} - -void PreferencesDialog::on_sys_color_changed() -{ -#ifdef _WIN32 - wxGetApp().UpdateDlgDarkUI(this); -#endif -} - -void PreferencesDialog::layout() -{ - const int em = em_unit(); - - SetMinSize(wxSize(47 * em, 28 * em)); - Fit(); - - Refresh(); -} - -void PreferencesDialog::clear_cache() -{ - m_values.clear(); - m_custom_toolbar_size = -1; -} - -void PreferencesDialog::refresh_og(std::shared_ptr og) -{ - og->parent()->Layout(); - tabs->Layout(); - this->layout(); -} - -void PreferencesDialog::create_icon_size_slider() -{ - const auto app_config = get_app_config(); - - const int em = em_unit(); - - m_icon_size_sizer = new wxBoxSizer(wxHORIZONTAL); - - wxWindow* parent = m_optgroup_gui->parent(); - wxGetApp().UpdateDarkUI(parent); - - if (isOSX) - // For correct rendering of the slider and value label under OSX - // we should use system default background - parent->SetBackgroundStyle(wxBG_STYLE_ERASE); - - auto label = new wxStaticText(parent, wxID_ANY, _L("Icon size in a respect to the default size") + " (%) :"); - - m_icon_size_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL| wxRIGHT | (isOSX ? 0 : wxLEFT), em); - - const int def_val = atoi(app_config->get("custom_toolbar_size").c_str()); - - long style = wxSL_HORIZONTAL; - if (!isOSX) - style |= wxSL_LABELS | wxSL_AUTOTICKS; - - m_icon_size_slider = new wxSlider(parent, wxID_ANY, def_val, 30, 100, - wxDefaultPosition, wxDefaultSize, style); - - m_icon_size_slider->SetTickFreq(10); - m_icon_size_slider->SetPageSize(10); - m_icon_size_slider->SetToolTip(_L("Select toolbar icon size in respect to the default one.")); - - m_icon_size_sizer->Add(m_icon_size_slider, 1, wxEXPAND); - - wxStaticText* val_label{ nullptr }; - if (isOSX) { - val_label = new wxStaticText(parent, wxID_ANY, wxString::Format("%d", def_val)); - m_icon_size_sizer->Add(val_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, em); - } - - m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label, app_config](wxCommandEvent e) { - auto val = m_icon_size_slider->GetValue(); - - app_config->set("custom_toolbar_size", (boost::format("%d") % val).str()); - app_config->save(); - wxGetApp().plater()->get_current_canvas3D()->render(); - - if (val_label) - val_label->SetLabelText(wxString::Format("%d", val)); - }), m_icon_size_slider->GetId()); - - for (wxWindow* win : std::vector{ m_icon_size_slider, label, val_label }) { - if (!win) continue; - win->SetFont(wxGetApp().normal_font()); - - if (isOSX) continue; // under OSX we use wxBG_STYLE_ERASE - win->SetBackgroundStyle(wxBG_STYLE_PAINT); - } - - m_optgroup_gui->sizer->Add(m_icon_size_sizer, 0, wxEXPAND | wxALL, em); -} - -void PreferencesDialog::create_settings_mode_widget() -{ - wxWindow* parent = m_optgroup_gui->parent(); - wxGetApp().UpdateDarkUI(parent); - - wxString title = L("Layout Options"); - wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); - wxGetApp().UpdateDarkUI(stb); - if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); - stb->SetFont(wxGetApp().normal_font()); - - wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); - - auto app_config = get_app_config(); - std::vector choices = { _L("Old regular layout with the tab bar"), - _L("New layout, access via settings button in the top menu"), - _L("Settings in non-modal window") }; - int id = -1; - auto add_radio = [this, parent, stb_sizer, choices](wxRadioButton** rb, int id, bool select) { - *rb = new wxRadioButton(parent, wxID_ANY, choices[id], wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); - stb_sizer->Add(*rb); - (*rb)->SetValue(select); - (*rb)->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) { - m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0"; - m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0"; - m_values["dlg_settings_layout_mode"] = (id == 2) ? "1" : "0"; - }); - }; - - add_radio(&m_rb_old_settings_layout_mode, ++id, app_config->get("old_settings_layout_mode") == "1"); - add_radio(&m_rb_new_settings_layout_mode, ++id, app_config->get("new_settings_layout_mode") == "1"); - add_radio(&m_rb_dlg_settings_layout_mode, ++id, app_config->get("dlg_settings_layout_mode") == "1"); - -#ifdef _MSW_DARK_MODE - if (app_config->get("tabs_as_menu") == "1") { - m_rb_new_settings_layout_mode->Hide(); - if (m_rb_new_settings_layout_mode->GetValue()) { - m_rb_new_settings_layout_mode->SetValue(false); - m_rb_old_settings_layout_mode->SetValue(true); - } - } -#endif - - std::string opt_key = "settings_layout_mode"; - m_blinkers[opt_key] = new BlinkingBitmap(parent); - - auto sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); - sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); - m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); - - append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); -} - -void PreferencesDialog::create_settings_text_color_widget() -{ - wxWindow* parent = m_optgroup_gui->parent(); - - wxString title = L("Text colors"); - wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); - wxGetApp().UpdateDarkUI(stb); - if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); - - std::string opt_key = "text_colors"; - m_blinkers[opt_key] = new BlinkingBitmap(parent); - - wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); - ButtonsDescription::FillSizerWithTextColorDescriptions(stb_sizer, parent, &m_sys_colour, &m_mod_colour); - - auto sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); - sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); - - m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); - - append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); -} - -void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key) -{ - if (m_blinkers.find(opt_key) != m_blinkers.end()) - if (BlinkingBitmap* blinker = m_blinkers.at(opt_key); blinker) { - m_highlighter.init(blinker); - return; - } - - for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui -#ifdef _WIN32 - , m_optgroup_dark_mode -#endif // _WIN32 -#if ENABLE_ENVIRONMENT_MAP - , m_optgroup_render -#endif // ENABLE_ENVIRONMENT_MAP - }) { - std::pair ctrl = opt_group->get_custom_ctrl_with_blinking_ptr(opt_key, -1); - if (ctrl.first && ctrl.second) { - m_highlighter.init(ctrl); - break; - } - } -} - -} // GUI -} // Slic3r +#include "Preferences.hpp" +#include "OptionsGroup.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "MsgDialog.hpp" +#include "I18N.hpp" +#include "libslic3r/AppConfig.hpp" +#include +#include "Notebook.hpp" +#include "ButtonsDescription.hpp" +#include "OG_CustomCtrl.hpp" +#include "GLCanvas3D.hpp" + +namespace Slic3r { + + static t_config_enum_names enum_names_from_keys_map(const t_config_enum_values& enum_keys_map) + { + t_config_enum_names names; + int cnt = 0; + for (const auto& kvp : enum_keys_map) + cnt = std::max(cnt, kvp.second); + cnt += 1; + names.assign(cnt, ""); + for (const auto& kvp : enum_keys_map) + names[kvp.second] = kvp.first; + return names; + } + +#define CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NAME) \ + static t_config_enum_names s_keys_names_##NAME = enum_names_from_keys_map(s_keys_map_##NAME); \ + template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values() { return s_keys_map_##NAME; } \ + template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names() { return s_keys_names_##NAME; } + + + + static const t_config_enum_values s_keys_map_NotifyReleaseMode = { + {"all", NotifyReleaseAll}, + {"release", NotifyReleaseOnly}, + {"none", NotifyReleaseNone}, + }; + + CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NotifyReleaseMode) + +namespace GUI { + +PreferencesDialog::PreferencesDialog(wxWindow* parent) : + DPIDialog(parent, wxID_ANY, _L("Preferences"), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_DIALOG_STYLE) +{ +#ifdef __WXOSX__ + isOSX = true; +#endif + build(); + + m_highlighter.set_timer_owner(this, 0); +} + +static void update_color(wxColourPickerCtrl* color_pckr, const wxColour& color) +{ + if (color_pckr->GetColour() != color) { + color_pckr->SetColour(color); + wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED)); + } +} + +void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + int selected_tab = 0; + for ( ; selected_tab < int(tabs->GetPageCount()); selected_tab++) + if (tabs->GetPageText(selected_tab) == _(tab_name)) + break; + if (selected_tab < int(tabs->GetPageCount())) + tabs->SetSelection(selected_tab); + + if (!highlight_opt_key.empty()) + init_highlighter(highlight_opt_key); + + // cache input values for custom toolbar size + m_custom_toolbar_size = atoi(get_app_config()->get("custom_toolbar_size").c_str()); + m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1"; + + // update colors for color pickers + update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); + update_color(m_mod_colour, wxGetApp().get_label_clr_modified()); + + this->ShowModal(); +} + +static std::shared_ptrcreate_options_tab(const wxString& title, wxBookCtrlBase* tabs) +{ + wxPanel* tab = new wxPanel(tabs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); + tabs->AddPage(tab, _(title)); + tab->SetFont(wxGetApp().normal_font()); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->SetSizeHints(tab); + tab->SetSizer(sizer); + + std::shared_ptr optgroup = std::make_shared(tab); + optgroup->label_width = 40; + optgroup->set_config_category_and_type(title, int(Preset::TYPE_PREFERENCES)); + return optgroup; +} + +static void activate_options_tab(std::shared_ptr optgroup) +{ + optgroup->activate([](){}, wxALIGN_RIGHT); + optgroup->update_visibility(comSimple); + wxBoxSizer* sizer = static_cast(static_cast(optgroup->parent())->GetSizer()); + sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10); + + // apply sercher + wxGetApp().sidebar().get_searcher().append_preferences_options(optgroup->get_lines()); +} + +static void append_bool_option( std::shared_ptr optgroup, + const std::string& opt_key, + const std::string& label, + const std::string& tooltip, + bool def_val, + ConfigOptionMode mode = comSimple) +{ + ConfigOptionDef def = {opt_key, coBool}; + def.label = label; + def.tooltip = tooltip; + def.mode = mode; + def.set_default_value(new ConfigOptionBool{ def_val }); + Option option(def, opt_key); + optgroup->append_single_option_line(option); + + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); +} + +static void append_enum_option( std::shared_ptr optgroup, + const std::string& opt_key, + const std::string& label, + const std::string& tooltip, + const ConfigOption* def_val, + const t_config_enum_values *enum_keys_map, + std::initializer_list enum_values, + std::initializer_list enum_labels, + ConfigOptionMode mode = comSimple) +{ + ConfigOptionDef def = {opt_key, coEnum }; + def.label = label; + def.tooltip = tooltip; + def.mode = mode; + def.enum_keys_map = enum_keys_map; + def.enum_values = std::vector(enum_values); + def.enum_labels = std::vector(enum_labels); + + def.set_default_value(def_val); + Option option(def, opt_key); + optgroup->append_single_option_line(option); + + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); +} + +static void append_preferences_option_to_searcer(std::shared_ptr optgroup, + const std::string& opt_key, + const wxString& label) +{ + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); + // apply sercher + wxGetApp().sidebar().get_searcher().append_preferences_option(Line(opt_key, label, "")); +} + +void PreferencesDialog::build() +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this); +#else + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif + const wxFont& font = wxGetApp().normal_font(); + SetFont(font); + + auto app_config = get_app_config(); + +#ifdef _MSW_DARK_MODE + tabs = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); +#else + tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL |wxNB_NOPAGETHEME | wxNB_DEFAULT ); + tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif + + // Add "General" tab + m_optgroup_general = create_options_tab(L("General"), tabs); + m_optgroup_general->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + if (opt_key == "default_action_on_close_application" || opt_key == "default_action_on_select_preset" || opt_key == "default_action_on_new_project") + m_values[opt_key] = boost::any_cast(value) ? "none" : "discard"; + else if (opt_key == "default_action_on_dirty_project") + m_values[opt_key] = boost::any_cast(value) ? "" : "0"; + else + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + bool is_editor = wxGetApp().is_editor(); + + if (is_editor) { + append_bool_option(m_optgroup_general, "remember_output_path", + L("Remember output directory"), + L("If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files."), + app_config->has("remember_output_path") ? app_config->get("remember_output_path") == "1" : true); + + append_bool_option(m_optgroup_general, "autocenter", + L("Auto-center parts"), + L("If this is enabled, Slic3r will auto-center objects around the print bed center."), + app_config->get("autocenter") == "1"); + + append_bool_option(m_optgroup_general, "background_processing", + L("Background processing"), + L("If this is enabled, Slic3r will pre-process objects as soon " + "as they\'re loaded in order to save time when exporting G-code."), + app_config->get("background_processing") == "1"); + + m_optgroup_general->append_separator(); + + // Please keep in sync with ConfigWizard + append_bool_option(m_optgroup_general, "export_sources_full_pathnames", + L("Export sources full pathnames to 3mf and amf"), + L("If enabled, allows the Reload from disk command to automatically find and load the files when invoked."), + app_config->get("export_sources_full_pathnames") == "1"); + +#ifdef _WIN32 +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + // file association is not possible anymore starting with Win 8 + if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + // Please keep in sync with ConfigWizard + append_bool_option(m_optgroup_general, "associate_3mf", + L("Associate .3mf files to PrusaSlicer"), + L("If enabled, sets PrusaSlicer as default application to open .3mf files."), + app_config->get("associate_3mf") == "1"); + + append_bool_option(m_optgroup_general, "associate_stl", + L("Associate .stl files to PrusaSlicer"), + L("If enabled, sets PrusaSlicer as default application to open .stl files."), + app_config->get("associate_stl") == "1"); +#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + } +#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER +#endif // _WIN32 + + m_optgroup_general->append_separator(); + + // Please keep in sync with ConfigWizard + append_bool_option(m_optgroup_general, "preset_update", + L("Update built-in Presets automatically"), + L("If enabled, Slic3r downloads updates of built-in system presets in the background. These updates are downloaded " + "into a separate temporary location. When a new preset version becomes available it is offered at application startup."), + app_config->get("preset_update") == "1"); + + append_bool_option(m_optgroup_general, "no_defaults", + L("Suppress \" - default - \" presets"), + L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."), + app_config->get("no_defaults") == "1"); + + append_bool_option(m_optgroup_general, "show_incompatible_presets", + L("Show incompatible print and filament presets"), + L("When checked, the print and filament presets are shown in the preset editor " + "even if they are marked as incompatible with the active printer"), + app_config->get("show_incompatible_presets") == "1"); + + m_optgroup_general->append_separator(); + + append_bool_option(m_optgroup_general, "show_drop_project_dialog", + L("Show drop project dialog"), + L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load."), + app_config->get("show_drop_project_dialog") == "1"); + + append_bool_option(m_optgroup_general, "single_instance", +#if __APPLE__ + L("Allow just a single PrusaSlicer instance"), + L("On OSX there is always only one instance of app running by default. However it is allowed to run multiple instances " + "of same app from the command line. In such case this settings will allow only one instance."), +#else + L("Allow just a single PrusaSlicer instance"), + L("If this is enabled, when starting PrusaSlicer and another instance of the same PrusaSlicer is already running, that instance will be reactivated instead."), +#endif + app_config->has("single_instance") ? app_config->get("single_instance") == "1" : false ); + + m_optgroup_general->append_separator(); + + append_bool_option(m_optgroup_general, "default_action_on_dirty_project", + L("Ask for unsaved changes in project"), + L("Always ask for unsaved changes in project, when: \n" + "- Closing PrusaSlicer,\n" + "- Loading or creating a new project"), + app_config->get("default_action_on_dirty_project").empty()); + + m_optgroup_general->append_separator(); + + append_bool_option(m_optgroup_general, "default_action_on_close_application", + L("Ask to save unsaved changes in presets when closing the application or when loading a new project"), + L("Always ask for unsaved changes in presets, when: \n" + "- Closing PrusaSlicer while some presets are modified,\n" + "- Loading a new project while some presets are modified"), + app_config->get("default_action_on_close_application") == "none"); + + append_bool_option(m_optgroup_general, "default_action_on_select_preset", + L("Ask for unsaved changes in presets when selecting new preset"), + L("Always ask for unsaved changes in presets when selecting new preset or resetting a preset"), + app_config->get("default_action_on_select_preset") == "none"); + + append_bool_option(m_optgroup_general, "default_action_on_new_project", + L("Ask for unsaved changes in presets when creating new project"), + L("Always ask for unsaved changes in presets when creating new project"), + app_config->get("default_action_on_new_project") == "none"); + } +#ifdef _WIN32 + else { + append_bool_option(m_optgroup_general, "associate_gcode", + L("Associate .gcode files to PrusaSlicer G-code Viewer"), + L("If enabled, sets PrusaSlicer G-code Viewer as default application to open .gcode files."), + app_config->get("associate_gcode") == "1"); + } +#endif // _WIN32 + +#if __APPLE__ + append_bool_option(m_optgroup_general, "use_retina_opengl", + L("Use Retina resolution for the 3D scene"), + L("If enabled, the 3D scene will be rendered in Retina resolution. " + "If you are experiencing 3D performance problems, disabling this option may help."), + app_config->get("use_retina_opengl") == "1"); +#endif + + m_optgroup_general->append_separator(); + + // Show/Hide splash screen + append_bool_option(m_optgroup_general, "show_splash_screen", + L("Show splash screen"), + L("Show splash screen"), + app_config->get("show_splash_screen") == "1"); + + append_bool_option(m_optgroup_general, "restore_win_position", + L("Restore window position on start"), + L("If enabled, PrusaSlicer will be open at the position it was closed"), + app_config->get("restore_win_position") == "1"); + + // Clear Undo / Redo stack on new project + append_bool_option(m_optgroup_general, "clear_undo_redo_stack_on_new_project", + L("Clear Undo / Redo stack on new project"), + L("Clear Undo / Redo stack on new project or when an existing project is loaded."), + app_config->get("clear_undo_redo_stack_on_new_project") == "1"); + +#if defined(_WIN32) || defined(__APPLE__) + append_bool_option(m_optgroup_general, "use_legacy_3DConnexion", + L("Enable support for legacy 3DConnexion devices"), + L("If enabled, the legacy 3DConnexion devices settings dialog is available by pressing CTRL+M"), + app_config->get("use_legacy_3DConnexion") == "1"); +#endif // _WIN32 || __APPLE__ + + activate_options_tab(m_optgroup_general); + + // Add "Camera" tab + m_optgroup_camera = create_options_tab(L("Camera"), tabs); + m_optgroup_camera->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key);it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_camera, "use_perspective_camera", + L("Use perspective camera"), + L("If enabled, use perspective camera. If not enabled, use orthographic camera."), + app_config->get("use_perspective_camera") == "1"); + + append_bool_option(m_optgroup_camera, "use_free_camera", + L("Use free camera"), + L("If enabled, use free camera. If not enabled, use constrained camera."), + app_config->get("use_free_camera") == "1"); + + append_bool_option(m_optgroup_camera, "reverse_mouse_wheel_zoom", + L("Reverse direction of zoom with mouse wheel"), + L("If enabled, reverses the direction of zoom with mouse wheel"), + app_config->get("reverse_mouse_wheel_zoom") == "1"); + + activate_options_tab(m_optgroup_camera); + + // Add "GUI" tab + m_optgroup_gui = create_options_tab(L("GUI"), tabs); + m_optgroup_gui->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (opt_key == "notify_release") { + int val_int = boost::any_cast(value); + for (const auto& item : s_keys_map_NotifyReleaseMode) { + if (item.second == val_int) { + m_values[opt_key] = item.first; + return; + } + } + } + if (opt_key == "use_custom_toolbar_size") { + m_icon_size_sizer->ShowItems(boost::any_cast(value)); + refresh_og(m_optgroup_gui); + get_app_config()->set("use_custom_toolbar_size", boost::any_cast(value) ? "1" : "0"); + get_app_config()->save(); + wxGetApp().plater()->get_current_canvas3D()->render(); + return; + } + if (opt_key == "tabs_as_menu") { + bool disable_new_layout = boost::any_cast(value); + m_rb_new_settings_layout_mode->Show(!disable_new_layout); + if (disable_new_layout && m_rb_new_settings_layout_mode->GetValue()) { + m_rb_new_settings_layout_mode->SetValue(false); + m_rb_old_settings_layout_mode->SetValue(true); + } + refresh_og(m_optgroup_gui); + } + + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + + if (opt_key == "suppress_hyperlinks") + m_values[opt_key] = boost::any_cast(value) ? "1" : ""; + else + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_gui, "seq_top_layer_only", + L("Sequential slider applied only to top layer"), + L("If enabled, changes made using the sequential slider, in preview, apply only to gcode top layer." + "If disabled, changes made using the sequential slider, in preview, apply to the whole gcode."), + app_config->get("seq_top_layer_only") == "1"); + + if (is_editor) { + append_bool_option(m_optgroup_gui, "show_collapse_button", + L("Show sidebar collapse/expand button"), + L("If enabled, the button for the collapse sidebar will be appeared in top right corner of the 3D Scene"), + app_config->get("show_collapse_button") == "1"); + + append_bool_option(m_optgroup_gui, "suppress_hyperlinks", + L("Suppress to open hyperlink in browser"), + L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."), + //L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. " + // "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."), + app_config->get("suppress_hyperlinks") == "1"); + + append_bool_option(m_optgroup_gui, "color_mapinulation_panel", + L("Use colors for axes values in Manipulation panel"), + L("If enabled, the axes names and axes values will be colorized according to the axes colors. " + "If disabled, old UI will be used."), + app_config->get("color_mapinulation_panel") == "1"); + + append_bool_option(m_optgroup_gui, "order_volumes", + L("Order object volumes by types"), + L("If enabled, volumes will be always ordered inside the object. Correct order is Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. " + "If disabled, you can reorder Model Parts, Negative Volumes and Modifiers. But one of the model parts have to be on the first place."), + app_config->get("order_volumes") == "1"); + +#if ENABLE_SHOW_NON_MANIFOLD_EDGES + append_bool_option(m_optgroup_gui, "non_manifold_edges", + L("Show non-manifold edges"), + L("If enabled, shows non-manifold edges."), + app_config->get("non_manifold_edges") == "1"); +#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES + +#ifdef _MSW_DARK_MODE + append_bool_option(m_optgroup_gui, "tabs_as_menu", + L("Set settings tabs as menu items (experimental)"), + L("If enabled, Settings Tabs will be placed as menu items. If disabled, old UI will be used."), + app_config->get("tabs_as_menu") == "1"); +#endif + + m_optgroup_gui->append_separator(); + + append_bool_option(m_optgroup_gui, "show_hints", + L("Show \"Tip of the day\" notification after start"), + L("If enabled, useful hints are displayed at startup."), + app_config->get("show_hints") == "1"); + + append_enum_option(m_optgroup_gui, "notify_release", + L("Notify about new releases"), + L("You will be notified about new release after startup acordingly: All = Regular release and alpha / beta releases. Release only = regular release."), + new ConfigOptionEnum(static_cast(s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")))), + &ConfigOptionEnum::get_enum_values(), + {"all", "release", "none"}, + {L("All"), L("Release only"), L("None")}); + + m_optgroup_gui->append_separator(); + + append_bool_option(m_optgroup_gui, "use_custom_toolbar_size", + L("Use custom size for toolbar icons"), + L("If enabled, you can change size of toolbar icons manually."), + app_config->get("use_custom_toolbar_size") == "1"); + } + + activate_options_tab(m_optgroup_gui); + + if (is_editor) { + // set Field for notify_release to its value to activate the object + boost::any val = s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")); + m_optgroup_gui->get_field("notify_release")->set_value(val, false); + + create_icon_size_slider(); + m_icon_size_sizer->ShowItems(app_config->get("use_custom_toolbar_size") == "1"); + + create_settings_mode_widget(); + create_settings_text_color_widget(); + +#if ENABLE_ENVIRONMENT_MAP + // Add "Render" tab + m_optgroup_render = create_options_tab(L("Render"), tabs); + m_optgroup_render->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_render, "use_environment_map", + L("Use environment map"), + L("If enabled, renders object using the environment map."), + app_config->get("use_environment_map") == "1"); + + activate_options_tab(m_optgroup_render); +#endif // ENABLE_ENVIRONMENT_MAP + +#ifdef _WIN32 + // Add "Dark Mode" tab + m_optgroup_dark_mode = create_options_tab(_L("Dark mode (experimental)"), tabs); + m_optgroup_dark_mode->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_dark_mode, "dark_color_mode", + L("Enable dark mode"), + L("If enabled, UI will use Dark mode colors. If disabled, old UI will be used."), + app_config->get("dark_color_mode") == "1"); + + if (wxPlatformInfo::Get().GetOSMajorVersion() >= 10) // Use system menu just for Window newer then Windows 10 + // Use menu with ownerdrawn items by default on systems older then Windows 10 + { + append_bool_option(m_optgroup_dark_mode, "sys_menu_enabled", + L("Use system menu for application"), + L("If enabled, application will use the standart Windows system menu,\n" + "but on some combination od display scales it can look ugly. If disabled, old UI will be used."), + app_config->get("sys_menu_enabled") == "1"); + } + + activate_options_tab(m_optgroup_dark_mode); +#endif //_WIN32 + } + + // update alignment of the controls for all tabs + update_ctrls_alignment(); + + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(tabs, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); + + auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + this->Bind(wxEVT_BUTTON, &PreferencesDialog::accept, this, wxID_OK); + this->Bind(wxEVT_BUTTON, &PreferencesDialog::revert, this, wxID_CANCEL); + + for (int id : {wxID_OK, wxID_CANCEL}) + wxGetApp().UpdateDarkUI(static_cast(FindWindowById(id, this))); + + sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM | wxTOP, 10); + + SetSizer(sizer); + sizer->SetSizeHints(this); + this->CenterOnParent(); +} + +std::vector PreferencesDialog::optgroups() +{ + std::vector out; + out.reserve(4); + for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get() +#ifdef _WIN32 + , m_optgroup_dark_mode.get() +#endif // _WIN32 +#if ENABLE_ENVIRONMENT_MAP + , m_optgroup_render.get() +#endif // ENABLE_ENVIRONMENT_MAP + }) + if (opt) + out.emplace_back(opt); + return out; +} + +void PreferencesDialog::update_ctrls_alignment() +{ + int max_ctrl_width{ 0 }; + for (ConfigOptionsGroup* og : this->optgroups()) + if (int max = og->custom_ctrl->get_max_win_width(); + max_ctrl_width < max) + max_ctrl_width = max; + if (max_ctrl_width) + for (ConfigOptionsGroup* og : this->optgroups()) + og->custom_ctrl->set_max_win_width(max_ctrl_width); +} + +void PreferencesDialog::accept(wxEvent&) +{ + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; + + for (const std::string& option : options_to_recreate_GUI) { + if (m_values.find(option) != m_values.end()) { + wxString title = wxGetApp().is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Changes for the critical options"); + MessageDialog dialog(nullptr, + _L("Changing some options will trigger application restart.\n" + "You will lose the content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxYES | wxNO); + if (dialog.ShowModal() == wxID_YES) { + m_recreate_GUI = true; + } + else { + for (const std::string& option : options_to_recreate_GUI) + m_values.erase(option); + } + break; + } + } + + auto app_config = get_app_config(); + + m_seq_top_layer_only_changed = false; + if (auto it = m_values.find("seq_top_layer_only"); it != m_values.end()) + m_seq_top_layer_only_changed = app_config->get("seq_top_layer_only") != it->second; + + m_settings_layout_changed = false; + for (const std::string& key : { "old_settings_layout_mode", + "new_settings_layout_mode", + "dlg_settings_layout_mode" }) + { + auto it = m_values.find(key); + if (it != m_values.end() && app_config->get(key) != it->second) { + m_settings_layout_changed = true; + break; + } + } + +#if 0 //#ifdef _WIN32 // #ysDarkMSW - Allow it when we deside to support the sustem colors for application + if (m_values.find("always_dark_color_mode") != m_values.end()) + wxGetApp().force_sys_colors_update(); +#endif + + for (std::map::iterator it = m_values.begin(); it != m_values.end(); ++it) + app_config->set(it->first, it->second); + + app_config->save(); + if (wxGetApp().is_editor()) { + wxGetApp().set_label_clr_sys(m_sys_colour->GetColour()); + wxGetApp().set_label_clr_modified(m_mod_colour->GetColour()); + } + + EndModal(wxID_OK); + +#ifdef _WIN32 + if (m_values.find("dark_color_mode") != m_values.end()) + wxGetApp().force_colors_update(); +#ifdef _MSW_DARK_MODE + if (m_values.find("sys_menu_enabled") != m_values.end()) + wxGetApp().force_menu_update(); +#endif //_MSW_DARK_MODE +#endif // _WIN32 + + wxGetApp().update_ui_from_settings(); + clear_cache(); +} + +void PreferencesDialog::revert(wxEvent&) +{ + auto app_config = get_app_config(); + + bool save_app_config = false; + if (m_custom_toolbar_size != atoi(app_config->get("custom_toolbar_size").c_str())) { + app_config->set("custom_toolbar_size", (boost::format("%d") % m_custom_toolbar_size).str()); + m_icon_size_slider->SetValue(m_custom_toolbar_size); + save_app_config |= true; + } + if (m_use_custom_toolbar_size != (get_app_config()->get("use_custom_toolbar_size") == "1")) { + app_config->set("use_custom_toolbar_size", m_use_custom_toolbar_size ? "1" : "0"); + save_app_config |= true; + + m_optgroup_gui->set_value("use_custom_toolbar_size", m_use_custom_toolbar_size); + m_icon_size_sizer->ShowItems(m_use_custom_toolbar_size); + refresh_og(m_optgroup_gui); + } + if (save_app_config) + app_config->save(); + + + for (auto value : m_values) { + bool reverted = false; + const std::string& key = value.first; + + if (key == "default_action_on_dirty_project") { + m_optgroup_general->set_value(key, app_config->get(key).empty()); + continue; + } + if (key == "default_action_on_close_application" || key == "default_action_on_select_preset" || key == "default_action_on_new_project") { + m_optgroup_general->set_value(key, app_config->get(key) == "none"); + continue; + } + if (key == "notify_release") { + m_optgroup_gui->set_value(key, s_keys_map_NotifyReleaseMode.at(app_config->get(key))); + continue; + } + if (key == "old_settings_layout_mode") { + m_rb_old_settings_layout_mode->SetValue(app_config->get(key) == "1"); + continue; + } + if (key == "new_settings_layout_mode") { + m_rb_new_settings_layout_mode->SetValue(app_config->get(key) == "1"); + continue; + } + if (key == "dlg_settings_layout_mode") { + m_rb_dlg_settings_layout_mode->SetValue(app_config->get(key) == "1"); + continue; + } + + for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui +#ifdef _WIN32 + , m_optgroup_dark_mode +#endif // _WIN32 +#if ENABLE_ENVIRONMENT_MAP + , m_optgroup_render +#endif // ENABLE_ENVIRONMENT_MAP + }) { + if (opt_group->set_value(key, app_config->get(key) == "1")) + break; + } + if (key == "tabs_as_menu") { + m_rb_new_settings_layout_mode->Show(app_config->get(key) != "1"); + refresh_og(m_optgroup_gui); + continue; + } + } + + clear_cache(); + EndModal(wxID_CANCEL); +} + +void PreferencesDialog::msw_rescale() +{ + for (ConfigOptionsGroup* og : this->optgroups()) + og->msw_rescale(); +#ifdef _WIN32 + m_optgroup_dark_mode->msw_rescale(); +#endif //_WIN32 +#if ENABLE_ENVIRONMENT_MAP + m_optgroup_render->msw_rescale(); +#endif // ENABLE_ENVIRONMENT_MAP + + msw_buttons_rescale(this, em_unit(), { wxID_OK, wxID_CANCEL }); + + layout(); +} + +void PreferencesDialog::on_sys_color_changed() +{ +#ifdef _WIN32 + wxGetApp().UpdateDlgDarkUI(this); +#endif +} + +void PreferencesDialog::layout() +{ + const int em = em_unit(); + + SetMinSize(wxSize(47 * em, 28 * em)); + Fit(); + + Refresh(); +} + +void PreferencesDialog::clear_cache() +{ + m_values.clear(); + m_custom_toolbar_size = -1; +} + +void PreferencesDialog::refresh_og(std::shared_ptr og) +{ + og->parent()->Layout(); + tabs->Layout(); + this->layout(); +} + +void PreferencesDialog::create_icon_size_slider() +{ + const auto app_config = get_app_config(); + + const int em = em_unit(); + + m_icon_size_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxWindow* parent = m_optgroup_gui->parent(); + wxGetApp().UpdateDarkUI(parent); + + if (isOSX) + // For correct rendering of the slider and value label under OSX + // we should use system default background + parent->SetBackgroundStyle(wxBG_STYLE_ERASE); + + auto label = new wxStaticText(parent, wxID_ANY, _L("Icon size in a respect to the default size") + " (%) :"); + + m_icon_size_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL| wxRIGHT | (isOSX ? 0 : wxLEFT), em); + + const int def_val = atoi(app_config->get("custom_toolbar_size").c_str()); + + long style = wxSL_HORIZONTAL; + if (!isOSX) + style |= wxSL_LABELS | wxSL_AUTOTICKS; + + m_icon_size_slider = new wxSlider(parent, wxID_ANY, def_val, 30, 100, + wxDefaultPosition, wxDefaultSize, style); + + m_icon_size_slider->SetTickFreq(10); + m_icon_size_slider->SetPageSize(10); + m_icon_size_slider->SetToolTip(_L("Select toolbar icon size in respect to the default one.")); + + m_icon_size_sizer->Add(m_icon_size_slider, 1, wxEXPAND); + + wxStaticText* val_label{ nullptr }; + if (isOSX) { + val_label = new wxStaticText(parent, wxID_ANY, wxString::Format("%d", def_val)); + m_icon_size_sizer->Add(val_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, em); + } + + m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label, app_config](wxCommandEvent e) { + auto val = m_icon_size_slider->GetValue(); + + app_config->set("custom_toolbar_size", (boost::format("%d") % val).str()); + app_config->save(); + wxGetApp().plater()->get_current_canvas3D()->render(); + + if (val_label) + val_label->SetLabelText(wxString::Format("%d", val)); + }), m_icon_size_slider->GetId()); + + for (wxWindow* win : std::vector{ m_icon_size_slider, label, val_label }) { + if (!win) continue; + win->SetFont(wxGetApp().normal_font()); + + if (isOSX) continue; // under OSX we use wxBG_STYLE_ERASE + win->SetBackgroundStyle(wxBG_STYLE_PAINT); + } + + m_optgroup_gui->sizer->Add(m_icon_size_sizer, 0, wxEXPAND | wxALL, em); +} + +void PreferencesDialog::create_settings_mode_widget() +{ + wxWindow* parent = m_optgroup_gui->parent(); + wxGetApp().UpdateDarkUI(parent); + + wxString title = L("Layout Options"); + wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); + wxGetApp().UpdateDarkUI(stb); + if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); + stb->SetFont(wxGetApp().normal_font()); + + wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); + + auto app_config = get_app_config(); + std::vector choices = { _L("Old regular layout with the tab bar"), + _L("New layout, access via settings button in the top menu"), + _L("Settings in non-modal window") }; + int id = -1; + auto add_radio = [this, parent, stb_sizer, choices](wxRadioButton** rb, int id, bool select) { + *rb = new wxRadioButton(parent, wxID_ANY, choices[id], wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); + stb_sizer->Add(*rb); + (*rb)->SetValue(select); + (*rb)->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) { + m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0"; + m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0"; + m_values["dlg_settings_layout_mode"] = (id == 2) ? "1" : "0"; + }); + }; + + add_radio(&m_rb_old_settings_layout_mode, ++id, app_config->get("old_settings_layout_mode") == "1"); + add_radio(&m_rb_new_settings_layout_mode, ++id, app_config->get("new_settings_layout_mode") == "1"); + add_radio(&m_rb_dlg_settings_layout_mode, ++id, app_config->get("dlg_settings_layout_mode") == "1"); + +#ifdef _MSW_DARK_MODE + if (app_config->get("tabs_as_menu") == "1") { + m_rb_new_settings_layout_mode->Hide(); + if (m_rb_new_settings_layout_mode->GetValue()) { + m_rb_new_settings_layout_mode->SetValue(false); + m_rb_old_settings_layout_mode->SetValue(true); + } + } +#endif + + std::string opt_key = "settings_layout_mode"; + m_blinkers[opt_key] = new BlinkingBitmap(parent); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); + sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); + m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); + + append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); +} + +void PreferencesDialog::create_settings_text_color_widget() +{ + wxWindow* parent = m_optgroup_gui->parent(); + + wxString title = L("Text colors"); + wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); + wxGetApp().UpdateDarkUI(stb); + if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); + + std::string opt_key = "text_colors"; + m_blinkers[opt_key] = new BlinkingBitmap(parent); + + wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); + ButtonsDescription::FillSizerWithTextColorDescriptions(stb_sizer, parent, &m_sys_colour, &m_mod_colour); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); + sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); + + m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); + + append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); +} + +void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key) +{ + if (m_blinkers.find(opt_key) != m_blinkers.end()) + if (BlinkingBitmap* blinker = m_blinkers.at(opt_key); blinker) { + m_highlighter.init(blinker); + return; + } + + for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui +#ifdef _WIN32 + , m_optgroup_dark_mode +#endif // _WIN32 +#if ENABLE_ENVIRONMENT_MAP + , m_optgroup_render +#endif // ENABLE_ENVIRONMENT_MAP + }) { + std::pair ctrl = opt_group->get_custom_ctrl_with_blinking_ptr(opt_key, -1); + if (ctrl.first && ctrl.second) { + m_highlighter.init(ctrl); + break; + } + } +} + +} // GUI +} // Slic3r diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index 296c58622..6291d8734 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -1,449 +1,449 @@ -#ifdef HAS_WIN10SDK - -#ifndef NOMINMAX -# define NOMINMAX -#endif - -// Windows Runtime -#include -// for ComPtr -#include - -// from C:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/ -#include -#include -#include - -#include "FixModelByWin10.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Model.hpp" -#include "libslic3r/Print.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Format/3mf.hpp" -#include "../GUI/GUI.hpp" -#include "../GUI/I18N.hpp" -#include "../GUI/MsgDialog.hpp" - -#include -#include - -extern "C"{ - // from rapi.h - typedef HRESULT (__stdcall* FunctionRoInitialize)(int); - typedef HRESULT (__stdcall* FunctionRoUninitialize)(); - typedef HRESULT (__stdcall* FunctionRoActivateInstance)(HSTRING activatableClassId, IInspectable **instance); - typedef HRESULT (__stdcall* FunctionRoGetActivationFactory)(HSTRING activatableClassId, REFIID iid, void **factory); - // from winstring.h - typedef HRESULT (__stdcall* FunctionWindowsCreateString)(LPCWSTR sourceString, UINT32 length, HSTRING *string); - typedef HRESULT (__stdcall* FunctionWindowsDelteString)(HSTRING string); -} - -namespace Slic3r { - -HMODULE s_hRuntimeObjectLibrary = nullptr; -FunctionRoInitialize s_RoInitialize = nullptr; -FunctionRoUninitialize s_RoUninitialize = nullptr; -FunctionRoActivateInstance s_RoActivateInstance = nullptr; -FunctionRoGetActivationFactory s_RoGetActivationFactory = nullptr; -FunctionWindowsCreateString s_WindowsCreateString = nullptr; -FunctionWindowsDelteString s_WindowsDeleteString = nullptr; - -bool winrt_load_runtime_object_library() -{ - if (s_hRuntimeObjectLibrary == nullptr) - s_hRuntimeObjectLibrary = LoadLibrary(L"ComBase.dll"); - if (s_hRuntimeObjectLibrary != nullptr) { - s_RoInitialize = (FunctionRoInitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoInitialize"); - s_RoUninitialize = (FunctionRoUninitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoUninitialize"); - s_RoActivateInstance = (FunctionRoActivateInstance) GetProcAddress(s_hRuntimeObjectLibrary, "RoActivateInstance"); - s_RoGetActivationFactory = (FunctionRoGetActivationFactory) GetProcAddress(s_hRuntimeObjectLibrary, "RoGetActivationFactory"); - s_WindowsCreateString = (FunctionWindowsCreateString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsCreateString"); - s_WindowsDeleteString = (FunctionWindowsDelteString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsDeleteString"); - } - return s_RoInitialize && s_RoUninitialize && s_RoActivateInstance && s_WindowsCreateString && s_WindowsDeleteString; -} - -static HRESULT winrt_activate_instance(const std::wstring &class_name, IInspectable **pinst) -{ - HSTRING hClassName; - HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); - if (S_OK != hr) - return hr; - hr = (*s_RoActivateInstance)(hClassName, pinst); - (*s_WindowsDeleteString)(hClassName); - return hr; -} - -template -static HRESULT winrt_activate_instance(const std::wstring &class_name, TYPE **pinst) -{ - IInspectable *pinspectable = nullptr; - HRESULT hr = winrt_activate_instance(class_name, &pinspectable); - if (S_OK != hr) - return hr; - hr = pinspectable->QueryInterface(__uuidof(TYPE), (void**)pinst); - pinspectable->Release(); - return hr; -} - -static HRESULT winrt_get_activation_factory(const std::wstring &class_name, REFIID iid, void **pinst) -{ - HSTRING hClassName; - HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); - if (S_OK != hr) - return hr; - hr = (*s_RoGetActivationFactory)(hClassName, iid, pinst); - (*s_WindowsDeleteString)(hClassName); - return hr; -} - -template -static HRESULT winrt_get_activation_factory(const std::wstring &class_name, TYPE **pinst) -{ - return winrt_get_activation_factory(class_name, __uuidof(TYPE), reinterpret_cast(pinst)); -} - -// To be called often to test whether to cancel the operation. -typedef std::function ThrowOnCancelFn; - -template -static AsyncStatus winrt_async_await(const Microsoft::WRL::ComPtr &asyncAction, ThrowOnCancelFn throw_on_cancel, int blocking_tick_ms = 100) -{ - Microsoft::WRL::ComPtr asyncInfo; - asyncAction.As(&asyncInfo); - AsyncStatus status; - // Ugly blocking loop until the RepairAsync call finishes. -//FIXME replace with a callback. -// https://social.msdn.microsoft.com/Forums/en-US/a5038fb4-b7b7-4504-969d-c102faa389fb/trying-to-block-an-async-operation-and-wait-for-a-particular-time?forum=vclanguage - for (;;) { - asyncInfo->get_Status(&status); - if (status != AsyncStatus::Started) - return status; - throw_on_cancel(); - ::Sleep(blocking_tick_ms); - } -} - -static HRESULT winrt_open_file_stream( - const std::wstring &path, - ABI::Windows::Storage::FileAccessMode mode, - ABI::Windows::Storage::Streams::IRandomAccessStream **fileStream, - ThrowOnCancelFn throw_on_cancel) -{ - // Get the file factory. - Microsoft::WRL::ComPtr fileFactory; - HRESULT hr = winrt_get_activation_factory(L"Windows.Storage.StorageFile", fileFactory.GetAddressOf()); - if (FAILED(hr)) return hr; - - // Open the file asynchronously. - HSTRING hstr_path; - hr = (*s_WindowsCreateString)(path.c_str(), path.size(), &hstr_path); - if (FAILED(hr)) return hr; - Microsoft::WRL::ComPtr> fileOpenAsync; - hr = fileFactory->GetFileFromPathAsync(hstr_path, fileOpenAsync.GetAddressOf()); - if (FAILED(hr)) return hr; - (*s_WindowsDeleteString)(hstr_path); - - // Wait until the file gets open, get the actual file. - AsyncStatus status = winrt_async_await(fileOpenAsync, throw_on_cancel); - Microsoft::WRL::ComPtr storageFile; - if (status == AsyncStatus::Completed) { - hr = fileOpenAsync->GetResults(storageFile.GetAddressOf()); - } else { - Microsoft::WRL::ComPtr asyncInfo; - hr = fileOpenAsync.As(&asyncInfo); - if (FAILED(hr)) return hr; - HRESULT err; - hr = asyncInfo->get_ErrorCode(&err); - return FAILED(hr) ? hr : err; - } - - Microsoft::WRL::ComPtr> fileStreamAsync; - hr = storageFile->OpenAsync(mode, fileStreamAsync.GetAddressOf()); - if (FAILED(hr)) return hr; - - status = winrt_async_await(fileStreamAsync, throw_on_cancel); - if (status == AsyncStatus::Completed) { - hr = fileStreamAsync->GetResults(fileStream); - } else { - Microsoft::WRL::ComPtr asyncInfo; - hr = fileStreamAsync.As(&asyncInfo); - if (FAILED(hr)) return hr; - HRESULT err; - hr = asyncInfo->get_ErrorCode(&err); - if (!FAILED(hr)) - hr = err; - } - return hr; -} - -bool is_windows10() -{ - HKEY hKey; - LONG lRes = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey); - if (lRes == ERROR_SUCCESS) { - WCHAR szBuffer[512]; - DWORD dwBufferSize = sizeof(szBuffer); - lRes = RegQueryValueExW(hKey, L"ProductName", 0, nullptr, (LPBYTE)szBuffer, &dwBufferSize); - if (lRes == ERROR_SUCCESS) - return wcsncmp(szBuffer, L"Windows 10", 10) == 0; - RegCloseKey(hKey); - } - return false; -} - -// Progress function, to be called regularly to update the progress. -typedef std::function ProgressFn; - -void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path_dst, ProgressFn on_progress, ThrowOnCancelFn throw_on_cancel) -{ - if (! is_windows10()) - throw Slic3r::RuntimeError("fix_model_by_win10_sdk called on non Windows 10 system"); - - if (! winrt_load_runtime_object_library()) - throw Slic3r::RuntimeError("Failed to initialize the WinRT library."); - - HRESULT hr = (*s_RoInitialize)(RO_INIT_MULTITHREADED); - { - on_progress(L("Exporting source model"), 20); - - Microsoft::WRL::ComPtr fileStream; - hr = winrt_open_file_stream(boost::nowide::widen(path_src), ABI::Windows::Storage::FileAccessMode::FileAccessMode_Read, fileStream.GetAddressOf(), throw_on_cancel); - - Microsoft::WRL::ComPtr printing3d3mfpackage; - hr = winrt_activate_instance(L"Windows.Graphics.Printing3D.Printing3D3MFPackage", printing3d3mfpackage.GetAddressOf()); - - Microsoft::WRL::ComPtr> modelAsync; - hr = printing3d3mfpackage->LoadModelFromPackageAsync(fileStream.Get(), modelAsync.GetAddressOf()); - - AsyncStatus status = winrt_async_await(modelAsync, throw_on_cancel); - Microsoft::WRL::ComPtr model; - if (status == AsyncStatus::Completed) - hr = modelAsync->GetResults(model.GetAddressOf()); - else - throw Slic3r::RuntimeError(L("Failed loading the input model.")); - - Microsoft::WRL::ComPtr> meshes; - hr = model->get_Meshes(meshes.GetAddressOf()); - unsigned num_meshes = 0; - hr = meshes->get_Size(&num_meshes); - - on_progress(L("Repairing model by the Netfabb service"), 40); - - Microsoft::WRL::ComPtr repairAsync; - hr = model->RepairAsync(repairAsync.GetAddressOf()); - status = winrt_async_await(repairAsync, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Mesh repair failed.")); - repairAsync->GetResults(); - - on_progress(L("Loading repaired model"), 60); - - // Verify the number of meshes returned after the repair action. - meshes.Reset(); - hr = model->get_Meshes(meshes.GetAddressOf()); - hr = meshes->get_Size(&num_meshes); - - // Save model to this class' Printing3D3MFPackage. - Microsoft::WRL::ComPtr saveToPackageAsync; - hr = printing3d3mfpackage->SaveModelToPackageAsync(model.Get(), saveToPackageAsync.GetAddressOf()); - status = winrt_async_await(saveToPackageAsync, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); - hr = saveToPackageAsync->GetResults(); - - Microsoft::WRL::ComPtr> generatorStreamAsync; - hr = printing3d3mfpackage->SaveAsync(generatorStreamAsync.GetAddressOf()); - status = winrt_async_await(generatorStreamAsync, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); - Microsoft::WRL::ComPtr generatorStream; - hr = generatorStreamAsync->GetResults(generatorStream.GetAddressOf()); - - // Go to the beginning of the stream. - generatorStream->Seek(0); - Microsoft::WRL::ComPtr inputStream; - hr = generatorStream.As(&inputStream); - - // Get the buffer factory. - Microsoft::WRL::ComPtr bufferFactory; - hr = winrt_get_activation_factory(L"Windows.Storage.Streams.Buffer", bufferFactory.GetAddressOf()); - - // Open the destination file. - FILE *fout = boost::nowide::fopen(path_dst.c_str(), "wb"); - try { - Microsoft::WRL::ComPtr buffer; - byte *buffer_ptr; - bufferFactory->Create(65536 * 2048, buffer.GetAddressOf()); - { - Microsoft::WRL::ComPtr bufferByteAccess; - buffer.As(&bufferByteAccess); - hr = bufferByteAccess->Buffer(&buffer_ptr); - } - uint32_t length; - hr = buffer->get_Length(&length); - Microsoft::WRL::ComPtr> asyncRead; - for (;;) { - hr = inputStream->ReadAsync(buffer.Get(), 65536 * 2048, ABI::Windows::Storage::Streams::InputStreamOptions_ReadAhead, asyncRead.GetAddressOf()); - status = winrt_async_await(asyncRead, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); - hr = buffer->get_Length(&length); - if (length == 0) - break; - fwrite(buffer_ptr, length, 1, fout); - } - } catch (...) { - fclose(fout); - throw; - } - fclose(fout); - // Here all the COM objects will be released through the ComPtr destructors. - } - (*s_RoUninitialize)(); -} - -class RepairCanceledException : public std::exception { -public: - const char* what() const throw() { return "Model repair has been canceled"; } -}; - -// returt FALSE, if fixing was canceled -// fix_result is empty, if fixing finished successfully -// fix_result containes a message if fixing failed -bool fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx, wxProgressDialog& progress_dialog, const wxString& msg_header, std::string& fix_result) -{ - std::mutex mutex; - std::condition_variable condition; - std::unique_lock lock(mutex); - struct Progress { - std::string message; - int percent = 0; - bool updated = false; - } progress; - std::atomic canceled = false; - std::atomic finished = false; - - std::vector volumes; - if (volume_idx == -1) - volumes = model_object.volumes; - else - volumes.emplace_back(model_object.volumes[volume_idx]); - - // Executing the calculation in a background thread, so that the COM context could be created with its own threading model. - // (It seems like wxWidgets initialize the COM contex as single threaded and we need a multi-threaded context). - bool success = false; - size_t ivolume = 0; - auto on_progress = [&mutex, &condition, &ivolume, &volumes, &progress](const char *msg, unsigned prcnt) { - std::lock_guard lk(mutex); - progress.message = msg; - progress.percent = (int)floor((float(prcnt) + float(ivolume) * 100.f) / float(volumes.size())); - progress.updated = true; - condition.notify_all(); - }; - auto worker_thread = boost::thread([&model_object, &volumes, &ivolume, on_progress, &success, &canceled, &finished]() { - try { - std::vector meshes_repaired; - meshes_repaired.reserve(volumes.size()); - for (; ivolume < volumes.size(); ++ ivolume) { - on_progress(L("Exporting source model"), 0); - boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); - path_src += ".3mf"; - Model model; - ModelObject *mo = model.add_object(); - mo->add_volume(*volumes[ivolume]); - - // We are about to save a 3mf, fix it by netfabb and load the fixed 3mf back. - // store_3mf currently bakes the volume transformation into the mesh itself. - // If we then loaded the repaired 3mf and pushed the mesh into the original ModelVolume - // (which remembers the matrix the whole time), the transformation would be used twice. - // We will therefore set the volume transform on the dummy ModelVolume to identity. - mo->volumes.back()->set_transformation(Geometry::Transformation()); - - mo->add_instance(); - if (!Slic3r::store_3mf(path_src.string().c_str(), &model, nullptr, false, nullptr, false)) { - boost::filesystem::remove(path_src); - throw Slic3r::RuntimeError(L("Export of a temporary 3mf file failed")); - } - model.clear_objects(); - model.clear_materials(); - boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); - path_dst += ".3mf"; - fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress, - [&canceled]() { if (canceled) throw RepairCanceledException(); }); - boost::filesystem::remove(path_src); - // PresetBundle bundle; - on_progress(L("Loading repaired model"), 80); - DynamicPrintConfig config; - ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; - bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); - boost::filesystem::remove(path_dst); - if (! loaded) - throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); - if (model.objects.size() == 0) - throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any object")); - if (model.objects.size() > 1) - throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one object")); - if (model.objects.front()->volumes.size() == 0) - throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any volume")); - if (model.objects.front()->volumes.size() > 1) - throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one volume")); - meshes_repaired.emplace_back(std::move(model.objects.front()->volumes.front()->mesh())); - } - for (size_t i = 0; i < volumes.size(); ++ i) { - volumes[i]->set_mesh(std::move(meshes_repaired[i])); - volumes[i]->calculate_convex_hull(); - volumes[i]->set_new_unique_id(); - } - model_object.invalidate_bounding_box(); - -- ivolume; - on_progress(L("Model repair finished"), 100); - success = true; - finished = true; - } catch (RepairCanceledException & /* ex */) { - canceled = true; - finished = true; - on_progress(L("Model repair canceled"), 100); - } catch (std::exception &ex) { - success = false; - finished = true; - on_progress(ex.what(), 100); - } - }); - while (! finished) { - condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; }); - // decrease progress.percent value to avoid closing of the progress dialog - if (!progress_dialog.Update(progress.percent-1, msg_header + _(progress.message))) - canceled = true; - else - progress_dialog.Fit(); - progress.updated = false; - } - - if (canceled) { - // Nothing to show. - } else if (success) { - fix_result = ""; - } else { - fix_result = progress.message; - } - worker_thread.join(); - return !canceled; -} - -} // namespace Slic3r - -#endif /* HAS_WIN10SDK */ +#ifdef HAS_WIN10SDK + +#ifndef NOMINMAX +# define NOMINMAX +#endif + +// Windows Runtime +#include +// for ComPtr +#include + +// from C:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/ +#include +#include +#include + +#include "FixModelByWin10.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Format/3mf.hpp" +#include "../GUI/GUI.hpp" +#include "../GUI/I18N.hpp" +#include "../GUI/MsgDialog.hpp" + +#include +#include + +extern "C"{ + // from rapi.h + typedef HRESULT (__stdcall* FunctionRoInitialize)(int); + typedef HRESULT (__stdcall* FunctionRoUninitialize)(); + typedef HRESULT (__stdcall* FunctionRoActivateInstance)(HSTRING activatableClassId, IInspectable **instance); + typedef HRESULT (__stdcall* FunctionRoGetActivationFactory)(HSTRING activatableClassId, REFIID iid, void **factory); + // from winstring.h + typedef HRESULT (__stdcall* FunctionWindowsCreateString)(LPCWSTR sourceString, UINT32 length, HSTRING *string); + typedef HRESULT (__stdcall* FunctionWindowsDelteString)(HSTRING string); +} + +namespace Slic3r { + +HMODULE s_hRuntimeObjectLibrary = nullptr; +FunctionRoInitialize s_RoInitialize = nullptr; +FunctionRoUninitialize s_RoUninitialize = nullptr; +FunctionRoActivateInstance s_RoActivateInstance = nullptr; +FunctionRoGetActivationFactory s_RoGetActivationFactory = nullptr; +FunctionWindowsCreateString s_WindowsCreateString = nullptr; +FunctionWindowsDelteString s_WindowsDeleteString = nullptr; + +bool winrt_load_runtime_object_library() +{ + if (s_hRuntimeObjectLibrary == nullptr) + s_hRuntimeObjectLibrary = LoadLibrary(L"ComBase.dll"); + if (s_hRuntimeObjectLibrary != nullptr) { + s_RoInitialize = (FunctionRoInitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoInitialize"); + s_RoUninitialize = (FunctionRoUninitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoUninitialize"); + s_RoActivateInstance = (FunctionRoActivateInstance) GetProcAddress(s_hRuntimeObjectLibrary, "RoActivateInstance"); + s_RoGetActivationFactory = (FunctionRoGetActivationFactory) GetProcAddress(s_hRuntimeObjectLibrary, "RoGetActivationFactory"); + s_WindowsCreateString = (FunctionWindowsCreateString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsCreateString"); + s_WindowsDeleteString = (FunctionWindowsDelteString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsDeleteString"); + } + return s_RoInitialize && s_RoUninitialize && s_RoActivateInstance && s_WindowsCreateString && s_WindowsDeleteString; +} + +static HRESULT winrt_activate_instance(const std::wstring &class_name, IInspectable **pinst) +{ + HSTRING hClassName; + HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); + if (S_OK != hr) + return hr; + hr = (*s_RoActivateInstance)(hClassName, pinst); + (*s_WindowsDeleteString)(hClassName); + return hr; +} + +template +static HRESULT winrt_activate_instance(const std::wstring &class_name, TYPE **pinst) +{ + IInspectable *pinspectable = nullptr; + HRESULT hr = winrt_activate_instance(class_name, &pinspectable); + if (S_OK != hr) + return hr; + hr = pinspectable->QueryInterface(__uuidof(TYPE), (void**)pinst); + pinspectable->Release(); + return hr; +} + +static HRESULT winrt_get_activation_factory(const std::wstring &class_name, REFIID iid, void **pinst) +{ + HSTRING hClassName; + HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); + if (S_OK != hr) + return hr; + hr = (*s_RoGetActivationFactory)(hClassName, iid, pinst); + (*s_WindowsDeleteString)(hClassName); + return hr; +} + +template +static HRESULT winrt_get_activation_factory(const std::wstring &class_name, TYPE **pinst) +{ + return winrt_get_activation_factory(class_name, __uuidof(TYPE), reinterpret_cast(pinst)); +} + +// To be called often to test whether to cancel the operation. +typedef std::function ThrowOnCancelFn; + +template +static AsyncStatus winrt_async_await(const Microsoft::WRL::ComPtr &asyncAction, ThrowOnCancelFn throw_on_cancel, int blocking_tick_ms = 100) +{ + Microsoft::WRL::ComPtr asyncInfo; + asyncAction.As(&asyncInfo); + AsyncStatus status; + // Ugly blocking loop until the RepairAsync call finishes. +//FIXME replace with a callback. +// https://social.msdn.microsoft.com/Forums/en-US/a5038fb4-b7b7-4504-969d-c102faa389fb/trying-to-block-an-async-operation-and-wait-for-a-particular-time?forum=vclanguage + for (;;) { + asyncInfo->get_Status(&status); + if (status != AsyncStatus::Started) + return status; + throw_on_cancel(); + ::Sleep(blocking_tick_ms); + } +} + +static HRESULT winrt_open_file_stream( + const std::wstring &path, + ABI::Windows::Storage::FileAccessMode mode, + ABI::Windows::Storage::Streams::IRandomAccessStream **fileStream, + ThrowOnCancelFn throw_on_cancel) +{ + // Get the file factory. + Microsoft::WRL::ComPtr fileFactory; + HRESULT hr = winrt_get_activation_factory(L"Windows.Storage.StorageFile", fileFactory.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Open the file asynchronously. + HSTRING hstr_path; + hr = (*s_WindowsCreateString)(path.c_str(), path.size(), &hstr_path); + if (FAILED(hr)) return hr; + Microsoft::WRL::ComPtr> fileOpenAsync; + hr = fileFactory->GetFileFromPathAsync(hstr_path, fileOpenAsync.GetAddressOf()); + if (FAILED(hr)) return hr; + (*s_WindowsDeleteString)(hstr_path); + + // Wait until the file gets open, get the actual file. + AsyncStatus status = winrt_async_await(fileOpenAsync, throw_on_cancel); + Microsoft::WRL::ComPtr storageFile; + if (status == AsyncStatus::Completed) { + hr = fileOpenAsync->GetResults(storageFile.GetAddressOf()); + } else { + Microsoft::WRL::ComPtr asyncInfo; + hr = fileOpenAsync.As(&asyncInfo); + if (FAILED(hr)) return hr; + HRESULT err; + hr = asyncInfo->get_ErrorCode(&err); + return FAILED(hr) ? hr : err; + } + + Microsoft::WRL::ComPtr> fileStreamAsync; + hr = storageFile->OpenAsync(mode, fileStreamAsync.GetAddressOf()); + if (FAILED(hr)) return hr; + + status = winrt_async_await(fileStreamAsync, throw_on_cancel); + if (status == AsyncStatus::Completed) { + hr = fileStreamAsync->GetResults(fileStream); + } else { + Microsoft::WRL::ComPtr asyncInfo; + hr = fileStreamAsync.As(&asyncInfo); + if (FAILED(hr)) return hr; + HRESULT err; + hr = asyncInfo->get_ErrorCode(&err); + if (!FAILED(hr)) + hr = err; + } + return hr; +} + +bool is_windows10() +{ + HKEY hKey; + LONG lRes = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey); + if (lRes == ERROR_SUCCESS) { + WCHAR szBuffer[512]; + DWORD dwBufferSize = sizeof(szBuffer); + lRes = RegQueryValueExW(hKey, L"ProductName", 0, nullptr, (LPBYTE)szBuffer, &dwBufferSize); + if (lRes == ERROR_SUCCESS) + return wcsncmp(szBuffer, L"Windows 10", 10) == 0; + RegCloseKey(hKey); + } + return false; +} + +// Progress function, to be called regularly to update the progress. +typedef std::function ProgressFn; + +void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path_dst, ProgressFn on_progress, ThrowOnCancelFn throw_on_cancel) +{ + if (! is_windows10()) + throw Slic3r::RuntimeError("fix_model_by_win10_sdk called on non Windows 10 system"); + + if (! winrt_load_runtime_object_library()) + throw Slic3r::RuntimeError("Failed to initialize the WinRT library."); + + HRESULT hr = (*s_RoInitialize)(RO_INIT_MULTITHREADED); + { + on_progress(L("Exporting source model"), 20); + + Microsoft::WRL::ComPtr fileStream; + hr = winrt_open_file_stream(boost::nowide::widen(path_src), ABI::Windows::Storage::FileAccessMode::FileAccessMode_Read, fileStream.GetAddressOf(), throw_on_cancel); + + Microsoft::WRL::ComPtr printing3d3mfpackage; + hr = winrt_activate_instance(L"Windows.Graphics.Printing3D.Printing3D3MFPackage", printing3d3mfpackage.GetAddressOf()); + + Microsoft::WRL::ComPtr> modelAsync; + hr = printing3d3mfpackage->LoadModelFromPackageAsync(fileStream.Get(), modelAsync.GetAddressOf()); + + AsyncStatus status = winrt_async_await(modelAsync, throw_on_cancel); + Microsoft::WRL::ComPtr model; + if (status == AsyncStatus::Completed) + hr = modelAsync->GetResults(model.GetAddressOf()); + else + throw Slic3r::RuntimeError(L("Failed loading the input model.")); + + Microsoft::WRL::ComPtr> meshes; + hr = model->get_Meshes(meshes.GetAddressOf()); + unsigned num_meshes = 0; + hr = meshes->get_Size(&num_meshes); + + on_progress(L("Repairing model by the Netfabb service"), 40); + + Microsoft::WRL::ComPtr repairAsync; + hr = model->RepairAsync(repairAsync.GetAddressOf()); + status = winrt_async_await(repairAsync, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Mesh repair failed.")); + repairAsync->GetResults(); + + on_progress(L("Loading repaired model"), 60); + + // Verify the number of meshes returned after the repair action. + meshes.Reset(); + hr = model->get_Meshes(meshes.GetAddressOf()); + hr = meshes->get_Size(&num_meshes); + + // Save model to this class' Printing3D3MFPackage. + Microsoft::WRL::ComPtr saveToPackageAsync; + hr = printing3d3mfpackage->SaveModelToPackageAsync(model.Get(), saveToPackageAsync.GetAddressOf()); + status = winrt_async_await(saveToPackageAsync, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); + hr = saveToPackageAsync->GetResults(); + + Microsoft::WRL::ComPtr> generatorStreamAsync; + hr = printing3d3mfpackage->SaveAsync(generatorStreamAsync.GetAddressOf()); + status = winrt_async_await(generatorStreamAsync, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); + Microsoft::WRL::ComPtr generatorStream; + hr = generatorStreamAsync->GetResults(generatorStream.GetAddressOf()); + + // Go to the beginning of the stream. + generatorStream->Seek(0); + Microsoft::WRL::ComPtr inputStream; + hr = generatorStream.As(&inputStream); + + // Get the buffer factory. + Microsoft::WRL::ComPtr bufferFactory; + hr = winrt_get_activation_factory(L"Windows.Storage.Streams.Buffer", bufferFactory.GetAddressOf()); + + // Open the destination file. + FILE *fout = boost::nowide::fopen(path_dst.c_str(), "wb"); + try { + Microsoft::WRL::ComPtr buffer; + byte *buffer_ptr; + bufferFactory->Create(65536 * 2048, buffer.GetAddressOf()); + { + Microsoft::WRL::ComPtr bufferByteAccess; + buffer.As(&bufferByteAccess); + hr = bufferByteAccess->Buffer(&buffer_ptr); + } + uint32_t length; + hr = buffer->get_Length(&length); + Microsoft::WRL::ComPtr> asyncRead; + for (;;) { + hr = inputStream->ReadAsync(buffer.Get(), 65536 * 2048, ABI::Windows::Storage::Streams::InputStreamOptions_ReadAhead, asyncRead.GetAddressOf()); + status = winrt_async_await(asyncRead, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); + hr = buffer->get_Length(&length); + if (length == 0) + break; + fwrite(buffer_ptr, length, 1, fout); + } + } catch (...) { + fclose(fout); + throw; + } + fclose(fout); + // Here all the COM objects will be released through the ComPtr destructors. + } + (*s_RoUninitialize)(); +} + +class RepairCanceledException : public std::exception { +public: + const char* what() const throw() { return "Model repair has been canceled"; } +}; + +// returt FALSE, if fixing was canceled +// fix_result is empty, if fixing finished successfully +// fix_result containes a message if fixing failed +bool fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx, wxProgressDialog& progress_dialog, const wxString& msg_header, std::string& fix_result) +{ + std::mutex mutex; + std::condition_variable condition; + std::unique_lock lock(mutex); + struct Progress { + std::string message; + int percent = 0; + bool updated = false; + } progress; + std::atomic canceled = false; + std::atomic finished = false; + + std::vector volumes; + if (volume_idx == -1) + volumes = model_object.volumes; + else + volumes.emplace_back(model_object.volumes[volume_idx]); + + // Executing the calculation in a background thread, so that the COM context could be created with its own threading model. + // (It seems like wxWidgets initialize the COM contex as single threaded and we need a multi-threaded context). + bool success = false; + size_t ivolume = 0; + auto on_progress = [&mutex, &condition, &ivolume, &volumes, &progress](const char *msg, unsigned prcnt) { + std::lock_guard lk(mutex); + progress.message = msg; + progress.percent = (int)floor((float(prcnt) + float(ivolume) * 100.f) / float(volumes.size())); + progress.updated = true; + condition.notify_all(); + }; + auto worker_thread = boost::thread([&model_object, &volumes, &ivolume, on_progress, &success, &canceled, &finished]() { + try { + std::vector meshes_repaired; + meshes_repaired.reserve(volumes.size()); + for (; ivolume < volumes.size(); ++ ivolume) { + on_progress(L("Exporting source model"), 0); + boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + path_src += ".3mf"; + Model model; + ModelObject *mo = model.add_object(); + mo->add_volume(*volumes[ivolume]); + + // We are about to save a 3mf, fix it by netfabb and load the fixed 3mf back. + // store_3mf currently bakes the volume transformation into the mesh itself. + // If we then loaded the repaired 3mf and pushed the mesh into the original ModelVolume + // (which remembers the matrix the whole time), the transformation would be used twice. + // We will therefore set the volume transform on the dummy ModelVolume to identity. + mo->volumes.back()->set_transformation(Geometry::Transformation()); + + mo->add_instance(); + if (!Slic3r::store_3mf(path_src.string().c_str(), &model, nullptr, false, nullptr, false)) { + boost::filesystem::remove(path_src); + throw Slic3r::RuntimeError(L("Export of a temporary 3mf file failed")); + } + model.clear_objects(); + model.clear_materials(); + boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + path_dst += ".3mf"; + fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress, + [&canceled]() { if (canceled) throw RepairCanceledException(); }); + boost::filesystem::remove(path_src); + // PresetBundle bundle; + on_progress(L("Loading repaired model"), 80); + DynamicPrintConfig config; + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; + bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); + boost::filesystem::remove(path_dst); + if (! loaded) + throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); + if (model.objects.size() == 0) + throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any object")); + if (model.objects.size() > 1) + throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one object")); + if (model.objects.front()->volumes.size() == 0) + throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any volume")); + if (model.objects.front()->volumes.size() > 1) + throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one volume")); + meshes_repaired.emplace_back(std::move(model.objects.front()->volumes.front()->mesh())); + } + for (size_t i = 0; i < volumes.size(); ++ i) { + volumes[i]->set_mesh(std::move(meshes_repaired[i])); + volumes[i]->calculate_convex_hull(); + volumes[i]->set_new_unique_id(); + } + model_object.invalidate_bounding_box(); + -- ivolume; + on_progress(L("Model repair finished"), 100); + success = true; + finished = true; + } catch (RepairCanceledException & /* ex */) { + canceled = true; + finished = true; + on_progress(L("Model repair canceled"), 100); + } catch (std::exception &ex) { + success = false; + finished = true; + on_progress(ex.what(), 100); + } + }); + while (! finished) { + condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; }); + // decrease progress.percent value to avoid closing of the progress dialog + if (!progress_dialog.Update(progress.percent-1, msg_header + _(progress.message))) + canceled = true; + else + progress_dialog.Fit(); + progress.updated = false; + } + + if (canceled) { + // Nothing to show. + } else if (success) { + fix_result = ""; + } else { + fix_result = progress.message; + } + worker_thread.join(); + return !canceled; +} + +} // namespace Slic3r + +#endif /* HAS_WIN10SDK */