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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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