diff --git a/resources/images/notification_open.svg b/resources/images/notification_open.svg new file mode 100644 index 0000000000..0532391f05 --- /dev/null +++ b/resources/images/notification_open.svg @@ -0,0 +1,40 @@ + +image/svg+xml + + + + diff --git a/resources/images/notification_open_dark.svg b/resources/images/notification_open_dark.svg new file mode 100644 index 0000000000..00589c1819 --- /dev/null +++ b/resources/images/notification_open_dark.svg @@ -0,0 +1,40 @@ + +image/svg+xml + + + + diff --git a/resources/images/notification_open_hover.svg b/resources/images/notification_open_hover.svg new file mode 100644 index 0000000000..207e9ac75c --- /dev/null +++ b/resources/images/notification_open_hover.svg @@ -0,0 +1,40 @@ + +image/svg+xml + + + + diff --git a/resources/images/notification_open_hover_dark.svg b/resources/images/notification_open_hover_dark.svg new file mode 100644 index 0000000000..07ac166190 --- /dev/null +++ b/resources/images/notification_open_hover_dark.svg @@ -0,0 +1,40 @@ + +image/svg+xml + + + + diff --git a/resources/images/notification_pause.svg b/resources/images/notification_pause.svg new file mode 100644 index 0000000000..fe108b8cae --- /dev/null +++ b/resources/images/notification_pause.svg @@ -0,0 +1,46 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_pause_dark.svg b/resources/images/notification_pause_dark.svg new file mode 100644 index 0000000000..f2dc08fb8d --- /dev/null +++ b/resources/images/notification_pause_dark.svg @@ -0,0 +1,46 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_pause_hover.svg b/resources/images/notification_pause_hover.svg new file mode 100644 index 0000000000..012e34f5d4 --- /dev/null +++ b/resources/images/notification_pause_hover.svg @@ -0,0 +1,48 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_pause_hover_dark.svg b/resources/images/notification_pause_hover_dark.svg new file mode 100644 index 0000000000..8933199eb7 --- /dev/null +++ b/resources/images/notification_pause_hover_dark.svg @@ -0,0 +1,48 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_play.svg b/resources/images/notification_play.svg new file mode 100644 index 0000000000..6ff4389ac8 --- /dev/null +++ b/resources/images/notification_play.svg @@ -0,0 +1,46 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_play_dark.svg b/resources/images/notification_play_dark.svg new file mode 100644 index 0000000000..974e653f3a --- /dev/null +++ b/resources/images/notification_play_dark.svg @@ -0,0 +1,46 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_play_hover.svg b/resources/images/notification_play_hover.svg new file mode 100644 index 0000000000..3a0ff0b223 --- /dev/null +++ b/resources/images/notification_play_hover.svg @@ -0,0 +1,46 @@ + +image/svg+xml + + + + + diff --git a/resources/images/notification_play_hover_dark.svg b/resources/images/notification_play_hover_dark.svg new file mode 100644 index 0000000000..5a41f14fea --- /dev/null +++ b/resources/images/notification_play_hover_dark.svg @@ -0,0 +1,46 @@ + +image/svg+xml + + + + + diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index e51e1f9e14..b230fa718c 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -210,6 +210,20 @@ namespace ImGui const wchar_t ExpandArrowIcon = 0x0843; const wchar_t CompleteIcon = 0x0844; -// void MyFunction(const char* name, const MyMatrix44& v); + // Orca + const wchar_t PlayButton = 0x0850; + const wchar_t PlayDarkButton = 0x0851; + const wchar_t PlayHoverButton = 0x0852; + const wchar_t PlayHoverDarkButton = 0x0853; + const wchar_t PauseButton = 0x0854; + const wchar_t PauseDarkButton = 0x0855; + const wchar_t PauseHoverButton = 0x0856; + const wchar_t PauseHoverDarkButton = 0x0857; + const wchar_t OpenButton = 0x0858; + const wchar_t OpenDarkButton = 0x0859; + const wchar_t OpenHoverButton = 0x085A; + const wchar_t OpenHoverDarkButton = 0x085B; + + // void MyFunction(const char* name, const MyMatrix44& v); } diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 49a7228f1f..1121a785c5 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -333,6 +333,11 @@ void AppConfig::set_defaults() set("download_path", ""); } + // Orca + if (get("ps_url_registered").empty()) { + set_bool("ps_url_registered", false); + } + if (get("mouse_wheel").empty()) { set("mouse_wheel", "0"); } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 2a37967de5..3ed7972d22 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -145,6 +145,8 @@ set(lisbslic3r_sources Format/SL1.cpp Format/svg.hpp Format/svg.cpp + Format/ZipperArchiveImport.hpp + Format/ZipperArchiveImport.cpp GCode/ThumbnailData.cpp GCode/ThumbnailData.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/Format/ZipperArchiveImport.cpp b/src/libslic3r/Format/ZipperArchiveImport.cpp new file mode 100644 index 0000000000..426e0d3252 --- /dev/null +++ b/src/libslic3r/Format/ZipperArchiveImport.cpp @@ -0,0 +1,147 @@ +///|/ Copyright (c) Prusa Research 2022 - 2023 Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "ZipperArchiveImport.hpp" + +#include "libslic3r/miniz_extension.hpp" +#include "libslic3r/Exception.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include +#include +#include + +namespace Slic3r { + +namespace { + +// Read an ini file into boost property tree +boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, + MZ_Archive &zip) +{ + std::string buf(size_t(entry.m_uncomp_size), '\0'); + + if (!mz_zip_reader_extract_to_mem(&zip.arch, entry.m_file_index, + buf.data(), buf.size(), 0)) + throw Slic3r::FileIOError(zip.get_errorstr()); + + boost::property_tree::ptree tree; + std::stringstream ss(buf); + boost::property_tree::read_ini(ss, tree); + return tree; +} + +// Read an arbitrary file into EntryBuffer +EntryBuffer read_entry(const mz_zip_archive_file_stat &entry, + MZ_Archive &zip, + const std::string &name) +{ + std::vector buf(entry.m_uncomp_size); + + if (!mz_zip_reader_extract_to_mem(&zip.arch, entry.m_file_index, + buf.data(), buf.size(), 0)) + throw Slic3r::FileIOError(zip.get_errorstr()); + + return {std::move(buf), (name.empty() ? entry.m_filename : name)}; +} + +} // namespace + +ZipperArchive read_zipper_archive(const std::string &zipfname, + const std::vector &includes, + const std::vector &excludes) +{ + ZipperArchive arch; + + // Little RAII + struct Arch : public MZ_Archive + { + Arch(const std::string &fname) + { + if (!open_zip_reader(&arch, fname)) + throw Slic3r::FileIOError(get_errorstr()); + } + + ~Arch() { close_zip_reader(&arch); } + } zip(zipfname); + + mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); + + for (mz_uint i = 0; i < num_entries; ++i) { + mz_zip_archive_file_stat entry; + + if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) { + std::string name = entry.m_filename; + boost::algorithm::to_lower(name); + + if (!std::any_of(includes.begin(), includes.end(), + [&name](const std::string &incl) { + return boost::algorithm::contains(name, incl); + })) + continue; + + if (std::any_of(excludes.begin(), excludes.end(), + [&name](const std::string &excl) { + return boost::algorithm::contains(name, excl); + })) + continue; + + if (name == CONFIG_FNAME) { + arch.config = read_ini(entry, zip); + continue; + } + + if (name == PROFILE_FNAME) { + arch.profile = read_ini(entry, zip); + continue; + } + + auto it = std::lower_bound( + arch.entries.begin(), arch.entries.end(), + EntryBuffer{{}, name}, + [](const EntryBuffer &r1, const EntryBuffer &r2) { + return std::less()(r1.fname, r2.fname); + }); + + arch.entries.insert(it, read_entry(entry, zip, name)); + } + } + + return arch; +} + +std::pair extract_profile( + const ZipperArchive &arch, DynamicPrintConfig &profile_out) +{ + DynamicPrintConfig profile_in, profile_use; + ConfigSubstitutions config_substitutions = + profile_in.load(arch.profile, + ForwardCompatibilitySubstitutionRule::Enable); + + if (profile_in.empty()) { // missing profile... do guess work + // try to recover the layer height from the config.ini which was + // present in all versions of sl1 files. + if (auto lh_opt = arch.config.find("layerHeight"); + lh_opt != arch.config.not_found()) { + auto lh_str = lh_opt->second.data(); + + size_t pos = 0; + double lh = string_to_double_decimal_point(lh_str, &pos); + if (pos) { // TODO: verify that pos is 0 when parsing fails + profile_out.set("layer_height", lh); + profile_out.set("initial_layer_height", lh); + } + } + } + + // If the archive contains an empty profile, use the one that was passed + // as output argument then replace it with the readed profile to report + // that it was empty. + profile_use = profile_in.empty() ? profile_out : profile_in; + profile_out = profile_in; + + return {profile_use, std::move(config_substitutions)}; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/ZipperArchiveImport.hpp b/src/libslic3r/Format/ZipperArchiveImport.hpp new file mode 100644 index 0000000000..d375cf55d6 --- /dev/null +++ b/src/libslic3r/Format/ZipperArchiveImport.hpp @@ -0,0 +1,58 @@ +///|/ Copyright (c) Prusa Research 2022 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef ZIPPERARCHIVEIMPORT_HPP +#define ZIPPERARCHIVEIMPORT_HPP + +#include +#include +#include + +#include + +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r { + +// Buffer for arbitraryfiles inside a zipper archive. +struct EntryBuffer +{ + std::vector buf; + std::string fname; +}; + +// Structure holding the data read from a zipper archive. +struct ZipperArchive +{ + boost::property_tree::ptree profile, config; + std::vector entries; +}; + +// Names of the files containing metadata inside the archive. +const constexpr char *CONFIG_FNAME = "config.ini"; +const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; + +// Read an archive that was written using the Zipper class. +// The includes parameter is a set of file name substrings that the entries +// must contain to be included in ZipperArchive. +// The excludes parameter may contain substrings that filenames must not +// contain. +// Every file in the archive is read into ZipperArchive::entries +// except the files CONFIG_FNAME, and PROFILE_FNAME which are read into +// ZipperArchive::config and ZipperArchive::profile structures. +ZipperArchive read_zipper_archive(const std::string &zipfname, + const std::vector &includes, + const std::vector &excludes); + +// Extract the print profile form the archive into 'out'. +// Returns a profile that has correct parameters to use for model reconstruction +// even if the needed parameters were not fully found in the archive's metadata. +// The inout argument shall be a usable fallback profile if the archive +// has totally corrupted metadata. +std::pair extract_profile( + const ZipperArchive &arch, DynamicPrintConfig &inout); + +} // namespace Slic3r + +#endif // ZIPPERARCHIVEIMPORT_HPP diff --git a/src/miniz/miniz.c b/src/miniz/miniz.c index d649adb1bc..c6cd29e003 100644 --- a/src/miniz/miniz.c +++ b/src/miniz/miniz.c @@ -8000,6 +8000,40 @@ mz_uint mz_zip_reader_get_extra(mz_zip_archive* pZip, mz_uint file_index, char* return ne + 1; } +mz_uint mz_zip_reader_get_filename_from_extra(mz_zip_archive* pZip, mz_uint file_index, char* buffer, mz_uint extra_buf_size) +{ + if (extra_buf_size == 0) + return 0; + mz_uint nf; + mz_uint ne; + const mz_uint8* p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + if (extra_buf_size) + buffer[0] = '\0'; + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return 0; + } + nf = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + ne = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + int copy = 0; + char const* p_nf = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + nf; + char const* e = p_nf + ne + 1; + while (p_nf + 4 < e) { + mz_uint16 len = ((mz_uint16)p_nf[2]) | ((mz_uint16)p_nf[3] << 8); + if (p_nf[0] == '\x75' && p_nf[1] == '\x70' && len >= 5 && p_nf + 4 + len < e && p_nf[4] == '\x01') { + mz_uint length = MZ_MIN(len - 5, extra_buf_size - 1); + memcpy(buffer, p_nf + 9, length); + return length; + } + else { + p_nf += 4 + len; + } + } + return 0; +} + mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat) { return mz_zip_file_stat_internal(pZip, file_index, mz_zip_get_cdh(pZip, file_index), pStat, NULL); diff --git a/src/miniz/miniz.h b/src/miniz/miniz.h index 4ed3320a1c..f0549f3de2 100644 --- a/src/miniz/miniz.h +++ b/src/miniz/miniz.h @@ -1171,6 +1171,9 @@ mz_uint mz_zip_reader_get_extra(mz_zip_archive *pZip, mz_uint file_index, char * int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); int mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index); +/* Retrieves the filename of an archive file entry from EXTRA ID. */ +mz_uint mz_zip_reader_get_filename_from_extra(mz_zip_archive * pZip, mz_uint file_index, char* buffer, mz_uint extra_buf_size); + /* Returns detailed information about an archive file entry. */ mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 6ff0adf18b..d03a0c20dc 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -111,7 +111,7 @@ set(SLIC3R_GUI_SOURCES GUI/GLCanvas3D.hpp GUI/GLCanvas3D.cpp GUI/SceneRaycaster.hpp - GUI/SceneRaycaster.cpp + GUI/SceneRaycaster.cpp GUI/OpenGLManager.hpp GUI/OpenGLManager.cpp GUI/Selection.hpp @@ -290,6 +290,8 @@ set(SLIC3R_GUI_SOURCES GUI/ConfigManipulation.hpp GUI/Field.cpp GUI/Field.hpp + GUI/FileArchiveDialog.cpp + GUI/FileArchiveDialog.hpp GUI/OptionsGroup.cpp GUI/OptionsGroup.hpp GUI/OG_CustomCtrl.cpp @@ -342,6 +344,10 @@ set(SLIC3R_GUI_SOURCES GUI/ConfigWizard_private.hpp GUI/MsgDialog.cpp GUI/MsgDialog.hpp + GUI/Downloader.hpp + GUI/Downloader.cpp + GUI/DownloaderFileGet.hpp + GUI/DownloaderFileGet.cpp GUI/DownloadProgressDialog.hpp GUI/DownloadProgressDialog.cpp GUI/UpdateDialogs.cpp diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp index 05bedf536f..a9d8708462 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.cpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp @@ -455,6 +455,171 @@ void DesktopIntegrationDialog::undo_desktop_intgration() wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationSuccess); } +void DesktopIntegrationDialog::perform_downloader_desktop_integration() +{ + BOOST_LOG_TRIVIAL(debug) << "performing downloader desktop integration."; + // Path to appimage + const char* appimage_env = std::getenv("APPIMAGE"); + std::string excutable_path; + if (appimage_env) { + try { + excutable_path = boost::filesystem::canonical(boost::filesystem::path(appimage_env)).string(); + } + catch (std::exception&) { + BOOST_LOG_TRIVIAL(error) << "Performing downloader desktop integration failed - boost::filesystem::canonical did not return appimage path."; + show_error(nullptr, _L("Performing downloader desktop integration failed - boost::filesystem::canonical did not return appimage path.")); + return; + } + } + else { + // not appimage - find executable + excutable_path = boost::dll::program_location().string(); + //excutable_path = wxStandardPaths::Get().GetExecutablePath().string(); + BOOST_LOG_TRIVIAL(debug) << "non-appimage path to executable: " << excutable_path; + if (excutable_path.empty()) + { + BOOST_LOG_TRIVIAL(error) << "Performing downloader desktop integration failed - no executable found."; + show_error(nullptr, _L("Performing downloader desktop integration failed - Could not find executable.")); + return; + } + } + + // Escape ' characters in appimage, other special symbols will be esacaped in desktop file by 'excutable_path' + //boost::replace_all(excutable_path, "'", "'\\''"); + excutable_path = escape_string(excutable_path); + + // Find directories icons and applications + // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. + // If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. + // $XDG_DATA_DIRS defines the preference-ordered set of base directories to search for data files in addition to the $XDG_DATA_HOME base directory. + // The directories in $XDG_DATA_DIRS should be seperated with a colon ':'. + // If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used. + std::vectortarget_candidates; + resolve_path_from_var("XDG_DATA_HOME", target_candidates); + resolve_path_from_var("XDG_DATA_DIRS", target_candidates); + + AppConfig* app_config = wxGetApp().app_config; + // suffix string to create different desktop file for alpha, beta. + + std::string version_suffix; + std::string name_suffix; + std::string version(SLIC3R_VERSION); + if (version.find("alpha") != std::string::npos) + { + version_suffix = "-alpha"; + name_suffix = " - alpha"; + } + else if (version.find("beta") != std::string::npos) + { + version_suffix = "-beta"; + name_suffix = " - beta"; + } + + // theme path to icon destination + std::string icon_theme_path; + std::string icon_theme_dirs; + + if (platform_flavor() == PlatformFlavor::LinuxOnChromium) { + icon_theme_path = "hicolor/96x96/apps/"; + icon_theme_dirs = "/hicolor/96x96/apps"; + } + + std::string target_dir_desktop; + + // desktop file + // iterate thru target_candidates to find applications folder + + std::string desktop_file_downloader = GUI::format( + "[Desktop Entry]\n" + "Name=PrusaSlicer URL Protocol%1%\n" + "Exec=\"%2%\" %%u\n" + "Terminal=false\n" + "Type=Application\n" + "MimeType=x-scheme-handler/prusaslicer;\n" + "StartupNotify=false\n" + "NoDisplay=true\n" + , name_suffix, excutable_path); + + // desktop file for downloader as part of main app + std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerURLProtocol%2%.desktop", target_dir_desktop, version_suffix); + if (create_desktop_file(desktop_path, desktop_file_downloader)) { + // save path to desktop file + app_config->set("desktop_integration_URL_path", desktop_path); + // finish registration on mime type + std::string command = GUI::format("xdg-mime default PrusaSlicerURLProtocol%1%.desktop x-scheme-handler/prusaslicer", version_suffix); + BOOST_LOG_TRIVIAL(debug) << "system command: " << command; + int r = system(command.c_str()); + BOOST_LOG_TRIVIAL(debug) << "system result: " << r; + + } + + bool candidate_found = false; + for (size_t i = 0; i < target_candidates.size(); ++i) { + if (contains_path_dir(target_candidates[i], "applications")) { + target_dir_desktop = target_candidates[i]; + // Write slicer desktop file + std::string path = GUI::format("%1%/applications/PrusaSlicerURLProtocol%2%.desktop", target_dir_desktop, version_suffix); + if (create_desktop_file(path, desktop_file_downloader)) { + app_config->set("desktop_integration_URL_path", path); + candidate_found = true; + BOOST_LOG_TRIVIAL(debug) << "PrusaSlicerURLProtocol.desktop file installation success."; + break; + } + else { + // write failed - try another path + BOOST_LOG_TRIVIAL(debug) << "Attempt to PrusaSlicerURLProtocol.desktop file installation failed. failed path: " << target_candidates[i]; + target_dir_desktop.clear(); + } + } + } + // if all failed - try creating default home folder + if (!candidate_found) { + // create $HOME/.local/share + create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications"); + // create desktop file + target_dir_desktop = GUI::format("%1%/.local/share", wxFileName::GetHomeDir()); + std::string path = GUI::format("%1%/applications/PrusaSlicerURLProtocol%2%.desktop", target_dir_desktop, version_suffix); + if (contains_path_dir(target_dir_desktop, "applications")) { + if (!create_desktop_file(path, desktop_file_downloader)) { + // Desktop file not written - end desktop integration + BOOST_LOG_TRIVIAL(error) << "Performing downloader desktop integration failed - could not create desktop file."; + return; + } + app_config->set("desktop_integration_URL_path", path); + } + else { + // Desktop file not written - end desktop integration + BOOST_LOG_TRIVIAL(error) << "Performing downloader desktop integration failed because the application directory was not found."; + return; + } + } + assert(!target_dir_desktop.empty()); + if (target_dir_desktop.empty()) { + // Desktop file not written - end desktop integration + BOOST_LOG_TRIVIAL(error) << "Performing downloader desktop integration failed because the application directory was not found."; + show_error(nullptr, _L("Performing downloader desktop integration failed because the application directory was not found.")); + return; + } + + // finish registration on mime type + std::string command = GUI::format("xdg-mime default PrusaSlicerURLProtocol%1%.desktop x-scheme-handler/prusaslicer", version_suffix); + BOOST_LOG_TRIVIAL(debug) << "system command: " << command; + int r = system(command.c_str()); + BOOST_LOG_TRIVIAL(debug) << "system result: " << r; + + wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess); +} +void DesktopIntegrationDialog::undo_downloader_registration() +{ + const AppConfig *app_config = wxGetApp().app_config; + std::string path = std::string(app_config->get("desktop_integration_URL_path")); + if (!path.empty()) { + BOOST_LOG_TRIVIAL(debug) << "removing " << path; + std::remove(path.c_str()); + } + // There is no need to undo xdg-mime default command. It is done automatically when desktop file is deleted. +} + DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent) : wxDialog(parent, wxID_ANY, _(L("Desktop Integration")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.hpp b/src/slic3r/GUI/DesktopIntegrationDialog.hpp index 4bb7c5e2c0..6605773234 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.hpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.hpp @@ -29,6 +29,9 @@ public: static void perform_desktop_integration(); // Deletes Desktop files and icons for both PrusaSlicer and GcodeViewer at paths stored in App Config. static void undo_desktop_intgration(); + + static void perform_downloader_desktop_integration(); + static void undo_downloader_registration(); private: }; diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp new file mode 100644 index 0000000000..229936160f --- /dev/null +++ b/src/slic3r/GUI/Downloader.cpp @@ -0,0 +1,270 @@ +///|/ Copyright (c) Prusa Research 2023 David Kocík @kocikdav, Oleksandra Iushchenko @YuSanka +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "Downloader.hpp" +#include "GUI_App.hpp" +#include "NotificationManager.hpp" +#include "format.hpp" +#include "MainFrame.hpp" + +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +namespace { +void open_folder(const std::string& path) +{ + // Code taken from NotificationManager.cpp + + // Execute command to open a file explorer, platform dependent. + // FIXME: The const_casts aren't needed in wxWidgets 3.1, remove them when we upgrade. + +#ifdef _WIN32 + const wxString widepath = from_u8(path); + const wchar_t* argv[] = { L"explorer", widepath.GetData(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); +#elif __APPLE__ + const char* argv[] = { "open", path.data(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); +#else + const char* argv[] = { "xdg-open", path.data(), nullptr }; + + // Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars, + // because they may mess up the environment expected by the file manager. + // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure. + if (wxGetEnv("APPIMAGE", nullptr)) { + // We're running from AppImage + wxEnvVariableHashMap env_vars; + wxGetEnvMap(&env_vars); + + env_vars.erase("APPIMAGE"); + env_vars.erase("APPDIR"); + env_vars.erase("LD_LIBRARY_PATH"); + env_vars.erase("LD_PRELOAD"); + env_vars.erase("UNION_PRELOAD"); + + wxExecuteEnv exec_env; + exec_env.env = std::move(env_vars); + + wxString owd; + if (wxGetEnv("OWD", &owd)) { + // This is the original work directory from which the AppImage image was run, + // set it as CWD for the child process: + exec_env.cwd = std::move(owd); + } + + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, &exec_env); + } + else { + // Looks like we're NOT running from AppImage, we'll make no changes to the environment. + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, nullptr); + } +#endif +} + +std::string filename_from_url(const std::string& url) +{ + // TODO: can it be done with curl? + size_t slash = url.find_last_of("/"); + if (slash == std::string::npos && slash != url.size() - 1) + return {}; + return url.substr(slash + 1, url.size() - slash + 1); +} +} + +Download::Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) + : m_id(ID) + , m_filename(filename_from_url(url)) + , m_dest_folder(dest_folder) +{ + assert(boost::filesystem::is_directory(dest_folder)); + m_final_path = dest_folder / m_filename; + m_file_get = std::make_shared(ID, std::move(url), m_filename, evt_handler, dest_folder); +} + +void Download::start() +{ + m_state = DownloadState::DownloadOngoing; + m_file_get->get(); +} +void Download::cancel() +{ + m_state = DownloadState::DownloadStopped; + m_file_get->cancel(); +} +void Download::pause() +{ + //assert(m_state == DownloadState::DownloadOngoing); + // if instead of assert - it can happen that user clicks on pause several times before the pause happens + if (m_state != DownloadState::DownloadOngoing) + return; + m_state = DownloadState::DownloadPaused; + m_file_get->pause(); +} +void Download::resume() +{ + //assert(m_state == DownloadState::DownloadPaused); + if (m_state != DownloadState::DownloadPaused) + return; + m_state = DownloadState::DownloadOngoing; + m_file_get->resume(); +} + + +Downloader::Downloader() + : wxEvtHandler() +{ + //Bind(EVT_DWNLDR_FILE_COMPLETE, [](const wxCommandEvent& evt) {}); + //Bind(EVT_DWNLDR_FILE_PROGRESS, [](const wxCommandEvent& evt) {}); + //Bind(EVT_DWNLDR_FILE_ERROR, [](const wxCommandEvent& evt) {}); + //Bind(EVT_DWNLDR_FILE_NAME_CHANGE, [](const wxCommandEvent& evt) {}); + + Bind(EVT_DWNLDR_FILE_COMPLETE, &Downloader::on_complete, this); + Bind(EVT_DWNLDR_FILE_PROGRESS, &Downloader::on_progress, this); + Bind(EVT_DWNLDR_FILE_ERROR, &Downloader::on_error, this); + Bind(EVT_DWNLDR_FILE_NAME_CHANGE, &Downloader::on_name_change, this); + Bind(EVT_DWNLDR_FILE_PAUSED, &Downloader::on_paused, this); + Bind(EVT_DWNLDR_FILE_CANCELED, &Downloader::on_canceled, this); +} + +void Downloader::start_download(const std::string& full_url) +{ + assert(m_initialized); + + // Orca: check if url is registered + if (!wxGetApp().app_config->has("ps_url_registered") || !wxGetApp().app_config->get_bool("ps_url_registered")) { + BOOST_LOG_TRIVIAL(error) << "PrusaSlicer links are not enabled. Download aborted: " << full_url; + show_error(nullptr, "PrusaSlicer links are not enabled in preferences. Download aborted."); + return; + } + + // Orca: Move to the 3D view + MainFrame* mainframe = wxGetApp().mainframe; + Plater* plater = wxGetApp().plater(); + + mainframe->Freeze(); + mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + plater->select_view_3D("3D"); + plater->select_view("plate"); + plater->get_current_canvas3D()->zoom_to_bed(); + mainframe->Thaw(); + + // Orca: Replace PS workaround for "mysterious slash" with a more dynamic approach + // Windows seems to have fixed the issue and this provides backwards compatability for those it still affects + boost::regex re(R"(^prusaslicer:\/\/open[\/]?\?file=)", boost::regbase::icase); + boost::smatch results; + + if (!boost::regex_search(full_url, results, re)) { + BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url; + // Orca: show error + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, + "Could not start download due to malformed URL"); + return; + } + size_t id = get_next_id(); + std::string escaped_url = FileGet::escape_url(full_url.substr(results.length())); + if (!boost::starts_with(escaped_url, "https://") || !FileGet::is_subdomain(escaped_url, "printables.com")) { + std::string msg = format(_L("Download won't start. Download URL doesn't point to https://printables.com : %1%"), escaped_url); + BOOST_LOG_TRIVIAL(error) << msg; + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, + "Download failed. Download URL doesn't point to https://printables.com."); + return; + } + + std::string text(escaped_url); + m_downloads.emplace_back(std::make_unique(id, std::move(escaped_url), this, m_dest_folder)); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->push_download_URL_progress_notification(id, m_downloads.back()->get_filename(), std::bind(&Downloader::user_action_callback, this, std::placeholders::_1, std::placeholders::_2)); + m_downloads.back()->start(); + BOOST_LOG_TRIVIAL(debug) << "started download"; +} + +void Downloader::on_progress(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + float percent = (float)std::stoi(boost::nowide::narrow(event.GetString())) / 100.f; + //BOOST_LOG_TRIVIAL(error) << "progress " << id << ": " << percent; + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + BOOST_LOG_TRIVIAL(trace) << "Download "<< id << ": " << percent; + ntf_mngr->set_download_URL_progress(id, percent); +} +void Downloader::on_error(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + set_download_state(event.GetInt(), DownloadState::DownloadError); + BOOST_LOG_TRIVIAL(error) << "Download error: " << event.GetString(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_error(id, boost::nowide::narrow(event.GetString())); + show_error(nullptr, format_wxstr(L"%1%\n%2%", _L("The download has failed") + ":", event.GetString())); +} +void Downloader::on_complete(wxCommandEvent& event) +{ + // TODO: is this always true? : + // here we open the file itself, notification should get 1.f progress from on progress. + set_download_state(event.GetInt(), DownloadState::DownloadDone); + wxArrayString paths; + paths.Add(event.GetString()); + wxGetApp().plater()->load_files(paths); +} +bool Downloader::user_action_callback(DownloaderUserAction action, int id) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + switch (action) { + case DownloadUserCanceled: + m_downloads[i]->cancel(); + return true; + case DownloadUserPaused: + m_downloads[i]->pause(); + return true; + case DownloadUserContinued: + m_downloads[i]->resume(); + return true; + case DownloadUserOpenedFolder: + open_folder(m_downloads[i]->get_dest_folder()); + return true; + default: + return false; + } + } + } + return false; +} + +void Downloader::on_name_change(wxCommandEvent& event) +{ + +} + +void Downloader::on_paused(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_paused(id); +} + +void Downloader::on_canceled(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_canceled(id); +} + +void Downloader::set_download_state(int id, DownloadState state) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + m_downloads[i]->set_state(state); + return; + } + } +} + +} +} \ No newline at end of file diff --git a/src/slic3r/GUI/Downloader.hpp b/src/slic3r/GUI/Downloader.hpp new file mode 100644 index 0000000000..4fe896f5ce --- /dev/null +++ b/src/slic3r/GUI/Downloader.hpp @@ -0,0 +1,103 @@ +///|/ Copyright (c) Prusa Research 2023 David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_Downloader_hpp_ +#define slic3r_Downloader_hpp_ + +#include "DownloaderFileGet.hpp" +#include +#include + +namespace Slic3r { +namespace GUI { + +class NotificationManager; + +enum DownloadState +{ + DownloadPending = 0, + DownloadOngoing, + DownloadStopped, + DownloadDone, + DownloadError, + DownloadPaused, + DownloadStateUnknown +}; + +enum DownloaderUserAction +{ + DownloadUserCanceled, + DownloadUserPaused, + DownloadUserContinued, + DownloadUserOpenedFolder +}; + +class Download { +public: + Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); + void start(); + void cancel(); + void pause(); + void resume(); + + int get_id() const { return m_id; } + boost::filesystem::path get_final_path() const { return m_final_path; } + std::string get_filename() const { return m_filename; } + DownloadState get_state() const { return m_state; } + void set_state(DownloadState state) { m_state = state; } + std::string get_dest_folder() { return m_dest_folder.string(); } +private: + const int m_id; + std::string m_filename; + boost::filesystem::path m_final_path; + boost::filesystem::path m_dest_folder; + std::shared_ptr m_file_get; + DownloadState m_state { DownloadState::DownloadPending }; +}; + +class Downloader : public wxEvtHandler { +public: + Downloader(); + + bool get_initialized() { return m_initialized; } + void init(const boost::filesystem::path& dest_folder) + { + m_dest_folder = dest_folder; + m_initialized = true; + } + void start_download(const std::string& full_url); + // cancel = false -> just pause + bool user_action_callback(DownloaderUserAction action, int id); +private: + bool m_initialized { false }; + + std::vector> m_downloads; + boost::filesystem::path m_dest_folder; + + size_t m_next_id { 0 }; + size_t get_next_id() { return ++m_next_id; } + + void on_progress(wxCommandEvent& event); + void on_error(wxCommandEvent& event); + void on_complete(wxCommandEvent& event); + void on_name_change(wxCommandEvent& event); + void on_paused(wxCommandEvent& event); + void on_canceled(wxCommandEvent& event); + + void set_download_state(int id, DownloadState state); + /* + bool is_in_state(int id, DownloadState state) const; + DownloadState get_download_state(int id) const; + bool cancel_download(int id); + bool pause_download(int id); + bool resume_download(int id); + bool delete_download(int id); + wxString get_path_of(int id) const; + wxString get_folder_path_of(int id) const; + */ +}; + +} +} +#endif \ No newline at end of file diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp new file mode 100644 index 0000000000..137a7e9bc1 --- /dev/null +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -0,0 +1,395 @@ +///|/ Copyright (c) Prusa Research 2023 Oleksandra Iushchenko @YuSanka, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "DownloaderFileGet.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "format.hpp" +#include "GUI.hpp" +#include "I18N.hpp" + +namespace Slic3r { +namespace GUI { + +const size_t DOWNLOAD_MAX_CHUNK_SIZE = 10 * 1024 * 1024; +const size_t DOWNLOAD_SIZE_LIMIT = 1024 * 1024 * 1024; + +std::string FileGet::escape_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + int decodelen; + char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +bool FileGet::is_subdomain(const std::string& url, const std::string& domain) +{ + // domain should be f.e. printables.com (.com including) + char* host; + std::string host_string; + CURLUcode rc; + CURLU* curl = curl_url(); + if (!curl) { + BOOST_LOG_TRIVIAL(error) << "Failed to init Curl library in function is_domain."; + return false; + } + rc = curl_url_set(curl, CURLUPART_URL, url.c_str(), 0); + if (rc != CURLUE_OK) { + curl_url_cleanup(curl); + return false; + } + rc = curl_url_get(curl, CURLUPART_HOST, &host, 0); + if (rc != CURLUE_OK || !host) { + curl_url_cleanup(curl); + return false; + } + host_string = std::string(host); + curl_free(host); + // now host should be subdomain.domain or just domain + if (domain == host_string) { + curl_url_cleanup(curl); + return true; + } + if(boost::ends_with(host_string, "." + domain)) { + curl_url_cleanup(curl); + return true; + } + curl_url_cleanup(curl); + return false; +} + +namespace { +unsigned get_current_pid() +{ +#ifdef WIN32 + return GetCurrentProcessId(); +#else + return ::getpid(); +#endif +} +} + +// int = DOWNLOAD ID; string = file path +wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent); +// int = DOWNLOAD ID; string = error msg +wxDEFINE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent); +// int = DOWNLOAD ID; string = progress percent +wxDEFINE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent); +// int = DOWNLOAD ID; string = name +wxDEFINE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent); +// int = DOWNLOAD ID; +wxDEFINE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent); +// int = DOWNLOAD ID; +wxDEFINE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent); + +struct FileGet::priv +{ + const int m_id; + std::string m_url; + std::string m_filename; + std::thread m_io_thread; + wxEvtHandler* m_evt_handler; + boost::filesystem::path m_dest_folder; + boost::filesystem::path m_tmp_path; // path when ongoing download + std::atomic_bool m_cancel { false }; + std::atomic_bool m_pause { false }; + std::atomic_bool m_stopped { false }; // either canceled or paused - download is not running + size_t m_written { 0 }; + size_t m_absolute_size { 0 }; + priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); + + void get_perform(); +}; + +FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) + : m_id(ID) + , m_url(std::move(url)) + , m_filename(filename) + , m_evt_handler(evt_handler) + , m_dest_folder(dest_folder) +{ +} + +void FileGet::priv::get_perform() +{ + assert(m_evt_handler); + assert(!m_url.empty()); + assert(!m_filename.empty()); + assert(boost::filesystem::is_directory(m_dest_folder)); + + m_stopped = false; + + // open dest file + if (m_written == 0) + { + boost::filesystem::path dest_path = m_dest_folder / m_filename; + std::string extension = boost::filesystem::extension(dest_path); + std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size()); + std::string final_filename = just_filename; + // Find unsed filename + try { + size_t version = 0; + while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download"))) + { + ++version; + if (version > 999) { + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / (final_filename + extension)).string())); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + final_filename = GUI::format("%1%(%2%)", just_filename, std::to_string(version)); + } + } catch (const boost::filesystem::filesystem_error& e) + { + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString(e.what()); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + + m_filename = final_filename + extension; + + m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download"); + + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE); + evt->SetString(boost::nowide::widen(m_filename)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + } + + boost::filesystem::path dest_path = m_dest_folder / m_filename; + + wxString temp_path_wstring(m_tmp_path.wstring()); + + //std::cout << "dest_path: " << dest_path.string() << std::endl; + //std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl; + + BOOST_LOG_TRIVIAL(info) << GUI::format("Starting download from %1% to %2%. Temp path is %3%",m_url, dest_path, m_tmp_path); + + FILE* file; + // open file for writting + if (m_written == 0) + file = fopen(temp_path_wstring.c_str(), "wb"); + else + file = fopen(temp_path_wstring.c_str(), "ab"); + + //assert(file != NULL); + if (file == NULL) { + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + // TRN %1% = file path + evt->SetString(GUI::format_wxstr(_L("Can't create file at %1%"), temp_path_wstring)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + + std:: string range_string = std::to_string(m_written) + "-"; + + size_t written_previously = m_written; + size_t written_this_session = 0; + Http::get(m_url) + .size_limit(DOWNLOAD_SIZE_LIMIT) //more? + .set_range(range_string) + .on_progress([&](Http::Progress progress, bool& cancel) { + // to prevent multiple calls into following ifs (m_cancel / m_pause) + if (m_stopped){ + cancel = true; + return; + } + if (m_cancel) { + m_stopped = true; + fclose(file); + // remove canceled file + std::remove(m_tmp_path.string().c_str()); + m_written = 0; + cancel = true; + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + // TODO: send canceled event? + } + if (m_pause) { + m_stopped = true; + fclose(file); + cancel = true; + if (m_written == 0) + std::remove(m_tmp_path.string().c_str()); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + + if (m_absolute_size < progress.dltotal) { + m_absolute_size = progress.dltotal; + } + + if (progress.dlnow != 0) { + if (progress.dlnow - written_this_session > DOWNLOAD_MAX_CHUNK_SIZE || progress.dlnow == progress.dltotal) { + try + { + std::string part_for_write = progress.buffer.substr(written_this_session, progress.dlnow); + fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); + } + catch (const std::exception& e) + { + // fclose(file); do it? + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString(e.what()); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + cancel = true; + return; + } + written_this_session = progress.dlnow; + m_written = written_previously + written_this_session; + } + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS); + int percent_total = (written_previously + progress.dlnow) * 100 / m_absolute_size; + evt->SetString(std::to_string(percent_total)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + } + + }) + .on_error([&](std::string body, std::string error, unsigned http_status) { + if (file != NULL) + fclose(file); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + if (!error.empty()) + evt->SetString(GUI::from_u8(error)); + else + evt->SetString(GUI::from_u8(body)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + }) + .on_complete([&](std::string body, unsigned /* http_status */) { + + // TODO: perform a body size check + // + //size_t body_size = body.size(); + //if (body_size != expected_size) { + // return; + //} + try + { + /* + if (m_written < body.size()) + { + // this code should never be entered. As there should be on_progress call after last bit downloaded. + std::string part_for_write = body.substr(m_written); + fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); + } + */ + fclose(file); + boost::filesystem::rename(m_tmp_path, dest_path); + } + catch (const std::exception& /*e*/) + { + //TODO: report? + //error_message = GUI::format("Failed to write and move %1% to %2%", tmp_path, dest_path); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString("Failed to write and move."); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_COMPLETE); + evt->SetString(dest_path.wstring()); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + }) + .perform_sync(); + +} + +FileGet::FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) + : p(new priv(ID, std::move(url), filename, evt_handler, dest_folder)) +{} + +FileGet::FileGet(FileGet&& other) : p(std::move(other.p)) {} + +FileGet::~FileGet() +{ + if (p && p->m_io_thread.joinable()) { + p->m_cancel = true; + p->m_io_thread.join(); + } +} + +void FileGet::get() +{ + assert(p); + if (p->m_io_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_io_thread.join(); + } + p->m_cancel = false; + p->m_pause = false; + p->m_io_thread = std::thread([this]() { + p->get_perform(); + }); +} + +void FileGet::cancel() +{ + if(p && p->m_stopped) { + if (p->m_io_thread.joinable()) { + p->m_cancel = true; + p->m_io_thread.join(); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED); + evt->SetInt(p->m_id); + p->m_evt_handler->QueueEvent(evt); + } + } + + if (p) + p->m_cancel = true; + +} + +void FileGet::pause() +{ + if (p) { + p->m_pause = true; + } +} +void FileGet::resume() +{ + assert(p); + if (p->m_io_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_io_thread.join(); + } + p->m_cancel = false; + p->m_pause = false; + p->m_io_thread = std::thread([this]() { + p->get_perform(); + }); +} +} +} diff --git a/src/slic3r/GUI/DownloaderFileGet.hpp b/src/slic3r/GUI/DownloaderFileGet.hpp new file mode 100644 index 0000000000..37a59ec30e --- /dev/null +++ b/src/slic3r/GUI/DownloaderFileGet.hpp @@ -0,0 +1,49 @@ +///|/ Copyright (c) Prusa Research 2023 David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_DownloaderFileGet_hpp_ +#define slic3r_DownloaderFileGet_hpp_ + +#include "../Utils/Http.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { +class FileGet : public std::enable_shared_from_this { +private: + struct priv; +public: + FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler,const boost::filesystem::path& dest_folder); + FileGet(FileGet&& other); + ~FileGet(); + + void get(); + void cancel(); + void pause(); + void resume(); + static std::string escape_url(const std::string& url); + static bool is_subdomain(const std::string& url, const std::string& domain); +private: + std::unique_ptr p; +}; +// int = DOWNLOAD ID; string = file path +wxDECLARE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent); +// int = DOWNLOAD ID; string = error msg +wxDECLARE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent); +// int = DOWNLOAD ID; string = progress percent +wxDECLARE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent); +// int = DOWNLOAD ID; string = name +wxDECLARE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent); +// int = DOWNLOAD ID; +wxDECLARE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent); +// int = DOWNLOAD ID; +wxDECLARE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent); +} +} +#endif diff --git a/src/slic3r/GUI/FileArchiveDialog.cpp b/src/slic3r/GUI/FileArchiveDialog.cpp new file mode 100644 index 0000000000..5f3927753f --- /dev/null +++ b/src/slic3r/GUI/FileArchiveDialog.cpp @@ -0,0 +1,447 @@ +///|/ Copyright (c) Prusa Research 2023 David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "FileArchiveDialog.hpp" + +#include "I18N.hpp" +#include "GUI_App.hpp" +#include "GUI.hpp" +#include "MainFrame.hpp" +#include "ExtraRenderers.hpp" +#include "format.hpp" +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +#define BTN_SIZE wxSize(FromDIP(58), FromDIP(24)) +#define BTN_GAP FromDIP(20) + +ArchiveViewModel::ArchiveViewModel(wxWindow* parent) + :m_parent(parent) +{} +ArchiveViewModel::~ArchiveViewModel() +{} + +std::shared_ptr ArchiveViewModel::AddFile(std::shared_ptr parent, const wxString& name, bool container) +{ + std::shared_ptr node = std::make_shared(ArchiveViewNode(name)); + node->set_container(container); + + if (parent.get() != nullptr) { + parent->get_children().push_back(node); + node->set_parent(parent); + parent->set_is_folder(true); + } else { + m_top_children.emplace_back(node); + } + + wxDataViewItem child = wxDataViewItem((void*)node.get()); + wxDataViewItem parent_item= wxDataViewItem((void*)parent.get()); + ItemAdded(parent_item, child); + + if (parent) + m_ctrl->Expand(parent_item); + return node; +} + +wxString ArchiveViewModel::GetColumnType(unsigned int col) const +{ + if (col == 0) + return "bool"; + return "string";//"DataViewBitmapText"; +} + +void ArchiveViewModel::Rescale() +{ + // There should be no pictures rendered +} + +void ArchiveViewModel::Delete(const wxDataViewItem& item) +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + assert(node->get_parent() != nullptr); + for (std::shared_ptr child : node->get_children()) + { + Delete(wxDataViewItem((void*)child.get())); + } + delete [] node; +} +void ArchiveViewModel::Clear() +{ +} + +wxDataViewItem ArchiveViewModel::GetParent(const wxDataViewItem& item) const +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + return wxDataViewItem((void*)node->get_parent().get()); +} +unsigned int ArchiveViewModel::GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const +{ + if (!parent.IsOk()) { + for (std::shared_ptrchild : m_top_children) { + array.push_back(wxDataViewItem((void*)child.get())); + } + return m_top_children.size(); + } + + ArchiveViewNode* node = static_cast(parent.GetID()); + for (std::shared_ptr child : node->get_children()) { + array.push_back(wxDataViewItem((void*)child.get())); + } + return node->get_children().size(); +} + +void ArchiveViewModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + if (col == 0) { + variant = node->get_toggle(); + } else { + variant = node->get_name(); + } +} + +void ArchiveViewModel::untoggle_folders(const wxDataViewItem& item) +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + node->set_toggle(false); + if (node->get_parent().get() != nullptr) + untoggle_folders(wxDataViewItem((void*)node->get_parent().get())); +} + +bool ArchiveViewModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + if (col == 0) { + node->set_toggle(variant.GetBool()); + // if folder recursivelly check all children + for (std::shared_ptr child : node->get_children()) { + SetValue(variant, wxDataViewItem((void*)child.get()), col); + } + if(!variant.GetBool() && node->get_parent()) + untoggle_folders(wxDataViewItem((void*)node->get_parent().get())); + } else { + node->set_name(variant.GetString()); + } + m_parent->Refresh(); + return true; +} +bool ArchiveViewModel::IsEnabled(const wxDataViewItem& item, unsigned int col) const +{ + // As of now, all items are always enabled. + // Returning false for col 1 would gray out text. + return true; +} + +bool ArchiveViewModel::IsContainer(const wxDataViewItem& item) const +{ + if(!item.IsOk()) + return true; + ArchiveViewNode* node = static_cast(item.GetID()); + return node->is_container(); +} + +ArchiveViewCtrl::ArchiveViewCtrl(wxWindow* parent, wxSize size) + : wxDataViewCtrl(parent, wxID_ANY, wxDefaultPosition, size, wxDV_VARIABLE_LINE_HEIGHT | wxDV_ROW_LINES +#ifdef _WIN32 + | wxBORDER_SIMPLE +#endif + ) + //, m_em_unit(em_unit(parent)) +{ + wxGetApp().UpdateDVCDarkUI(this); + + m_model = new ArchiveViewModel(parent); + this->AssociateModel(m_model); + m_model->SetAssociatedControl(this); +} + +ArchiveViewCtrl::~ArchiveViewCtrl() +{ + if (m_model) { + m_model->Clear(); + m_model->DecRef(); + } +} + +FileArchiveDialog::FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* archive, std::vector>& selected_paths_w_size) + : DPIDialog(parent_window, wxID_ANY, _(L("Archive preview")), wxDefaultPosition, + wxSize(45 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit()), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX) + , m_selected_paths_w_size (selected_paths_w_size) +{ +#ifdef _WIN32 + SetBackgroundColour(*wxWHITE); + wxGetApp().UpdateDarkUI(this); + wxGetApp().UpdateDlgDarkUI(this); +#else + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif + + int em = em_unit(); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + m_avc = new ArchiveViewCtrl(this, wxSize(45 * em, 30 * em)); + wxDataViewColumn* toggle_column = m_avc->AppendToggleColumn(L"\u2714", 0, wxDATAVIEW_CELL_ACTIVATABLE, 6 * em); + m_avc->AppendTextColumn("filename", 1); + + std::vector> stack; + + std::function >&, size_t)> reduce_stack = [] (std::vector>& stack, size_t size) { + if (size == 0) { + stack.clear(); + return; + } + while (stack.size() > size) + stack.pop_back(); + }; + // recursively stores whole structure of file onto function stack and synchoronize with stack object. + std::function>&)> adjust_stack = [&adjust_stack, &reduce_stack, &avc = m_avc](const boost::filesystem::path& const_file, std::vector>& stack)->size_t { + boost::filesystem::path file(const_file); + size_t struct_size = file.has_parent_path() ? adjust_stack(file.parent_path(), stack) : 0; + + if (stack.size() > struct_size && (file.has_extension() || file.filename().string() != stack[struct_size]->get_name())) + { + reduce_stack(stack, struct_size); + } + if (!file.has_extension() && stack.size() == struct_size) + stack.push_back(avc->get_model()->AddFile((stack.empty() ? std::shared_ptr(nullptr) : stack.back()), boost::nowide::widen(file.filename().string()), true)); // filename string to wstring? + return struct_size + 1; + }; + + const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|step|stp)", std::regex::icase); + mz_uint num_entries = mz_zip_reader_get_num_files(archive); + mz_zip_archive_file_stat stat; + std::vector> filtered_entries; // second is unzipped size + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(archive, i, &stat)) { + std::string extra(1024, 0); + boost::filesystem::path path; + size_t extra_size = mz_zip_reader_get_filename_from_extra(archive, i, extra.data(), extra.size()); + if (extra_size > 0) { + path = boost::filesystem::path(extra.substr(0, extra_size)); + } else { + wxString wname = boost::nowide::widen(stat.m_filename); + std::string name = boost::nowide::narrow(wname); + path = boost::filesystem::path(name); + } + assert(!path.empty()); + if (!path.has_extension()) + continue; + // filter out MACOS specific hidden files + if (boost::algorithm::starts_with(path.string(), "__MACOSX")) + continue; + filtered_entries.emplace_back(std::move(path), stat.m_uncomp_size); + } + } + // sorting files will help adjust_stack function to not create multiple same folders + std::sort(filtered_entries.begin(), filtered_entries.end(), [](const std::pair& p1, const std::pair& p2){ return p1.first.string() < p2.first.string(); }); + size_t entry_count = 0; + size_t depth = 1; + for (const auto& entry : filtered_entries) + { + const boost::filesystem::path& path = entry.first; + std::shared_ptr parent(nullptr); + + depth = std::max(depth, adjust_stack(path, stack)); + if (!stack.empty()) + parent = stack.back(); + if (std::regex_match(path.extension().string(), pattern_drop)) { // this leaves out non-compatible files + std::shared_ptr new_node = m_avc->get_model()->AddFile(parent, boost::nowide::widen(path.filename().string()), false); + new_node->set_fullpath(/*std::move(path)*/path); // filename string to wstring? + new_node->set_size(entry.second); + entry_count++; + } + } + if (entry_count == 1) + on_all_button(); + + toggle_column->SetWidth((4 + depth) * em); + + wxBoxSizer* btn_sizer = create_btn_sizer(); + + topSizer->Add(m_avc, 1, wxEXPAND | wxALL, 10); + topSizer->Add(btn_sizer, 0, wxEXPAND | wxALL, 10); + this->SetSizer(topSizer); + SetMinSize(wxSize(40 * em, 30 * em)); + + for (auto btn : m_button_list) + wxGetApp().UpdateDarkUI(btn); +} + +void FileArchiveDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + int em = em_unit(); + BOOST_LOG_TRIVIAL(error) << "on_dpi_changed"; + + for (auto btn : m_button_list) { + btn->SetMinSize(BTN_SIZE); + btn->SetCornerRadius(FromDIP(12)); + } + + const wxSize& size = wxSize(45 * em, 40 * em); + SetSize(size); + //m_tree->Rescale(em); + + Fit(); + Refresh(); +} + +void FileArchiveDialog::on_open_button() +{ + wxDataViewItemArray top_items; + m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items); + + std::function deep_fill = [&paths = m_selected_paths_w_size, &deep_fill](ArchiveViewNode* node){ + if (node == nullptr) + return; + if (node->get_children().empty()) { + if (node->get_toggle()) + paths.emplace_back(node->get_fullpath(), node->get_size()); + } else { + for (std::shared_ptr child : node->get_children()) + deep_fill(child.get()); + } + }; + + for (const auto& item : top_items) + { + ArchiveViewNode* node = static_cast(item.GetID()); + deep_fill(node); + } + this->EndModal(wxID_OK); +} + +void FileArchiveDialog::on_all_button() +{ + + wxDataViewItemArray top_items; + m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items); + + std::function deep_fill = [&deep_fill](ArchiveViewNode* node) { + if (node == nullptr) + return; + node->set_toggle(true); + if (!node->get_children().empty()) { + for (std::shared_ptr child : node->get_children()) + deep_fill(child.get()); + } + }; + + for (const auto& item : top_items) + { + ArchiveViewNode* node = static_cast(item.GetID()); + deep_fill(node); + // Fix for linux, where Refresh or Update wont help to redraw toggle checkboxes. + // It should be enough to call ValueChanged for top items. + m_avc->get_model()->ValueChanged(item, 0); + } + + Refresh(); +} + +void FileArchiveDialog::on_none_button() +{ + wxDataViewItemArray top_items; + m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items); + + std::function deep_fill = [&deep_fill](ArchiveViewNode* node) { + if (node == nullptr) + return; + node->set_toggle(false); + if (!node->get_children().empty()) { + for (std::shared_ptr child : node->get_children()) + deep_fill(child.get()); + } + }; + + for (const auto& item : top_items) + { + ArchiveViewNode* node = static_cast(item.GetID()); + deep_fill(node); + // Fix for linux, where Refresh or Update wont help to redraw toggle checkboxes. + // It should be enough to call ValueChanged for top items. + m_avc->get_model()->ValueChanged(item, 0); + } + + this->Refresh(); +} + +//Orca: Apply buttons style +wxBoxSizer* FileArchiveDialog::create_btn_sizer() +{ + auto btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + auto apply_highlighted_btn_colors = [](Button* btn) { + btn->SetBackgroundColor(StateColor(std::pair(wxColour(0, 137, 123), StateColor::Pressed), + std::pair(wxColour(38, 166, 154), StateColor::Hovered), + std::pair(wxColour(0, 150, 136), StateColor::Normal))); + + btn->SetBorderColor(StateColor(std::pair(wxColour(0, 150, 136), StateColor::Normal))); + + btn->SetTextColor(StateColor(std::pair(wxColour(255, 255, 254), StateColor::Normal))); + }; + + auto apply_std_btn_colors = [](Button* btn) { + btn->SetBackgroundColor(StateColor(std::pair(wxColour(206, 206, 206), StateColor::Pressed), + std::pair(wxColour(238, 238, 238), StateColor::Hovered), + std::pair(wxColour(255, 255, 255), StateColor::Normal))); + + btn->SetBorderColor(StateColor(std::pair(wxColour(38, 46, 48), StateColor::Normal))); + + btn->SetTextColor(StateColor(std::pair(wxColour(38, 46, 48), StateColor::Normal))); + }; + + auto style_btn = [this, apply_highlighted_btn_colors, apply_std_btn_colors](Button* btn, bool highlight) { + btn->SetMinSize(BTN_SIZE); + btn->SetCornerRadius(FromDIP(12)); + if (highlight) + apply_highlighted_btn_colors(btn); + else + apply_std_btn_colors(btn); + }; + + Button* all_btn = new Button(this, _L("All")); + style_btn(all_btn, false); + all_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_all_button(); }); + btn_sizer->Add(all_btn, 0, wxALIGN_CENTER_VERTICAL); + m_button_list.push_back(all_btn); + + Button* none_btn = new Button(this, _L("None")); + style_btn(none_btn, false); + none_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_none_button(); }); + btn_sizer->Add(none_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, BTN_GAP); + m_button_list.push_back(none_btn); + + btn_sizer->AddStretchSpacer(); + + Button* open_btn = new Button(this, _L("Open")); + style_btn(open_btn, true); + open_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_open_button(); }); + open_btn->SetFocus(); + open_btn->SetId(wxID_OK); + btn_sizer->Add(open_btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, BTN_GAP); + m_button_list.push_back(open_btn); + + Button* cancel_btn = new Button(this, _L("Cancel")); + style_btn(cancel_btn, false); + cancel_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { this->EndModal(wxID_CANCEL); }); + cancel_btn->SetId(wxID_CANCEL); + btn_sizer->Add(cancel_btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, BTN_GAP); + m_button_list.push_back(cancel_btn); + + return btn_sizer; +} + +} // namespace GUI +} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/FileArchiveDialog.hpp b/src/slic3r/GUI/FileArchiveDialog.hpp new file mode 100644 index 0000000000..f4b7c037e1 --- /dev/null +++ b/src/slic3r/GUI/FileArchiveDialog.hpp @@ -0,0 +1,131 @@ +///|/ Copyright (c) Prusa Research 2023 David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_GUI_FileArchiveDialog_hpp_ +#define slic3r_GUI_FileArchiveDialog_hpp_ + +#include "GUI_Utils.hpp" +#include "libslic3r/miniz_extension.hpp" + +#include +#include +#include +#include +#include "wxExtensions.hpp" + +namespace Slic3r { +namespace GUI { + +class ArchiveViewCtrl; + +class ArchiveViewNode +{ +public: + ArchiveViewNode(const wxString& name) : m_name(name) {} + + std::vector>& get_children() { return m_children; } + void set_parent(std::shared_ptr parent) { m_parent = parent; } + // On Linux, get_parent cannot just return size of m_children. ItemAdded would than crash. + std::shared_ptr get_parent() const { return m_parent; } + bool is_container() const { return m_container; } + void set_container(bool is_container) { m_container = is_container; } + wxString get_name() const { return m_name; } + void set_name(const wxString& name) { m_name = name; } + bool get_toggle() const { return m_toggle; } + void set_toggle(bool toggle) { m_toggle = toggle; } + bool get_is_folder() const { return m_folder; } + void set_is_folder(bool is_folder) { m_folder = is_folder; } + void set_fullpath(boost::filesystem::path path) { m_fullpath = path; } + boost::filesystem::path get_fullpath() const { return m_fullpath; } + void set_size(size_t size) { m_size = size; } + size_t get_size() const { return m_size; } + +private: + wxString m_name; + std::shared_ptr m_parent { nullptr }; + std::vector> m_children; + + bool m_toggle { false }; + bool m_folder { false }; + boost::filesystem::path m_fullpath; + bool m_container { false }; + size_t m_size { 0 }; +}; + +class ArchiveViewModel : public wxDataViewModel +{ +public: + ArchiveViewModel(wxWindow* parent); + ~ArchiveViewModel(); + + /* wxDataViewItem AddFolder(wxDataViewItem& parent, wxString name); + wxDataViewItem AddFile(wxDataViewItem& parent, wxString name);*/ + + std::shared_ptr AddFile(std::shared_ptr parent,const wxString& name, bool container); + + wxString GetColumnType(unsigned int col) const override; + unsigned int GetColumnCount() const override { return 2; } + + void Rescale(); + void Delete(const wxDataViewItem& item); + void Clear(); + + wxDataViewItem GetParent(const wxDataViewItem& item) const override; + unsigned int GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const override; + + void SetAssociatedControl(ArchiveViewCtrl* ctrl) { m_ctrl = ctrl; } + + void GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const override; + bool SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) override; + + void untoggle_folders(const wxDataViewItem& item); + + bool IsEnabled(const wxDataViewItem& item, unsigned int col) const override; + bool IsContainer(const wxDataViewItem& item) const override; + // Is the container just a header or an item with all columns + // In our case it is an item with all columns + bool HasContainerColumns(const wxDataViewItem& WXUNUSED(item)) const override { return true; } + +protected: + wxWindow* m_parent { nullptr }; + ArchiveViewCtrl* m_ctrl { nullptr }; + std::vector> m_top_children; +}; + +class ArchiveViewCtrl : public wxDataViewCtrl +{ + public: + ArchiveViewCtrl(wxWindow* parent, wxSize size); + ~ArchiveViewCtrl(); + + ArchiveViewModel* get_model() const {return m_model; } +protected: + ArchiveViewModel* m_model; +}; + + +class FileArchiveDialog : public DPIDialog +{ +public: + FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* archive, std::vector>& selected_paths_w_size); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + + void on_open_button(); + void on_all_button(); + void on_none_button(); + + wxBoxSizer* create_btn_sizer(); + + // chosen files are written into this vector and returned to caller via reference. + // path in archive and decompressed size. The size can be used to distinguish between files with same path. + std::vector>& m_selected_paths_w_size; + ArchiveViewCtrl* m_avc; + std::vector m_button_list; +}; + +} // namespace GU +} // namespace Slic3r +#endif // slic3r_GUI_FileArchiveDialog_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 98012eb163..46b0c4bc23 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -7,6 +7,7 @@ #include "slic3r/GUI/TaskManager.hpp" #include "format.hpp" #include "libslic3r_version.h" +#include "Downloader.hpp" // Localization headers: include libslic3r version first so everything in this file // uses the slic3r/GUI version (the macros will take precedence over the functions). @@ -517,6 +518,7 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_MODEL */ {"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv}}, #endif + /* FT_ZIP */ { "ZIP files"sv, { ".zip"sv } }, /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv} }, /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, @@ -816,18 +818,25 @@ void GUI_App::post_init() if (this->init_params->input_files.size() == 1 && - boost::starts_with(this->init_params->input_files.front(), "orcaslicer://open")) { - auto input_str_arr = split_str(this->init_params->input_files.front(), "orcaslicer://open/?file="); + (boost::starts_with(this->init_params->input_files.front(), "orcaslicer://open") || + boost::starts_with(this->init_params->input_files.front(), "prusaslicer://open"))) { - std::string download_origin_url; - for (auto input_str:input_str_arr) { - if (!input_str.empty()) download_origin_url = input_str; - } + if (boost::starts_with(this->init_params->input_files.front(), "prusaslicer://open")) { + switch_to_3d = true; + start_download(this->init_params->input_files.front()); + } else if (vector input_str_arr = split_str(this->init_params->input_files.front(), "orcaslicer://open/?file="); input_str_arr.size() > 1) { + std::string download_origin_url; + for (auto input_str : input_str_arr) { + if (!input_str.empty()) + download_origin_url = input_str; + } - std::string download_file_url = url_decode(download_origin_url); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << download_file_url; - if (!download_file_url.empty() && ( boost::starts_with(download_file_url, "http://") || boost::starts_with(download_file_url, "https://")) ) { - request_model_download(download_origin_url); + std::string download_file_url = url_decode(download_origin_url); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << download_file_url; + if (!download_file_url.empty() && + (boost::starts_with(download_file_url, "http://") || boost::starts_with(download_file_url, "https://"))) { + request_model_download(download_file_url); + } } m_open_method = "makerworld"; } @@ -1089,6 +1098,7 @@ GUI_App::GUI_App() , m_em_unit(10) , m_imgui(new ImGuiWrapper()) , m_removable_drive_manager(std::make_unique()) + , m_downloader(std::make_unique()) , m_other_instance_message_handler(std::make_unique()) { //app config initializes early becasuse it is used in instance checking in OrcaSlicer.cpp @@ -2361,6 +2371,10 @@ bool GUI_App::on_init_inner() associate_files(L"step"); associate_files(L"stp"); } + // Orca: add PS url functionality + if (app_config->get_bool("ps_url_registered")) { + associate_url(L"prusaslicer"); + } if (app_config->get("associate_gcode") == "true") associate_files(L"gcode"); #endif // __WXMSW__ @@ -3576,6 +3590,17 @@ void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const dialog.GetPaths(input_files); } +void GUI_App::import_zip(wxWindow* parent, wxString& input_file) const +{ + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose ZIP file") + ":", + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_ZIP), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const { input_file.Clear(); @@ -5508,6 +5533,10 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_ associate_files(L"step"); associate_files(L"stp"); } + // Orca: add PS url functionality + if (app_config->get_bool("ps_url_registered")) { + associate_url(L"prusaslicer"); + } } else { if (app_config->get("associate_gcode") == "true") @@ -5861,7 +5890,10 @@ void GUI_App::MacOpenURL(const wxString& url) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << "get mac url " << url; - if (!url.empty() && boost::starts_with(url, "orcasliceropen://")) { + if (url.empty()) + return; + + if (boost::starts_with(url, "orcasliceropen://")) { auto input_str_arr = split_str(url.ToStdString(), "orcasliceropen://"); std::string download_origin_url; @@ -5880,7 +5912,8 @@ void GUI_App::MacOpenURL(const wxString& url) m_download_file_url = download_file_url; } } - } + } else if (boost::starts_with(url, "prusasliceropen://")) + start_download(boost::nowide::narrow(url)); } // wxWidgets override to get an event on open files. @@ -6004,6 +6037,11 @@ Model& GUI_App::model() return plater_->model(); } +Downloader* GUI_App::downloader() +{ + return m_downloader.get(); +} + void GUI_App::load_url(wxString url) { if (mainframe) @@ -6593,9 +6631,60 @@ void GUI_App::disassociate_files(std::wstring extend) ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); } +void GUI_App::associate_url(std::wstring url_prefix) +{ + // Registry key creation for "prusaslicer://" URL + + boost::filesystem::path binary_path(boost::filesystem::canonical(boost::dll::program_location())); + // the path to binary needs to be correctly saved in string with respect to localized characters + wxString wbinary = wxString::FromUTF8(binary_path.string()); + std::string binary_string = (boost::format("%1%") % wbinary).str(); + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Path of binary: " << binary_string; + + std::string key_string = "\"" + binary_string + "\" \"%1\""; + + wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix); + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + if (!key_first.Exists()) { + key_first.Create(false); + } + key_first.SetValue("URL Protocol", ""); + + if (!key_full.Exists()) { + key_full.Create(false); + } + key_full = key_string; +} + +void GUI_App::disassociate_url(std::wstring url_prefix) +{ + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + if (!key_full.Exists()) { + return; + } + key_full = ""; +} #endif // __WXMSW__ +void GUI_App::start_download(std::string url) +{ + if (!plater_) { + BOOST_LOG_TRIVIAL(error) << "Could not start URL download: plater is nullptr."; + return; + } + //lets always init so if the download dest folder was changed, new dest is used + boost::filesystem::path dest_folder(app_config->get("download_path")); + if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) { + std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard."); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return; + } + m_downloader->init(dest_folder); + m_downloader->start_download(url); +} + bool is_support_filament(int extruder_id) { auto &filament_presets = Slic3r::GUI::wxGetApp().preset_bundle->filament_presets; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 62cce8a075..dc0b8e4ff3 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -74,6 +74,7 @@ class ObjectLayers; class Plater; class ParamsPanel; class NotificationManager; +class Downloader; struct GUI_InitParams; class ParamsDialog; class HMSQuery; @@ -90,6 +91,7 @@ enum FileType FT_3MF, FT_GCODE, FT_MODEL, + FT_ZIP, FT_PROJECT, FT_GALLERY, @@ -275,6 +277,8 @@ private: std::string m_instance_hash_string; size_t m_instance_hash_int; + std::unique_ptr m_downloader; + //BBS bool m_is_closing {false}; Slic3r::DeviceManager* m_device_manager { nullptr }; @@ -423,6 +427,7 @@ private: void keyboard_shortcuts(); void load_project(wxWindow *parent, wxString& input_file) const; void import_model(wxWindow *parent, wxArrayString& input_files) const; + void import_zip(wxWindow* parent, wxString& input_file) const; void load_gcode(wxWindow* parent, wxString& input_file) const; wxString transition_tridid(int trid_id); @@ -559,6 +564,7 @@ private: ParamsDialog* params_dialog(); Model& model(); NotificationManager * notification_manager(); + Downloader* downloader(); std::string m_mall_model_download_url; @@ -644,7 +650,13 @@ private: // extend is stl/3mf/gcode/step etc void associate_files(std::wstring extend); void disassociate_files(std::wstring extend); + void associate_url(std::wstring url_prefix); + void disassociate_url(std::wstring url_prefix); #endif // __WXMSW__ + + // URL download - PrusaSlicer gets system call to open prusaslicer:// URL which should contain address of download + void start_download(std::string url); + std::string get_plugin_url(std::string name, std::string country_code); int download_plugin(std::string name, std::string package_name, InstallProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr); int install_plugin(std::string name, std::string package_name, InstallProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr); diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 74082fde43..58a97fceb0 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -136,6 +136,19 @@ static const std::map font_icons_large = { {ImGui::PrevArrowBtnIcon, "notification_arrow_left" }, {ImGui::NextArrowBtnIcon, "notification_arrow_right" }, {ImGui::CompleteIcon, "notification_slicing_complete" }, + + {ImGui::PlayButton, "notification_play" }, + {ImGui::PlayDarkButton, "notification_play_dark" }, + {ImGui::PlayHoverButton, "notification_play_hover" }, + {ImGui::PlayHoverDarkButton, "notification_play_hover_dark" }, + {ImGui::PauseButton, "notification_pause" }, + {ImGui::PauseDarkButton, "notification_pause_dark" }, + {ImGui::PauseHoverButton, "notification_pause_hover" }, + {ImGui::PauseHoverDarkButton, "notification_pause_hover_dark" }, + {ImGui::OpenButton, "notification_open" }, + {ImGui::OpenDarkButton, "notification_open_dark" }, + {ImGui::OpenHoverButton, "notification_open_hover" }, + {ImGui::OpenHoverDarkButton, "notification_open_hover_dark" }, }; static const std::map font_icons_extra_large = { diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 757fa2226b..24bc9423cb 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -588,15 +588,9 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (evt.CmdDown() && evt.GetKeyCode() == 'P') #endif { - PreferencesDialog dlg(this); - dlg.ShowModal(); + // Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit + wxGetApp().open_preferences(); plater()->get_current_canvas3D()->force_set_focus(); -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (dlg.seq_top_layer_only_changed()) -#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - plater()->refresh_print(); return; } @@ -2314,6 +2308,9 @@ void MainFrame::init_menubar_as_editor() [this](wxCommandEvent&) { if (m_plater) { m_plater->add_model(); } }, "", nullptr, [this](){return can_add_models(); }, this); #endif + append_menu_item(import_menu, wxID_ANY, _L("Import Zip Archive") + dots, _L("Load models contained within a zip archive"), + [this](wxCommandEvent&) { if (m_plater) m_plater->import_zip_archive(); }, "menu_import", nullptr, + [this]() { return can_add_models(); }); append_menu_item(import_menu, wxID_ANY, _L("Import Configs") + dots /*+ "\tCtrl+I"*/, _L("Load configs"), [this](wxCommandEvent&) { load_config_file(); }, "menu_import", nullptr, [this](){return true; }, this); @@ -2787,15 +2784,9 @@ void MainFrame::init_menubar_as_editor() append_menu_item( m_topbar->GetTopMenu(), wxID_ANY, _L("Preferences") + "\t" + ctrl + "P", "", [this](wxCommandEvent &) { - PreferencesDialog dlg(this); - dlg.ShowModal(); + // Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit + wxGetApp().open_preferences(); plater()->get_current_canvas3D()->force_set_focus(); -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (dlg.seq_top_layer_only_changed()) -#endif - plater()->refresh_print(); }, "", nullptr, []() { return true; }, this); //m_topbar->AddDropDownMenuItem(preference_item); diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 390cae058b..08ef8c7493 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1283,6 +1283,219 @@ void NotificationManager::UpdatedItemsInfoNotification::add_type(InfoItemType ty update(data); } +//------URLDownloadNotification---------------- + +void NotificationManager::URLDownloadNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + if (m_percentage < 0.f || m_percentage >= 1.f) { + render_close_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + if (m_percentage >= 1.f) + render_open_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + } else + render_pause_cancel_buttons_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); +} +void NotificationManager::URLDownloadNotification::render_close_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + // Orca: Change based on dark mode + button_text = m_is_dark ? ImGui::CloseNotifDarkButton : ImGui::CloseNotifButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + // Orca: Change based on dark mode + button_text = m_is_dark ? ImGui::CloseNotifHoverDarkButton : ImGui::CloseNotifHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + close(); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + close(); + } + ImGui::PopStyleColor(5); + +} + +void NotificationManager::URLDownloadNotification::render_pause_cancel_buttons_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + + render_cancel_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + render_pause_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); +} +void NotificationManager::URLDownloadNotification::render_pause_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + std::wstring button_text; + // Orca: Change based on dark mode + button_text = m_is_dark ? (m_download_paused ? ImGui::PlayDarkButton : ImGui::PauseDarkButton) : (m_download_paused ? ImGui::PlayButton : ImGui::PauseButton); + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y), + ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y), + true)) + { + // Orca: Change based on dark mode + button_text = m_is_dark ? (m_download_paused ? ImGui::PlayHoverDarkButton : ImGui::PauseHoverDarkButton) : (m_download_paused ? ImGui::PlayHoverButton : ImGui::PauseHoverButton); + } + + ImVec2 button_pic_size = ImGui::CalcTextSize(boost::nowide::narrow(button_text).c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + trigger_user_action_callback(m_download_paused ? DownloaderUserAction::DownloadUserContinued : DownloaderUserAction::DownloadUserPaused); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.f, win_size.y)) + { + trigger_user_action_callback(m_download_paused ? DownloaderUserAction::DownloadUserContinued : DownloaderUserAction::DownloadUserPaused); + } + ImGui::PopStyleColor(5); +} + +void NotificationManager::URLDownloadNotification::render_open_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + std::wstring button_text; + // Orca: Change based on dark mode + button_text = m_is_dark ? ImGui::OpenDarkButton : ImGui::OpenButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y), + ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y), + true)) + { + // Orca: Change based on dark mode + button_text = m_is_dark ? ImGui::OpenHoverDarkButton : ImGui::OpenHoverButton; + } + + ImVec2 button_pic_size = ImGui::CalcTextSize(boost::nowide::narrow(button_text).c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserOpenedFolder); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.f, win_size.y)) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserOpenedFolder); + } + ImGui::PopStyleColor(5); +} + +void NotificationManager::URLDownloadNotification::render_cancel_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CancelButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CancelHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserCanceled); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserCanceled); + } + ImGui::PopStyleColor(5); + +} + +void NotificationManager::URLDownloadNotification::trigger_user_action_callback(DownloaderUserAction action) +{ + if (m_user_action_callback) { + if (m_user_action_callback(action, m_download_id)) {} + } +} + + +void NotificationManager::URLDownloadNotification::render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + std::string text; + if (m_percentage < 0.f) { + text = _u8L("ERROR") + ": " + m_error_message; + } else if (m_percentage >= 1.f) { + text = _u8L("COMPLETED"); + } else { + std::stringstream stream; + stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "%"; + text = stream.str(); + } + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4)); + imgui.text(text.c_str()); +} + +void NotificationManager::URLDownloadNotification::count_spaces() +{ + ProgressBarNotification::count_spaces(); + m_window_width_offset = m_line_height * 6; +} + //------PrintHostUploadNotification---------------- void NotificationManager::PrintHostUploadNotification::init() { @@ -1841,6 +2054,81 @@ void NotificationManager::push_import_finished_notification(const std::string& p set_slicing_progress_hidden(); } +void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function user_action_callback) +{ + // If already exists + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload && dynamic_cast(notification.get())->get_download_id() == id) { + return; + } + } + // push new one + NotificationData data{ NotificationType::URLDownload, NotificationLevel::ProgressBarNotificationLevel, 5, _u8L("Download") + ": " + text }; + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, id, user_action_callback), 0); +} + +void NotificationManager::set_download_URL_progress(size_t id, float percentage) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + // if this changes the percentage, it should be shown now + float percent_b4 = ntf->get_percentage(); + ntf->set_percentage(percentage); + ntf->set_paused(false); + if (ntf->get_percentage() != percent_b4) + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} + +void NotificationManager::set_download_URL_paused(size_t id) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + ntf->set_paused(true); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} + +void NotificationManager::set_download_URL_canceled(size_t id) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + ntf->close(); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} +void NotificationManager::set_download_URL_error(size_t id, const std::string& text) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + float percent_b4 = ntf->get_percentage(); + ntf->set_percentage(-1.f); + ntf->set_error_message(text); + if (ntf->get_percentage() != percent_b4) + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} + void NotificationManager::push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage) { // find if upload with same id was not already in notification diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 68e26f4a0e..d971fd2d74 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -11,6 +11,7 @@ #include "Event.hpp" #include "I18N.hpp" #include "Jobs/ProgressIndicator.hpp" +#include "Downloader.hpp" #include #include @@ -129,6 +130,8 @@ enum class NotificationType NetfabbFinished, // Short meesage to fill space between start and finish of export ExportOngoing, + // Progressbar of download from prusaslicer://url + URLDownload, // BBS: Short meesage to fill space between start and finish of arranging ArrangeOngoing, // BBL: Plate Info ,Design For @YangLeDuo @@ -247,6 +250,14 @@ public: // Exporting finished, show this information with path, button to open containing folder and if ejectable - eject button void push_exporting_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable); void push_import_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable); + + // Download URL progress notif + void push_download_URL_progress_notification(size_t id, const std::string& text, std::function user_action_callback); + void set_download_URL_progress(size_t id, float percentage); + void set_download_URL_paused(size_t id); + void set_download_URL_canceled(size_t id); + void set_download_URL_error(size_t id, const std::string& text); + // notifications with progress bar // slicing progress void init_slicing_progress_notification(std::function cancel_callback); @@ -593,6 +604,7 @@ private: ProgressBarNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler) : PopNotification(n, id_provider, evt_handler) { } virtual void set_percentage(float percent) { m_percentage = percent; } + float get_percentage() const { return m_percentage; } protected: virtual void init() override; virtual void render_text(ImGuiWrapper& imgui, @@ -615,6 +627,62 @@ private: }; + class URLDownloadNotification : public ProgressBarNotification + { + public: + URLDownloadNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, size_t download_id, std::function user_action_callback) + //: ProgressBarWithCancelNotification(n, id_provider, evt_handler, cancel_callback) + : ProgressBarNotification(n, id_provider, evt_handler) + , m_download_id(download_id) + , m_user_action_callback(user_action_callback) + { + } + void set_percentage(float percent) override + { + m_percentage = percent; + if (m_percentage >= 1.f) { + m_notification_start = GLCanvas3D::timestamp_now(); + m_state = EState::Shown; + } else + m_state = EState::NotFading; + } + size_t get_download_id() { return m_download_id; } + void set_user_action_callback(std::function user_action_callback) { m_user_action_callback = user_action_callback; } + void set_paused(bool paused) { m_download_paused = paused; } + void set_error_message(const std::string& message) { m_error_message = message; } + bool compare_text(const std::string& text) const override { return false; }; + protected: + void render_close_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void render_close_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_pause_cancel_buttons_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_open_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_cancel_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_pause_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_bar(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void trigger_user_action_callback(DownloaderUserAction action); + + void count_spaces() override; + + size_t m_download_id; + std::function m_user_action_callback; + bool m_download_paused {false}; + std::string m_error_message; + }; + class PrintHostUploadNotification : public ProgressBarNotification { public: diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 59381b93f3..abb5f33ddf 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -156,6 +156,7 @@ #include #include // Needs to be last because reasons :-/ +#include #include "WipeTowerDialog.hpp" #include "ObjColorDialog.hpp" @@ -168,6 +169,7 @@ #include "PlateSettingsDialog.hpp" #include "DailyTips.hpp" #include "CreatePresetsDialog.hpp" +#include "FileArchiveDialog.hpp" using boost::optional; namespace fs = boost::filesystem; @@ -8996,8 +8998,9 @@ void Plater::import_model_id(wxString download_info) /* prepare project and profile */ boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &cancel, &retry_count, max_retries, &msg, &target_path, &download_ok, download_url, &filename] { - NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); - if (!m_agent) return; + // Orca: NetworkAgent is not needed and only prevents this from running +// NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); +// if (!m_agent) return; int res = 0; unsigned int http_code; @@ -9148,7 +9151,11 @@ void Plater::import_model_id(wxString download_info) if (download_ok) { BOOST_LOG_TRIVIAL(trace) << "import_model_id: target_path = " << target_path.string(); /* load project */ - this->load_project(target_path.wstring()); + // Orca: If download is a zip file, treat it as if file has been drag and dropped on the plater + if (target_path.extension() == ".zip") + this->load_files(wxArrayString(1, target_path.string())); + else + this->load_project(target_path.wstring()); /*BBS set project info after load project, project info is reset in load project */ //p->project.project_model_id = model_id; //p->project.project_design_id = design_id; @@ -9818,6 +9825,19 @@ void Plater::calib_VFA(const Calib_Params& params) p->background_process.fff_print()->set_calib_params(params); } BuildVolume_Type Plater::get_build_volume_type() const { return p->bed.get_build_volume_type(); } + +void Plater::import_zip_archive() +{ + wxString input_file; + wxGetApp().import_zip(this, input_file); + if (input_file.empty()) + return; + + wxArrayString arr; + arr.Add(input_file); + load_files(arr); +} + void Plater::import_sl1_archive() { auto &w = get_ui_job_worker(); @@ -10003,6 +10023,186 @@ std::vector Plater::load_files(const std::vector& input_fil return p->load_files(paths, strategy, ask_multi); } +bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) +{ + //std::vector unzipped_paths; + std::vector non_project_paths; + std::vector project_paths; + try + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, archive_path.string())) { + // TRN %1% is archive path + std::string err_msg = GUI::format(_u8L("Loading of a ZIP archive on path %1% has failed."), archive_path.string()); + throw Slic3r::FileIOError(err_msg); + } + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + mz_zip_archive_file_stat stat; + // selected_paths contains paths and its uncompressed size. The size is used to distinguish between files with same path. + std::vector> selected_paths; + FileArchiveDialog dlg(static_cast(wxGetApp().mainframe), &archive, selected_paths); + if (dlg.ShowModal() == wxID_OK) + { + std::string archive_path_string = archive_path.string(); + archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); + fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data()); + + for (auto& path_w_size : selected_paths) { + const fs::path& path = path_w_size.first; + size_t size = path_w_size.second; + // find path in zip archive + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + if (size != stat.m_uncomp_size) // size must fit + continue; + wxString wname = boost::nowide::widen(stat.m_filename); + std::string name = boost::nowide::narrow(wname); + fs::path archive_path(name); + + std::string extra(1024, 0); + size_t extra_size = mz_zip_reader_get_filename_from_extra(&archive, i, extra.data(), extra.size()); + if (extra_size > 0) { + archive_path = fs::path(extra.substr(0, extra_size)); + name = archive_path.string(); + } + + if (archive_path.empty()) + continue; + if (path != archive_path) + continue; + // decompressing + try + { + std::replace(name.begin(), name.end(), '\\', '/'); + // rename if file exists + std::string filename = path.filename().string(); + std::string extension = boost::filesystem::extension(path); + std::string just_filename = filename.substr(0, filename.size() - extension.size()); + std::string final_filename = just_filename; + + size_t version = 0; + while (fs::exists(archive_dir / (final_filename + extension))) + { + ++version; + final_filename = just_filename + "(" + std::to_string(version) + ")"; + } + filename = final_filename + extension; + fs::path final_path = archive_dir / filename; + std::string buffer((size_t)stat.m_uncomp_size, 0); + // Decompress action. We already has correct file index in stat structure. + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + // TRN: First argument = path to file, second argument = error description + wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive))); + BOOST_LOG_TRIVIAL(error) << error_log; + show_error(nullptr, error_log); + break; + } + // write buffer to file + fs::fstream file(final_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(buffer.c_str(), buffer.size()); + file.close(); + if (!fs::exists(final_path)) { + wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string()); + BOOST_LOG_TRIVIAL(error) << error_log; + show_error(nullptr, error_log); + break; + } + BOOST_LOG_TRIVIAL(info) << "Unzipped " << final_path; + if (!boost::algorithm::iends_with(filename, ".3mf") && !boost::algorithm::iends_with(filename, ".amf")) { + non_project_paths.emplace_back(final_path); + break; + } + // if 3mf - read archive headers to find project file + if (/*(boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) ||*/ + (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) { + non_project_paths.emplace_back(final_path); + break; + } + + project_paths.emplace_back(final_path); + break; + } + catch (const std::exception& e) + { + // ensure the zip archive is closed and rethrow the exception + close_zip_reader(&archive); + throw Slic3r::FileIOError(e.what()); + } + } + } + } + close_zip_reader(&archive); + if (non_project_paths.size() + project_paths.size() != selected_paths.size()) + BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " + << selected_paths.size() + << " Decopressed files: " + << non_project_paths.size() + project_paths.size(); + } else { + close_zip_reader(&archive); + return false; + } + + } + catch (const Slic3r::FileIOError& e) { + // zip reader should be already closed or not even opened + GUI::show_error(this, e.what()); + return false; + } + // none selected + if (project_paths.empty() && non_project_paths.empty()) + { + return false; + } + + // 1 project file and some models - behave like drag n drop of 3mf and then load models + if (project_paths.size() == 1) + { + wxArrayString aux; + aux.Add(from_u8(project_paths.front().string())); + bool loaded3mf = load_files(aux); + load_files(non_project_paths, LoadStrategy::LoadModel); + boost::system::error_code ec; + if (loaded3mf) { + fs::remove(project_paths.front(), ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + for (const fs::path& path : non_project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + return true; + } + + // load all projects and all models as geometry + load_files(project_paths, LoadStrategy::LoadModel); + load_files(non_project_paths, LoadStrategy::LoadModel); + + + for (const fs::path& path : project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + for (const fs::path& path : non_project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + + return true; +} + class RadioBox; class RadioSelector { @@ -10341,7 +10541,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) //BBS: remove GCodeViewer as seperate APP logic bool Plater::load_files(const wxArrayString& filenames) { - const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg)", std::regex::icase); + const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip)", std::regex::icase); const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase); std::vector normal_paths; @@ -10431,6 +10631,21 @@ bool Plater::load_files(const wxArrayString& filenames) } } + // Orca: Iters through given paths and imports files from zip then remove zip from paths + // returns true if zip files were found + auto handle_zips = [this](vector& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly + bool res = false; + for (auto it = paths.begin(); it != paths.end();) { + if (boost::algorithm::iends_with(it->string(), ".zip")) { + res = true; + preview_zip_archive(*it); + it = paths.erase(it); + } else + it++; + } + return res; + }; + switch (loadfiles_type) { case LoadFilesType::Single3MF: open_3mf_file(normal_paths[0]); @@ -10438,6 +10653,7 @@ bool Plater::load_files(const wxArrayString& filenames) case LoadFilesType::SingleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); + if (handle_zips(normal_paths)) return true; if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { res = false; } break; } @@ -10453,6 +10669,9 @@ bool Plater::load_files(const wxArrayString& filenames) case LoadFilesType::MultipleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); + if (handle_zips(normal_paths)) { + if (normal_paths.empty()) return true; + } if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { res = false; } break; } @@ -10471,6 +10690,9 @@ bool Plater::load_files(const wxArrayString& filenames) open_3mf_file(first_file[0]); if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { res = false; } + if (res && handle_zips(other_file)) { + if (normal_paths.empty()) return true; + } if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { res = false; } break; default: break; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e03fd3d60c..dd9c58fd57 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -262,6 +262,7 @@ public: int get_3mf_file_count(std::vector paths); void add_file(); void add_model(bool imperial_units = false, std::string fname = ""); + void import_zip_archive(); void import_sl1_archive(); void extract_config_from_project(); void load_gcode(); @@ -298,6 +299,8 @@ public: static wxColour get_next_color_for_filament(); static wxString get_slice_warning_string(GCodeProcessorResult::SliceWarning& warning); + bool preview_zip_archive(const boost::filesystem::path& archive_path); + // BBS: restore std::vector load_files(const std::vector& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false); // To be called when providing a list of files to the GUI slic3r on command line. @@ -803,7 +806,7 @@ private: bool m_only_gcode { false }; bool m_exported_file { false }; bool skip_thumbnail_invalid { false }; - bool m_loading_project {false }; + bool m_loading_project { false }; std::string m_preview_only_filename; int m_valid_plates_count { 0 }; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index da217703e9..42ec0a49c7 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1088,6 +1088,8 @@ wxWindow* PreferencesDialog::create_general_page() //downloads auto title_downloads = create_item_title(_L("Downloads"), page, _L("Downloads")); auto item_downloads = create_item_downloads(page,50,"download_path"); + auto ps_download_url_registered = create_item_checkbox(_L("Allow downloads from Printables.com"), page, + _L("Allow downloads from Printables.com"), 50, "ps_url_registered"); //dark mode #ifdef _WIN32 @@ -1150,6 +1152,7 @@ wxWindow* PreferencesDialog::create_general_page() sizer_page->Add(title_downloads, 0, wxTOP| wxEXPAND, FromDIP(20)); sizer_page->Add(item_downloads, 0, wxEXPAND, FromDIP(3)); + sizer_page->Add(ps_download_url_registered, 0, wxTOP | wxEXPAND, FromDIP(20)); #ifdef _WIN32 sizer_page->Add(title_darkmode, 0, wxTOP | wxEXPAND, FromDIP(20)); diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 12ff954002..88d9acf934 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -138,6 +138,7 @@ struct Http::priv void set_post_body(const std::string &body); void set_put_body(const fs::path &path); void set_del_body(const std::string& body); + void set_range(const std::string &range); std::string curl_error(CURLcode curlcode); std::string body_size_error(); @@ -237,7 +238,7 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o curl_easy_getinfo(self->curl, CURLINFO_SPEED_UPLOAD, &speed); if (speed > 0.01) speed = speed; - Progress progress(dltotal, dlnow, ultotal, ulnow, speed); + Progress progress(dltotal, dlnow, ultotal, ulnow, self->buffer, speed); self->progressfn(progress, cb_cancel); } @@ -370,6 +371,11 @@ void Http::priv::set_del_body(const std::string& body) postfields = body; } +void Http::priv::set_range(const std::string& range) +{ + ::curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str()); +} + std::string Http::priv::curl_error(CURLcode curlcode) { return (boost::format("curl:%1%:\n%2%\n[Error %3%]") @@ -434,7 +440,7 @@ void Http::priv::http_perform() if (res == CURLE_ABORTED_BY_CALLBACK) { if (cancel) { // The abort comes from the request being cancelled programatically - Progress dummyprogress(0, 0, 0, 0); + Progress dummyprogress(0, 0, 0, 0, std::string()); bool cancel = true; if (progressfn) { progressfn(dummyprogress, cancel); } } else { @@ -510,6 +516,12 @@ Http& Http::size_limit(size_t sizeLimit) return *this; } +Http& Http::set_range(const std::string& range) +{ + if (p) { p->set_range(range); } + return *this; +} + Http& Http::header(std::string name, const std::string &value) { if (!p) { return * this; } diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index c7f7ac2cd9..1218a562a6 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -42,14 +42,15 @@ public: size_t dlnow; // Bytes downloaded so far size_t ultotal; // Total bytes to upload size_t ulnow; // Bytes uploaded so far + const std::string& buffer; // reference to buffer containing all data double upload_spd{0.0f}; - Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) : - dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow) + Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow, const std::string& buffer) : + dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow), buffer(buffer) {} - Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow, double ulspd) : - dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow), upload_spd(ulspd) + Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow, const std::string& buffer, double ulspd) : + dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow), buffer(buffer), upload_spd(ulspd) {} }; @@ -102,6 +103,8 @@ public: // Sets a maximum size of the data that can be received. // A value of zero sets the default limit, which is is 5MB. Http& size_limit(size_t sizeLimit); + // range of donloaded bytes. example: curl_easy_setopt(curl, CURLOPT_RANGE, "0-199"); + Http& set_range(const std::string& range); // Sets a HTTP header field. Http& header(std::string name, const std::string &value); // Removes a header field.