diff --git a/.gitignore b/.gitignore index c4df3f3f8..704289a22 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ local-lib build-linux/* deps/build-linux/* **/.DS_Store -/.idea/ +**/.idea/ diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 85bcd69ba..e744d72ca 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,7 +1,7 @@ #include -#include "PresetBundle.hpp" #include "libslic3r.h" +#include "PresetBundle.hpp" #include "Utils.hpp" #include "Model.hpp" #include "format.hpp" @@ -453,6 +453,11 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset:: presets.get_edited_preset().config.apply_only(presets.get_selected_preset().config, unselected_options); } +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + if (type == Preset::TYPE_PRINTER) + copy_bed_model_and_texture_if_needed(presets.get_edited_preset().config); +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini presets.save_current_preset(new_name); // Mark the print & filament enabled if they are compatible with the currently selected preset. @@ -1860,4 +1865,32 @@ void PresetBundle::set_default_suppressed(bool default_suppressed) printers.set_default_suppressed(default_suppressed); } +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE +void copy_bed_model_and_texture_if_needed(DynamicPrintConfig& config) +{ + const boost::filesystem::path user_dir = boost::filesystem::absolute(boost::filesystem::path(data_dir()) / "printer").make_preferred(); + const boost::filesystem::path res_dir = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + auto do_copy = [&user_dir, &res_dir](ConfigOptionString* cfg, const std::string& type) { + if (cfg == nullptr || cfg->value.empty()) + return; + + const boost::filesystem::path src_dir = boost::filesystem::absolute(boost::filesystem::path(cfg->value)).make_preferred().parent_path(); + if (src_dir != user_dir && src_dir.parent_path() != res_dir) { + const std::string dst_value = (user_dir / boost::filesystem::path(cfg->value).filename()).string(); + std::string error; + if (copy_file_inner(cfg->value, dst_value, error) == SUCCESS) + cfg->value = dst_value; + else { + BOOST_LOG_TRIVIAL(error) << "Copying from " << cfg->value << " to " << dst_value << " failed. Unable to set custom bed " << type << ". [" << error << "]"; + cfg->value = ""; + } + } + }; + + do_copy(config.option("bed_custom_texture"), "texture"); + do_copy(config.option("bed_custom_model"), "model"); +} +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 2a5ce6839..b36f6ed6e 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -178,6 +178,12 @@ private: ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute) +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE +// Copies bed texture and model files to 'data_dir()\printer' folder, if needed +// and updates the config accordingly +extern void copy_bed_model_and_texture_if_needed(DynamicPrintConfig& config); +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + } // namespace Slic3r #endif /* slic3r_PresetBundle_hpp_ */ diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 847e638e6..16cd28130 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -288,7 +288,7 @@ template struct RotfinderBoilerplate { static constexpr unsigned MAX_TRIES = MAX_ITER; - int status = 0; + int status = 0, prev_status = 0; TriangleMesh mesh; unsigned max_tries; const RotOptimizeParams ¶ms; @@ -314,13 +314,20 @@ struct RotfinderBoilerplate { RotfinderBoilerplate(const ModelObject &mo, const RotOptimizeParams &p) : mesh{get_mesh_to_rotate(mo)} - , params{p} , max_tries(p.accuracy() * MAX_TRIES) - { + , params{p} + {} + void statusfn() { + int s = status * 100 / max_tries; + if (s != prev_status) { + params.statuscb()(s); + prev_status = s; + } + + ++status; } - void statusfn() { params.statuscb()(++status * 100.0 / max_tries); } bool stopcond() { return ! params.statuscb()(-1); } }; diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 587edabd4..9bdbfb1a5 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -91,8 +91,7 @@ #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) - +// Enable copy of custom bed model and texture +#define ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE (1 && ENABLE_2_5_0_ALPHA1) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 3ef668a5d..36f044e22 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -180,6 +180,7 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/Worker.hpp GUI/Jobs/BoostThreadWorker.hpp GUI/Jobs/BoostThreadWorker.cpp + GUI/Jobs/UIThreadWorker.hpp GUI/Jobs/BusyCursorJob.hpp GUI/Jobs/PlaterWorker.hpp GUI/Jobs/ArrangeJob.hpp @@ -268,6 +269,8 @@ set(SLIC3R_GUI_SOURCES Utils/TCPConsole.hpp Utils/MKS.cpp Utils/MKS.hpp + Utils/WinRegistry.cpp + Utils/WinRegistry.hpp Utils/WxFontUtils.cpp Utils/WxFontUtils.hpp ) diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index b9dcc1a4f..080de997e 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1934,10 +1934,7 @@ void ConfigWizard::priv::load_pages() 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); + index->add_page(page_files_association); #endif // _WIN32 index->add_page(page_mode); @@ -2750,32 +2747,20 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese 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"); + 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 + 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 { - 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 - +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } #endif // _WIN32 page_mode->serialize_mode(app_config); @@ -2795,6 +2780,10 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese page_diams->apply_custom_config(*custom_config); page_temps->apply_custom_config(*custom_config); +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + copy_bed_model_and_texture_if_needed(*custom_config); +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + const std::string profile_name = page_custom->profile_name(); preset_bundle->load_config_from_wizard(profile_name, *custom_config); } @@ -2935,11 +2924,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) 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)); + 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)); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 2134a795f..1e28d1287 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -991,11 +991,13 @@ void GCodeViewer::render() render_toolpaths(); render_shells(); float legend_height = 0.0f; - render_legend(legend_height); - if (m_sequential_view.current.last != m_sequential_view.endpoints.last) { - m_sequential_view.marker.set_world_position(m_sequential_view.current_position); - m_sequential_view.marker.set_world_offset(m_sequential_view.current_offset); - m_sequential_view.render(legend_height); + if (!m_layers.empty()) { + render_legend(legend_height); + if (m_sequential_view.current.last != m_sequential_view.endpoints.last) { + m_sequential_view.marker.set_world_position(m_sequential_view.current_position); + m_sequential_view.marker.set_world_offset(m_sequential_view.current_offset); + m_sequential_view.render(legend_height); + } } #if ENABLE_GCODE_VIEWER_STATISTICS render_statistics(); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index dd1f74e24..b94b4ac30 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -59,6 +59,7 @@ #include "../Utils/Process.hpp" #include "../Utils/MacDarkMode.hpp" #include "../Utils/AppUpdater.hpp" +#include "../Utils/WinRegistry.hpp" #include "slic3r/Config/Snapshot.hpp" #include "ConfigSnapshotDialog.hpp" #include "FirmwareDialog.hpp" @@ -1204,17 +1205,10 @@ bool GUI_App::on_init_inner() 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 + 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(); @@ -1252,15 +1246,8 @@ bool GUI_App::on_init_inner() } 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 + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); #endif // __WXMSW__ } @@ -2433,23 +2420,16 @@ void GUI_App::open_preferences(const std::string& highlight_option /*= std::stri 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 + 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 // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER #endif // _WIN32 if (mainframe->preferences_dialog->settings_layout_changed()) { @@ -3157,119 +3137,22 @@ bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* pa #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); + associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); } 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); + associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); } 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); + associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); } #endif // __WXMSW__ - void GUI_App::on_version_read(wxCommandEvent& evt) { app_config->set("version_online", into_u8(evt.GetString())); diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 9f62fa0b8..88bd42a66 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -983,10 +983,11 @@ void Preview::load_print_as_fff(bool keep_z_range) if (gcode_preview_data_valid) { // Load the real G-code preview. m_canvas->load_gcode_preview(*m_gcode_result, colors); - m_left_sizer->Show(m_bottom_toolbar_panel); m_left_sizer->Layout(); Refresh(); zs = m_canvas->get_gcode_layers_zs(); + if (!zs.empty()) + m_left_sizer->Show(m_bottom_toolbar_panel); m_loaded = true; } else if (wxGetApp().is_editor()) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 61608c6ad..93726f8c7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -840,7 +840,9 @@ GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_fil GLGizmoRotate(parent, GLGizmoRotate::X), GLGizmoRotate(parent, GLGizmoRotate::Y), GLGizmoRotate(parent, GLGizmoRotate::Z) }) -{} +{ + load_rotoptimize_state(); +} bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) { diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp index 573590272..58bd1ec32 100644 --- a/src/slic3r/GUI/Jobs/PlaterWorker.hpp +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -40,6 +40,12 @@ class PlaterWorker: public Worker { { wxWakeUpIdle(); ctl.update_status(st, msg); + + // If the worker is not using additional threads, the UI + // is refreshed with this call. If the worker is running + // in it's own thread, the yield should not have any + // visible effects. + wxYieldIfNeeded(); } bool was_canceled() const override { return ctl.was_canceled(); } diff --git a/src/slic3r/GUI/Jobs/UIThreadWorker.hpp b/src/slic3r/GUI/Jobs/UIThreadWorker.hpp new file mode 100644 index 000000000..610d205cf --- /dev/null +++ b/src/slic3r/GUI/Jobs/UIThreadWorker.hpp @@ -0,0 +1,125 @@ +#ifndef UITHREADWORKER_HPP +#define UITHREADWORKER_HPP + +#include +#include + +#include "Worker.hpp" +#include "ProgressIndicator.hpp" + +namespace Slic3r { namespace GUI { + +// Implementation of a worker which does not create any additional threads. +class UIThreadWorker : public Worker, private Job::Ctl { + std::queue, std::deque>> m_jobqueue; + std::shared_ptr m_progress; + bool m_running = false; + bool m_canceled = false; + + void process_front() + { + std::unique_ptr job; + + if (!m_jobqueue.empty()) { + job = std::move(m_jobqueue.front()); + m_jobqueue.pop(); + } + + if (job) { + std::exception_ptr eptr; + m_running = true; + + try { + job->process(*this); + } catch (...) { + eptr= std::current_exception(); + } + + job->finalize(m_canceled, eptr); + + // Unhandled exceptions are rethrown without mercy. + if (eptr) + std::rethrow_exception(eptr); + + m_running = false; + + m_canceled = false; + } + } + +protected: + // Implement Job::Ctl interface: + + void update_status(int st, const std::string &msg = "") override + { + if (m_progress) { + m_progress->set_progress(st); + m_progress->set_status_text(msg.c_str()); + } + } + + bool was_canceled() const override { return m_canceled; } + + std::future call_on_main_thread(std::function fn) override + { + return std::async(std::launch::deferred, [fn]{ fn(); }); + } + +public: + explicit UIThreadWorker(std::shared_ptr pri, + const std::string & /*name*/ = "") + : m_progress{pri} + { + if (m_progress) + m_progress->set_cancel_callback([this]() { cancel(); }); + } + + UIThreadWorker() = default; + + bool push(std::unique_ptr job) override + { + m_canceled = false; + + if (job) + m_jobqueue.push(std::move(job)); + + return bool(job); + } + + bool is_idle() const override { return !m_running && m_jobqueue.empty(); } + + void cancel() override { m_canceled = true; } + + void cancel_all() override + { + m_canceled = true; + process_front(); + + while (!m_jobqueue.empty()) + m_jobqueue.pop(); + } + + void process_events() override { + while (!m_jobqueue.empty()) + process_front(); + } + + bool wait_for_current_job(unsigned /*timeout_ms*/ = 0) override { + process_front(); + + return true; + } + + bool wait_for_idle(unsigned /*timeout_ms*/ = 0) override { + process_events(); + + return true; + } + + ProgressIndicator * get_pri() { return m_progress.get(); } + const ProgressIndicator * get_pri() const { return m_progress.get(); } +}; + +}} // namespace Slic3r::GUI + +#endif // UITHREADWORKER_HPP diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index be91d298d..1718c581f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1656,6 +1656,9 @@ struct Plater::priv // objects would be frozen for the user. In case of arrange, an animation // could be shown, or with the optimize orientations, partial results // could be displayed. + // + // UIThreadWorker can be used as a replacement for BoostThreadWorker if + // no additional worker threads are desired (useful for debugging or profiling) PlaterWorker m_worker; SLAImportDialog * m_sla_import_dlg; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 3da843f89..a4317d8f4 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -79,9 +79,11 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin 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()); + if (wxGetApp().is_editor()) { + // 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(); } @@ -230,23 +232,16 @@ void PreferencesDialog::build() 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"); + // 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 + 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(); diff --git a/src/slic3r/Utils/WinRegistry.cpp b/src/slic3r/Utils/WinRegistry.cpp new file mode 100644 index 000000000..068a7fff1 --- /dev/null +++ b/src/slic3r/Utils/WinRegistry.cpp @@ -0,0 +1,490 @@ +#include "libslic3r/Technologies.hpp" +#include "WinRegistry.hpp" + +#ifdef _WIN32 +#include +#include +#include +#include + +namespace Slic3r { + +// Helper class which automatically closes the handle when +// going out of scope +class AutoHandle +{ + HANDLE m_handle{ nullptr }; + +public: + explicit AutoHandle(HANDLE handle) : m_handle(handle) {} + ~AutoHandle() { if (m_handle != nullptr) ::CloseHandle(m_handle); } + HANDLE get() { return m_handle; } +}; + +// Helper class which automatically closes the key when +// going out of scope +class AutoRegKey +{ + HKEY m_key{ nullptr }; + +public: + explicit AutoRegKey(HKEY key) : m_key(key) {} + ~AutoRegKey() { if (m_key != nullptr) ::RegCloseKey(m_key); } + HKEY get() { return m_key; } +}; + +// returns true if the given value is set/modified into Windows registry +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); + + LSTATUS iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); + + const 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; +} + +static std::wstring get_current_user_string_sid() +{ + HANDLE rawProcessToken; + if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, + &rawProcessToken)) + return L""; + + AutoHandle processToken(rawProcessToken); + + DWORD userSize = 0; + if (!(!::GetTokenInformation(processToken.get(), TokenUser, nullptr, 0, &userSize) && + GetLastError() == ERROR_INSUFFICIENT_BUFFER)) + return L""; + + std::vector userBytes(userSize); + if (!::GetTokenInformation(processToken.get(), TokenUser, userBytes.data(), userSize, &userSize)) + return L""; + + wchar_t* rawSid = nullptr; + if (!::ConvertSidToStringSidW(reinterpret_cast(userBytes.data())->User.Sid, &rawSid)) + return L""; + + return rawSid; +} + +/* + * Create the string which becomes the input to the UserChoice hash. + * + * @see generate_user_choice_hash() for parameters. + * + * @return The formatted string, empty string on failure. + * + * NOTE: This uses the format as of Windows 10 20H2 (latest as of this writing), + * used at least since 1803. + * There was at least one older version, not currently supported: On Win10 RTM + * (build 10240, aka 1507) the hash function is the same, but the timestamp and + * User Experience string aren't included; instead (for protocols) the string + * ends with the exe path. The changelog of SetUserFTA suggests the algorithm + * changed in 1703, so there may be two versions: before 1703, and 1703 to now. + */ +static std::wstring format_user_choice_string(const wchar_t* aExt, const wchar_t* aUserSid, const wchar_t* aProgId, SYSTEMTIME aTimestamp) +{ + aTimestamp.wSecond = 0; + aTimestamp.wMilliseconds = 0; + + FILETIME fileTime = { 0 }; + if (!::SystemTimeToFileTime(&aTimestamp, &fileTime)) + return L""; + + // This string is built into Windows as part of the UserChoice hash algorithm. + // It might vary across Windows SKUs (e.g. Windows 10 vs. Windows Server), or + // across builds of the same SKU, but this is the only currently known + // version. There isn't any known way of deriving it, so we assume this + // constant value. If we are wrong, we will not be able to generate correct + // UserChoice hashes. + const wchar_t* userExperience = + L"User Choice set via Windows User Experience " + L"{D18B6DD5-6124-4341-9318-804003BAFA0B}"; + + const wchar_t* userChoiceFmt = + L"%s%s%s" + L"%08lx" + L"%08lx" + L"%s"; + + int userChoiceLen = _scwprintf(userChoiceFmt, aExt, aUserSid, aProgId, + fileTime.dwHighDateTime, fileTime.dwLowDateTime, userExperience); + userChoiceLen += 1; // _scwprintf does not include the terminator + + std::wstring userChoice(userChoiceLen, L'\0'); + _snwprintf_s(userChoice.data(), userChoiceLen, _TRUNCATE, userChoiceFmt, aExt, + aUserSid, aProgId, fileTime.dwHighDateTime, fileTime.dwLowDateTime, userExperience); + + ::CharLowerW(userChoice.data()); + return userChoice; +} + +// @return The MD5 hash of the input, nullptr on failure. +static std::vector cng_md5(const unsigned char* bytes, ULONG bytesLen) { + constexpr ULONG MD5_BYTES = 16; + constexpr ULONG MD5_DWORDS = MD5_BYTES / sizeof(DWORD); + std::vector hash; + + BCRYPT_ALG_HANDLE hAlg = nullptr; + if (NT_SUCCESS(::BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, nullptr, 0))) { + BCRYPT_HASH_HANDLE hHash = nullptr; + // As of Windows 7 the hash handle will manage its own object buffer when + // pbHashObject is nullptr and cbHashObject is 0. + if (NT_SUCCESS(::BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0))) { + // BCryptHashData promises not to modify pbInput. + if (NT_SUCCESS(::BCryptHashData(hHash, const_cast(bytes), bytesLen, 0))) { + hash.resize(MD5_DWORDS); + if (!NT_SUCCESS(::BCryptFinishHash(hHash, reinterpret_cast(hash.data()), + MD5_DWORDS * sizeof(DWORD), 0))) + hash.clear(); + } + ::BCryptDestroyHash(hHash); + } + ::BCryptCloseAlgorithmProvider(hAlg, 0); + } + + return hash; +} + +static inline DWORD word_swap(DWORD v) +{ + return (v >> 16) | (v << 16); +} + +// @return The input bytes encoded as base64, nullptr on failure. +static std::wstring crypto_api_base64_encode(const unsigned char* bytes, DWORD bytesLen) { + DWORD base64Len = 0; + if (!::CryptBinaryToStringW(bytes, bytesLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Len)) + return L""; + + std::wstring base64(base64Len, L'\0'); + if (!::CryptBinaryToStringW(bytes, bytesLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, base64.data(), &base64Len)) + return L""; + + return base64; +} + +/* + * Generate the UserChoice Hash. + * + * This implementation is based on the references listed above. + * It is organized to show the logic as clearly as possible, but at some + * point the reasoning is just "this is how it works". + * + * @param inputString A null-terminated string to hash. + * + * @return The base64-encoded hash, or empty string on failure. + */ +static std::wstring hash_string(const wchar_t* inputString) +{ + auto inputBytes = reinterpret_cast(inputString); + int inputByteCount = (::lstrlenW(inputString) + 1) * sizeof(wchar_t); + + constexpr size_t DWORDS_PER_BLOCK = 2; + constexpr size_t BLOCK_SIZE = sizeof(DWORD) * DWORDS_PER_BLOCK; + // Incomplete blocks are ignored. + int blockCount = inputByteCount / BLOCK_SIZE; + + if (blockCount == 0) + return L""; + + // Compute an MD5 hash. md5[0] and md5[1] will be used as constant multipliers + // in the scramble below. + auto md5 = cng_md5(inputBytes, inputByteCount); + if (md5.empty()) + return L""; + + // The following loop effectively computes two checksums, scrambled like a + // hash after every DWORD is added. + + // Constant multipliers for the scramble, one set for each DWORD in a block. + const DWORD C0s[DWORDS_PER_BLOCK][5] = { + {md5[0] | 1, 0xCF98B111uL, 0x87085B9FuL, 0x12CEB96DuL, 0x257E1D83uL}, + {md5[1] | 1, 0xA27416F5uL, 0xD38396FFuL, 0x7C932B89uL, 0xBFA49F69uL} }; + const DWORD C1s[DWORDS_PER_BLOCK][5] = { + {md5[0] | 1, 0xEF0569FBuL, 0x689B6B9FuL, 0x79F8A395uL, 0xC3EFEA97uL}, + {md5[1] | 1, 0xC31713DBuL, 0xDDCD1F0FuL, 0x59C3AF2DuL, 0x35BD1EC9uL} }; + + // The checksums. + DWORD h0 = 0; + DWORD h1 = 0; + // Accumulated total of the checksum after each DWORD. + DWORD h0Acc = 0; + DWORD h1Acc = 0; + + for (int i = 0; i < blockCount; ++i) { + for (int j = 0; j < DWORDS_PER_BLOCK; ++j) { + const DWORD* C0 = C0s[j]; + const DWORD* C1 = C1s[j]; + + DWORD input; + memcpy(&input, &inputBytes[(i * DWORDS_PER_BLOCK + j) * sizeof(DWORD)], sizeof(DWORD)); + + h0 += input; + // Scramble 0 + h0 *= C0[0]; + h0 = word_swap(h0) * C0[1]; + h0 = word_swap(h0) * C0[2]; + h0 = word_swap(h0) * C0[3]; + h0 = word_swap(h0) * C0[4]; + h0Acc += h0; + + h1 += input; + // Scramble 1 + h1 = word_swap(h1) * C1[1] + h1 * C1[0]; + h1 = (h1 >> 16) * C1[2] + h1 * C1[3]; + h1 = word_swap(h1) * C1[4] + h1; + h1Acc += h1; + } + } + + DWORD hash[2] = { h0 ^ h1, h0Acc ^ h1Acc }; + return crypto_api_base64_encode(reinterpret_cast(hash), sizeof(hash)); +} + +static std::wstring generate_user_choice_hash(const wchar_t* aExt, const wchar_t* aUserSid, const wchar_t* aProgId, SYSTEMTIME aTimestamp) +{ + const std::wstring userChoice = format_user_choice_string(aExt, aUserSid, aProgId, aTimestamp); + if (userChoice.empty()) + return L""; + + return hash_string(userChoice.c_str()); +} + +static bool add_milliseconds_to_system_time(SYSTEMTIME& aSystemTime, ULONGLONG aIncrementMS) +{ + FILETIME fileTime; + ULARGE_INTEGER fileTimeInt; + if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) + return false; + + fileTimeInt.LowPart = fileTime.dwLowDateTime; + fileTimeInt.HighPart = fileTime.dwHighDateTime; + + // FILETIME is in units of 100ns. + fileTimeInt.QuadPart += aIncrementMS * 1000 * 10; + + fileTime.dwLowDateTime = fileTimeInt.LowPart; + fileTime.dwHighDateTime = fileTimeInt.HighPart; + SYSTEMTIME tmpSystemTime; + if (!::FileTimeToSystemTime(&fileTime, &tmpSystemTime)) + return false; + + aSystemTime = tmpSystemTime; + return true; +} + +// Compare two SYSTEMTIMEs as FILETIME after clearing everything +// below minutes. +static bool check_equal_minutes(SYSTEMTIME aSystemTime1, SYSTEMTIME aSystemTime2) +{ + aSystemTime1.wSecond = 0; + aSystemTime1.wMilliseconds = 0; + + aSystemTime2.wSecond = 0; + aSystemTime2.wMilliseconds = 0; + + FILETIME fileTime1; + FILETIME fileTime2; + if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) || !::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) + return false; + + return (fileTime1.dwLowDateTime == fileTime2.dwLowDateTime) && (fileTime1.dwHighDateTime == fileTime2.dwHighDateTime); +} + +static std::wstring get_association_key_path(const wchar_t* aExt) +{ + const wchar_t* keyPathFmt; + if (aExt[0] == L'.') + keyPathFmt = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\%s"; + else + keyPathFmt = L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\%s"; + + int keyPathLen = _scwprintf(keyPathFmt, aExt); + keyPathLen += 1; // _scwprintf does not include the terminator + + std::wstring keyPath(keyPathLen, '\0'); + _snwprintf_s(keyPath.data(), keyPathLen, _TRUNCATE, keyPathFmt, aExt); + return keyPath; +} + +/* + * Set an association with a UserChoice key + * + * Removes the old key, creates a new one with ProgID and Hash set to + * enable a new asociation. + * + * @param aExt File type or protocol to associate + * @param aProgID ProgID to use for the asociation + * + * @return true if successful, false on error. + */ +static bool set_user_choice(const wchar_t* aExt, const wchar_t* aProgID) { + + const std::wstring aSid = get_current_user_string_sid(); + if (aSid.empty()) + return false; + + SYSTEMTIME hashTimestamp; + ::GetSystemTime(&hashTimestamp); + std::wstring hash = generate_user_choice_hash(aExt, aSid.c_str(), aProgID, hashTimestamp); + if (hash.empty()) + return false; + + // The hash changes at the end of each minute, so check that the hash should + // be the same by the time we're done writing. + const ULONGLONG kWriteTimingThresholdMilliseconds = 100; + // Generating the hash could have taken some time, so start from now. + SYSTEMTIME writeEndTimestamp; + ::GetSystemTime(&writeEndTimestamp); + if (!add_milliseconds_to_system_time(writeEndTimestamp, +kWriteTimingThresholdMilliseconds)) + return false; + + if (!check_equal_minutes(hashTimestamp, writeEndTimestamp)) { + ::Sleep(kWriteTimingThresholdMilliseconds * 2); + + // For consistency, use the current time. + ::GetSystemTime(&hashTimestamp); + hash = generate_user_choice_hash(aExt, aSid.c_str(), aProgID, hashTimestamp); + if (hash.empty()) + return false; + } + + const std::wstring assocKeyPath = get_association_key_path(aExt); + if (assocKeyPath.empty()) + return false; + + LSTATUS ls; + HKEY rawAssocKey; + ls = ::RegOpenKeyExW(HKEY_CURRENT_USER, assocKeyPath.data(), 0, KEY_READ | KEY_WRITE, &rawAssocKey); + if (ls != ERROR_SUCCESS) + return false; + + AutoRegKey assocKey(rawAssocKey); + + HKEY currUserChoiceKey; + ls = ::RegOpenKeyExW(assocKey.get(), L"UserChoice", 0, KEY_READ, &currUserChoiceKey); + if (ls == ERROR_SUCCESS) { + ::RegCloseKey(currUserChoiceKey); + // When Windows creates this key, it is read-only (Deny Set Value), so we need + // to delete it first. + // We don't set any similar special permissions. + ls = ::RegDeleteKeyW(assocKey.get(), L"UserChoice"); + if (ls != ERROR_SUCCESS) + return false; + } + + HKEY rawUserChoiceKey; + ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr, + 0 /* options */, KEY_READ | KEY_WRITE, + 0 /* security attributes */, &rawUserChoiceKey, + nullptr); + if (ls != ERROR_SUCCESS) + return false; + + AutoRegKey userChoiceKey(rawUserChoiceKey); + DWORD progIdByteCount = (::lstrlenW(aProgID) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"ProgID", 0, REG_SZ, reinterpret_cast(aProgID), progIdByteCount); + if (ls != ERROR_SUCCESS) + return false; + + DWORD hashByteCount = (::lstrlenW(hash.data()) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ, reinterpret_cast(hash.data()), hashByteCount); + if (ls != ERROR_SUCCESS) + return false; + + return true; +} + +static bool set_as_default_per_file_type(const std::wstring& extension, const std::wstring& prog_id) +{ + const std::wstring reg_extension = get_association_key_path(extension.c_str()); + if (reg_extension.empty()) + return false; + + bool needs_update = true; + bool modified = false; + HKEY rawAssocKey = nullptr; + LSTATUS res = ::RegOpenKeyExW(HKEY_CURRENT_USER, reg_extension.c_str(), 0, KEY_READ, &rawAssocKey); + AutoRegKey assoc_key(rawAssocKey); + if (res == ERROR_SUCCESS) { + DWORD data_size_bytes = 0; + res = ::RegGetValueW(assoc_key.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ, nullptr, nullptr, &data_size_bytes); + if (res == ERROR_SUCCESS) { + // +1 in case dataSizeBytes was odd, +1 to ensure termination + DWORD data_size_chars = (data_size_bytes / sizeof(wchar_t)) + 2; + std::wstring curr_prog_id(data_size_chars, L'\0'); + res = ::RegGetValueW(assoc_key.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ, nullptr, curr_prog_id.data(), &data_size_bytes); + if (res == ERROR_SUCCESS) { + const std::wstring::size_type pos = curr_prog_id.find_first_of(L'\0'); + if (pos != std::wstring::npos) + curr_prog_id = curr_prog_id.substr(0, pos); + needs_update = !boost::algorithm::iequals(curr_prog_id, prog_id); + } + } + } + + if (needs_update) + modified = set_user_choice(extension.c_str(), prog_id.c_str()); + + return modified; +} + +void associate_file_type(const std::wstring& extension, const std::wstring& prog_id, const std::wstring& prog_desc, bool set_as_default) +{ + assert(!extension.empty() && extension.front() == L'.'); + + const std::wstring reg_extension = L"SOFTWARE\\Classes\\" + extension; + const std::wstring reg_prog_id = L"SOFTWARE\\Classes\\" + prog_id; + const std::wstring reg_prog_id_command = L"SOFTWARE\\Classes\\" + prog_id + +L"\\Shell\\Open\\Command"; + + wchar_t app_path[1040]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + const std::wstring prog_command = L"\"" + std::wstring(app_path) + L"\"" + L" \"%1\""; + + bool modified = false; + modified |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + modified |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + modified |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + if (set_as_default) + modified |= set_as_default_per_file_type(extension, prog_id); + + // notify Windows only when any of the values gets changed + if (modified) + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} + +} // namespace Slic3r + +#endif // _WIN32 diff --git a/src/slic3r/Utils/WinRegistry.hpp b/src/slic3r/Utils/WinRegistry.hpp new file mode 100644 index 000000000..bc0875d32 --- /dev/null +++ b/src/slic3r/Utils/WinRegistry.hpp @@ -0,0 +1,23 @@ +#ifndef slic3r_Utils_WinRegistry_hpp_ +#define slic3r_Utils_WinRegistry_hpp_ + +#ifdef _WIN32 + +#include + +namespace Slic3r { + +// Creates a Windows registry key for the files with the given 'extension' and associates them to the application 'prog_id'. +// If 'set_as_default' is true, the application 'prog_id' is set ad default application for the file type 'extension'. +// The file type registration implementation is based on code taken from: +// https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association +// The set as default implementation is based on code taken from: +// https://hg.mozilla.org/mozilla-central/rev/e928b3e95a6c3b7257d0ba475fc2303bfbad1874 +// https://hg.mozilla.org/releases/mozilla-release/diff/7e775ce432b599c6daf7ac379aa42f1e9b3b33ed/browser/components/shell/WindowsUserChoice.cpp +void associate_file_type(const std::wstring& extension, const std::wstring& prog_id, const std::wstring& prog_desc, bool set_as_default); + +} // namespace Slic3r + +#endif // _WIN32 + +#endif // slic3r_Utils_WinRegistry_hpp_ diff --git a/tests/slic3rutils/slic3r_jobs_tests.cpp b/tests/slic3rutils/slic3r_jobs_tests.cpp index b68a72b86..b0d01d2ed 100644 --- a/tests/slic3rutils/slic3r_jobs_tests.cpp +++ b/tests/slic3rutils/slic3r_jobs_tests.cpp @@ -4,10 +4,9 @@ #include #include "slic3r/GUI/Jobs/BoostThreadWorker.hpp" +#include "slic3r/GUI/Jobs/UIThreadWorker.hpp" #include "slic3r/GUI/Jobs/ProgressIndicator.hpp" -//#include - struct Progress: Slic3r::ProgressIndicator { int range = 100; int pr = 0; @@ -19,17 +18,30 @@ struct Progress: Slic3r::ProgressIndicator { int get_range() const override { return range; } }; -TEST_CASE("nullptr job should be ignored", "[Jobs]") { - Slic3r::GUI::BoostThreadWorker worker{std::make_unique()}; +using TestClasses = std::tuple< Slic3r::GUI::UIThreadWorker, Slic3r::GUI::BoostThreadWorker >; + +TEMPLATE_LIST_TEST_CASE("Empty worker should not do anything", "[Jobs]", TestClasses) { + TestType worker{std::make_unique()}; + + REQUIRE(worker.is_idle()); + + worker.wait_for_current_job(); + worker.process_events(); + + REQUIRE(worker.is_idle()); +} + +TEMPLATE_LIST_TEST_CASE("nullptr job should be ignored", "[Jobs]", TestClasses) { + TestType worker{std::make_unique()}; worker.push(nullptr); REQUIRE(worker.is_idle()); } -TEST_CASE("State should not be idle while running a job", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("State should not be idle while running a job", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; - BoostThreadWorker worker{std::make_unique(), "worker_thread"}; + TestType worker{std::make_unique(), "worker_thread"}; queue_job(worker, [&worker](Job::Ctl &ctl) { ctl.call_on_main_thread([&worker] { @@ -42,11 +54,11 @@ TEST_CASE("State should not be idle while running a job", "[Jobs]") { REQUIRE(worker.is_idle()); } -TEST_CASE("Status messages should be received by the main thread during job execution", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("Status messages should be received by the main thread during job execution", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; queue_job(worker, [](Job::Ctl &ctl){ for (int s = 0; s <= 100; ++s) { @@ -60,12 +72,12 @@ TEST_CASE("Status messages should be received by the main thread during job exec REQUIRE(pri->statustxt == "Running"); } -TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; queue_job( worker, @@ -88,12 +100,12 @@ TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]") { REQUIRE(pri->pr != 100); } -TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; std::array jobres = {false, false, false, false}; @@ -128,12 +140,12 @@ TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]") { REQUIRE(jobres[3] == false); } -TEST_CASE("Exception should be properly forwarded to finalize()", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("Exception should be properly forwarded to finalize()", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; queue_job( worker, [](Job::Ctl &) { throw std::runtime_error("test"); },