mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
* Save device url in all cases and load printer url after hot-reload finishes * Recreate web view from scratch as only URL fix seems not robust enough * Add the same robust browser recreation for WebViewDialog It should eliminate possible issue with blank Home and other pages in the same way as Printer page * Remove redundant fallback leftover * Fix webview reset state and replay Project info on page reload The first-show webview reset now runs only on Windows, reloads the last printer URL and resets the Project page's ready state. The Project tab replays its 3MF info whenever the page reloads, so it no longer goes blank after a theme switch or a slow first load. The Device tab no longer loads an extra time on first open, and the Home tab no longer navigates twice. NeedsRecreateOnShow() logs is_recreating_gui so one language switch shows whether the reset ever fires. * Build plugin pages on first show A language switch rebuilt every plugin page's browser while the main window was being recreated, which left plugin tabs blank on Windows. Plugin pages are now lazy pages, never prebuilt, and are removed left to right so removing pages never builds one only to destroy it. A plugin page's script now starts when its tab is first opened; messages posted before that are dropped. --------- Co-authored-by: SoftFever <softfeverever@gmail.com> Co-authored-by: Noisyfox <timemanager.rick@gmail.com> Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
554 lines
18 KiB
C++
554 lines
18 KiB
C++
#include "Tab.hpp"
|
|
#include "Project.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
#include "libslic3r/Model.hpp"
|
|
#include "libslic3r/Format/bbs_3mf.hpp"
|
|
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <atomic>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <boost/filesystem/directory.hpp>
|
|
#include <array>
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/algorithm/string/case_conv.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
#include <vector>
|
|
#include <string>
|
|
#include "slic3r/GUI/GUI.hpp"
|
|
#include "slic3r/GUI/Auxiliary.hpp"
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <map>
|
|
#include "slic3r/GUI/Printer/PrinterFileSystem.h"
|
|
#include <exception>
|
|
#include <cmath>
|
|
#include <fstream>
|
|
#include <ios>
|
|
#include <sstream>
|
|
#include <wx/app.h>
|
|
#include <wx/base64.h>
|
|
#include <wx/button.h>
|
|
#include <wx/event.h>
|
|
#include <wx/gdicmn.h>
|
|
#include <wx/panel.h>
|
|
#include <wx/filefn.h>
|
|
#include <wx/scrolwin.h>
|
|
#include <wx/sizer.h>
|
|
|
|
#include <wx/bmpcbox.h>
|
|
#include <wx/bmpbuttn.h>
|
|
#include <wx/string.h>
|
|
#include <wx/treectrl.h>
|
|
#include <wx/imaglist.h>
|
|
#include <wx/settings.h>
|
|
#include <wx/filedlg.h>
|
|
#include <wx/webview.h>
|
|
#include <wx/utils.h>
|
|
#include <wx/wupdlock.h>
|
|
#include <wx/dataview.h>
|
|
#include <wx/tokenzr.h>
|
|
#include <wx/arrstr.h>
|
|
#include <wx/tglbtn.h>
|
|
|
|
#include "wxExtensions.hpp"
|
|
#include "GUI_App.hpp"
|
|
#include "GUI_ObjectList.hpp"
|
|
#include "MainFrame.hpp"
|
|
#include <slic3r/GUI/Widgets/WebView.hpp>
|
|
|
|
namespace Slic3r { namespace GUI {
|
|
|
|
wxDEFINE_EVENT(EVT_PROJECT_RELOAD, wxCommandEvent);
|
|
|
|
const std::vector<std::string> license_list = {
|
|
"BSD License",
|
|
"Apache License",
|
|
"GPL License",
|
|
"LGPL License",
|
|
"MIT License",
|
|
"CC License"
|
|
};
|
|
|
|
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
|
|
{
|
|
SetBackgroundColour(*wxWHITE);
|
|
m_project_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/model/index.html");
|
|
wxString strlang = wxGetApp().current_language_code_safe();
|
|
if (strlang != "")
|
|
m_project_home_url += "?lang=" + strlang;
|
|
|
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
create_browser();
|
|
m_reset_on_show = WebView::NeedsRecreateOnShow();
|
|
//m_browser->Hide();
|
|
main_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
|
|
|
Bind(EVT_PROJECT_RELOAD, &ProjectPanel::on_reload, this);
|
|
|
|
m_auxiliary = new AuxiliaryPanel(this);
|
|
m_auxiliary->Hide();
|
|
main_sizer->Add(m_auxiliary, wxSizerFlags().Expand().Proportion(1));
|
|
add_build_steps_of(*m_auxiliary);
|
|
Bind(EVT_AUXILIARY_DONE, [this](wxCommandEvent& e) { update_model_data();});
|
|
|
|
SetSizer(main_sizer);
|
|
Layout();
|
|
Fit();
|
|
}
|
|
|
|
ProjectPanel::~ProjectPanel()
|
|
{
|
|
shutdown();
|
|
}
|
|
|
|
void ProjectPanel::shutdown()
|
|
{
|
|
m_reload_cancel_token->store(true, std::memory_order_release);
|
|
if (m_reload_task && m_reload_task->joinable())
|
|
m_reload_task->join();
|
|
m_reload_task.reset();
|
|
}
|
|
|
|
void ProjectPanel::create_browser()
|
|
{
|
|
m_browser = WebView::CreateWebView(this, m_project_home_url);
|
|
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &ProjectPanel::on_navigated, this);
|
|
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ProjectPanel::OnScriptMessage, this, m_browser->GetId());
|
|
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATING, &ProjectPanel::onWebNavigating, this);
|
|
}
|
|
|
|
void ProjectPanel::reset_browser()
|
|
{
|
|
m_browser->Destroy(); // also removes it from the sizer
|
|
create_browser();
|
|
GetSizer()->Insert(0, m_browser, wxSizerFlags().Expand().Proportion(1));
|
|
Layout();
|
|
m_web_init_completed.store(false, std::memory_order_release);
|
|
}
|
|
|
|
// Helper to convert newlines to <br>
|
|
static std::string convert_newlines_to_br(const std::string& text) {
|
|
std::string result = text;
|
|
size_t pos = 0;
|
|
while ((pos = result.find('\n', pos)) != std::string::npos) {
|
|
result.replace(pos, 1, "<br>");
|
|
pos += 4;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ProjectPanel::onWebNavigating(wxWebViewEvent& evt)
|
|
{
|
|
wxString tmpUrl = evt.GetURL();
|
|
//wxString NowUrl = m_browser->GetCurrentURL();
|
|
|
|
if (boost::starts_with(tmpUrl, "http://") || boost::starts_with(tmpUrl, "https://")) {
|
|
m_browser->Stop();
|
|
evt.Veto();
|
|
wxLaunchDefaultApplication(tmpUrl);
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::on_reload(wxCommandEvent& evt)
|
|
{
|
|
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
|
|
m_reload_cancel_token->load(std::memory_order_acquire))
|
|
return;
|
|
|
|
if (m_reload_task && m_reload_task->joinable())
|
|
m_reload_task->join();
|
|
|
|
const auto cancel_token = m_reload_cancel_token;
|
|
m_reload_task = std::make_unique<boost::thread>([this, cancel_token] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
std::string update_type;
|
|
std::string license;
|
|
std::string model_name;
|
|
std::string model_author;
|
|
std::string cover_file;
|
|
std::string description;
|
|
std::string p_name;
|
|
std::string p_author;
|
|
std::string p_description;
|
|
std::string p_cover_file;
|
|
|
|
std::map<std::string, std::vector<json>> files;
|
|
|
|
if (wxGetApp().plater() == nullptr)
|
|
return;
|
|
|
|
Model model = wxGetApp().plater()->model();
|
|
|
|
auto model_info = model.model_info;
|
|
if (model_info != nullptr) {
|
|
license = model_info->license;
|
|
model_name = model_info->model_name;
|
|
cover_file = model_info->cover_file;
|
|
description = model_info->description;
|
|
update_type = model_info->origin;
|
|
|
|
try {
|
|
if (!model_info->copyright.empty()) {
|
|
json copy_right = json::parse(model_info->copyright);
|
|
|
|
if (copy_right.is_array()) {
|
|
for (auto it = copy_right.begin(); it != copy_right.end(); it++) {
|
|
if ((*it).contains("author")) {
|
|
model_author = (*it)["author"].get<std::string>();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
;
|
|
}
|
|
}
|
|
|
|
if (model_author.empty() && model.design_info != nullptr)
|
|
model_author = model.design_info->Designer;
|
|
|
|
if (model.profile_info != nullptr) {
|
|
p_name = model.profile_info->ProfileTile;
|
|
p_description = model.profile_info->ProfileDescription;
|
|
p_cover_file = model.profile_info->ProfileCover;
|
|
p_author = model.profile_info->ProfileUserName;
|
|
}
|
|
|
|
// file info
|
|
std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str());
|
|
if (!file_path.empty()) {
|
|
files = Reload(file_path);
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
|
|
wxGetApp().CallAfter([this, cancel_token, file_path, files] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
m_auxiliary->Reload(file_path, files);
|
|
});
|
|
} else {
|
|
clear_model_info();
|
|
return;
|
|
}
|
|
|
|
bool has_content = false;
|
|
for (const string& v : {
|
|
update_type,
|
|
license,
|
|
model_name,
|
|
model_author,
|
|
cover_file,
|
|
description,
|
|
p_name,
|
|
p_author,
|
|
p_description,
|
|
p_cover_file,
|
|
}) {
|
|
if (!v.empty()) {
|
|
has_content = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!has_content) {
|
|
for (const auto & file : files) {
|
|
if (!file.second.empty()) {
|
|
has_content = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!has_content) {
|
|
// Nothing to show, just return
|
|
clear_model_info();
|
|
return;
|
|
}
|
|
|
|
json j;
|
|
j["model"]["license"] = license;
|
|
j["model"]["name"] = wxGetApp().url_encode(model_name);
|
|
j["model"]["author"] = wxGetApp().url_encode(model_author);;
|
|
j["model"]["cover_img"] = wxGetApp().url_encode(cover_file);
|
|
j["model"]["description"] = wxGetApp().url_encode(convert_newlines_to_br(description));
|
|
j["model"]["preview_img"] = files["Model Pictures"];
|
|
j["model"]["upload_type"] = update_type;
|
|
|
|
j["file"]["BOM"] = files["Bill of Materials"];
|
|
j["file"]["Assembly"] = files["Assembly Guide"];
|
|
j["file"]["Other"] = files["Others"];
|
|
|
|
j["profile"]["name"] = wxGetApp().url_encode(p_name);
|
|
j["profile"]["author"] = wxGetApp().url_encode(p_author);
|
|
j["profile"]["description"] = wxGetApp().url_encode(p_description);
|
|
j["profile"]["cover_img"] = wxGetApp().url_encode(p_cover_file);
|
|
j["profile"]["preview_img"] = files["Profile Pictures"];
|
|
|
|
json m_Res = json::object();
|
|
m_Res["command"] = "show_3mf_info";
|
|
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
|
|
m_Res["model"] = j;
|
|
|
|
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
|
|
|
|
if (!cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
|
|
wxGetApp().CallAfter([this, cancel_token, strJS] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
m_info_script = strJS.ToStdString();
|
|
if (m_web_init_completed.load(std::memory_order_acquire))
|
|
RunScript(m_info_script);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void ProjectPanel::msw_rescale()
|
|
{
|
|
m_auxiliary->msw_rescale();
|
|
}
|
|
|
|
void ProjectPanel::on_size(wxSizeEvent &event)
|
|
{
|
|
event.Skip();
|
|
}
|
|
|
|
void ProjectPanel::on_navigated(wxWebViewEvent& event)
|
|
{
|
|
event.Skip();
|
|
}
|
|
|
|
void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
|
{
|
|
try {
|
|
wxString strInput = evt.GetString();
|
|
json j = json::parse(strInput.utf8_string());
|
|
|
|
wxString strCmd = j["command"];
|
|
|
|
if (strCmd == "open_3mf_accessory") {
|
|
wxString accessory_path = j["accessory_path"];
|
|
|
|
if (!accessory_path.empty()) {
|
|
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
|
fs::path path(decode_path);
|
|
if (!desktop_open_project_attachment(this, path))
|
|
BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: not opening " << decode_path;
|
|
}
|
|
}
|
|
else if (strCmd == "request_3mf_info") {
|
|
m_web_init_completed.store(true, std::memory_order_release);
|
|
// Replay the stored info after each page load.
|
|
CallAfter([this] {
|
|
if (!m_info_script.empty())
|
|
RunScript(m_info_script);
|
|
});
|
|
}
|
|
else if (strCmd == "edit_project_info") {
|
|
show_info_editor(true);
|
|
}
|
|
else if (strCmd == "debug_info") {
|
|
//wxString msg = j["msg"];
|
|
//OutputDebugString(wxString::Format("Model_Web: msg = %s \r\n", msg));
|
|
//BOOST_LOG_TRIVIAL(info) << wxString::Format("Model_Web: msg = %s", msg);
|
|
}
|
|
|
|
}
|
|
catch (std::exception&) {
|
|
// wxMessageBox(e.what(), "json Exception", MB_OK);
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::show_info_editor(bool show)
|
|
{
|
|
m_browser->Show(!show);
|
|
m_auxiliary->Show(show);
|
|
Layout();
|
|
}
|
|
|
|
void ProjectPanel::update_model_data()
|
|
{
|
|
Model model = wxGetApp().plater()->model();
|
|
show_info_editor(false);
|
|
clear_model_info();
|
|
|
|
m_auxiliary->init_auxiliary();
|
|
|
|
//basics info
|
|
//if (model.model_info == nullptr)
|
|
// return;
|
|
|
|
auto event = wxCommandEvent(EVT_PROJECT_RELOAD);
|
|
event.SetEventObject(this);
|
|
wxPostEvent(this, event);
|
|
}
|
|
|
|
void ProjectPanel::clear_model_info()
|
|
{
|
|
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
|
|
m_reload_cancel_token->load(std::memory_order_acquire))
|
|
return;
|
|
|
|
json m_Res = json::object();
|
|
m_Res["command"] = "clear_3mf_info";
|
|
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
|
|
|
|
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
|
|
|
|
const auto cancel_token = m_reload_cancel_token;
|
|
wxGetApp().CallAfter([this, cancel_token, strJS] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
// Runs after any store queued by an earlier reload pass, so stale info is never replayed.
|
|
m_info_script.clear();
|
|
RunScript(strJS.ToStdString());
|
|
});
|
|
}
|
|
|
|
std::map<std::string, std::vector<json>> ProjectPanel::Reload(wxString aux_path)
|
|
{
|
|
std::vector<fs::path> dir_cache;
|
|
fs::directory_iterator iter_end;
|
|
std::map<std::string, std::vector<json>> m_paths_list;
|
|
|
|
const static std::array<wxString, 5> s_default_folders = {
|
|
("Model Pictures"),
|
|
("Bill of Materials"),
|
|
("Assembly Guide"),
|
|
("Others"),
|
|
//(".thumbnails"),
|
|
("Profile Pictures"),
|
|
};
|
|
|
|
for (auto folder : s_default_folders)
|
|
m_paths_list[folder.ToStdString()] = std::vector<json>{};
|
|
|
|
|
|
fs::path new_aux_path(aux_path.ToStdWstring());
|
|
|
|
// Check new path. If not exist, create a new one.
|
|
if (!fs::exists(new_aux_path)) {
|
|
fs::create_directory(new_aux_path);
|
|
}
|
|
|
|
// Create default folders if they are not loaded
|
|
for (auto folder : s_default_folders) {
|
|
wxString folder_path = aux_path + "/" + folder;
|
|
if (fs::exists(folder_path.ToStdWstring())) continue;
|
|
fs::create_directory(folder_path.ToStdWstring());
|
|
}
|
|
|
|
// Load from new path
|
|
for (fs::directory_iterator iter(new_aux_path); iter != iter_end; iter++) {
|
|
wxString path = iter->path().generic_wstring();
|
|
dir_cache.push_back(iter->path());
|
|
}
|
|
|
|
|
|
for (auto dir : dir_cache) {
|
|
for (fs::directory_iterator iter(dir); iter != iter_end; iter++) {
|
|
if (fs::is_directory(iter->path())) continue;
|
|
|
|
json pfile_obj;
|
|
|
|
std::string file_path = iter->path().string();
|
|
fs::path file_path_obj = fs::path(iter->path().string());
|
|
|
|
for (auto folder : s_default_folders) {
|
|
auto idx = file_path.find(folder.ToStdString());
|
|
if (idx != std::string::npos) {
|
|
|
|
wxStructStat strucStat;
|
|
wxString file_name = encode_path(file_path.c_str());
|
|
wxStat(file_name, &strucStat);
|
|
wxFileOffset filelen = strucStat.st_size;
|
|
|
|
pfile_obj["_filepath"] = file_path;
|
|
pfile_obj["filename"] = wxGetApp().url_encode(file_path_obj.filename().string().c_str());
|
|
pfile_obj["size"] = formatBytes((unsigned long)filelen);
|
|
|
|
std::string file_extension = file_path_obj.extension().string();
|
|
boost::algorithm::to_lower(file_extension);
|
|
|
|
//image
|
|
if (file_extension == ".jpg" ||
|
|
file_extension == ".jpeg" ||
|
|
file_extension == ".pjpeg" ||
|
|
file_extension == ".png" ||
|
|
file_extension == ".jfif" ||
|
|
file_extension == ".pjp" ||
|
|
file_extension == ".webp" ||
|
|
file_extension == ".bmp")
|
|
{
|
|
|
|
wxString base64_str = to_base64(file_path);
|
|
pfile_obj["filepath"] = base64_str.ToStdString();
|
|
m_paths_list[folder.ToStdString()].push_back(pfile_obj);
|
|
break;
|
|
}
|
|
else {
|
|
pfile_obj["filepath"] = wxGetApp().url_encode(file_path);
|
|
m_paths_list[folder.ToStdString()].push_back(pfile_obj);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return m_paths_list;
|
|
}
|
|
|
|
std::string ProjectPanel::formatBytes(unsigned long bytes)
|
|
{
|
|
double dValidData = round(double(bytes) / (1024 * 1024) * 1000) / 1000;
|
|
return wxString::Format("%.2fMB", dValidData).ToStdString();
|
|
}
|
|
|
|
wxString ProjectPanel::to_base64(std::string file_path)
|
|
{
|
|
|
|
std::ifstream imageFile(encode_path(file_path.c_str()), std::ios::binary);
|
|
if (!imageFile) {
|
|
return wxEmptyString;
|
|
}
|
|
|
|
std::ostringstream imageStream;
|
|
imageStream << imageFile.rdbuf();
|
|
|
|
std::string binaryImageData = imageStream.str();
|
|
|
|
std::string extension;
|
|
size_t last_dot = file_path.find_last_of(".");
|
|
|
|
if (last_dot != std::string::npos) {
|
|
extension = file_path.substr(last_dot + 1);
|
|
}
|
|
|
|
wxString bease64_head = wxString::Format("data:image/%s;base64,", extension);
|
|
|
|
|
|
std::wstringstream wss;
|
|
wss << bease64_head;
|
|
wss << wxBase64Encode(binaryImageData.data(), binaryImageData.size());
|
|
|
|
wxString base64_str = wss.str();
|
|
return base64_str;
|
|
}
|
|
|
|
void ProjectPanel::RunScript(std::string content)
|
|
{
|
|
WebView::RunScript(m_browser, content);
|
|
}
|
|
|
|
bool ProjectPanel::Show(bool show)
|
|
{
|
|
if (show && std::exchange(m_reset_on_show, false))
|
|
reset_browser();
|
|
if (show) update_model_data();
|
|
return wxPanel::Show(show);
|
|
}
|
|
|
|
}} // namespace Slic3r::GUI
|