From 97dcae6c6e5767b624a4a8b61780d62cbdfc9d6d Mon Sep 17 00:00:00 2001 From: yw4z Date: Sat, 6 Jun 2026 18:44:39 +0300 Subject: [PATCH] Troubleshoot Center Dialog (#12416) * init * update translations * clarify warning for rebuild * auto fill issue field on github with gathered system information * add detection for linux package type * fix build errors * update * update * improve reading windows version * add multi file support for zip and use timestamp on exported zip name * fix errors * fix errorz * fix URL encoding * fix CPU info * use home or desktop as default location * fix distro name on flatpak * improve detecting local build on linux * check package type on all platforms * optimize margins * improve monitor detection * add support for adding text file to zip and add system info on export * exclude user related info from config * improve exporting profile info * fix linux monitor info * update * update detecting monitor info * Update TroubleshootDialog.cpp * Update TroubleshootDialog.cpp * Update TroubleshootDialog.cpp * revert scaling and resolution detection for linux * include project file to zip and notify after saving zip successfully saved * improve monitor info on mac * update * improve packing selection menu * update * Update TroubleshootDialog.cpp * Update TroubleshootDialog.cpp * Update TroubleshootDialog.cpp * Update TroubleshootDialog.cpp * update * update * make hash clickable * fix compatible process counting * export profiles overview instead copying to clipboard * auto restart app after cleaning system folder --------- Co-authored-by: Noisyfox --- .../images/OrcaSlicer_horizontal_dark.svg | 1 + .../images/OrcaSlicer_horizontal_light.svg | 1 + src/libslic3r/Utils.hpp | 1 + src/libslic3r/utils.cpp | 8 + src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/GUI_App.cpp | 7 + src/slic3r/GUI/GUI_App.hpp | 1 + src/slic3r/GUI/MainFrame.cpp | 19 +- src/slic3r/GUI/TroubleshootDialog.cpp | 1399 +++++++++++++++++ src/slic3r/GUI/TroubleshootDialog.hpp | 201 +++ 10 files changed, 1635 insertions(+), 5 deletions(-) create mode 100644 resources/images/OrcaSlicer_horizontal_dark.svg create mode 100644 resources/images/OrcaSlicer_horizontal_light.svg create mode 100644 src/slic3r/GUI/TroubleshootDialog.cpp create mode 100644 src/slic3r/GUI/TroubleshootDialog.hpp diff --git a/resources/images/OrcaSlicer_horizontal_dark.svg b/resources/images/OrcaSlicer_horizontal_dark.svg new file mode 100644 index 0000000000..3e48b252b6 --- /dev/null +++ b/resources/images/OrcaSlicer_horizontal_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/OrcaSlicer_horizontal_light.svg b/resources/images/OrcaSlicer_horizontal_light.svg new file mode 100644 index 0000000000..d17593cf16 --- /dev/null +++ b/resources/images/OrcaSlicer_horizontal_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 51cb2e5787..f1bdd89712 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -194,6 +194,7 @@ std::string debug_out_path(const char *name, ...); // smaller level means less log. level=5 means saving all logs. void set_log_path_and_level(const std::string& file, unsigned int level); void flush_logs(); +boost::filesystem::path get_log_file_name(); // A special type for strings encoded in the local Windows 8-bit code page. // This type is only needed for Perl bindings to relay to Perl that the string is raw, not UTF-8 encoded. diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 675286d3a1..42dcc14878 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -392,6 +392,14 @@ void flush_logs() return; } +// ORCA +boost::filesystem::path get_log_file_name() +{ + if (g_log_sink) + return g_log_sink->locked_backend()->get_current_file_name(); + return {}; +} + #ifdef _WIN32 // The following helpers are borrowed from the LLVM project https://github.com/llvm namespace WindowsSupport diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 805cbafbd5..7acb698ee0 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -466,6 +466,8 @@ set(SLIC3R_GUI_SOURCES GUI/TextLines.hpp GUI/TickCode.cpp GUI/TickCode.hpp + GUI/TroubleshootDialog.cpp + GUI/TroubleshootDialog.hpp GUI/UnsavedChangesDialog.cpp GUI/UnsavedChangesDialog.hpp GUI/UpdateDialogs.cpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 82eeb85a10..80a2887586 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -116,6 +116,7 @@ #include "ParamsDialog.hpp" #include "KBShortcutsDialog.hpp" #include "DownloadProgressDialog.hpp" +#include "TroubleshootDialog.hpp" #include "BitmapCache.hpp" #include "Notebook.hpp" @@ -4116,6 +4117,12 @@ void GUI_App::keyboard_shortcuts() dlg.ShowModal(); } +void GUI_App::troubleshoot() +{ + TroubleshootDialog dlg; + if (dlg.ShowModal() == wxID_REMOVE) + wxGetApp().mainframe->Close(false); +} void GUI_App::ShowUserGuide() { // BBS:Show NewUser Guide diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 4d429eaadf..60ef782f51 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -467,6 +467,7 @@ public: void recreate_GUI(const wxString& message); void system_info(); void keyboard_shortcuts(); + void troubleshoot(); 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; diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 4ad146f577..f95be9f679 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2546,10 +2546,24 @@ static wxMenu* generate_help_menu() append_menu_item(helpMenu, wxID_ANY, _L("Setup Wizard"), _L("Setup Wizard"), [](wxCommandEvent &) {wxGetApp().ShowUserGuide();}); helpMenu->AppendSeparator(); + // Open Config Folder append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"), [](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); }); + helpMenu->AppendSeparator(); + + // Troubleshoot center + append_menu_item(helpMenu, wxID_ANY, _L("Troubleshoot Center"), "", + [](wxCommandEvent&) { wxGetApp().troubleshoot(); }); + + append_menu_item(helpMenu, wxID_ANY, _L("Open Network Test"), _L("Open Network Test"), [](wxCommandEvent&) { + NetworkTestDialog dlg(wxGetApp().mainframe); + dlg.ShowModal(); + }); + + helpMenu->AppendSeparator(); + append_menu_item(helpMenu, wxID_ANY, _L("Show Tip of the Day"), _L("Show Tip of the Day"), [](wxCommandEvent&) { wxGetApp().plater()->get_dailytips()->open(); wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); @@ -2568,11 +2582,6 @@ static wxMenu* generate_help_menu() return true; }); - append_menu_item(helpMenu, wxID_ANY, _L("Open Network Test"), _L("Open Network Test"), [](wxCommandEvent&) { - NetworkTestDialog dlg(wxGetApp().mainframe); - dlg.ShowModal(); - }); - // About #ifndef __APPLE__ wxString about_title = wxString::Format(_L("&About %s"), SLIC3R_APP_FULL_NAME); diff --git a/src/slic3r/GUI/TroubleshootDialog.cpp b/src/slic3r/GUI/TroubleshootDialog.cpp new file mode 100644 index 0000000000..2a617251b2 --- /dev/null +++ b/src/slic3r/GUI/TroubleshootDialog.cpp @@ -0,0 +1,1399 @@ +#include "TroubleshootDialog.hpp" +#include "I18N.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "MainFrame.hpp" + +#include +#include +#include "wx/clipbrd.h" + +#include "libslic3r/libslic3r.h" +#include "libslic3r/Utils.hpp" +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Preset.hpp" + +#include + +#ifdef __WINDOWS__ +#include +#include +#include +#endif + +#ifdef __LINUX__ +#include +#include +#include +#endif + +#ifdef __APPLE__ +#include +#define Rect Mac_Rect +#define RectPtr Mac_RectPtr +#define Point Mac_Point +#define Size Mac_Size +#include +#undef Rect +#undef RectPtr +#undef Point +#undef Size +#endif + +#include "NetworkTestDialog.hpp" + +#include "Widgets/StaticLine.hpp" +#include "Widgets/HyperLink.hpp" +#include "Widgets/Button.hpp" + +namespace Slic3r { +namespace GUI { + +wxFlexGridSizer* TroubleshootDialog::create_item_loaded_profiles() +{ + auto create_label = [this](wxString title, wxFont font = Label::Body_14) { + auto label = new Label(this, font, title); + label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + return label; + }; + + auto g_sizer = new wxFlexGridSizer(1, 6, FromDIP(3), FromDIP(15)); + + g_sizer->AddSpacer(0); + g_sizer->Add(create_label(_L("Active"), Label::Body_12), 0, wxALIGN_CENTER); + g_sizer->AddSpacer(0); + g_sizer->Add(create_label(_L("System"), Label::Body_12), 0, wxALIGN_CENTER); + g_sizer->AddSpacer(0); + g_sizer->Add(create_label(_L("User"), Label::Body_12) , 0, wxALIGN_CENTER); + + auto gen_stats = GetProfilesOverview(); + gen_stats = ""; // clear mem. not needed after generating m_..._act, m_..._usr variables + + auto add_sizer = [this, g_sizer, create_label](PresetCollection* col, wxString label, int in_use, int user) { + int sys = 0; + for (auto it = col->begin(); it != col->end(); it++) { + if (it->is_system) + sys++; + } + g_sizer->Add(create_label(label)); + g_sizer->Add(create_label(wxString::Format("%d", in_use)), 0, wxALIGN_CENTER); + g_sizer->Add(create_label("/", Label::Body_12) , 0, wxALIGN_CENTER); + g_sizer->Add(create_label(wxString::Format("%d", sys )), 0, wxALIGN_CENTER); + g_sizer->Add(create_label("+", Label::Body_12) , 0, wxALIGN_CENTER); + g_sizer->Add(create_label(wxString::Format("%d", user )), 0, wxALIGN_CENTER); + }; + + auto preset_bundle = wxGetApp().preset_bundle; + add_sizer(&preset_bundle->printers , _L("Printers") , m_printers__act, m_printers__usr); + add_sizer(&preset_bundle->filaments, _L("Filaments"), m_filaments_act, m_filaments_usr); + add_sizer(&preset_bundle->prints , _L("Processes"), m_processes_act, m_processes_usr); + + return g_sizer; +} + +ComboBox *TroubleshootDialog::create_item_log_level_combo() +{ + auto combobox = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); + + for (const auto& item : std::vector{_L("fatal"), _L("error"), _L("warning"), _L("info"), _L("debug"), _L("trace")}) + combobox->Append(item); + + auto severity_level = wxGetApp().app_config->get("log_severity_level"); + if (!severity_level.empty()) { combobox->SetValue(severity_level); } + + combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [](wxCommandEvent &e) { + auto level = Slic3r::get_string_logging_level(e.GetSelection()); + Slic3r::set_logging_level(Slic3r::level_string_to_boost(level)); + wxGetApp().app_config->set("log_severity_level",level); + e.Skip(); + }); + + return combobox; +} + +TroubleshootDialog::TroubleshootDialog() + : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, _L("Troubleshoot Center"), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) +{ + SetFont(wxGetApp().normal_font()); + SetBackgroundColour(*wxWHITE); + + auto data_dir = boost::filesystem::path(Slic3r::data_dir()); + auto app_config = wxGetApp().app_config; + bool is_dark = app_config->get("dark_color_mode") == "1"; + + // LEFT SIZER ////////////////////// + + // HEADER + m_logo = ScalableBitmap(this, is_dark ? "OrcaSlicer_horizontal_dark" : "OrcaSlicer_horizontal_light", 64); + m_header_logo = new wxStaticBitmap(this, wxID_ANY, m_logo.bmp()); + auto logo_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); + logo_line->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#009687"))); + auto version = new Label(this, wxString(SoftFever_VERSION), wxALIGN_CENTRE_HORIZONTAL); + wxFont version_font = GetFont(); + version_font = version_font.Scaled(1.65f); // SetPointSize(18) not works on macOS because it uses a 72 PPI reference + version->SetFont(version_font); + version->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + + auto build = new Button(this, wxString(GIT_COMMIT_HASH)); + build->SetStyle(ButtonStyle::Regular, ButtonType::Window); + auto hash_url = "https://github.com/OrcaSlicer/OrcaSlicer/commit/" + wxString(GIT_COMMIT_HASH); + build->SetToolTip(hash_url); + build->Bind(wxEVT_BUTTON, [hash_url](wxCommandEvent &e) { + wxLaunchDefaultBrowser(hash_url); + }); + + // SYSTEM INFO + auto sys_info_lines = [this](bool show_sys) { + if(!show_sys) + return std::vector{GetOStype()}; + return std::vector{ + GetOSinfo(), + GetPackageType(), + GetCPUinfo(), + GetRAMinfo() + " RAM", + GetGPUinfo(), + GetMONinfo() + }; + }; + + auto* sys_panel = new CenteredMultiLinePanel(this, sys_info_lines(true)); + sys_panel->SetBackgroundColour(*wxWHITE); + sys_panel->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + + auto sys_less_btn = new Button(this, _L("Hide")); + sys_less_btn->SetStyle(ButtonStyle::Regular, ButtonType::Window); + sys_less_btn->SetToolTip(_L("Show/Hide system information")); + + auto sys_copy_btn = new Button(this, _L("Copy")); + sys_copy_btn->SetStyle(ButtonStyle::Regular, ButtonType::Window); + sys_copy_btn->SetToolTip(_L("Copy system information to clipboard")); + + sys_copy_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + wxClipboardLocker lock; + if (!lock) + return false; + return wxTheClipboard->SetData(new wxTextDataObject(GetSysInfoAll())); + }); + + sys_less_btn->Bind(wxEVT_BUTTON, [this, sys_panel, sys_less_btn, sys_info_lines, sys_copy_btn](wxCommandEvent &e) { + m_sys_panel_mode = !m_sys_panel_mode; + sys_panel->SetText(sys_info_lines(m_sys_panel_mode)); + sys_less_btn->SetLabel(m_sys_panel_mode ? _L("Hide") : _L("Show")); + Layout(); + Fit(); + }); + + auto link_wiki = new HyperLink(this, _L("Wiki Guide")); + + // RIGHT SIZER ////////////////////// + + auto create_title = [this](wxString title) { + auto line = new StaticLine(this, false, title); + line->SetFont(Label::Head_16); + line->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + return line; + }; + + auto create_label = [this](wxString title, wxString tooltip, Label** label_out = nullptr) { + wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); + auto label = new Label(this, title, LB_AUTO_WRAP, wxSize(FromDIP(275),-1)); // 400 - 120 - 5 + label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + label->SetToolTip(tooltip); + sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(15)); + + if (label_out) *label_out = label; + + return sizer; + }; + + auto create_btn = [this](wxString title, wxString tooltip) { + auto btn = new Button(this, title); + btn->SetToolTip(tooltip); + btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); + return btn; + }; + + // INFORMATION + auto create_info_line = [this](wxString title) { + auto info = new Label(this, title); + info->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + info->SetMaxSize(wxSize(FromDIP(400),-1)); + info->Wrap(FromDIP(400)); + return info; + }; + + auto info_desc_1 = create_info_line(_L("We need information for diagnosing source of the issue. Check wiki page for detailed guide.")); + auto info_desc_2 = create_info_line(_L("Pack button collects project file and logs of current session onto a zip file.")); + auto info_desc_3 = create_info_line(_L("Any additional visual examples like images or screen recordings might be helpful while reporting the issue.")); + wxBoxSizer *info_desc_sizer = new wxBoxSizer(wxVERTICAL); + info_desc_sizer->Add(info_desc_1, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + info_desc_sizer->Add(info_desc_2, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + info_desc_sizer->Add(info_desc_3, 0, wxEXPAND); + + auto link_report = new HyperLink(this, _L("Report issue") + " "); + link_report->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &e) { + auto encodeStr = [](const wxString& text) { + wxString out; + const wxScopedCharBuffer utf8 = text.utf8_str(); + for (const unsigned char* p = (const unsigned char*)utf8.data(); *p; ++p) { + unsigned char c = *p; + if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') + out += (wxChar)c; + else + out += wxString::Format(wxT("%%%02X"), c); + } + return out; + }; + + wxString url = "https://github.com/OrcaSlicer/OrcaSlicer/issues/new?template=bug_report.yml"; + wxString os = GetOStype(); + if(!os.IsEmpty()) + url += "&os_type=%22" + os +"%22"; + url += "&version=" + encodeStr(wxString(SoftFever_VERSION)); + url += "&os_version=" + encodeStr(GetOSinfo()); + wxLaunchDefaultBrowser(url); + }); + + auto pack_btn = new Button(this, _L("Pack") + "..."); + pack_btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); + pack_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + PackAll(); + }); + + // PROFILES + auto prf_sys_cache_tip = _L("Cleans and rebuilds system profiles cache on next launch"); + auto prf_sys_cache_szr = create_label(_L("Clean system profiles cache"), prf_sys_cache_tip); + auto prf_sys_cache_btn = create_btn(_L("Clean"), prf_sys_cache_tip); + prf_sys_cache_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + RebuildSystemProfiles(); + }); + prf_sys_cache_szr->Add(prf_sys_cache_btn, 0, wxALIGN_CENTER_VERTICAL); + + auto prf_loaded_szr = create_label(_L("Loaded profiles overview"), _L("This section shows information for loaded profiles")); + auto prf_loaded_btn = create_btn(_L("Export") + dots, _L("Exports detailed overview of loaded profiles in json format")); + prf_loaded_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + return ExportAsJson(GetProfilesOverview(),"ProfilesOverview"); + }); + prf_loaded_szr->Add(prf_loaded_btn, 0, wxALIGN_CENTER_VERTICAL); + + auto profiles_loaded = create_item_loaded_profiles(); + + // MORE + auto cfg_folder_szr = create_label(_L("Configurations folder"), ""); + auto cfg_folder_btn = create_btn(_L("Browse") + "...", _L("Opens configurations folder")); + cfg_folder_btn->Bind(wxEVT_BUTTON, [this, data_dir](wxCommandEvent &e) { + BrowseFolder(data_dir.string()); + }); + cfg_folder_szr->Add(cfg_folder_btn, 0, wxALIGN_CENTER_VERTICAL); + + auto log_level_szr = create_label(_L("Log level"), ""); + log_level_szr->Add(create_item_log_level_combo(), 0, wxALIGN_CENTER_VERTICAL); + + auto log_pack_szr = create_label(_L("Stored logs"), _L("Packs all stored logs onto a zip file.")); + auto log_pack_btn = create_btn(_L("Pack") + "...", ""); + log_pack_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + auto data_dir = boost::filesystem::path(Slic3r::data_dir()); + ExportAsZip({wxString((data_dir / "log").string())}, "OrcaSlicer_Logs_" + GetTimestamp()); + }); + log_pack_szr->Add(log_pack_btn, 0, wxALIGN_CENTER_VERTICAL); + + auto log_clear_szr = create_label("-", "", &m_logs_storage); + auto log_clear_btn = create_btn(_L("Clear"), ""); + log_clear_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + ClearLogs(); + }); + log_clear_szr->Add(log_clear_btn, 0, wxALIGN_CENTER_VERTICAL); + + UpdateLogsStorage(); + + // LAYOUT ////////////////////// + wxBoxSizer *left_sizer = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer *sys_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + sys_btn_sizer->Add(sys_less_btn, 0, wxLEFT | wxRIGHT, FromDIP(5)); + sys_btn_sizer->AddStretchSpacer(); + sys_btn_sizer->Add(sys_copy_btn, 0, wxLEFT | wxRIGHT, FromDIP(5)); + + left_sizer->Add(m_header_logo , 0, wxEXPAND | wxALIGN_CENTER); + left_sizer->Add(logo_line , 0, wxEXPAND | wxTOP, FromDIP(12)); + left_sizer->Add(version , 0, wxEXPAND | wxTOP, FromDIP(6)); + left_sizer->Add(build , 0, wxALIGN_CENTER | wxTOP, FromDIP(4)); + left_sizer->Add(sys_panel , 0, wxEXPAND | wxTOP, FromDIP(15)); + left_sizer->AddStretchSpacer(); + left_sizer->Add(sys_btn_sizer , 0, wxEXPAND | wxTOP, FromDIP(15)); + left_sizer->Add(link_wiki , 0, wxALIGN_CENTER | wxTOP, FromDIP(15)); + left_sizer->AddSpacer(FromDIP(5)); + + wxBoxSizer *right_sizer = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer *pack_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + pack_btn_sizer->Add(link_report , 0, wxRIGHT, FromDIP(5)); + pack_btn_sizer->AddStretchSpacer(); + pack_btn_sizer->Add(pack_btn); + + right_sizer->Add(create_title(_L("Information")), 0, wxEXPAND); + right_sizer->Add(info_desc_sizer , 0, wxEXPAND | wxTOP, FromDIP(5)); + right_sizer->Add(pack_btn_sizer , 0, wxEXPAND | wxTOP, FromDIP(12)); + + right_sizer->Add(create_title(_L("Profiles")) , 0, wxEXPAND | wxTOP, FromDIP(12)); + right_sizer->Add(prf_sys_cache_szr , 0, wxEXPAND | wxTOP, FromDIP(8)); + right_sizer->Add(prf_loaded_szr , 0, wxEXPAND | wxTOP, FromDIP(5)); + right_sizer->Add(profiles_loaded , 0, wxEXPAND | wxTOP, FromDIP(5)); + + right_sizer->Add(create_title(_L("More")) , 0, wxEXPAND | wxTOP, FromDIP(12)); + right_sizer->Add(cfg_folder_szr , 0, wxEXPAND | wxTOP, FromDIP(8)); + right_sizer->Add(log_level_szr , 0, wxEXPAND | wxTOP, FromDIP(5)); + right_sizer->Add(log_pack_szr , 0, wxEXPAND | wxTOP, FromDIP(5)); + right_sizer->Add(log_clear_szr , 0, wxEXPAND | wxTOP, FromDIP(5)); + + wxBoxSizer *m_sizer = new wxBoxSizer(wxHORIZONTAL); + m_sizer->Add(left_sizer , 0, wxEXPAND | wxTOP | wxBOTTOM | wxLEFT , FromDIP(15)); + m_sizer->AddSpacer(FromDIP(20)); + m_sizer->Add(right_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, FromDIP(15)); + + SetSizer(m_sizer); + Layout(); + Fit(); + CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); +} + +wxString TroubleshootDialog::GetTimestamp() +{ + wxDateTime now = wxDateTime::Now(); + return now.Format("%Y%m%d_%H%M"); // %S +} + +wxString TroubleshootDialog::GetSysInfoAll() +{ + wxString info; + info += "Version : " + wxString(SoftFever_VERSION) + "\n" + + "Build : " + wxString(GIT_COMMIT_HASH) + "\n" + + "Package : " + GetPackageType() + "\n" + + "Platform : " + GetOSinfo() + "\n" + + "Processor : " + GetCPUinfo() + "\n" + + "Memory : " + GetRAMinfo() + "RAM" + "\n" + + "Renderer : " + GetGPUinfo() + "\n" + + "Monitors : " + GetMONinfo(); + return info; +}; + +/* +wxString TroubleshootDialog::GetConfigStr() +{ + wxString config_path = wxGetApp().app_config->config_path(); + std::ifstream file(config_path.ToUTF8().data()); + if (!file.is_open()) + return "{}"; + nlohmann::json root; + try { + file >> root; + } catch (const nlohmann::json::exception&) { + return "{}"; + } + for (const auto& key : std::vector{"recent_projects", "recent", "custom_color_list", "orca_presets"}) + root.erase(key); + if (root.contains("app")) { + for (const auto& key : std::vector{"last_backup_path", "last_export_path", "download_path", "slicer_uuid", "preset_folder"}) + root["app"].erase(key); + } + return wxString::FromUTF8(root.dump(4)); +}; +*/ + +wxString TroubleshootDialog::GetProfilesOverview() +{ + using ojson = nlohmann::ordered_json; + + auto preset_bundle = wxGetApp().preset_bundle; + auto app_config = wxGetApp().app_config; + auto vendors = app_config->vendors(); + + ojson root = ojson::object(); + + m_printers__act = 0; m_printers__usr = 0; + m_filaments_act = 0; m_filaments_usr = 0; + m_processes_act = 0; m_processes_usr = 0; + + { // OVERVIEW + root["Overview"] = ""; + } + { // PRINTERS - enabled + ojson arr = ojson::array(); + for (const auto& [vendor_name, models] : vendors) + for (const auto& [model_name, _] : models){ + arr.push_back(model_name); + m_printers__act++; + } + root["printers_enabled"] = arr; + } + { // PRINTERS - user defined + ojson arr = ojson::array(); + for (auto it = preset_bundle->printers.begin(); it != preset_bundle->printers.end(); ++it) { + if (!it->is_user()) continue; + ojson entry; + entry["name"] = it->name; + entry["inherits"] = it->inherits(); + arr.push_back(entry); + m_printers__usr++; + } + root["printers_user"] = arr; + } + { // FILAMENTS - enabled + if(app_config->has_section("filaments")){ + auto filaments = app_config->get_section("filaments"); + if(!filaments.empty()){ + ojson arr = ojson::array(); + for (const auto& filament : app_config->get_section("filaments")){ + arr.push_back(filament.first); + m_filaments_act++; + } + root["filaments_enabled"] = arr; + } + } + } + { // FILAMENTS - user defined + ojson arr = ojson::array(); + for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); ++it) { + if (!it->is_user()) continue; + ojson entry; + entry["name"] = it->name; + entry["inherits"] = it->inherits(); + auto *compatible_printers = dynamic_cast(it->config.option("compatible_printers")); + if(compatible_printers != nullptr && !compatible_printers->values.empty()){ + ojson c_arr = ojson::array(); + for (const auto& c_item : compatible_printers->values) + c_arr.push_back(c_item); + entry["compatible_printers"] = c_arr; + } + auto *compatible_processes = dynamic_cast(it->config.option("compatible_prints")); + if(compatible_processes != nullptr && !compatible_processes->values.empty()){ + ojson c_arr = ojson::array(); + for (const auto& c_item : compatible_processes->values) + c_arr.push_back(c_item); + entry["compatible_processes"] = c_arr; + } + arr.push_back(entry); + m_filaments_usr++; + } + root["filaments_user"] = arr; + } + { // PROCESSES - enabled, grouped by compatible printer + ojson obj = ojson::object(); + for (auto it = preset_bundle->printers.begin(); it != preset_bundle->printers.end(); ++it) { + if (!it->is_visible || !it->is_compatible) + continue; + ojson arr = ojson::array(); + for (auto jt = preset_bundle->prints.begin(); jt != preset_bundle->prints.end(); ++jt) { + if (!jt->is_visible || jt->is_user() || !jt->loaded) continue; + auto *compatible_printers = dynamic_cast(jt->config.option("compatible_printers")); + bool is_compatible = false; + if (compatible_printers == nullptr || compatible_printers->values.empty()) { + is_compatible = false; + } + else { + for (const auto& p : compatible_printers->values) { + if (p == it->name) { + is_compatible = true; + break; + } + } + } + if (!is_compatible) + continue; + arr.push_back(jt->name); + m_processes_act++; + } + if (!arr.empty()) + obj[it->name] = arr; + } + root["processes_enabled"] = obj; + } + { // PROCESSES - user defined + ojson arr = ojson::array(); + for (auto it = preset_bundle->prints.begin(); it != preset_bundle->prints.end(); ++it) { + if (!it->is_user()) continue; + ojson entry; + entry["name"] = it->name; + entry["inherits"] = it->inherits(); + auto *compatible_printers = dynamic_cast(it->config.option("compatible_printers")); + if(compatible_printers != nullptr && !compatible_printers->values.empty()){ + ojson c_arr = ojson::array(); + for (const auto& c_item : compatible_printers->values) + c_arr.push_back(c_item); + entry["compatible_printers"] = c_arr; + } + arr.push_back(entry); + m_processes_usr++; + } + root["processes_user"] = arr; + } + { // OVERVIEW + ojson entry; + entry["printers__act"] = m_printers__act; + entry["printers__usr"] = m_printers__usr; + entry["filaments_act"] = m_filaments_act; + entry["filaments_usr"] = m_filaments_usr; + entry["processes_act"] = m_processes_act; + entry["processes_usr"] = m_processes_usr; + root["Overview"] = entry; + } + return wxString::FromUTF8(root.dump(4)); +} + +wxString TroubleshootDialog::GetOStype() +{ + wxString os = ""; + #ifdef __WINDOWS__ + os = "Windows"; + #elif defined(__LINUX__) + os = "Linux"; + #elif defined(__APPLE__) + os = "macOS"; + #endif + return os; +} + +wxString TroubleshootDialog::GetOSinfo() +{ + wxString result; +#ifdef __WINDOWS__ + result = GetWinVersion(); +#elif defined(__LINUX__) + result = GetLinuxDistroName() + " " + GetLinuxDisplayServer(); +#elif defined(__APPLE__) + result = wxGetOsDescription(); // returns "macOS Version 26.3 (Build 25D125)" + result.Replace("Version ", ""); // simplify naming + result.Replace("Build ", "Build-"); // dash for wrapping build info on next line + + // No public API for naming + auto GetMacOSName = [](const wxString& ver) -> wxString { + if (ver.StartsWith("26")) return "Tahoe"; + if (ver.StartsWith("15")) return "Sequoia"; + if (ver.StartsWith("14")) return "Sonoma"; + if (ver.StartsWith("13")) return "Ventura"; + if (ver.StartsWith("12")) return "Monterey"; + if (ver.StartsWith("11")) return "Big Sur"; + return ""; + }; + + wxRegEx reVer("([0-9]+\\.[0-9]+)"); + if (reVer.Matches(result)) { + wxString ver = reVer.GetMatch(result, 1); + wxString name = GetMacOSName(ver); + if (!name.IsEmpty()) + result.Replace("(Build-", name + " (Build-"); + } +#endif + return result; +} + +#ifdef __WINDOWS__ +wxString TroubleshootDialog::GetWinDisplayVersion() { + HKEY hKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey) != ERROR_SUCCESS) + return wxString(); + + wchar_t buf[64] = {}; + DWORD size = sizeof(buf); + DWORD type = 0; + + // "DisplayVersion" exists on Windows 10 20H2+ and Windows 11 + LONG res = RegQueryValueExW(hKey, L"DisplayVersion", nullptr, &type, reinterpret_cast(buf), &size); + if (res != ERROR_SUCCESS || type != REG_SZ) { // Fallback: older builds use "ReleaseId" (e.g. "2004", "1909") + size = sizeof(buf); + RegQueryValueExW(hKey, L"ReleaseId", nullptr, &type, reinterpret_cast(buf), &size); + } + + RegCloseKey(hKey); + return wxString(buf); +} +wxString TroubleshootDialog::GetWinVersion() +{ + typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); + if (hNtdll) { + auto RtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hNtdll, "RtlGetVersion"); + if (RtlGetVersion) { + RTL_OSVERSIONINFOW osvi = {}; + osvi.dwOSVersionInfoSize = sizeof(osvi); + if (RtlGetVersion(&osvi) == 0) { + int build = osvi.dwBuildNumber; + wxString win = (build >= 22000) ? "11" + : (build >= 10240) ? "10" + : (build >= 9200) ? "8" + : (build >= 7601) ? "7" + : "?"; + + wxString displayVer = GetWinDisplayVersion(); + + if (!displayVer.IsEmpty()) + return wxString::Format("Windows %s %s %d", win, displayVer, build); + else + return wxString::Format("Windows %s %d", win, build); + } + } + } + return "Windows (unknown)"; +} +#elif defined(__LINUX__) +wxString TroubleshootDialog::GetLinuxDistroName() +{ + // Try host os-release first (works when running as Flatpak) + wxArrayString candidates; + candidates.Add("/run/host/os-release"); + candidates.Add("/run/host/etc/os-release"); + candidates.Add("/etc/os-release"); + candidates.Add("/usr/lib/os-release"); + + for (const wxString& path : candidates) { + if (!wxFileExists(path)) + continue; + wxTextFile file; + if (!file.Open(path)) + continue; + for (wxString line = file.GetFirstLine(); !file.Eof(); line = file.GetNextLine()) { + if (line.StartsWith("PRETTY_NAME=")) { + wxString value = line.Mid(12); + value.Replace("\"", ""); + return value; + } + } + } + return "Linux"; +} + +wxString TroubleshootDialog::GetLinuxDisplayServer() +{ + const char* wayland = getenv("WAYLAND_DISPLAY"); + if (wayland && wayland[0] != '\0') // WAYLAND_DISPLAY is set when running under Wayland + return "Wayland"; + + const char* sessionType = getenv("XDG_SESSION_TYPE"); + if (sessionType) { // XDG_SESSION_TYPE is more explicit + if (wxString(sessionType).IsSameAs("wayland", false)) + return "Wayland"; + if (wxString(sessionType).IsSameAs("x11", false)) + return "X11"; + } + + const char* display = getenv("DISPLAY"); + if (display && display[0] != '\0') + return "X11"; // DISPLAY being set suggests X11 + + return ""; +} +#endif + +wxString TroubleshootDialog::GetPackageType() +{ + wxString result; +#ifdef __WINDOWS__ + wxString path = wxStandardPaths::Get().GetExecutablePath(); + wxString dir = wxPathOnly(path); + + if (path.Contains("OrcaSlicer\\build")) + return "Local Build"; + + if (wxFileExists(dir + "\\Uninstall.exe")) + return "Installed"; + + return "Portable"; +#elif defined(__LINUX__) + if (wxGetEnv("APPIMAGE" , nullptr)) return "AppImage"; + if (wxGetEnv("FLATPAK_ID", nullptr)) return "Flatpak"; + //if (wxGetEnv("SNAP" , nullptr)) return "Snap"; + if (wxFileExists("/.flatpak-info")) return "Flatpak"; + //if (wxFileExists("/usr/bin/dpkg")) return "Debian/Ubuntu (deb)"; + //if (wxFileExists("/usr/bin/rpm")) return "RPM-based (rpm)"; + //if (wxFileExists("/usr/bin/pacman")) return "Arch (pacman)"; + + wxString path = wxStandardPaths::Get().GetExecutablePath(); + if (path.Contains("OrcaSlicer/build")) return "Local Build"; + //if (path.StartsWith("/usr/local")) return "Compiled (local)"; + if (path.StartsWith("/opt")) return "Third-party"; + + return "Native Package"; // (deb/rpm/etc) +#elif defined(__APPLE__) + wxString path = wxStandardPaths::Get().GetExecutablePath(); + wxString dir = wxPathOnly(path); + + if (path.Contains("OrcaSlicer/build")) + return "Local Build"; + + //if (wxDirExists(dir + "/../_MASReceipt")) + // return "Mac App Store"; + + if (path.Contains("/Cellar/") || wxGetEnv("HOMEBREW_PREFIX", nullptr)) + return "Homebrew"; + + if (path.StartsWith("/Volumes/OrcaSlicer")) // running from .dmg + return "Temporary"; + + if (path.StartsWith("/Applications")) + return "Applications"; + + return "Custom"; // running from Downloads, Desktop or custom folder +#endif + return result; +} + +wxString TroubleshootDialog::GetCPUinfo() +{ + wxString info; +#ifdef __WINDOWS__ + info = get_cpu_info_from_registry(); +#elif __APPLE__ + std::map cpu_info = parse_lscpu_etc("sysctl -a", ':'); + info = wxString(cpu_info["machdep.cpu.brand_string"]); +#else // linux/BSD + std::map cpu_info = parse_lscpu_etc("cat /proc/cpuinfo", ':'); + info = wxString(cpu_info["model name"]); +#endif + info.Trim(); + return info; +} + +#ifdef __WINDOWS__ +wxString TroubleshootDialog::get_cpu_info_from_registry() +{ + const std::string dir = "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\"; + char buf[500] = {}; + DWORD bufsize = sizeof(buf) - 1; + + for (const auto& path : { dir, dir + "0\\" }) + if (RegGetValueA(HKEY_LOCAL_MACHINE, path.c_str(), "ProcessorNameString", RRF_RT_REG_SZ, NULL, buf, &bufsize) == ERROR_SUCCESS) + return buf; + + return "Unknown"; +} +#else // macOS / linux +std::map TroubleshootDialog::parse_lscpu_etc(const std::string& name, char delimiter) +{ + std::map out; + constexpr size_t max_len = 1000; + char cline[max_len] = ""; + FILE* fp = popen(name.data(), "r"); + if (fp != NULL) { + while (fgets(cline, max_len, fp) != NULL) { + std::string line(cline); + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { return c == '\"' || c == '\r' || c == '\n'; }), + line.end()); + size_t pos = line.find(delimiter); + if (pos < line.size() - 1) { + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + boost::trim_all(key); // remove leading and trailing spaces + boost::trim_all(value); + out.emplace(key, value); + } + } + pclose(fp); + } + return out; +} +#endif + +wxString TroubleshootDialog::GetRAMinfo() +{ + size_t n = std::round(Slic3r::total_physical_memory() / 107374182.40); + return std::to_string(n / 10) + "." + std::to_string(n % 10) + " GB"; +} + +wxString TroubleshootDialog::GetGPUinfo() +{ + auto gl_info = OpenGLManager::get_gl_info(); + return gl_info.get_renderer()+ " GLSL:" + gl_info.get_glsl_version(); +} + +wxString TroubleshootDialog::GetMONinfo() +{ + wxString m_str; + int d_count = wxDisplay::GetCount(); + if (d_count <= 0) + return "Unknown"; + double scale = 1.0; + +#if defined(__LINUX__) + for (int i = 0; i < d_count; ++i) { + wxDisplay disp(i); + if (!disp.IsOk()) continue; + + scale = disp.GetScaleFactor(); + wxRect rc = disp.GetGeometry(); + + wxString d_str = wxString::Format("%dx%d-%.0f%%", rc.width, rc.height, scale * 100.0); + + m_str += ((i > 0) ? " " : "") + d_str; + } +#elif defined(__APPLE__) + std::vector cgDisplays; + uint32_t displayCount = 0; + CGGetActiveDisplayList(0, nullptr, &displayCount); + if (displayCount > 0) { + cgDisplays.resize(displayCount); + CGGetActiveDisplayList(displayCount, cgDisplays.data(), &displayCount); + } + for (int i = 0; i < d_count; ++i) { + wxDisplay disp(i); + if (!disp.IsOk()) continue; + scale = disp.GetScaleFactor(); + wxRect rc = disp.GetGeometry(); + int physW = static_cast(std::round(rc.width * scale)); + int physH = static_cast(std::round(rc.height * scale)); + const char* type = "L-"; // Logical fallback. only visible when cant get native res + + CGDirectDisplayID cgDispID = (i < static_cast(cgDisplays.size())) ? cgDisplays[i] : CGMainDisplayID(); + + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(cgDispID, nullptr); + if (allModes) { + CFIndex count = CFArrayGetCount(allModes); + for (CFIndex m = 0; m < count; ++m) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, m); + if (CGDisplayModeGetIOFlags(mode) & kDisplayModeNativeFlag) { + size_t pw = CGDisplayModeGetPixelWidth(mode); + size_t ph = CGDisplayModeGetPixelHeight(mode); + if (pw >= 800 && ph >= 600 && pw <= 16384 && ph <= 16384) { + physW = static_cast(pw); + physH = static_cast(ph); + type = ""; // Native + } + break; + } + } + CFRelease(allModes); + } + + wxString d_str = wxString::Format("%dx%d-%s%.0f%%", physW, physH, type, scale * 100.0); + m_str += ((i > 0) ? " " : "") + d_str; + } +#elif defined(__WINDOWS__) + for (int i = 0; i < d_count; ++i) { + wxDisplay disp(i); + if (!disp.IsOk()) continue; + + scale = disp.GetScaleFactor(); + wxRect rc = disp.GetGeometry(); + + wxString d_str = wxString::Format("%dx%d-%.0f%%", rc.width, rc.height, scale * 100.0); + + m_str += ((i > 0) ? " " : "") + d_str; + } + + UINT dpi = 96; + HMODULE hUser = GetModuleHandleW(L"user32.dll"); + if (hUser) { + auto fn = reinterpret_cast(GetProcAddress(hUser, "GetDpiForSystem")); + if (fn) + dpi = fn(); + } + double text_scale = dpi / 96.0; + m_str += wxString::Format(" TextScaling-%.0f%%", text_scale * 100.0); +#endif + return m_str; +} + +void TroubleshootDialog::PackAll() +{ + std::vector include_zip; + //auto data_dir = boost::filesystem::path(Slic3r::data_dir()); + //include_zip.emplace_back(wxString((data_dir / "log").string())); + + // Collect logs for current session. does not includes debug_network_... + boost::filesystem::path current_log = get_log_file_name(); + if (!current_log.empty()){ + boost::filesystem::path base = current_log.parent_path() / current_log.stem(); // removes ".0" + std::vector result; + for (int n = 0; ; ++n) { + auto candidate = boost::filesystem::path(base.string() + "." + std::to_string(n)); + if (!boost::filesystem::exists(candidate)) + break; + include_zip.emplace_back(wxString(candidate.string())); + } + } + + auto project_name = wxGetApp().plater()->get_project_filename(".3mf"); + if(!project_name.IsEmpty()){ + if (wxGetApp().plater()->is_project_dirty()) { + auto res = MessageDialog(this, + _L("The current project has unsaved changes, save it before continue?") + + "\n\n" + + _L("Select NO to close dialog and review project"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE + ).ShowModal(); + if (res == wxID_YES) + wxGetApp().plater()->save_project(); + else { + if (res == wxID_NO) + EndModal(wxID_CLOSE); + return; + } + } + include_zip.emplace_back(project_name); + } + + if (project_name.IsEmpty()){ + MessageDialog(this, _L("No project file on current session. Only logs will be included to package"), + wxString(SLIC3R_APP_FULL_NAME), wxOK | wxICON_WARNING | wxCENTRE + ).ShowModal(); + } + + ExportAsZip(include_zip, "OrcaSlicer_PackedDebugInfo_" + GetTimestamp()); +} + +void TroubleshootDialog::RebuildSystemProfiles() +{ + if (wxGetApp().plater()->is_project_dirty()) { + auto res = MessageDialog(this, + _L("The current project has unsaved changes, save it before continue?") + + "\n\n" + + _L("Select NO to close dialog and review project."), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE + ).ShowModal(); + if (res == wxID_YES) + wxGetApp().plater()->save_project(); + else { + if (res == wxID_NO) + EndModal(wxID_CLOSE); + return; + } + } + + MessageDialog msg(this, + _L("Restart Required") + "\n" + + _L("Please make sure any instances of OrcaSlicer are not running") + "\n" + + _L("Do you want to continue?") + , wxString(SLIC3R_APP_FULL_NAME), wxICON_QUESTION | wxOK | wxCANCEL + ); + if (msg.ShowModal() == wxID_OK){ + auto sys_folder = boost::filesystem::path(Slic3r::data_dir()) / "system"; + if (boost::filesystem::exists(sys_folder)) { + bool is_deletable = true; + try { + for (const auto& entry : boost::filesystem::recursive_directory_iterator(sys_folder)) { + if (boost::filesystem::is_regular_file(entry.path())) { + std::ofstream file(entry.path().string(), std::ios::in | std::ios::out); + if (!file.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "File is locked: " << entry.path().string(); + is_deletable = false; + } + file.close(); + } + } + } + catch (const std::exception& e) { + is_deletable = false; + BOOST_LOG_TRIVIAL(warning) << e.what(); + } + if (!is_deletable) { + MessageDialog(this, _L("System folder cannot be deleted because some files are in use by another application. Please close any applications using these files and try again."), + wxString(SLIC3R_APP_FULL_NAME), wxOK | wxICON_WARNING | wxCENTRE + ).ShowModal(); + return; + } + try { + boost::filesystem::remove_all(sys_folder); + EndModal(wxID_REMOVE); + RestartApplication(); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "Failed to delete system folder..." << e.what(); + MessageDialog(this, _L("Failed to delete system folder..."), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Error"), wxOK | wxICON_WARNING | wxCENTRE + ).ShowModal(); + } + } + } +} + +bool TroubleshootDialog::RestartApplication() +{ + wxString execPath = wxStandardPaths::Get().GetExecutablePath(); + if (execPath.IsEmpty()) { + MessageDialog(this, + _L("Failed to determine executable path."), + wxString(SLIC3R_APP_FULL_NAME), + wxICON_WARNING | wxOK + ).ShowModal(); + return false; + } + + // Reconstruct original argv if you need to forward launch arguments, + // e.g. "--datadir", profile flags, etc. + // wxString args = "--some-flag"; + // wxString cmd = wxString::Format(R"("%s" %s)", execPath, args); + +#ifdef __WXMSW__ + wxString cmd = wxString::Format(R"("%s")", execPath); + + // CreateProcess needs a mutable buffer for lpCommandLine + std::vector cmdBuf(cmd.wc_str(), cmd.wc_str() + cmd.length() + 1); + + STARTUPINFO si = { sizeof(si) }; + PROCESS_INFORMATION pi = {}; + if (!CreateProcess(nullptr, cmdBuf.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { + MessageDialog(this, _L("Failed to launch a new instance."), wxString(SLIC3R_APP_FULL_NAME), wxICON_WARNING | wxOK).ShowModal(); + return false; + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); +#else + // macOS / Linux — wxExecute with ASYNC so it doesn't block + wxString cmd = wxString::Format(R"("%s")", execPath); + long pid = wxExecute(cmd, wxEXEC_ASYNC); + if (pid == 0) { + MessageDialog(this, + _L("Failed to launch a new instance."), + wxString(SLIC3R_APP_FULL_NAME), + wxICON_WARNING | wxOK + ).ShowModal(); + return false; + } +#endif + wxMilliSleep(500); + + // Close this instance cleanly + wxGetApp().GetTopWindow()->Close(true); // triggers EVT_CLOSE, runs cleanup + + return true; +} + +void TroubleshootDialog::ClearLogs() +{ + // Same method with GUI_App::post_init() only LOG_FILES_MAX_NUM replaced with 1 + auto data_dir = boost::filesystem::path(Slic3r::data_dir()); + auto log_folder = data_dir / "log"; + if (boost::filesystem::exists(log_folder)) { + std::vector> files_vec; + for (auto& it : boost::filesystem::directory_iterator(log_folder)) { + auto temp_path = it.path(); + try { + if (it.status().type() == boost::filesystem::regular_file) { + std::time_t lw_t = boost::filesystem::last_write_time(temp_path) ; + files_vec.push_back({ lw_t, temp_path.filename().string() }); + } + } catch (const std::exception &) { + } + } + std::sort(files_vec.begin(), files_vec.end(), []( + std::pair &a, std::pair &b) { + return a.first > b.first; + }); + + while (files_vec.size() > 1) { + auto full_path = log_folder / boost::filesystem::path(files_vec[files_vec.size() - 1].second); + BOOST_LOG_TRIVIAL(info) << "delete log file over " << LOG_FILES_MAX_NUM << ", filename: "<< files_vec[files_vec.size() - 1].second; + try { + boost::filesystem::remove(full_path); + } + catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "failed to delete log file: "<< files_vec[files_vec.size() - 1].second << ". Error: " << ex.what(); + } + files_vec.pop_back(); + } + } + UpdateLogsStorage(); +} + +void TroubleshootDialog::UpdateLogsStorage() +{ + boost::filesystem::path logs_path = boost::filesystem::path(Slic3r::data_dir()) / "log"; + + uintmax_t total_bytes = 0; + int file_count = 0; + if (boost::filesystem::exists(logs_path) && boost::filesystem::is_directory(logs_path)) { + for (const auto& entry : boost::filesystem::recursive_directory_iterator(logs_path)) { + if (boost::filesystem::is_regular_file(entry.path())){ + total_bytes += boost::filesystem::file_size(entry.path()); + file_count++; + } + } + } + + bool is_mb = total_bytes >= 1024 * 1024; + wxString label = total_bytes > 0 ? wxString::Format("%d %s (%.2f %s)", file_count, _L("log(s)"), total_bytes / (1024.0 * (is_mb ? 1024.0 : 1.0)), is_mb ? "MB" : "KB") : ""; + m_logs_storage->SetLabel(label); +} + +void TroubleshootDialog::BrowseFolder(std::string path) +{ + wxString wxpath = wxString::FromUTF8(path); + + if (!wxpath.IsEmpty() && !wxFileName::IsPathSeparator(wxpath.Last())) + wxpath += wxFileName::GetPathSeparator(); + + if (wxLaunchDefaultApplication(wxpath)) + return; + + auto ShellQuote = [](const wxString& arg) { + #ifdef __WXMSW__ + wxString result = "\""; + for (wxChar c : arg) { + if (c == '"') result += '\\'; + result += c; + } + result += "\""; + return result; + #else + wxString result = "'"; + for (wxChar c : arg) { + if (c == '\'') + result += wxString("'\\''"); + else + result += c; + } + result += "'"; + return result; + #endif + }; + + #ifdef _WIN32 + ::wxExecute(L"explorer.exe " + ShellQuote(wxpath), wxEXEC_ASYNC); + #elif defined(__APPLE__) + ::wxExecute(wxString::Format("open %s", ShellQuote(wxpath)), wxEXEC_ASYNC); + #else + const char* argv[] = { "xdg-open", nullptr, nullptr }; + + wxString utf8_path = wxpath.ToUTF8(); + argv[1] = utf8_path.c_str(); + + if (wxGetEnv("APPIMAGE", nullptr)) { + 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)) + exec_env.cwd = std::move(owd); + + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, &exec_env); + } + else + ::wxExecute(const_cast(argv), wxEXEC_ASYNC); + #endif +} + +bool TroubleshootDialog::ExportAsJson(const wxString& json_data, const wxString& export_name) +{ + wxString home = wxGetHomeDir(); + + wxFileName desktop(home, ""); + desktop.AppendDir("Desktop"); + wxString defaultPath = wxDirExists(desktop.GetPath()) ? desktop.GetPath() : home; + + wxFileDialog dialog(this, _L("Choose where to save the exported JSON file"), defaultPath, + export_name.IsEmpty() ? "export.json" : export_name + ".json", + "JSON files (*.json)|*.json", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + + if (dialog.ShowModal() != wxID_OK) + return false; + + wxString jsonPath = dialog.GetPath(); + + wxFile file(jsonPath, wxFile::write); + if (!file.IsOpened()) { + MessageDialog(this, _L("Export failed\nPlease check write permissions or file in use by another application"), + wxString(SLIC3R_APP_FULL_NAME), wxICON_WARNING | wxOK + ).ShowModal(); + return false; + } + + bool success = file.Write(json_data, wxConvUTF8); + file.Close(); + + if (!success) { + wxRemoveFile(jsonPath); + MessageDialog(this, _L("Export failed\nPlease check write permissions or file in use by another application"), + wxString(SLIC3R_APP_FULL_NAME), wxICON_WARNING | wxOK + ).ShowModal(); + return false; + } + + MessageDialog(this, _L("Export successful"), wxString(SLIC3R_APP_FULL_NAME), wxICON_INFORMATION | wxOK).ShowModal(); + + return true; +} + +bool TroubleshootDialog::ExportAsZip(const std::vector& sources, const wxString& export_name) +{ + wxString home = wxGetHomeDir(); + + wxFileName desktop(home, ""); + desktop.AppendDir("Desktop"); + wxString defaultPath = wxDirExists(desktop.GetPath()) ? desktop.GetPath() : home; + + wxDirDialog dialog(this, _L("Choose where to save the exported ZIP file"), defaultPath, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + wxString destDir = (dialog.ShowModal() == wxID_OK) ? dialog.GetPath() : ""; + if (destDir.IsEmpty()) + return false; + wxString baseName = export_name.IsEmpty() ? wxFileName(sources[0]).GetFullName() : export_name; + wxString zipPath = wxFileName(destDir, baseName + ".zip").GetFullPath(); + if (wxFileExists(zipPath)) { + MessageDialog msg(this, _L("File already exists. Overwrite?"), + wxString(SLIC3R_APP_FULL_NAME), wxICON_QUESTION | wxYES_NO + ); + if (msg.ShowModal() != wxID_YES) + return false; + } + if (!SaveAsZip(sources, zipPath)) { + MessageDialog(this, _L("Export failed\nPlease check write permissions or file in use by another application"), + wxString(SLIC3R_APP_FULL_NAME), wxICON_WARNING | wxOK + ).ShowModal(); + return false; + } + else { + MessageDialog(this, _L("Export successful"), wxString(SLIC3R_APP_FULL_NAME), wxICON_INFORMATION | wxOK).ShowModal(); + } + return true; +} + +bool TroubleshootDialog::AddToZip(wxZipOutputStream& zip, const wxString& fullPathOrTextData, const wxString& rootDir) +{ + if (fullPathOrTextData.StartsWith(wxT("TxtData:"))) { // add text to zip + // Format: "TxtData:|" + wxString payload = fullPathOrTextData.Mid(8); // strip "TxtData:" + int sep = payload.Find('|'); + if (sep == wxNOT_FOUND) + return false; + wxString entryName = payload.Left(sep); + wxString content = payload.Mid(sep + 1); + + if (!zip.PutNextEntry(entryName)) + return false; + wxScopedCharBuffer buf = content.utf8_str(); + zip.Write(buf.data(), buf.length()); + return zip.CloseEntry(); + } + + wxString relPath = fullPathOrTextData.Mid(rootDir.length()); + if (relPath.StartsWith(wxFileName::GetPathSeparator())) + relPath = relPath.Mid(1); + relPath.Replace(wxFileName::GetPathSeparator(), wxT("/")); + if (wxDirExists(fullPathOrTextData)) { + if (!relPath.IsEmpty()) { + if (!relPath.EndsWith(wxT("/"))) + relPath += wxT("/"); + if (!zip.PutNextDirEntry(relPath)) + return false; + } + + wxDir dir(fullPathOrTextData); + if (!dir.IsOpened()) + return false; + + wxString filename; + bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES | wxDIR_DIRS | wxDIR_HIDDEN); + while (cont) { + wxString childPath = fullPathOrTextData; + if (!wxEndsWithPathSeparator(childPath)) + childPath += wxFileName::GetPathSeparator(); + childPath += filename; + + if (!AddToZip(zip, childPath, rootDir)) + return false; + + cont = dir.GetNext(&filename); + } + } + else if (wxFileExists(fullPathOrTextData)) { + wxFileInputStream in(fullPathOrTextData); + if (!in.IsOk()) + return false; + if (!zip.PutNextEntry(relPath)) + return false; + zip.Write(in); + if (!zip.CloseEntry()) + return false; + } + else + return false; + + return true; +} + +bool TroubleshootDialog::SaveAsZip(const std::vector& sourcePaths, const wxString& zipFullPath) +{ + wxFileOutputStream out(zipFullPath); + if (!out.IsOk()) + return false; + wxZipOutputStream zip(out); + if (!zip.IsOk()) { + out.Close(); + return false; + } + bool success = true; + for (const auto& sourcePath : sourcePaths) { + if (sourcePath.StartsWith(wxT("TxtData:"))) { + wxString rootDir; // unused for virtual entries + if (!AddToZip(zip, sourcePath, rootDir)) { + success = false; + break; + } + continue; + } + if (!wxDirExists(sourcePath) && !wxFileExists(sourcePath)) { + success = false; + break; + } + wxString rootDir = wxFileName(sourcePath).GetPath(); + if (!wxEndsWithPathSeparator(rootDir)) + rootDir += wxFileName::GetPathSeparator(); + if (!AddToZip(zip, sourcePath, rootDir)) { + success = false; + break; + } + } + if (!zip.Close()) success = false; + if (!out.Close()) success = false; + if (!success) + wxRemoveFile(zipFullPath); + return success; +} + +void TroubleshootDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + m_logo.msw_rescale(); + m_header_logo->SetBitmap(m_logo.bmp()); + + auto processCtrls = [&](auto&& self, wxWindow* win) -> void { + if (!win) + return; + + if (Button* btn = dynamic_cast(win)) + btn->Rescale(); + + if (ComboBox* combo = dynamic_cast(win)) + combo->Rescale(); + + wxWindowList children = win->GetChildren(); + for (auto child : children) + self(self, child); + }; + + processCtrls(processCtrls, this); + + Layout(); + Fit(); + Refresh(); +} + +//TroubleshootDialog::~TroubleshootDialog() +//{ +//} + +} // namespace GUI +} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/TroubleshootDialog.hpp b/src/slic3r/GUI/TroubleshootDialog.hpp new file mode 100644 index 0000000000..c9214ed06a --- /dev/null +++ b/src/slic3r/GUI/TroubleshootDialog.hpp @@ -0,0 +1,201 @@ +#ifndef slic3r_GUI_TroublesootDialog_hpp_ +#define slic3r_GUI_TroublesootDialog_hpp_ + +#include +#include +#include +#include + +#include "GUI_Utils.hpp" +#include "wxExtensions.hpp" + +#include "Widgets/Label.hpp" +#include "Widgets/ComboBox.hpp" + +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +class TroubleshootDialog : public DPIDialog +{ + +#define DESIGN_COMBOBOX_SIZE wxSize(FromDIP(120), -1) + +public: + TroubleshootDialog(); + //~TroubleshootDialog(); + +private: + ScalableBitmap m_logo; + wxStaticBitmap* m_header_logo; + Label* m_logs_storage; + int m_printers__act; + int m_printers__usr; + int m_filaments_act; + int m_filaments_usr; + int m_processes_act; + int m_processes_usr; + bool m_sys_panel_mode = true; + +protected: + wxFlexGridSizer* create_item_loaded_profiles(); + ComboBox* create_item_log_level_combo(); + + wxString GetTimestamp(); + + wxString GetSysInfoAll(); + //wxString GetConfigStr(); + wxString GetProfilesOverview(); + wxString GetOStype(); + wxString GetOSinfo(); +#ifdef __WINDOWS__ + wxString GetWinVersion(); + wxString GetWinDisplayVersion(); +#elif defined(__LINUX__) + wxString GetLinuxDistroName(); + wxString GetLinuxDisplayServer(); +#endif + wxString GetPackageType(); + wxString GetCPUinfo(); + wxString GetGPUinfo(); + wxString GetRAMinfo(); + wxString GetMONinfo(); + + void PackAll(); + void RebuildSystemProfiles(); + bool RestartApplication(); + void ClearLogs(); + void UpdateLogsStorage(); + + void BrowseFolder(std::string path); + +#ifdef __WINDOWS__ + static wxString get_cpu_info_from_registry(); +#else + static std::map parse_lscpu_etc(const std::string& name, char delimiter); +#endif + + bool ExportAsJson(const wxString& json_data, const wxString& export_name = wxEmptyString); + bool ExportAsZip(const std::vector& sources, const wxString& export_name); + bool AddToZip(wxZipOutputStream& zip, const wxString& fullPathOrTextData, const wxString& rootDir); + bool SaveAsZip(const std::vector& sourcePaths, const wxString& zipFullPath); + + void on_dpi_changed(const wxRect &suggested_rect) override; +}; + +class CenteredMultiLinePanel : public wxPanel +{ + static constexpr double m_block_gap = 0.55; + static constexpr double m_line_height = 1.15; + std::vector m_lines; + +public: + CenteredMultiLinePanel(wxWindow* parent, const std::vector& lines = {}) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE) + { + SetFont(Label::Body_14); + SetBackgroundStyle(wxBG_STYLE_PAINT); + Bind(wxEVT_PAINT, &CenteredMultiLinePanel::OnPaint, this); + Bind(wxEVT_SIZE, &CenteredMultiLinePanel::OnSize, this); + + SetText(lines); + } + + void SetText(const std::vector& lines) + { + if(!lines.empty()){ + m_lines = lines; + Refresh(); + UpdateMinSize(); + } + } +private: + std::vector Wrap(wxDC& dc, const wxString& text, int maxW) + { + std::vector out; + wxString cur; + for (const auto& word : wxSplit(text, ' ', true)) { + wxString trial = cur.empty() ? word : cur + " " + word; + wxCoord w, h; + dc.GetTextExtent(trial, &w, &h); + if (w <= maxW) + cur = trial; + else if (!cur.empty()){ + out.push_back(cur); + cur = word; + } + else + out.push_back(word); + } + if (!cur.empty()) + out.push_back(cur); + return out; + } + + void OnPaint(wxPaintEvent&) + { + wxAutoBufferedPaintDC dc(this); + dc.Clear(); + + int cWidth = GetClientSize().GetWidth(); + if (m_lines.empty() || cWidth < 50) return; + + dc.SetTextForeground(GetForegroundColour()); + + int y = 0; + for (size_t i = 0; i < m_lines.size(); ++i) { + wxCoord tw, th; + for (auto& line : Wrap(dc, m_lines[i], cWidth)) { + line.Trim(); + line.Trim(false); + dc.GetTextExtent(line, &tw, &th); + dc.DrawText(line, (cWidth - tw) / 2, y); + y += static_cast(th * m_line_height); + } + if (i < m_lines.size() - 1) + y += static_cast(th * m_block_gap); + } + } + + void UpdateMinSize() + { + if (m_lines.empty() || !GetParent()) { + SetMinSize(wxDefaultSize); + return; + } + wxClientDC dc(this); + int cWidth = GetClientSize().GetWidth(); + + int y = 0; + for (size_t i = 0; i < m_lines.size(); ++i) { + wxCoord th; + for (auto& line : Wrap(dc, m_lines[i], cWidth)) { + dc.GetTextExtent(line, nullptr, &th); + y += static_cast(th * m_line_height); + } + if (i < m_lines.size() - 1) + y += static_cast(th * m_block_gap); + } + + if(GetMinSize().GetHeight() != y){ + SetMinSize(wxSize(-1, y)); + if(GetParent()) + GetParent()->Layout(); + } + } + + void OnSize(wxSizeEvent& e) + { + Refresh(); + UpdateMinSize(); + e.Skip(); + } +}; + +} // namespace GUI +} // namespace Slic3r + +#endif