From f13f67959c9c41ad7f67e077bfabf596b6e8f166 Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Sun, 28 Jun 2026 19:22:23 -0600 Subject: [PATCH 1/7] fix: PA pattern calibration crash with first layer line width 0 (#14447) CalibPressureAdvancePattern::line_width_first_layer() returned the raw initial_layer_line_width, so a value of 0 (which means "use the default") fed 0 into the Flow spacing math and threw FlowErrorNegativeSpacing, crashing the whole app when slicing a PA pattern calibration. Mirror the guard already present in the sibling line_width(): when the configured width is non-positive, fall back to auto_extrusion_width. Add a libslic3r regression test covering the width resolution. Fixes #13188 --- src/libslic3r/calib.cpp | 10 +++++++++ src/libslic3r/calib.hpp | 7 +----- tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_calib.cpp | 40 ++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 tests/libslic3r/test_calib.cpp diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 8ac3240e42..9f33b4459a 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -756,6 +756,16 @@ Vec3d CalibPressureAdvancePattern::get_start_offset() return m_starting_point; } +double CalibPressureAdvancePattern::line_width_first_layer() const +{ + // TODO: FIXME: find out current filament/extruder? + const double nozzle_diameter = m_config.opt_float("nozzle_diameter", m_params.extruder_id); + const double width = m_config.get_abs_value("initial_layer_line_width", nozzle_diameter); + if (width <= 0.) + return Flow::auto_extrusion_width(frExternalPerimeter, nozzle_diameter); + return width; +}; + double CalibPressureAdvancePattern::line_width() const { // TODO: FIXME: find out current filament/extruder? diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index ea535bcf95..f69ad3e1a3 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -315,12 +315,7 @@ protected: double speed_first_layer() const { return m_config.get_abs_value_at("initial_layer_speed", m_params.extruder_id); }; double speed_perimeter() const { return m_config.get_abs_value_at("outer_wall_speed", m_params.extruder_id); }; double accel_perimeter() const { return m_config.get_abs_value_at("outer_wall_acceleration", m_params.extruder_id); } - double line_width_first_layer() const - { - // TODO: FIXME: find out current filament/extruder? - const double nozzle_diameter = m_config.opt_float("nozzle_diameter", m_params.extruder_id); - return m_config.get_abs_value("initial_layer_line_width", nozzle_diameter); - }; + double line_width_first_layer() const; double line_width() const; int wall_count() const { return m_config.option("wall_loops")->value; }; diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 66f7dc9ae1..d5e6eb958e 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_arachne_walls.cpp test_arrange.cpp test_bambu_networking.cpp + test_calib.cpp test_clipper_offset.cpp test_clipper_utils.cpp test_config.cpp diff --git a/tests/libslic3r/test_calib.cpp b/tests/libslic3r/test_calib.cpp new file mode 100644 index 0000000000..e72eb73e0d --- /dev/null +++ b/tests/libslic3r/test_calib.cpp @@ -0,0 +1,40 @@ +#include + +#include "libslic3r/calib.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/PrintConfig.hpp" + +using namespace Slic3r; + +namespace { + +// The width-resolution getters are protected; expose them so the resolution can be asserted directly. +struct PaPatternProbe : public CalibPressureAdvancePattern +{ + using CalibPressureAdvancePattern::CalibPressureAdvancePattern; + using CalibPressureAdvancePattern::line_width; + using CalibPressureAdvancePattern::line_width_first_layer; +}; + +} // namespace + +TEST_CASE("Zero calibration line width resolves to a positive default", "[Calib][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + {"line_width", "0"}, + {"initial_layer_line_width", "0"}, + }); + + Model model; + model.add_object("cube", "", make_cube(20, 20, 20))->add_instance(); + + Calib_Params params; + params.mode = CalibMode::Calib_PA_Pattern; + + PaPatternProbe pattern(params, config, /* is_bbl_machine */ true, *model.objects.front(), Vec3d(0, 0, 0)); + + REQUIRE(pattern.line_width() > 0.); + REQUIRE(pattern.line_width_first_layer() > 0.); +} From 8e868118d2a8fd1e0a0a3b46f2c1c1020684ed56 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 29 Jun 2026 15:16:11 +0800 Subject: [PATCH 2/7] fix: fix type name from preset to process in the preset bundle dialog --- resources/web/dialog/PresetBundleDialog/index.js | 10 +++++----- src/slic3r/GUI/PresetBundleDialog.cpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/web/dialog/PresetBundleDialog/index.js b/resources/web/dialog/PresetBundleDialog/index.js index b370fe8018..4ff3167d07 100644 --- a/resources/web/dialog/PresetBundleDialog/index.js +++ b/resources/web/dialog/PresetBundleDialog/index.js @@ -2,7 +2,7 @@ const bundlesById = new Map(); // bundleId -> bundle object const printersByBundle = new Map(); // bundleId -> Map(index -> printerName) const filamentsByBundle = new Map(); // bundleId -> Map(index -> filamentName) -const presetsByBundle = new Map(); // bundleId -> Map(index -> presetName) +const processesByBundle = new Map(); // bundleId -> Map(index -> presetName) const UPDATE_TOOLTIP = "Update available"; const UNAUTHORIZED_TOOLTIP = "Unauthorized bundle"; @@ -193,7 +193,7 @@ function unpackPayload(payload) { bundlesById.clear(); printersByBundle.clear(); filamentsByBundle.clear(); - presetsByBundle.clear(); + processesByBundle.clear(); const list = payload?.data || []; for (const bundle of list) { @@ -212,7 +212,7 @@ function unpackPayload(payload) { printersByBundle.set(id, new Map((bundle.printers || []).map((name, i) => [i, name]))); filamentsByBundle.set(id, new Map((bundle.filaments || []).map((name, i) => [i, name]))); - presetsByBundle.set(id, new Map((bundle.presets || []).map((name, i) => [i, name]))); + processesByBundle.set(id, new Map((bundle.processes || []).map((name, i) => [i, name]))); } } @@ -277,14 +277,14 @@ function renderBottomForBundle(bundleId) { const key = String(bundleId || ""); const printers = printersByBundle.get(key) || new Map(); const filaments = filamentsByBundle.get(key) || new Map(); - const presets = presetsByBundle.get(key) || new Map(); + const processes = processesByBundle.get(key) || new Map(); // Convert to a flat list of rows { typeLabel, name } const rows = []; for (const [, name] of printers) rows.push({ type: "Printer", name }); for (const [, name] of filaments) rows.push({ type: "Filament", name }); - for (const [, name] of presets) rows.push({ type: "Preset", name }); + for (const [, name] of processes) rows.push({ type: "Process", name }); bottomList.innerHTML = rows.map((r, idx) => `
diff --git a/src/slic3r/GUI/PresetBundleDialog.cpp b/src/slic3r/GUI/PresetBundleDialog.cpp index c94695c175..ac1b33dd1e 100644 --- a/src/slic3r/GUI/PresetBundleDialog.cpp +++ b/src/slic3r/GUI/PresetBundleDialog.cpp @@ -387,7 +387,7 @@ void PresetBundleDialog::ListBundles() temp["printers"] = strip_prefix(metadata.printer_presets); temp["filaments"] = strip_prefix(metadata.filament_presets); - temp["presets"] = strip_prefix(metadata.print_presets); + temp["processes"] = strip_prefix(metadata.print_presets); temp["update_available"] = metadata.update_available; temp["unauthorized"] = metadata.unauthorized; From 19be9604de5257224c6f0838b9d04d5104f324e0 Mon Sep 17 00:00:00 2001 From: yw4z Date: Mon, 29 Jun 2026 21:23:56 +0300 Subject: [PATCH 3/7] Preferences Dialog fixes & improvements (Wiki link support) (#13857) * init * update * update * Update Preferences.cpp * Update Preferences.cpp * Update Preferences.cpp * Update Preferences.cpp * move filament sync mode next to auto sync * move auto backup next to load behaviour --- src/slic3r/GUI/Preferences.cpp | 1039 +++++++++++++++-------------- src/slic3r/GUI/Preferences.hpp | 24 +- src/slic3r/GUI/Widgets/Button.cpp | 6 + src/slic3r/GUI/Widgets/Button.hpp | 1 + 4 files changed, 544 insertions(+), 526 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 7006902a02..f3e54adf45 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -38,6 +38,207 @@ public: bool ShouldScrollToChildOnFocus(wxWindow* child) override { return false; } }; +// TODO before replacing with HyperLink class +// make Wrap(-1) and Wrap(width) functional +// ellipsize_end on wrap(-1) +// add SetUnderlined() for allowing always highlighted while using as HyperLink +class WikiLabel : public wxPanel { +private: + wxString m_label; + wxString m_url; + wxArrayString m_lines; + bool m_hovered = false; + wxFont m_font; + int m_last_wrap_width = -1; + +public: + WikiLabel( + wxWindow* parent, + const wxString& label, + const wxString& url = wxEmptyString, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize + ) + : wxPanel(parent, wxID_ANY, pos, size, wxFULL_REPAINT_ON_RESIZE) + , m_label(label) + , m_url(url) + { +#ifndef __WXOSX__ + SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#endif + SetBackgroundColour(parent->GetBackgroundColour()); + + SetFont(Label::Body_14); + + Bind(wxEVT_PAINT, &WikiLabel::OnPaint, this); + Bind(wxEVT_SIZE, &WikiLabel::OnSize, this); + Bind(wxEVT_MOTION, &WikiLabel::OnMotion, this); + Bind(wxEVT_LEAVE_WINDOW, &WikiLabel::OnLeaveWin, this); + Bind(wxEVT_LEFT_DOWN, &WikiLabel::OnLeftDown, this); + } + + void SetLabel(const wxString& label) + { + m_label = label; + m_last_wrap_width = -1; // force re-wrap + ReflowText(); + Refresh(); + } + + bool SetFont(const wxFont& font) override + { + const bool changed = wxPanel::SetFont(font); + m_font = font; + m_last_wrap_width = -1; // force re-wrap + if (IsShownOnScreen()) { + ReflowText(); + Refresh(); + } + return changed; + } + + wxString GetLabel() const override { return m_label; } + void SetURL(const wxString& url) { m_url = url; } + wxString GetURL() const { return m_url; } + + void ReflowText() + { + const int clientW = GetClientSize().GetWidth(); + + if (clientW <= 0 || (clientW == m_last_wrap_width && !m_lines.IsEmpty())) + return; + + m_last_wrap_width = clientW; + + wxArrayString lines; + for (const wxString& para : wxSplit(m_label, '\n')) { + if (para.IsEmpty()) + lines.Add(wxEmptyString); + else { + wxString currentLine; + for (const wxString& word : wxSplit(para, ' ')) { + wxString candidate = currentLine.IsEmpty() ? word : (currentLine + ' ' + word); + + if (GetTextExtent(candidate).GetWidth() <= clientW) + currentLine = candidate; + else { + if (currentLine.IsEmpty()) + lines.Add(word); // single word wider than column + else { + lines.Add(currentLine); + currentLine = word; + } + } + } + if (!currentLine.IsEmpty()) + lines.Add(currentLine); + } + } + m_lines = lines; + + const int lineH = wxMax(1, wxWindow::GetCharHeight()); // GTK can return 0 from GetCharHeight() before the window is realized + const int nLines = m_lines.IsEmpty() ? 1 : static_cast(m_lines.size()); + const int totalH = static_cast(nLines * lineH * 1.3); + + SetMinSize(wxSize(-1, totalH)); + InvalidateBestSize(); + } + + wxSize DoGetBestSize() const override + { + const int lineH = wxMax(1, wxWindow::GetCharHeight()); // GTK can return 0 from GetCharHeight() before the window is realized + const int nLines = m_lines.IsEmpty() ? 1 : static_cast(m_lines.size()); + const int totalH = static_cast(nLines * lineH * 1.3); + + const int clientW = GetClientSize().GetWidth(); + + if (clientW > 0) + return wxSize(clientW, totalH); + + if (m_label.IsEmpty()) + return wxSize(1, lineH); + + int maxW = 0; + for (const wxString& line : wxSplit(m_label, '\n')) + maxW = wxMax(maxW, GetTextExtent(line).GetWidth()); + + return wxSize(wxMax(1, maxW), totalH); + } + + void Rescale() + { + m_last_wrap_width = -1; // force re-wrap + m_lines.Clear(); + InvalidateBestSize(); + } + +private: + void OnPaint(wxPaintEvent& evt) + { + wxPaintDC dc(this); + + dc.SetBackground(wxBrush(GetParent() ? GetParent()->GetBackgroundColour() : *wxWHITE)); + dc.Clear(); + + wxColour textCol = StateColor::darkModeColorFor(m_hovered ? "#26A69A" : "#363636"); + + dc.SetTextForeground(textCol); + dc.SetFont(m_font); + dc.SetBackgroundMode(wxTRANSPARENT); + + int lineH = dc.GetCharHeight(); + int y = lround(lineH * 0.15); + + for (const wxString& line : m_lines) { + if (!line.IsEmpty()) { + dc.DrawText(line, 0, y); + + if (m_hovered) { + int tw, th; + dc.GetTextExtent(line, &tw, &th); + + int underlineY = y + lineH - 1; // 1 px below the baseline + dc.SetPen(wxPen(textCol, 1)); + dc.DrawLine(0, underlineY, tw, underlineY); + } + } + y += lineH; + } + } + + void OnSize(wxSizeEvent& evt) + { + ReflowText(); + Refresh(); + evt.Skip(); + } + + void OnMotion(wxMouseEvent& evt) + { + if(!m_url.IsEmpty() && !m_hovered){ + m_hovered = true; + Refresh(); + } + evt.Skip(); + } + + void OnLeaveWin(wxMouseEvent& evt) + { + if(!m_url.IsEmpty() && m_hovered){ + m_hovered = false; + Refresh(); + } + evt.Skip(); + } + + void OnLeftDown(wxMouseEvent& evt) + { + if (!m_url.IsEmpty()) + wxLaunchDefaultBrowser(m_url); + evt.Skip(); + } +}; + wxBoxSizer *PreferencesDialog::create_item_title(wxString title) { wxBoxSizer *m_sizer_title = new wxBoxSizer(wxHORIZONTAL); @@ -52,24 +253,34 @@ wxBoxSizer *PreferencesDialog::create_item_title(wxString title) return m_sizer_title; } -std::tuple PreferencesDialog::create_item_combobox_base(wxString title, wxString tooltip, std::string param, std::vector vlist, unsigned int current_index) +wxBoxSizer *PreferencesDialog::create_item_label(wxString label, wxString tooltip, wxString wiki_url) { - wxBoxSizer *m_sizer_combox = new wxBoxSizer(wxHORIZONTAL); - m_sizer_combox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); + wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); + wxString url; + if(!wiki_url.IsEmpty()) + url = "https://www.orcaslicer.com/wiki/" + wiki_url; + + auto label_ctrl = new WikiLabel(m_parent, label, url, wxDefaultPosition, DESIGN_TITLE_SIZE); + + label_ctrl->SetToolTip(tooltip); + + sizer->Add(label_ctrl, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(3)); + sizer->AddSpacer(FromDIP(5)); + + return sizer; +} + +std::tuple PreferencesDialog::create_item_combobox_base(wxString title, wxString tooltip, std::string param, std::vector vlist, unsigned int current_index, const wxString wiki_url) +{ auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - auto combo_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - combo_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - combo_title->SetFont(::Label::Body_14); - combo_title->SetToolTip(tip); - combo_title->Wrap(DESIGN_TITLE_SIZE.x); - m_sizer_combox->Add(combo_title, 0, wxALIGN_CENTER); + wxBoxSizer *m_sizer = create_item_label(title, tip, wiki_url); auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); - combobox->SetFont(::Label::Body_14); - combobox->GetDropDown().SetFont(::Label::Body_14); combobox->GetDropDown().SetUseContentWidth(true); + combobox->SetToolTip(tip); std::vector::iterator iter; for (iter = vlist.begin(); iter != vlist.end(); iter++) { @@ -78,12 +289,12 @@ std::tuple PreferencesDialog::create_item_combobox_base( combobox->SetSelection(current_index); - m_sizer_combox->Add(combobox, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); + m_sizer->Add(combobox, 0, wxALIGN_CENTER); - return {m_sizer_combox, combobox}; + return {m_sizer, combobox}; } -wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::function onchange) +wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::function onchange, const wxString wiki_url) { unsigned int current_index = 0; @@ -92,7 +303,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too current_index = atoi(current_setting.c_str()); } - auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index); + auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index, wiki_url); //// save config combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, onchange](wxCommandEvent& e) { @@ -105,7 +316,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too return sizer; } -wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index) +wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index, const wxString wiki_url) { assert(vlist.size() == config_name_index.size()); unsigned int current_index = 0; @@ -177,23 +388,14 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS auto vlist = language_infos; auto param = "language"; - wxBoxSizer *m_sizer_combox = new wxBoxSizer(wxHORIZONTAL); - m_sizer_combox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - auto combo_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - combo_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - combo_title->SetFont(::Label::Body_14); - combo_title->SetToolTip(tip); - combo_title->Wrap(DESIGN_TITLE_SIZE.x); - m_sizer_combox->Add(combo_title, 0, wxALIGN_CENTER); - + wxBoxSizer *m_sizer = create_item_label(title, tip); auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); - combobox->SetFont(::Label::Body_14); - combobox->GetDropDown().SetFont(::Label::Body_14); combobox->GetDropDown().SetUseContentWidth(true); + combobox->SetToolTip(tip); + auto language = app_config->get(param); m_current_language_selected = -1; std::vector::iterator iter; @@ -280,7 +482,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS } combobox->SetSelection(m_current_language_selected); - m_sizer_combox->Add(combobox, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); + m_sizer->Add(combobox, 0, wxALIGN_CENTER); combobox->Bind(wxEVT_LEFT_DOWN, [this, combobox](wxMouseEvent &e) { m_current_language_selected = combobox->GetSelection(); @@ -335,7 +537,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS e.Skip(); }); - return m_sizer_combox; + return m_sizer; } wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxString tooltip) @@ -346,23 +548,15 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr auto vlist = Regions; - wxBoxSizer *m_sizer_combox = new wxBoxSizer(wxHORIZONTAL); - m_sizer_combox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - auto combo_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - combo_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - combo_title->SetFont(::Label::Body_14); - combo_title->SetToolTip(tip); - combo_title->Wrap(DESIGN_TITLE_SIZE.x); - m_sizer_combox->Add(combo_title, 0, wxALIGN_CENTER); + wxBoxSizer *m_sizer = create_item_label(title, tip); auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); - combobox->SetFont(::Label::Body_14); - combobox->GetDropDown().SetFont(::Label::Body_14); combobox->GetDropDown().SetUseContentWidth(true); - m_sizer_combox->Add(combobox, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); + combobox->SetToolTip(tip); + + m_sizer->Add(combobox, 0, wxALIGN_CENTER); std::vector::iterator iter; for (iter = vlist.begin(); iter != vlist.end(); iter++) { combobox->Append(*iter); } @@ -423,25 +617,18 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr e.Skip(); }); - return m_sizer_combox; + return m_sizer; } wxBoxSizer *PreferencesDialog::create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector vlist) { - wxBoxSizer *m_sizer_combox = new wxBoxSizer(wxHORIZONTAL); - m_sizer_combox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); + auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - auto combo_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - combo_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - combo_title->SetFont(::Label::Body_14); - combo_title->SetToolTip(tooltip); - combo_title->Wrap(DESIGN_TITLE_SIZE.x); - m_sizer_combox->Add(combo_title, 0, wxALIGN_CENTER); + wxBoxSizer *m_sizer = create_item_label(title, tip); auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); - combobox->SetFont(::Label::Body_14); - combobox->GetDropDown().SetFont(::Label::Body_14); combobox->GetDropDown().SetUseContentWidth(true); + combobox->SetToolTip(tip); std::vector::iterator iter; for (iter = vlist.begin(); iter != vlist.end(); iter++) { combobox->Append(*iter); } @@ -449,7 +636,7 @@ wxBoxSizer *PreferencesDialog::create_item_loglevel_combobox(wxString title, wxS auto severity_level = app_config->get("log_severity_level"); if (!severity_level.empty()) { combobox->SetValue(severity_level); } - m_sizer_combox->Add(combobox, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); + m_sizer->Add(combobox, 0, wxALIGN_CENTER); //// save config combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) { @@ -458,93 +645,31 @@ wxBoxSizer *PreferencesDialog::create_item_loglevel_combobox(wxString title, wxS app_config->set("log_severity_level",level); e.Skip(); }); - return m_sizer_combox; + return m_sizer; } -wxBoxSizer *PreferencesDialog::create_item_multiple_combobox( - wxString title, wxString tooltip, std::string param, std::vector vlista, std::vector vlistb) +wxBoxSizer *PreferencesDialog::create_item_input(wxString title, wxString title2, wxString tooltip, std::string param, std::function onchange, const wxString wiki_url) { - std::vector params; - Split(app_config->get(param), "/", params); + auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - std::vector::iterator iter; - - wxBoxSizer *m_sizer_tcombox= new wxBoxSizer(wxHORIZONTAL); - m_sizer_tcombox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - - auto combo_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - combo_title->SetToolTip(tooltip); - combo_title->Wrap(DESIGN_TITLE_SIZE.x); - combo_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - combo_title->SetFont(::Label::Body_14); - m_sizer_tcombox->Add(combo_title, 0, wxALIGN_CENTER); - - auto combobox_left = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); - combobox_left->SetFont(::Label::Body_14); - combobox_left->GetDropDown().SetFont(::Label::Body_14); - - - for (iter = vlista.begin(); iter != vlista.end(); iter++) { combobox_left->Append(*iter); } - combobox_left->SetValue(std::string(params[0].mb_str())); - m_sizer_tcombox->Add(combobox_left, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); - - auto combo_title_add = new wxStaticText(m_parent, wxID_ANY, wxT("+"), wxDefaultPosition, wxDefaultSize, 0); - combo_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - combo_title->SetFont(::Label::Body_14); - combo_title_add->Wrap(-1); - m_sizer_tcombox->Add(combo_title_add, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(5)); - - auto combobox_right = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); - combobox_right->SetFont(::Label::Body_14); - combobox_right->GetDropDown().SetFont(::Label::Body_14); - - for (iter = vlistb.begin(); iter != vlistb.end(); iter++) { combobox_right->Append(*iter); } - combobox_right->SetValue(std::string(params[1].mb_str())); - m_sizer_tcombox->Add(combobox_right, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); - - // save config - combobox_left->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, combobox_right](wxCommandEvent &e) { - auto config = e.GetString() + wxString("/") + combobox_right->GetValue(); - app_config->set(param, std::string(config.mb_str())); - e.Skip(); - }); - - combobox_right->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, combobox_left](wxCommandEvent &e) { - auto config = combobox_left->GetValue() + wxString("/") + e.GetString(); - app_config->set(param, std::string(config.mb_str())); - e.Skip(); - }); - - return m_sizer_tcombox; -} - -wxBoxSizer *PreferencesDialog::create_item_input(wxString title, wxString title2, wxString tooltip, std::string param, std::function onchange) -{ - wxBoxSizer *sizer_input = new wxBoxSizer(wxHORIZONTAL); - auto input_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - input_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - input_title->SetFont(::Label::Body_14); - input_title->SetToolTip(tooltip); - input_title->Wrap(DESIGN_TITLE_SIZE.x); + wxBoxSizer *m_sizer = create_item_label(title, tip, wiki_url); auto input = new ::TextInput(m_parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); StateColor input_bg(std::pair(wxColour("#F0F0F1"), StateColor::Disabled), std::pair(*wxWHITE, StateColor::Enabled)); input->SetBackgroundColor(input_bg); input->GetTextCtrl()->SetValue(app_config->get(param)); wxTextValidator validator(wxFILTER_DIGITS); - input->SetToolTip(tooltip); + input->SetToolTip(tip); input->GetTextCtrl()->SetValidator(validator); auto second_title = new wxStaticText(m_parent, wxID_ANY, title2, wxDefaultPosition, wxDefaultSize, 0); second_title->SetForegroundColour(DESIGN_GRAY900_COLOR); second_title->SetFont(::Label::Body_14); - second_title->SetToolTip(tooltip); + second_title->SetToolTip(tip); second_title->Wrap(-1); - sizer_input->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - sizer_input->Add(input_title , 0, wxALIGN_CENTER_VERTICAL); - sizer_input->Add(input , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); - sizer_input->Add(second_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + m_sizer->Add(input , 0, wxALIGN_CENTER_VERTICAL); + m_sizer->Add(second_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); input->GetTextCtrl()->Bind(wxEVT_TEXT_ENTER, [this, param, input, onchange](wxCommandEvent &e) { auto value = input->GetTextCtrl()->GetValue(); @@ -561,33 +686,26 @@ wxBoxSizer *PreferencesDialog::create_item_input(wxString title, wxString title2 e.Skip(); }); - return sizer_input; + return m_sizer; } -wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString title2, wxString side_label, wxString tooltip, std::string param, int min, int max, std::function onchange) +wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString title2, wxString side_label, wxString tooltip, std::string param, int min, int max, std::function onchange, const wxString wiki_url) { - wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); + auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - auto label = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - label->SetForegroundColour(DESIGN_GRAY900_COLOR); - label->SetFont(::Label::Body_14); - label->SetToolTip(tooltip); - label->Wrap(DESIGN_TITLE_SIZE.x); - label->Wrap(DESIGN_TITLE_SIZE.x); + wxBoxSizer *m_sizer = create_item_label(title, tip, wiki_url); auto input = new SpinInput(m_parent, wxEmptyString, side_label, wxDefaultPosition, DESIGN_INPUT_SIZE, wxSP_ARROW_KEYS, min, max, stoi(app_config->get(param))); - input->SetToolTip(tooltip); + input->SetToolTip(tip); - sizer->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL); - sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + m_sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL); if(!title2.empty()){ auto second_title = new wxStaticText(m_parent, wxID_ANY, title2, wxDefaultPosition, wxDefaultSize, 0); second_title->SetForegroundColour(DESIGN_GRAY900_COLOR); second_title->SetFont(::Label::Body_14); - second_title->SetToolTip(tooltip); - sizer->Add(second_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + second_title->SetToolTip(tip); + m_sizer->Add(second_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); } input->Bind(wxEVT_TEXT_ENTER, [this, param, input, onchange](wxCommandEvent& e) { @@ -613,17 +731,15 @@ wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString tit e.Skip(); }); - return sizer; + return m_sizer; } wxBoxSizer *PreferencesDialog::create_camera_orbit_mult_input(wxString title, wxString tooltip) { - wxBoxSizer *sizer_input = new wxBoxSizer(wxHORIZONTAL); - auto input_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - input_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - input_title->SetFont(::Label::Body_14); - input_title->SetToolTip(tooltip); - input_title->Wrap(DESIGN_TITLE_SIZE.x); + auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty + + wxBoxSizer *m_sizer = create_item_label(title, tip); + auto param = "camera_orbit_mult"; auto input = new ::TextInput(m_parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); @@ -631,12 +747,10 @@ wxBoxSizer *PreferencesDialog::create_camera_orbit_mult_input(wxString title, wx input->SetBackgroundColor(input_bg); input->GetTextCtrl()->SetValue(app_config->get(param)); wxTextValidator validator(wxFILTER_NUMERIC); - input->SetToolTip(tooltip); + input->SetToolTip(tip); input->GetTextCtrl()->SetValidator(validator); - sizer_input->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - sizer_input->Add(input_title, 0, wxALIGN_CENTER_VERTICAL); - sizer_input->Add(input , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + m_sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL); const double min = 0.05; const double max = 2.0; @@ -666,24 +780,18 @@ wxBoxSizer *PreferencesDialog::create_camera_orbit_mult_input(wxString title, wx e.Skip(); }); - return sizer_input; + return m_sizer; } wxBoxSizer *PreferencesDialog::create_item_backup(wxString title, wxString tooltip) { - wxBoxSizer *m_sizer_input = new wxBoxSizer(wxHORIZONTAL); + auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - m_sizer_input->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - - auto checkbox_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - checkbox_title->SetFont(::Label::Body_14); - checkbox_title->Wrap(DESIGN_TITLE_SIZE.x); - checkbox_title->SetToolTip(tooltip); + wxBoxSizer *m_sizer = create_item_label(title, tip); auto checkbox = new ::CheckBox(m_parent); checkbox->SetValue(app_config->get_bool("backup_switch")); - checkbox->SetToolTip(tooltip); + checkbox->SetToolTip(tip); checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox](wxCommandEvent &e) { app_config->set_bool("backup_switch", checkbox->GetValue()); @@ -706,9 +814,8 @@ wxBoxSizer *PreferencesDialog::create_item_backup(wxString title, wxString toolt input->SetToolTip(_L("The period of backup in seconds.")); input->GetTextCtrl()->SetValidator(validator); - m_sizer_input->Add(checkbox_title, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(3)); - m_sizer_input->Add(checkbox , 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(5)); - m_sizer_input->Add(input , 0, wxALIGN_CENTER_VERTICAL); + m_sizer->Add(checkbox, 0, wxALIGN_CENTER); + m_sizer->Add(input , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); input->GetTextCtrl()->Bind(wxEVT_COMMAND_TEXT_UPDATED, [this, input](wxCommandEvent &e) { m_backup_interval_time = input->GetTextCtrl()->GetValue(); @@ -738,20 +845,12 @@ wxBoxSizer *PreferencesDialog::create_item_backup(wxString title, wxString toolt input->Refresh(); m_backup_interval_textinput = input; - return m_sizer_input; + return m_sizer; } wxBoxSizer *PreferencesDialog::create_item_auto_reslice(wxString title, wxString checkbox_tooltip, wxString delay_tooltip) { - wxBoxSizer *sizer_row = new wxBoxSizer(wxHORIZONTAL); - - sizer_row->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - - auto checkbox_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - checkbox_title->SetFont(::Label::Body_14); - checkbox_title->Wrap(DESIGN_TITLE_SIZE.x); - checkbox_title->SetToolTip(checkbox_tooltip); + wxBoxSizer *m_sizer = create_item_label(title, checkbox_tooltip); auto checkbox = new ::CheckBox(m_parent); checkbox->SetValue(app_config->get_bool("auto_slice_after_change")); @@ -769,9 +868,8 @@ wxBoxSizer *PreferencesDialog::create_item_auto_reslice(wxString title, wxString input->SetToolTip(delay_tooltip); input->GetTextCtrl()->SetValidator(validator); - sizer_row->Add(checkbox_title, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(3)); - sizer_row->Add(checkbox, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(5)); - sizer_row->Add(input, 0, wxALIGN_CENTER_VERTICAL); + m_sizer->Add(checkbox, 0, wxALIGN_CENTER); + m_sizer->Add(input , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); auto commit_delay = [this, input]() { wxString value = input->GetTextCtrl()->GetValue(); @@ -806,78 +904,21 @@ wxBoxSizer *PreferencesDialog::create_item_auto_reslice(wxString title, wxString input->Enable(checkbox->GetValue()); input->Refresh(); - return sizer_row; -} - -wxBoxSizer* PreferencesDialog::create_item_draco(wxString title, wxString side_label, wxString tooltip) -{ - wxBoxSizer* sizer_input = new wxBoxSizer(wxHORIZONTAL); - - auto input_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - input_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - input_title->SetFont(::Label::Body_14); - input_title->SetToolTip(tooltip); - input_title->Wrap(DESIGN_TITLE_SIZE.x); - input_title->SetToolTip(tooltip); - - auto input = new ::TextInput(m_parent, wxEmptyString, side_label, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); - StateColor input_bg(std::pair(wxColour("#F0F0F1"), StateColor::Disabled), - std::pair(*wxWHITE, StateColor::Enabled)); - input->SetBackgroundColor(input_bg); - input->GetTextCtrl()->SetValue(app_config->get("drc_bits")); - wxTextValidator validator(wxFILTER_DIGITS); - input->SetToolTip(tooltip); - input->GetTextCtrl()->SetValidator(validator); - - sizer_input->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - sizer_input->Add(input_title, 0, wxALIGN_CENTER_VERTICAL); - sizer_input->Add(input , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); - - std::function set_draco_bits = [this, input]() { - long drc_bits = DRC_BITS_DEFAULT; - input->GetTextCtrl()->GetValue().ToLong(&drc_bits); - if (drc_bits > DRC_BITS_MAX) { - drc_bits = DRC_BITS_MAX; - input->GetTextCtrl()->SetValue(std::to_string(drc_bits)); - } else if (drc_bits < DRC_BITS_MIN && drc_bits != 0) { - drc_bits = DRC_BITS_MIN; - input->GetTextCtrl()->SetValue(std::to_string(drc_bits)); - } - - app_config->set("drc_bits", std::to_string(drc_bits)); - app_config->save(); - }; - - input->GetTextCtrl()->Bind(wxEVT_TEXT_ENTER, [set_draco_bits](wxCommandEvent& e) { - set_draco_bits(); - e.Skip(); - }); - - input->GetTextCtrl()->Bind(wxEVT_KILL_FOCUS, [set_draco_bits](wxFocusEvent& e) { - set_draco_bits(); - e.Skip(); - }); - - return sizer_input; + return m_sizer; } wxBoxSizer* PreferencesDialog::create_item_darkmode(wxString title,wxString tooltip, std::string param) { - wxBoxSizer* m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - m_sizer_checkbox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); + wxBoxSizer *m_sizer = create_item_label(title, tip); auto checkbox = new ::CheckBox(m_parent); checkbox->SetValue((app_config->get(param) == "1") ? true : false); + checkbox->SetToolTip(tip); m_dark_mode_ckeckbox = checkbox; - auto checkbox_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - checkbox_title->SetFont(::Label::Body_14); - checkbox_title->Wrap(DESIGN_TITLE_SIZE.x); - - m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(3)); - m_sizer_checkbox->Add(checkbox , 0, wxALIGN_CENTER | wxRIGHT | wxLEFT, FromDIP(5)); + m_sizer->Add(checkbox, 0, wxALIGN_CENTER); //// save config checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent& e) { @@ -896,10 +937,8 @@ wxBoxSizer* PreferencesDialog::create_item_darkmode(wxString title,wxString tool e.Skip(); }); - auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - checkbox_title->SetToolTip(tip); - checkbox->SetToolTip(tip); - return m_sizer_checkbox; + + return m_sizer; } void PreferencesDialog::set_dark_mode() @@ -915,19 +954,11 @@ void PreferencesDialog::set_dark_mode() #endif } -wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString tooltip, std::string param, const wxString secondary_title) +wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString tooltip, std::string param, const wxString secondary_title, const wxString wiki_url) { - wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); - - m_sizer_checkbox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty - auto checkbox_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - checkbox_title->SetFont(::Label::Body_14); - checkbox_title->Wrap(DESIGN_TITLE_SIZE.x); - checkbox_title->SetToolTip(tip); + wxBoxSizer *m_sizer = create_item_label(title, tip, wiki_url); auto checkbox = new ::CheckBox(m_parent); checkbox->SetValue(app_config->get_bool(param)); @@ -935,8 +966,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too if (param == "sync_user_preset") { m_sync_user_preset_checkbox = checkbox; } - m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(3)); - m_sizer_checkbox->Add(checkbox , 0, wxALIGN_CENTER | wxRIGHT | wxLEFT, FromDIP(5)); + m_sizer->Add(checkbox, 0, wxALIGN_CENTER); if(!secondary_title.IsEmpty()){ auto sec_title = new wxStaticText(m_parent, wxID_ANY, secondary_title); @@ -944,7 +974,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too sec_title->SetFont(::Label::Body_14); sec_title->Wrap(-1); sec_title->SetToolTip(tip); - m_sizer_checkbox->Add(sec_title, 0, wxALIGN_CENTER); + m_sizer->Add(sec_title, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); } //// save config @@ -1088,20 +1118,14 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too if (param == "developer_mode") { m_developer_mode_ckeckbox = checkbox; } if (param == "internal_developer_mode") { m_internal_developer_mode_ckeckbox = checkbox; } - return m_sizer_checkbox; + return m_sizer; } -wxBoxSizer* PreferencesDialog::create_item_button(wxString title, wxString title2, wxString tooltip, wxString tooltip2, std::function onclick) +wxBoxSizer* PreferencesDialog::create_item_button(wxString title, wxString title2, wxString tooltip, wxString tooltip2, std::function onclick, const wxString wiki_url) { - wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + auto tip = tooltip.IsEmpty() ? tooltip2 : tooltip; // use button tooltip if label tooltip empty - m_sizer_checkbox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - auto m_staticTextPath = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - m_staticTextPath->SetForegroundColour(DESIGN_GRAY900_COLOR); - m_staticTextPath->SetFont(::Label::Body_14); - m_staticTextPath->Wrap(DESIGN_TITLE_SIZE.x); - - m_staticTextPath->SetToolTip(tooltip.IsEmpty() ? tooltip2 : tooltip); // use button tooltip if label tooltip empty + wxBoxSizer *m_sizer = create_item_label(title, tip, wiki_url); auto m_button_download = new Button(m_parent, title2); m_button_download->SetStyle(title2 == _L("Clear") ? ButtonStyle::Alert : ButtonStyle::Regular, ButtonType::Parameter); @@ -1109,47 +1133,28 @@ wxBoxSizer* PreferencesDialog::create_item_button(wxString title, wxString title m_button_download->Bind(wxEVT_BUTTON, [this, onclick](auto &e) { onclick(); }); - m_sizer_checkbox->Add(m_staticTextPath , 0, wxALIGN_CENTER_VERTICAL); - m_sizer_checkbox->Add(m_button_download, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + m_sizer->Add(m_button_download, 0, wxALIGN_CENTER_VERTICAL); - return m_sizer_checkbox; + return m_sizer; } wxBoxSizer* PreferencesDialog::create_item_downloads(wxString title, wxString tooltip) { wxString download_path = wxString::FromUTF8(app_config->get("download_path")); - wxBoxSizer* m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); - wxPanel* label_panel = new wxPanel(m_parent); - wxBoxSizer* label_sizer = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer *m_sizer = create_item_label(title, tooltip); - m_sizer_checkbox->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - - auto downloads_folder = new wxStaticText(label_panel, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE); - downloads_folder->SetForegroundColour(DESIGN_GRAY900_COLOR); - downloads_folder->SetFont(::Label::Body_14); - downloads_folder->SetToolTip(tooltip); - downloads_folder->Wrap(-1); - - auto m_staticTextPath = new wxStaticText(label_panel, wxID_ANY, download_path, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); + auto m_staticTextPath = new wxStaticText(m_parent, wxID_ANY, download_path, wxDefaultPosition, wxSize(FromDIP(120),-1), wxST_ELLIPSIZE_END); m_staticTextPath->SetForegroundColour(DESIGN_GRAY600_COLOR); m_staticTextPath->SetFont(::Label::Body_14); m_staticTextPath->Wrap(-1); m_staticTextPath->SetToolTip(download_path); - label_sizer->Add(downloads_folder , 0, wxALIGN_CENTER_VERTICAL); - label_sizer->Add(m_staticTextPath , 0, wxALIGN_CENTER_VERTICAL); - label_panel->SetSize( wxSize(DESIGN_TITLE_SIZE.x, -1)); - label_panel->SetMinSize(wxSize(DESIGN_TITLE_SIZE.x, -1)); - label_panel->SetMaxSize(wxSize(DESIGN_TITLE_SIZE.x, -1)); - label_panel->SetSizer(label_sizer); - label_panel->Layout(); - - auto m_button_download = new Button(m_parent, _L("Browse") + " " + dots); + auto m_button_download = new Button(m_parent, _L("Browse") + dots); m_button_download->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); m_button_download->SetToolTip(_L("Choose folder for downloaded items")); - m_button_download->Bind(wxEVT_BUTTON, [this, m_staticTextPath, m_sizer_checkbox](auto& e) { + m_button_download->Bind(wxEVT_BUTTON, [this, m_staticTextPath, m_sizer](auto& e) { wxString defaultPath = wxT("/"); wxDirDialog dialog(this, _L("Choose Download Directory"), defaultPath, wxDD_NEW_DIR_BUTTON); @@ -1159,14 +1164,159 @@ wxBoxSizer* PreferencesDialog::create_item_downloads(wxString title, wxString to app_config->set("download_path", download_path_str); m_staticTextPath->SetLabelText(download_path); m_staticTextPath->SetToolTip(download_path); - m_sizer_checkbox->Layout(); + m_sizer->Layout(); } - }); + }); - m_sizer_checkbox->Add(label_panel , 0, wxALIGN_CENTER_VERTICAL); - m_sizer_checkbox->Add(m_button_download, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + m_sizer->Add(m_button_download, 0, wxALIGN_CENTER_VERTICAL); + m_sizer->Add(m_staticTextPath , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); - return m_sizer_checkbox; + return m_sizer; +} + +wxBoxSizer *PreferencesDialog::create_item_bambu_cloud(wxString title, wxString tooltip) +{ + wxBoxSizer *m_sizer = create_item_label(title, tooltip); + + auto cb = new ::CheckBox(m_parent); + m_bambu_cloud_checkbox = cb; + cb->SetValue(app_config->has_cloud_provider(BBL_CLOUD_PROVIDER)); + cb->SetToolTip(tooltip); + + cb->Bind(wxEVT_TOGGLEBUTTON, [this, cb](wxCommandEvent &e) { + e.Skip(); // let CheckBox::update() refresh the bitmap + if (cb->GetValue()) { + app_config->add_cloud_provider(BBL_CLOUD_PROVIDER); + } else { + app_config->remove_cloud_provider(BBL_CLOUD_PROVIDER); + } + app_config->save(); + + // Update homepage visibility immediately + auto *mainframe = wxGetApp().mainframe; + if (mainframe && mainframe->m_webview) + mainframe->m_webview->SendCloudProvidersInfo(); + }); + + m_sizer->Add(cb, 0, wxALIGN_CENTER); + + return m_sizer; +}; + +wxBoxSizer *PreferencesDialog::create_item_network_plugin_version(wxString title, wxString tooltip) +{ + wxBoxSizer *m_sizer = create_item_label(title, tooltip); + + m_network_version_combo = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY); + m_network_version_combo->GetDropDown().SetUseContentWidth(true); + m_network_version_combo->SetToolTip(tooltip); + + std::string current_version = app_config->get_network_plugin_version(); + if (current_version.empty()) { + current_version = get_latest_network_version(); + } + int current_selection = 0; + + m_available_versions = get_all_available_versions(); + + for (size_t i = 0; i < m_available_versions.size(); i++) { + const auto& ver = m_available_versions[i]; + wxString label; + + if (!ver.suffix.empty()) { + label = wxString::FromUTF8("\xE2\x94\x94 ") + wxString::FromUTF8(ver.display_name); + } else { + label = wxString::FromUTF8(ver.display_name); + } + + if (ver.is_latest) { + label += " " + _L("(Latest)"); + } + m_network_version_combo->Append(label); + if (current_version == ver.version) { + current_selection = i; + } + } + + m_network_version_combo->SetSelection(current_selection); + m_sizer->Add(m_network_version_combo, 0, wxALIGN_CENTER); + + m_network_version_combo->GetDropDown().Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { + int selection = e.GetSelection(); + if (selection >= 0 && selection < (int)m_available_versions.size()) { + const auto& selected_ver = m_available_versions[selection]; + std::string new_version = selected_ver.version; + std::string old_version = app_config->get_network_plugin_version(); + if (old_version.empty()) { + old_version = get_latest_network_version(); + } + + app_config->set(SETTING_NETWORK_PLUGIN_VERSION, new_version); + app_config->save(); + + if (new_version != old_version) { + BOOST_LOG_TRIVIAL(info) << "Network plugin version changed from " << old_version << " to " << new_version; + + // Update the use_legacy_network flag immediately + bool is_legacy = (new_version == BAMBU_NETWORK_AGENT_VERSION_LEGACY); + bool was_legacy = (old_version == BAMBU_NETWORK_AGENT_VERSION_LEGACY); + if (is_legacy != was_legacy) { + Slic3r::NetworkAgent::use_legacy_network = is_legacy; + BOOST_LOG_TRIVIAL(info) << "Updated use_legacy_network flag to " << is_legacy; + } + + if (!selected_ver.warning.empty()) { + MessageDialog warn_dlg(this, wxString::FromUTF8(selected_ver.warning), _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + if (warn_dlg.ShowModal() != wxID_OK) { + app_config->set(SETTING_NETWORK_PLUGIN_VERSION, old_version); + app_config->save(); + Slic3r::NetworkAgent::use_legacy_network = was_legacy; + e.Skip(); + return; + } + } + + // Check if the selected version already exists on disk + if (Slic3r::NetworkAgent::versioned_library_exists(new_version)) { + BOOST_LOG_TRIVIAL(info) << "Version " << new_version << " already exists on disk, triggering hot reload"; + if (wxGetApp().hot_reload_network_plugin()) { + MessageDialog dlg(this, _L("Network plug-in switched successfully."), _L("Success"), wxOK | wxICON_INFORMATION); + dlg.ShowModal(); + } else { + MessageDialog dlg(this, _L("Failed to load network plug-in. Please restart the application."), _L("Restart Required"), wxOK | wxICON_WARNING); + dlg.ShowModal(); + } + } else { + wxString msg = wxString::Format( + _L("You've selected network plug-in version %s.\n\nWould you like to download and install this version now?\n\nNote: The application may need to restart after installation."), + wxString::FromUTF8(new_version)); + + MessageDialog dlg(this, msg, _L("Download Network Plug-in"), wxYES_NO | wxICON_QUESTION); + if (dlg.ShowModal() == wxID_YES) { + DownloadProgressDialog progress_dlg(_L("Downloading Network Plug-in")); + progress_dlg.ShowModal(); + } + } + } + } + e.Skip(); + }); + + auto reload_btn = new Button(m_parent, wxEmptyString, "refresh", 0, 16); + reload_btn->SetStyle(ButtonStyle::Regular, ButtonType::Icon); + reload_btn->SetToolTip(_L("Reload the network plug-in without restarting the application")); + reload_btn->Bind(wxEVT_BUTTON, [this](auto& e) { + if (wxGetApp().hot_reload_network_plugin()) { + MessageDialog dlg(this, _L("Network plug-in reloaded successfully."), _L("Reload"), wxOK | wxICON_INFORMATION); + dlg.ShowModal(); + } else { + MessageDialog dlg(this, _L("Failed to reload network plug-in. Please restart the application."), _L("Reload Failed"), wxOK | wxICON_ERROR); + dlg.ShowModal(); + } + }); + m_sizer->Add(reload_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); + + return m_sizer; } #ifdef WIN32 @@ -1309,7 +1459,47 @@ PreferencesDialog::~PreferencesDialog() { } -void PreferencesDialog::on_dpi_changed(const wxRect &suggested_rect) { this->Refresh(); } +void PreferencesDialog::on_dpi_changed(const wxRect &suggested_rect) { + m_pref_tabs->Rescale(); + + int sel = m_pref_tabs->GetSelection(); + for (size_t i = 0; i < m_pref_tabs->GetCount(); ++i) + f_sizers[i]->Show(true); + + std::function WalkControls; + WalkControls = [&](wxWindow* parent, int depth) -> void { + if (!parent) return; + + for (auto* child : parent->GetChildren()) { + if (!child) + continue; + else if (auto* btn = dynamic_cast(child)) + btn->Rescale(); + else if (auto* chk = dynamic_cast(child)) + chk->msw_rescale(); + else if (auto* txt = dynamic_cast(child)) + txt->Rescale(); + else if (auto* cmb = dynamic_cast(child)) + cmb->Rescale(); + else if (auto* spn = dynamic_cast(child)) + spn->Rescale(); + else if (auto* lbl = dynamic_cast(child)){ + lbl->SetSize(DESIGN_TITLE_SIZE); + lbl->Rescale(); + } + + WalkControls(child, depth + 1); + } + }; + WalkControls(this, 0); + + wxCommandEvent event(wxEVT_TAB_SEL_CHANGED, m_pref_tabs->GetId()); + event.SetInt(sel); + event.SetEventObject(m_pref_tabs); + m_pref_tabs->GetEventHandler()->ProcessEvent(event); + + Refresh(); +} void PreferencesDialog::Split(const std::string &src, const std::string &separator, std::vector &dest) { @@ -1383,9 +1573,6 @@ void PreferencesDialog::create_items() auto item_show_splash_scr = create_item_checkbox(_L("Show splash screen"), _L("Show the splash screen during startup."), "show_splash_screen"); g_sizer->Add(item_show_splash_scr); - auto item_shared_profiles = create_item_checkbox(_L("Show shared profiles notification"), _L("Show a notification with a link to browse shared profiles when the selected printer is changed."), "show_shared_profiles_notification"); - g_sizer->Add(item_shared_profiles); - #ifdef __linux__ auto item_window_button_pos = create_item_checkbox(_L("Use window buttons on left side"), "", "window_buttons_on_left", _L("(Requires restart)")); g_sizer->Add(item_window_button_pos); @@ -1394,7 +1581,7 @@ void PreferencesDialog::create_items() //auto item_hints = create_item_checkbox(_L("Show \"Daily Tips\" after start"), page, _L("If enabled, useful hints are displayed at startup."), "show_daily_tips"); //g_sizer->Add(item_hints); - auto item_downloads = create_item_downloads(_L("Downloads folder") + ": ", _L("Target folder for downloaded items")); + auto item_downloads = create_item_downloads(_L("Downloads folder"), _L("Target folder for downloaded items")); g_sizer->Add(item_downloads); //// GENERAL > Project @@ -1405,6 +1592,9 @@ void PreferencesDialog::create_items() auto item_project_load = create_item_combobox(_L("Load behaviour"), _L("Should printer/filament/process settings be loaded when opening a 3MF file?"), SETTING_PROJECT_LOAD_BEHAVIOUR, projectLoadSettingsBehaviourOptions, projectLoadSettingsConfigOptions); g_sizer->Add(item_project_load); + auto item_backup = create_item_backup(_L("Auto backup"), _L("Backup your project periodically to help with restoring from an occasional crash.")); + g_sizer->Add(item_backup); + auto item_max_recent_count = create_item_input(_L("Maximum recent files"), "", _L("Maximum count of recent files"), "max_recent_count", [](wxString value) { long max = 0; if (value.ToLong(&max)) @@ -1418,11 +1608,20 @@ void PreferencesDialog::create_items() auto item_gcodes_warning = create_item_checkbox(_L("Don't warn when loading 3MF with modified G-code"), "", "no_warn_when_modified_gcodes"); g_sizer->Add(item_gcodes_warning); - auto item_step_dialog = create_item_checkbox(_L("Show options when importing STEP file"), _L("If enabled, a parameter settings dialog will appear during STEP file import."), "enable_step_mesh_setting"); + auto item_step_dialog = create_item_checkbox( + _L("Show options when importing STEP file"), _L("If enabled, a parameter settings dialog will appear during STEP file import."), + "enable_step_mesh_setting", wxEmptyString, "import_export#dont-show-again" + ); g_sizer->Add(item_step_dialog); - auto item_backup = create_item_backup(_L("Auto backup"), _L("Backup your project periodically to help with restoring from an occasional crash.")); - g_sizer->Add(item_backup); + auto item_draco_bits = create_item_spinctrl(_L("Quality level for Draco export"), "", + _L("bits"), + _L("Controls the quantization bit depth used when compressing the mesh to Draco format.\n" + "0 = lossless compression (geometry is preserved at full precision). Valid lossy values range from 8 to 30.\n" + "Lower values produce smaller files but lose more geometric detail; higher values preserve more detail at the cost of larger files."), + "drc_bits", DRC_BITS_MIN, DRC_BITS_MAX, nullptr, "import_export#drc" + ); + g_sizer->Add(item_draco_bits); //// GENERAL > Preset g_sizer->Add(create_item_title(_L("Preset")), 1, wxEXPAND); @@ -1443,6 +1642,9 @@ void PreferencesDialog::create_items() "filaments_area_preferred_count", 8, 99, [this](int value) {m_filament_height_timer.StartOnce(500);}); g_sizer->Add(item_filament_area_height); + auto item_shared_profiles = create_item_checkbox(_L("Show shared profiles notification"), _L("Show a notification with a link to browse shared profiles when the selected printer is changed."), "show_shared_profiles_notification"); + g_sizer->Add(item_shared_profiles); + //// GENERAL > Features g_sizer->Add(create_item_title(_L("Features")), 1, wxEXPAND); @@ -1457,13 +1659,6 @@ void PreferencesDialog::create_items() g_sizer->Add(item_pop_up_filament_map_dialog); #endif - auto item_draco_bits = create_item_draco(_L("Quality level for Draco export"), - _L("bits"), - _L("Controls the quantization bit depth used when compressing the mesh to Draco format.\n" - "0 = lossless compression (geometry is preserved at full precision). Valid lossy values range from 8 to 30.\n" - "Lower values produce smaller files but lose more geometric detail; higher values preserve more detail at the cost of larger files.")); - g_sizer->Add(item_draco_bits); - g_sizer->AddSpacer(FromDIP(10)); sizer_page->Add(g_sizer, 0, wxEXPAND); @@ -1666,42 +1861,8 @@ void PreferencesDialog::create_items() //// ONLINE > Cloud Providers g_sizer->Add(create_item_title(_L("Cloud Providers")), 1, wxEXPAND); - { - auto sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - - auto text = new wxStaticText(m_parent, wxID_ANY, _L("Enable Bambu Cloud"), - wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - text->SetForegroundColour(DESIGN_GRAY900_COLOR); - text->SetFont(::Label::Body_14); - text->SetToolTip(_L("Allow logging into Bambu Cloud alongside Orca Cloud. When enabled, a Bambu login section appears on the homepage.")); - text->Wrap(DESIGN_TITLE_SIZE.x); - - auto cb = new ::CheckBox(m_parent); - m_bambu_cloud_checkbox = cb; - cb->SetValue(app_config->has_cloud_provider(BBL_CLOUD_PROVIDER)); - cb->SetToolTip(text->GetToolTipText()); - - cb->Bind(wxEVT_TOGGLEBUTTON, [this, cb](wxCommandEvent &e) { - e.Skip(); // let CheckBox::update() refresh the bitmap - if (cb->GetValue()) { - app_config->add_cloud_provider(BBL_CLOUD_PROVIDER); - } else { - app_config->remove_cloud_provider(BBL_CLOUD_PROVIDER); - } - app_config->save(); - - // Update homepage visibility immediately - auto *mainframe = wxGetApp().mainframe; - if (mainframe && mainframe->m_webview) - mainframe->m_webview->SendCloudProvidersInfo(); - }); - - sizer->Add(text, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(3)); - sizer->Add(cb, 0, wxALIGN_CENTER | wxRIGHT | wxLEFT, FromDIP(5)); - - g_sizer->Add(sizer); - } + auto item_bambu_cloud = create_item_bambu_cloud(_L("Enable Bambu Cloud"), _L("Allow logging into Bambu Cloud alongside Orca Cloud. When enabled, a Bambu login section appears on the homepage.")); + g_sizer->Add(item_bambu_cloud); //// ONLINE > Update & sync g_sizer->Add(create_item_title(_L("Update & sync")), 1, wxEXPAND); @@ -1717,6 +1878,13 @@ void PreferencesDialog::create_items() if (m_sync_user_preset_checkbox) m_sync_user_preset_checkbox->Enable(false); } + auto item_filament_sync_mode = create_item_combobox( + _L("Filament sync mode"), + _L("Choose whether sync updates both filament preset and color, or only color."), + "sync_ams_filament_mode", + {_L("Filament & Color"), _L("Color only")}); + g_sizer->Add(item_filament_sync_mode); + auto item_system_sync = create_item_checkbox(_L("Update built-in presets automatically."), "", "sync_system_preset"); g_sizer->Add(item_system_sync); @@ -1725,128 +1893,14 @@ void PreferencesDialog::create_items() SETTING_USE_ENCRYPTED_TOKEN_FILE); g_sizer->Add(item_token_storage); - //// ONLINE > Filament Sync Options - g_sizer->Add(create_item_title(_L("Filament Sync Options")), 1, wxEXPAND); - - auto item_filament_sync_mode = create_item_combobox( - _L("Filament sync mode"), - _L("Choose whether sync updates both filament preset and color, or only color."), - "sync_ams_filament_mode", - {_L("Filament & Color"), _L("Color only")}); - g_sizer->Add(item_filament_sync_mode); - //// ONLINE > Network plugin g_sizer->Add(create_item_title(_L("Bambu network plug-in")), 1, wxEXPAND); auto item_enable_plugin = create_item_checkbox(_L("Enable Bambu network plug-in"), "", "installed_networking"); g_sizer->Add(item_enable_plugin); - m_network_version_sizer = new wxBoxSizer(wxHORIZONTAL); - m_network_version_sizer->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN)); - - auto version_title = new wxStaticText(m_parent, wxID_ANY, _L("Network plug-in version"), wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE); - version_title->SetForegroundColour(DESIGN_GRAY900_COLOR); - version_title->SetFont(::Label::Body_14); - version_title->SetToolTip(_L("Select the network plug-in version to use")); - version_title->Wrap(DESIGN_TITLE_SIZE.x); - m_network_version_sizer->Add(version_title, 0, wxALIGN_CENTER); - - m_network_version_combo = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(180), -1), 0, nullptr, wxCB_READONLY); - m_network_version_combo->SetFont(::Label::Body_14); - m_network_version_combo->GetDropDown().SetFont(::Label::Body_14); - - std::string current_version = app_config->get_network_plugin_version(); - if (current_version.empty()) { - current_version = get_latest_network_version(); - } - int current_selection = 0; - - m_available_versions = get_all_available_versions(); - - for (size_t i = 0; i < m_available_versions.size(); i++) { - const auto& ver = m_available_versions[i]; - wxString label; - - if (!ver.suffix.empty()) { - label = wxString::FromUTF8("\xE2\x94\x94 ") + wxString::FromUTF8(ver.display_name); - } else { - label = wxString::FromUTF8(ver.display_name); - } - - if (ver.is_latest) { - label += " " + _L("(Latest)"); - } - m_network_version_combo->Append(label); - if (current_version == ver.version) { - current_selection = i; - } - } - - m_network_version_combo->SetSelection(current_selection); - m_network_version_sizer->Add(m_network_version_combo, 0, wxALIGN_CENTER | wxLEFT, FromDIP(5)); - - m_network_version_combo->GetDropDown().Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { - int selection = e.GetSelection(); - if (selection >= 0 && selection < (int)m_available_versions.size()) { - const auto& selected_ver = m_available_versions[selection]; - std::string new_version = selected_ver.version; - std::string old_version = app_config->get_network_plugin_version(); - if (old_version.empty()) { - old_version = get_latest_network_version(); - } - - app_config->set(SETTING_NETWORK_PLUGIN_VERSION, new_version); - app_config->save(); - - if (new_version != old_version) { - BOOST_LOG_TRIVIAL(info) << "Network plugin version changed from " << old_version << " to " << new_version; - - // Update the use_legacy_network flag immediately - bool is_legacy = (new_version == BAMBU_NETWORK_AGENT_VERSION_LEGACY); - bool was_legacy = (old_version == BAMBU_NETWORK_AGENT_VERSION_LEGACY); - if (is_legacy != was_legacy) { - Slic3r::NetworkAgent::use_legacy_network = is_legacy; - BOOST_LOG_TRIVIAL(info) << "Updated use_legacy_network flag to " << is_legacy; - } - - if (!selected_ver.warning.empty()) { - MessageDialog warn_dlg(this, wxString::FromUTF8(selected_ver.warning), _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); - if (warn_dlg.ShowModal() != wxID_OK) { - app_config->set(SETTING_NETWORK_PLUGIN_VERSION, old_version); - app_config->save(); - Slic3r::NetworkAgent::use_legacy_network = was_legacy; - e.Skip(); - return; - } - } - - // Check if the selected version already exists on disk - if (Slic3r::NetworkAgent::versioned_library_exists(new_version)) { - BOOST_LOG_TRIVIAL(info) << "Version " << new_version << " already exists on disk, triggering hot reload"; - if (wxGetApp().hot_reload_network_plugin()) { - MessageDialog dlg(this, _L("Network plug-in switched successfully."), _L("Success"), wxOK | wxICON_INFORMATION); - dlg.ShowModal(); - } else { - MessageDialog dlg(this, _L("Failed to load network plug-in. Please restart the application."), _L("Restart Required"), wxOK | wxICON_WARNING); - dlg.ShowModal(); - } - } else { - wxString msg = wxString::Format( - _L("You've selected network plug-in version %s.\n\nWould you like to download and install this version now?\n\nNote: The application may need to restart after installation."), - wxString::FromUTF8(new_version)); - - MessageDialog dlg(this, msg, _L("Download Network Plug-in"), wxYES_NO | wxICON_QUESTION); - if (dlg.ShowModal() == wxID_YES) { - DownloadProgressDialog progress_dlg(_L("Downloading Network Plug-in")); - progress_dlg.ShowModal(); - } - } - } - } - e.Skip(); - }); - - g_sizer->Add(m_network_version_sizer); + auto item_plugin_version = create_item_network_plugin_version(_L("Network plug-in version"), _L("Select the network plug-in version to use")); + g_sizer->Add(item_plugin_version); g_sizer->AddSpacer(FromDIP(10)); sizer_page->Add(g_sizer, 0, wxEXPAND); @@ -1923,39 +1977,33 @@ void PreferencesDialog::create_items() //// DEVELOPER > Settings g_sizer->Add(create_item_title(_L("Settings")), 1, wxEXPAND); - auto item_develop_mode = create_item_checkbox(_L("Developer mode"), "", "developer_mode"); + auto item_develop_mode = create_item_checkbox(_L("Developer mode"), "", "developer_mode", wxEmptyString, "option_mode#developer+mode"); g_sizer->Add(item_develop_mode); auto item_ams_blacklist = create_item_checkbox(_L("Skip AMS blacklist check"), "", "skip_ams_blacklist_check"); g_sizer->Add(item_ams_blacklist); - - auto item_keep_painting = create_item_checkbox(_L("(Experimental) Keep painted feature after mesh change"), _L("Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\nHighly experimental! Slow and may create artifact."), "keep_painting"); - g_sizer->Add(item_keep_painting); - + auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets"); g_sizer->Add(item_show_unsupported); + //// DEVELOPER > Experimental Features + g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND); + + auto item_keep_painting = create_item_checkbox(_L("Keep painted feature after mesh change"), _L("Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\nHighly experimental! Slow and may create artifact."), "keep_painting"); + g_sizer->Add(item_keep_painting); + + //// DEVELOPER > Storage + g_sizer->Add(create_item_title(_L("Storage")), 1, wxEXPAND); auto item_allow_abnormal_storage = create_item_checkbox(_L("Allow Abnormal Storage"), _L("This allows the use of Storage that is marked as abnormal by the Printer.\nUse at your own risk, can cause issues!"), "allow_abnormal_storage"); g_sizer->Add(item_allow_abnormal_storage); + //// DEVELOPER > Log Level g_sizer->Add(create_item_title(_L("Log Level")), 1, wxEXPAND); auto log_level_list = std::vector{_L("fatal"), _L("error"), _L("warning"), _L("info"), _L("debug"), _L("trace")}; auto loglevel_combox = create_item_loglevel_combobox(_L("Log Level"), _L("Log Level"), log_level_list); g_sizer->Add(loglevel_combox); - g_sizer->Add(create_item_title(_L("Network plug-in")), 1, wxEXPAND); - auto item_reload_plugin = create_item_button(_L("Network plug-in"), _L("Reload"), _L("Reload the network plug-in without restarting the application"), "", [this]() { - if (wxGetApp().hot_reload_network_plugin()) { - MessageDialog dlg(this, _L("Network plug-in reloaded successfully."), _L("Reload"), wxOK | wxICON_INFORMATION); - dlg.ShowModal(); - } else { - MessageDialog dlg(this, _L("Failed to reload network plug-in. Please restart the application."), _L("Reload Failed"), wxOK | wxICON_ERROR); - dlg.ShowModal(); - } - }); - g_sizer->Add(item_reload_plugin); - //// DEVELOPER > Debug #if !BBL_RELEASE_TO_PUBLIC g_sizer->Add(create_item_title(_L("Debug")), 1, wxEXPAND); @@ -2003,39 +2051,6 @@ void PreferencesDialog::create_sync_page() sizer_page->Fit(page); } -void PreferencesDialog::create_shortcuts_page() -{ - auto page = new wxWindow(this, wxID_ANY); - wxBoxSizer *sizer_page = new wxBoxSizer(wxVERTICAL); - - auto title_view_control = create_item_title(_L("View control settings")); - std::vector keyboard_supported; - Split(app_config->get("keyboard_supported"), "/", keyboard_supported); - - std::vector mouse_supported; - Split(app_config->get("mouse_supported"), "/", mouse_supported); - - auto item_rotate_view = create_item_multiple_combobox(_L("Rotate view"), _L("Rotate view"), "rotate_view", keyboard_supported, - mouse_supported); - auto item_move_view = create_item_multiple_combobox(_L("Pan view"), _L("Pan view"), "move_view", keyboard_supported, mouse_supported); - auto item_zoom_view = create_item_multiple_combobox(_L("Zoom view"), _L("Zoom view"), "rotate_view", keyboard_supported, mouse_supported); - - auto title_other = create_item_title(_L("Other")); - auto item_other = create_item_checkbox(_L("Reverse scroll direction while zooming"), _L("Reverse scroll direction while zooming"), "mouse_wheel"); - - sizer_page->Add(title_view_control, 0, wxTOP, 26); - sizer_page->Add(item_rotate_view, 0, wxTOP, 8); - sizer_page->Add(item_move_view, 0, wxTOP, 8); - sizer_page->Add(item_zoom_view, 0, wxTOP, 8); - // sizer_page->Add(item_precise_control, 0, wxTOP, 0); - sizer_page->Add(title_other, 0, wxTOP, 20); - sizer_page->Add(item_other, 0, wxTOP, 5); - - page->SetSizer(sizer_page); - page->Layout(); - sizer_page->Fit(page); -} - wxBoxSizer* PreferencesDialog::create_debug_page() { m_internal_developer_mode_def = app_config->get("internal_developer_mode"); diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index d7daca5ff8..86d1e06358 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -28,9 +28,6 @@ namespace Slic3r { namespace GUI { #define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1) #define DESIGN_LEFT_MARGIN 25 -class CheckBox; -class TextInput; - class PreferencesDialog : public DPIDialog { private: @@ -75,7 +72,6 @@ public: ::CheckBox * m_bambu_cloud_checkbox = {nullptr}; ::TextInput *m_backup_interval_textinput = {nullptr}; ::ComboBox * m_network_version_combo = {nullptr}; - wxBoxSizer * m_network_version_sizer = {nullptr}; std::vector m_available_versions; wxString m_developer_mode_def; @@ -86,30 +82,30 @@ public: std::vector f_sizers; wxBoxSizer *create_item_title(wxString title); - wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::function onchange = {}); - wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index); + wxBoxSizer *create_item_label(wxString label, const wxString tooltip = "", const wxString wiki_url = ""); + wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::function onchange = {}, const wxString wiki_url = ""); + wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index, const wxString wiki_url = ""); wxBoxSizer *create_item_region_combobox(wxString title, wxString tooltip); wxBoxSizer *create_item_language_combobox(wxString title, wxString tooltip); wxBoxSizer *create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector vlist); - wxBoxSizer *create_item_checkbox(wxString title, wxString tooltip, std::string param, const wxString secondary_title = ""); + wxBoxSizer *create_item_checkbox(wxString title, wxString tooltip, std::string param, const wxString secondary_title = "", const wxString wiki_url = ""); wxBoxSizer *create_item_darkmode(wxString title,wxString tooltip, std::string param); void set_dark_mode(); - wxBoxSizer *create_item_button(wxString title, wxString title2, wxString tooltip, wxString tooltip2, std::function onclick); + wxBoxSizer *create_item_button(wxString title, wxString title2, wxString tooltip, wxString tooltip2, std::function onclick, const wxString wiki_url = ""); wxBoxSizer *create_item_downloads(wxString title, wxString tooltip); - wxBoxSizer *create_item_input(wxString title, wxString title2, wxString tooltip, std::string param, std::function onchange = {}); - wxBoxSizer *create_item_spinctrl(wxString title, wxString title2, wxString side_label, wxString tooltip, std::string param, int min, int max, std::function onchange = nullptr); + wxBoxSizer *create_item_input(wxString title, wxString title2, wxString tooltip, std::string param, std::function onchange = {}, const wxString wiki_url = ""); + wxBoxSizer *create_item_spinctrl(wxString title, wxString title2, wxString side_label, wxString tooltip, std::string param, int min, int max, std::function onchange = nullptr, const wxString wiki_url = ""); wxBoxSizer *create_camera_orbit_mult_input(wxString title, wxString tooltip); wxBoxSizer *create_item_backup(wxString title, wxString tooltip); wxBoxSizer *create_item_auto_reslice(wxString title, wxString checkbox_tooltip, wxString delay_tooltip); - wxBoxSizer *create_item_draco(wxString title, wxString side_label, wxString tooltip); - wxBoxSizer *create_item_multiple_combobox(wxString title, wxString tooltip, std::string parama, std::vector vlista, std::vector vlistb); + wxBoxSizer *create_item_bambu_cloud(wxString title, wxString tooltip); + wxBoxSizer *create_item_network_plugin_version(wxString title, wxString tooltip); #ifdef WIN32 wxBoxSizer *create_item_link_association(wxString url_prefix, wxString website_name); #endif // WIN32 void create_items(); void create_sync_page(); - void create_shortcuts_page(); wxBoxSizer* create_debug_page(); void UpdateSidebarLayout(); @@ -121,7 +117,7 @@ public: int m_current_language_selected = {0}; private: - std::tuple create_item_combobox_base(wxString title, wxString tooltip, std::string param, std::vector vlist, unsigned int current_index); + std::tuple create_item_combobox_base(wxString title, wxString tooltip, std::string param, std::vector vlist, unsigned int current_index, const wxString wiki_url = ""); }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 4400b13fa6..e236c84e67 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -203,6 +203,12 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type) this->SetCornerRadius(this->FromDIP(4)); this->SetFont(Label::Body_14); } + else if (type == ButtonType::Icon) { + this->SetPaddingSize(FromDIP(wxSize(5,5))); + this->SetMinSize(FromDIP(wxSize(26,26))); + this->SetSize(FromDIP(wxSize(26,26))); + this->SetCornerRadius(this->FromDIP(4)); + } else if (type == ButtonType::Expanded) { this->SetMinSize(FromDIP(wxSize(-1,32))); this->SetPaddingSize(FromDIP(wxSize(12,8))); diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index a5ba989db3..bc093b512a 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -23,6 +23,7 @@ enum class ButtonType{ Window , // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes Choice , // Font14 Semi-Rounded For dialog/window choice buttons Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes + Icon , // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while creation of button Expanded , // Font14 Semi-Rounded For full length buttons. ex. buttons in static box }; From 59e655dd1dd331ed977b0ee3be2e68a6148fc828 Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:55:09 -0300 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=92=A5Crash=20Fix=20Object=20settings?= =?UTF-8?q?=20table=20(#14479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Crash Fix Object settings * Update GUI_ObjectTable.cpp --- src/slic3r/GUI/GUI_ObjectTable.cpp | 69 +++++++++++++++++++++--------- src/slic3r/GUI/GUI_ObjectTable.hpp | 4 +- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index 48d92a4d58..1fc07bf5a2 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -1393,8 +1393,10 @@ wxString ObjectGridTable::GetValue (int row, int col) else if (grid_col->type == coInt) { ConfigOptionInt& option_value = dynamic_cast(option); return wxString::Format("%d", option_value.value); - } - else if (grid_col->type == coFloat) { + } else if (grid_col->type == coFloat) { + if (auto option_values = dynamic_cast(&option)) { + return wxString::Format("%.2f", option_values->get_at(0)); + } ConfigOptionFloat& option_value = dynamic_cast(option); return wxString::Format("%.2f", option_value.value); } @@ -1592,16 +1594,25 @@ void ObjectGridTable::SetValue( int row, int col, const wxString& value ) else { update_value_to_object(m_panel->m_model, grid_row, col); } - } - else if (grid_col->type == coFloat) { - ConfigOptionFloat &option_value = dynamic_cast((*grid_row)[(GridColType)col]); - ConfigOptionFloat &option_ori_value = dynamic_cast((*grid_row)[(GridColType)(col+1)]); + } else if (grid_col->type == coFloat) { + if (auto option_values = dynamic_cast(&(*grid_row)[(GridColType) col])) { + ConfigOptionFloatsNullable& option_ori_values = dynamic_cast((*grid_row)[(GridColType) (col + 1)]); - double double_value; - value.ToDouble(&double_value); - option_value.value = (float)double_value; + double double_value; + value.ToDouble(&double_value); + option_values->values.at(0) = (float) double_value; - update_value_to_config(grid_row->config, grid_col->key, option_value, option_ori_value); + update_value_to_config(grid_row->config, grid_col->key, *option_values, option_ori_values); + } else { + ConfigOptionFloat& option_value = dynamic_cast((*grid_row)[(GridColType) col]); + ConfigOptionFloat& option_ori_value = dynamic_cast((*grid_row)[(GridColType) (col + 1)]); + + double double_value; + value.ToDouble(&double_value); + option_value.value = (float) double_value; + + update_value_to_config(grid_row->config, grid_col->key, option_value, option_ori_value); + } } else if (grid_col->type == coInt) { ConfigOptionInt &option_value = dynamic_cast((*grid_row)[(GridColType)col]); @@ -1676,8 +1687,11 @@ double ObjectGridTable::GetValueAsDouble( int row, int col ) return 0; ObjectGridRow* grid_row = m_grid_data[row - 1]; - ConfigOptionFloat &option_value = dynamic_cast((*grid_row)[(GridColType)col]); - return (double )option_value.getFloat(); + if (auto option_values = dynamic_cast(&(*grid_row)[(GridColType) col])) { + return (double) option_values->get_at(0); + } + ConfigOptionFloat& option_value = dynamic_cast((*grid_row)[(GridColType) col]); + return (double) option_value.getFloat(); } void ObjectGridTable::SetValueAsLong( int row, int col, long value ) @@ -1722,6 +1736,17 @@ void ObjectGridTable::SetValueAsDouble(int row, int col, double value) if ((value > 100.f) || (value < 0.f)) return; } + + if (auto option_values = dynamic_cast(&(*grid_row)[(GridColType) col])) { + ConfigOptionFloatsNullable &option_ori_values = dynamic_cast((*grid_row)[(GridColType) (col + 1)]); + + option_values->values.at(0) = (float) value; + + update_value_to_config(grid_row->config, grid_col->key, *option_values, option_ori_values); + + return; + } + ConfigOptionFloat &option_value = dynamic_cast((*grid_row)[(GridColType)col]); ConfigOptionFloat &option_ori_value = dynamic_cast((*grid_row)[(GridColType)(col+1)]); @@ -1981,8 +2006,8 @@ void ObjectGridTable::construct_object_configs(ObjectGrid *object_grid) object_grid->ori_enable_support = *(global_config.option(m_col_data[col_enable_support]->key)); object_grid->brim_type = *(get_object_config_value>(global_config, object_grid->config, m_col_data[col_brim_type]->key)); object_grid->ori_brim_type = *(global_config.option>(m_col_data[col_brim_type]->key)); - object_grid->speed_perimeter = *(get_object_config_value(global_config, object_grid->config, m_col_data[col_speed_perimeter]->key)); - object_grid->ori_speed_perimeter = *(global_config.option(m_col_data[col_speed_perimeter]->key)); + object_grid->speed_perimeter = *(get_object_config_value(global_config, object_grid->config, m_col_data[col_speed_perimeter]->key)); + object_grid->ori_speed_perimeter = *(global_config.option(m_col_data[col_speed_perimeter]->key)); m_grid_data.push_back(object_grid); int volume_count = object->volumes.size(); @@ -2031,7 +2056,7 @@ void ObjectGridTable::construct_object_configs(ObjectGrid *object_grid) volume_grid->ori_enable_support = object_grid->enable_support; volume_grid->brim_type = *(get_volume_config_value>(global_config, object_grid->config, volume_grid->config, m_col_data[col_brim_type]->key)); volume_grid->ori_brim_type = object_grid->brim_type; - volume_grid->speed_perimeter = *(get_volume_config_value(global_config, object_grid->config, volume_grid->config, m_col_data[col_speed_perimeter]->key)); + volume_grid->speed_perimeter = *(get_volume_config_value(global_config, object_grid->config, volume_grid->config, m_col_data[col_speed_perimeter]->key)); volume_grid->ori_speed_perimeter = object_grid->speed_perimeter; m_grid_data.push_back(volume_grid); } @@ -2077,8 +2102,8 @@ void ObjectGridTable::reload_object_data(ObjectGridRow* grid_row, const std::str grid_row->ori_enable_support = *(global_config.option(m_col_data[col_enable_support]->key)); grid_row->brim_type = *(get_object_config_value>(global_config, grid_row->config, m_col_data[col_brim_type]->key)); grid_row->ori_brim_type = *(global_config.option>(m_col_data[col_brim_type]->key)); - grid_row->speed_perimeter = *(get_object_config_value(global_config, grid_row->config, m_col_data[col_speed_perimeter]->key)); - grid_row->ori_speed_perimeter = *(global_config.option(m_col_data[col_speed_perimeter]->key)); + grid_row->speed_perimeter = *(get_object_config_value(global_config, grid_row->config, m_col_data[col_speed_perimeter]->key)); + grid_row->ori_speed_perimeter = *(global_config.option(m_col_data[col_speed_perimeter]->key)); } else if (category == L("Quality")) { grid_row->layer_height = *(get_object_config_value(global_config, grid_row->config, m_col_data[col_layer_height]->key)); @@ -2099,8 +2124,8 @@ void ObjectGridTable::reload_object_data(ObjectGridRow* grid_row, const std::str grid_row->ori_brim_type = *(global_config.option>(m_col_data[col_brim_type]->key)); } else if (category == L("Speed")) { - grid_row->speed_perimeter = *(get_object_config_value(global_config, grid_row->config, m_col_data[col_speed_perimeter]->key)); - grid_row->ori_speed_perimeter = *(global_config.option(m_col_data[col_speed_perimeter]->key)); + grid_row->speed_perimeter = *(get_object_config_value(global_config, grid_row->config, m_col_data[col_speed_perimeter]->key)); + grid_row->ori_speed_perimeter = *(global_config.option(m_col_data[col_speed_perimeter]->key)); } } @@ -2117,7 +2142,7 @@ void ObjectGridTable::reload_part_data(ObjectGridRow* volume_row, ObjectGridRow* volume_row->ori_enable_support = object_row->enable_support; volume_row->brim_type = *(get_volume_config_value>(global_config, object_row->config, volume_row->config, m_col_data[col_brim_type]->key)); volume_row->ori_brim_type = object_row->brim_type; - volume_row->speed_perimeter = *(get_volume_config_value(global_config, object_row->config, volume_row->config, m_col_data[col_speed_perimeter]->key)); + volume_row->speed_perimeter = *(get_volume_config_value(global_config, object_row->config, volume_row->config, m_col_data[col_speed_perimeter]->key)); volume_row->ori_speed_perimeter = object_row->speed_perimeter; } else if (category == L("Quality")) { @@ -2154,7 +2179,7 @@ void ObjectGridTable::reload_part_data(ObjectGridRow* volume_row, ObjectGridRow* volume_row->ori_brim_type = object_row->brim_type; } else if (category == L("Speed")) { - volume_row->speed_perimeter = *(get_volume_config_value(global_config, object_row->config, volume_row->config, m_col_data[col_speed_perimeter]->key)); + volume_row->speed_perimeter = *(get_volume_config_value(global_config, object_row->config, volume_row->config, m_col_data[col_speed_perimeter]->key)); if (volume_row->speed_perimeter == object_row->speed_perimeter) { volume_row->config->erase(m_col_data[col_speed_perimeter]->key); } @@ -2549,6 +2574,8 @@ bool ObjectGridTable::OnCellLeftClick(int row, int col, ConfigOptionType &type) void ObjectGridTable::OnSelectCell(int row, int col) { m_selected_cells.clear(); + if (!m_panel->m_side_window) + return; m_panel->m_side_window->Freeze(); if (row == 0 || col == col_filaments) { m_panel->m_object_settings->UpdateAndShow(row, false, false, false, nullptr, nullptr, std::string()); diff --git a/src/slic3r/GUI/GUI_ObjectTable.hpp b/src/slic3r/GUI/GUI_ObjectTable.hpp index a21436fe0e..a5a0059e1b 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.hpp +++ b/src/slic3r/GUI/GUI_ObjectTable.hpp @@ -344,8 +344,8 @@ public: ConfigOptionBool ori_enable_support; ConfigOptionEnum brim_type; ConfigOptionEnum ori_brim_type; - ConfigOptionFloat speed_perimeter; - ConfigOptionFloat ori_speed_perimeter; + ConfigOptionFloatsNullable speed_perimeter; + ConfigOptionFloatsNullable ori_speed_perimeter; ModelConfig* config; ModelVolumeType model_volume_type; From 3cf0f4dba5f092d2ddb2dacc86acbac76d04d4a3 Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Mon, 29 Jun 2026 19:03:44 -0600 Subject: [PATCH 5/7] test(fff_print): re-enable tests disabled by the m_origin clipper throw (#14482) PR #13712 fixed the uninitialized Print::m_origin (commit 99dea01cc3, "Fix coord out-of-range exception caused by m_origin memory not initialized to 0") that made headless slice() intermittently throw ClipperLib's "Coordinate outside allowed range". With that root cause fixed, the three tests disabled for it pass again, so drop their [NotWorking] tags. --- tests/fff_print/test_printgcode.cpp | 5 +---- tests/fff_print/test_skirt_brim.cpp | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/fff_print/test_printgcode.cpp b/tests/fff_print/test_printgcode.cpp index 4e09072fff..8a65679104 100644 --- a/tests/fff_print/test_printgcode.cpp +++ b/tests/fff_print/test_printgcode.cpp @@ -31,10 +31,7 @@ boost::regex perimeters_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; perimeter"); boost::regex infill_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; infill"); boost::regex skirt_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; skirt"); -// [NotWorking]: slice() intermittently throws clipper's "Coordinate outside allowed -// range" in CI (Linux) while passing locally. Disabled pending a root-cause fix in a -// follow-up PR. -SCENARIO( "PrintGCode basic functionality", "[PrintGCode][NotWorking]") { +SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") { GIVEN("A default configuration and a print test object") { WHEN("the output is executed with no support material") { Slic3r::Print print; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 7bd3259925..5529abe615 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -32,10 +32,7 @@ using namespace Slic3r; return brim_tool; } -// [NotWorking]: slice() intermittently throws clipper's "Coordinate outside allowed -// range" in CI (Linux) while passing locally. Disabled pending a root-cause fix in a -// follow-up PR. -TEST_CASE("Skirt height is honored", "[SkirtBrim][NotWorking]") { +TEST_CASE("Skirt height is honored", "[SkirtBrim]") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ { "skirt_loops", 1 }, @@ -55,8 +52,7 @@ TEST_CASE("Skirt height is honored", "[SkirtBrim][NotWorking]") { REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t)config.opt_int("skirt_height")); } -// [NotWorking]: see "Skirt height is honored" above; same CI-only clipper range throw. -SCENARIO("Skirt and brim generation", "[SkirtBrim][NotWorking]") { +SCENARIO("Skirt and brim generation", "[SkirtBrim]") { GIVEN("A default configuration") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); From 03568b52009c89d4a7e414d86679ca8a8322a1ae Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Mon, 29 Jun 2026 19:12:12 -0600 Subject: [PATCH 6/7] fix: slicing fails when filament_end_gcode uses layer_z with a wipe tower (#14462) WipeTowerIntegration::append_tcr processed filament_end_gcode with only layer_num in its placeholder config, so a filament_end_gcode referencing {layer_z} could not be evaluated and slicing aborted. This affects any multi-filament print that routes tool changes through the prime/wipe tower (for example a support filament on a Bambu printer); the same macro works in machine_end_gcode and on the non-wipe-tower set_extruder path, which both define layer_z. Set layer_z to tcr.print_z, the value this function already provides to its change_filament_gcode and tcr_rotated_gcode placeholders. Fixes #10119 --- src/libslic3r/GCode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ba9120d3bd..80b9cb47ee 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -781,6 +781,7 @@ static std::vector get_path_of_change_filament(const Print& print) if (gcodegen.writer().filament() != nullptr && !filament_end_gcode.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z)); if (!gcodegen.m_filament_instances_code.empty()) { end_filament_gcode_str += ("M624 " + gcodegen.m_filament_instances_code + "\n"); gcodegen.m_filament_instances_code = ""; From 3c8c667dbd402eb52e24fcc69aea3a1b5c5c72a1 Mon Sep 17 00:00:00 2001 From: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:48:58 +0100 Subject: [PATCH 7/7] Add "First layer fan speed" option (per filament) (#14270) Implement First layer fan speed --- src/libslic3r/GCode/CoolingBuffer.cpp | 49 ++++++++++++++++++++++++--- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintConfig.cpp | 20 ++++++++++- src/libslic3r/PrintConfig.hpp | 2 ++ src/slic3r/GUI/Tab.cpp | 25 ++++++++++++++ 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index edcf60cdb6..c0c79be466 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -744,6 +744,13 @@ std::string CoolingBuffer::apply_layer_cooldown( int close_fan_the_first_x_layers = EXTRUDER_CONFIG(close_fan_the_first_x_layers); // Is the fan speed ramp enabled? int full_fan_speed_layer = EXTRUDER_CONFIG(full_fan_speed_layer); + // ORCA: explicit per-filament first-layer override (-1 = disabled, 0-100 = forced PWM percent on layer 0). + // The override is only honoured when the "No cooling for the first" gate is 0; otherwise the gate would + // force layers 1..N-1 to zero while the override sets layer 0 to a non-zero value, which is confusing + // and non-monotonic. The UI greys out and resets the value in that case, but we also guard here so + // legacy profiles loaded with both set are neutralised at the slicer level. + int initial_layer_fan_speed = EXTRUDER_CONFIG(initial_layer_fan_speed); + const bool has_initial_layer_override = initial_layer_fan_speed >= 0 && close_fan_the_first_x_layers <= 0; supp_interface_fan_speed = EXTRUDER_CONFIG(support_material_interface_fan_speed); // ORCA: previously a silent override forced `close_fan_the_first_x_layers` from 0 up to 1 whenever a ramp @@ -751,7 +758,24 @@ std::string CoolingBuffer::apply_layer_cooldown( // That hid the user's literal "no cooling for the first 0 layers" setting and produced a non-zero starting // factor on the ramp denominator. The override has been removed: with N=0 and M>0 the ramp now genuinely // starts on layer 0 at a factor of 1/M and reaches 100% at layer M-1, matching the intent of the option. - if (int(layer_id) >= close_fan_the_first_x_layers) { + // + // ORCA: First-layer hard override (`initial_layer_fan_speed`). When the user has set this option to a + // value >= 0, layer 0 emits exactly that percentage so the entire first layer + // is at one stable fan speed. The override wins over the `close_fan_the_first_x_layers` gate when + // layer_id == 0. From layer 1 onwards the regular logic resumes. + if (has_initial_layer_override && layer_id == 0) { + fan_speed_new = initial_layer_fan_speed; + overhang_fan_speed = initial_layer_fan_speed; + overhang_fan_control = false; + internal_bridge_fan_speed = initial_layer_fan_speed; + internal_bridge_fan_control = false; + supp_interface_fan_speed = initial_layer_fan_speed; + supp_interface_fan_control = false; + ironing_fan_speed = initial_layer_fan_speed; + ironing_fan_control = false; + // additional_fan_speed_new is left at its configured value (auxiliary fan is independent of the + // part-cooling override). + } else if (int(layer_id) >= close_fan_the_first_x_layers) { float fan_max_speed = EXTRUDER_CONFIG(fan_max_speed); float slow_down_layer_time = float(EXTRUDER_CONFIG(slow_down_layer_time)); float fan_cooling_layer_time = float(EXTRUDER_CONFIG(fan_cooling_layer_time)); @@ -769,10 +793,25 @@ std::string CoolingBuffer::apply_layer_cooldown( //} overhang_fan_speed = EXTRUDER_CONFIG(overhang_fan_speed); if (int(layer_id) >= close_fan_the_first_x_layers && int(layer_id) + 1 < full_fan_speed_layer) { - // Ramp up the fan speed from close_fan_the_first_x_layers to full_fan_speed_layer. - float factor = float(int(layer_id + 1) - close_fan_the_first_x_layers) / float(full_fan_speed_layer - close_fan_the_first_x_layers); - fan_speed_new = std::clamp(int(float(fan_speed_new) * factor + 0.5f), 0, 255); - overhang_fan_speed = std::clamp(int(float(overhang_fan_speed) * factor + 0.5f), 0, 255); + if (has_initial_layer_override && close_fan_the_first_x_layers == 0 && full_fan_speed_layer > 1) { + // ORCA: Option-B anchored ramp. When the first-layer override is configured and there's + // no "no cooling" gate, the ramp interpolates linearly from `initial_layer_fan_speed` on + // layer 0 up to the computed target on layer `full_fan_speed_layer - 1` instead of + // scaling the target by `factor` from zero. Layer 0 itself is handled by the override + // branch above; this branch only runs for layer_id >= 1, but the formula uses t = 0 at + // layer 0 conceptually so the curve is continuous. Guarantees a monotonic transition + // even when the override is larger than the natural 1/M starting value. + const float anchor = float(initial_layer_fan_speed); + const float denom = float(full_fan_speed_layer - 1); + const float t = float(int(layer_id)) / denom; + fan_speed_new = std::clamp(int(anchor + t * (float(fan_speed_new) - anchor) + 0.5f), 0, 255); + overhang_fan_speed = std::clamp(int(anchor + t * (float(overhang_fan_speed) - anchor) + 0.5f), 0, 255); + } else { + // Ramp up the fan speed from close_fan_the_first_x_layers to full_fan_speed_layer. + float factor = float(int(layer_id + 1) - close_fan_the_first_x_layers) / float(full_fan_speed_layer - close_fan_the_first_x_layers); + fan_speed_new = std::clamp(int(float(fan_speed_new) * factor + 0.5f), 0, 255); + overhang_fan_speed = std::clamp(int(float(overhang_fan_speed) * factor + 0.5f), 0, 255); + } } supp_interface_fan_speed = EXTRUDER_CONFIG(support_material_interface_fan_speed); supp_interface_fan_control = supp_interface_fan_speed >= 0; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index cc429a026d..bc728f62e5 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1318,7 +1318,7 @@ static std::vector s_Preset_filament_options {/*"filament_colour", // "bed_type", //BBS:temperature_vitrification "temperature_vitrification", "reduce_fan_stop_start_freq","dont_slow_down_outer_wall", "slow_down_for_layer_cooling", "fan_min_speed", - "fan_max_speed", "enable_overhang_bridge_fan", "overhang_fan_speed", "overhang_fan_threshold", "close_fan_the_first_x_layers", "close_additional_fan_first_x_layers", "first_x_layer_fan_speed", "full_fan_speed_layer", "additional_fan_full_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", + "fan_max_speed", "enable_overhang_bridge_fan", "overhang_fan_speed", "overhang_fan_threshold", "close_fan_the_first_x_layers", "close_additional_fan_first_x_layers", "first_x_layer_fan_speed", "full_fan_speed_layer", "initial_layer_fan_speed", "additional_fan_full_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", "filament_start_gcode", "filament_end_gcode", "filament_change_extrusion_role_gcode", //exhaust fan control "activate_air_filtration","activate_air_filtration_during_print","activate_air_filtration_on_completion","during_print_exhaust_fan_speed","complete_print_exhaust_fan_speed", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 90866bf2b7..a676d24cc0 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -134,6 +134,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "dont_slow_down_outer_wall", "fan_cooling_layer_time", "full_fan_speed_layer", + "initial_layer_fan_speed", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_overhangs", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c6758e58f1..0369c70ee3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3348,7 +3348,25 @@ void PrintConfigDef::init_fff_params() def->max = 1000; def->mode = comAdvanced; def->set_default_value(new ConfigOptionInts { 0 }); - + + // ORCA: explicit override for the part cooling fan speed on the first printed layer. + def = this->add("initial_layer_fan_speed", coInts); + def->label = L("First layer fan speed"); + def->tooltip = L("Sets an exact fan speed for the first layer, overriding all other cooling settings. " + "Useful for protecting 3D-printed toolhead parts (e.g. Voron-style ABS/ASA ducts) from " + "a hot bed. A small amount of airflow cools the ducts down, without using full cooling that " + "may in certain conditions hurt first-layer adhesion." + "\nFrom the second layer onwards, normal cooling resumes." + "\nIf \"Full fan speed at layer\" is also set, the fan ramps smoothly from this value " + "on the first layer up to your target by the chosen layer." + "\nOnly available when \"No cooling for the first\" is 0." + "\nSet to -1 to disable it."); + def->sidetext = "%"; + def->min = -1; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInts { -1 }); + def = this->add("support_material_interface_fan_speed", coInts); def->label = L("Support interface fan speed"); def->tooltip = L("This part cooling fan speed is applied when printing support interfaces. Setting this parameter to a higher than regular speed " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index bc3cc4af8c..b51eef8aa3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1555,6 +1555,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloatsNullable, initial_layer_infill_speed)) ((ConfigOptionInts, nozzle_temperature_initial_layer)) ((ConfigOptionInts, full_fan_speed_layer)) + // ORCA: explicit override for the part cooling fan speed on the first printed layer. + ((ConfigOptionInts, initial_layer_fan_speed)) ((ConfigOptionFloats, fan_max_speed)) ((ConfigOptionFloats, max_layer_height)) ((ConfigOptionFloats, fan_min_speed)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 2f2940d60a..39512eb396 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4368,6 +4368,8 @@ void TabFilament::build() //optgroup->append_line(line); optgroup = page->new_optgroup(L("Cooling for specific layer"), L"param_cooling_specific_layer"); optgroup->append_single_option_line("close_fan_the_first_x_layers", "material_cooling#no-cooling-for-the-first"); + // ORCA: explicit override for the part cooling fan on layer 0; also anchors the ramp when "Full fan speed at layer" is set. + optgroup->append_single_option_line("initial_layer_fan_speed", "material_cooling#first-layer-fan-speed"); optgroup->append_single_option_line("full_fan_speed_layer", "material_cooling#full-fan-speed-at-layer"); optgroup = page->new_optgroup(L("Part cooling fan"), L"param_cooling_part_fan"); @@ -4589,6 +4591,29 @@ void TabFilament::toggle_options() bool has_slow_down_for_layer_cooling = m_config->opt_bool("slow_down_for_layer_cooling", 0); toggle_option("dont_slow_down_outer_wall", has_slow_down_for_layer_cooling); + // ORCA: First layer fan speed override only makes sense when no layers are gated off ("No cooling for + // the first" == 0). Otherwise the override would set layer 0 to a non-zero value while the gate forces + // layers 1..N-1 to zero, producing a confusing non-monotonic profile. When the gate is active we both + // grey out the UI line and force the underlying value to -1 so the cooling buffer never enters the + // override branch. + const int close_fan_first_n = m_config->opt_int("close_fan_the_first_x_layers", 0); + const bool initial_layer_fan_speed_enabled = close_fan_first_n <= 0; + toggle_line("initial_layer_fan_speed", initial_layer_fan_speed_enabled); + if (!initial_layer_fan_speed_enabled) { + if (auto* opt = dynamic_cast(m_config->option("initial_layer_fan_speed"))) { + bool needs_reset = false; + for (int v : opt->values) { + if (v != -1) { needs_reset = true; break; } + } + if (needs_reset) { + std::vector reset_values(opt->values.size(), -1); + m_config->set_key_value("initial_layer_fan_speed", new ConfigOptionInts(reset_values)); + update_dirty(); + reload_config(); + } + } + } + toggle_line("additional_cooling_fan_speed", printer_cfg.opt_bool("auxiliary_fan")); bool support_air_filtration = printer_cfg.opt_bool("support_air_filtration");