From 16c44940d25757143cf75b6975cb06d3d3dd2242 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 4 Aug 2026 14:30:59 +0800 Subject: [PATCH 1/7] Add developer flag for printer agents --- src/libslic3r/AppConfig.cpp | 6 ++++++ src/slic3r/GUI/Preferences.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 159d9bbeda..1b170bf884 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -626,6 +626,12 @@ void AppConfig::set_defaults() set_bool("window_buttons_on_left", false); #endif + if (get("use_printer_agents").empty()) + { + // false = legacy behavior using print hosts + set_bool("use_printer_agents", false); + } + // Remove legacy window positions/sizes erase("app", "main_frame_maximized"); erase("app", "main_frame_pos"); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6bcc00848b..1a3c6fd26a 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -2101,6 +2101,12 @@ void PreferencesDialog::create_items() 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); + auto item_plugin_printer_agents = create_item_checkbox( + _L("(Experimental) Use printer agents instead of print hosts"), _L( + "Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\nWhen disabled, OrcaSlicer uses the legacy print-host behavior."), + "use_printer_agents"); + g_sizer->Add(item_plugin_printer_agents); + //// DEVELOPER > Experimental Features g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND); From 501af81ba9fa29aa74c7d5d7e16e7d2217677f8c Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:21:05 +0800 Subject: [PATCH 2/7] Replace fake-enum printer agent dropdown (#121) A dedicated PrinterAgentChoice field reads rows straight from the live agent registry and stores the agent id string, replacing the fake-coEnum index mapping. The field moves to TabPrinter and registers with the searcher so UnsavedChanges renders it; the PhysicalPrinterDialog copy and its update hook are removed (#125). switch_printer_agent now resolves ids via resolve_printer_agent_id. --- src/libslic3r/Config.hpp | 2 + src/slic3r/GUI/Field.cpp | 268 +++++++++++++++-------- src/slic3r/GUI/Field.hpp | 38 ++++ src/slic3r/GUI/GUI_App.cpp | 23 +- src/slic3r/GUI/GUI_App.hpp | 7 +- src/slic3r/GUI/OptionsGroup.cpp | 26 +++ src/slic3r/GUI/PhysicalPrinterDialog.cpp | 88 +------- src/slic3r/GUI/PhysicalPrinterDialog.hpp | 1 - src/slic3r/GUI/Tab.cpp | 55 +++++ 9 files changed, 312 insertions(+), 196 deletions(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 6f4117d249..509095cbfc 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2273,6 +2273,8 @@ public: plugin_picker, // Raw JSON string value, edited through a dialog behind a button rather than in the row. plugin_config, + // PrinterAgentChoice + printer_agent_select, }; // Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map. diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 1fcaef1b52..8d05de13a4 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -35,6 +35,7 @@ #include "Widgets/TextCtrl.h" #include "../Utils/ColorSpaceConvert.hpp" +#include "../Utils/NetworkAgentFactory.hpp" #ifdef __WXOSX__ #define wxOSX true #else @@ -1403,39 +1404,6 @@ using choice_ctrl = ::ComboBox; // BBS static std::map dynamic_lists; -static bool is_plugin_printer_agent_key(const std::string& value) -{ - return value.rfind("plugin:", 0) == 0; -} - -static int printer_agent_item_for_enum_index(const choice_ctrl* field, int enum_index) -{ - if (!field) - return -1; - - const unsigned int count = field->GetCount(); - for (unsigned int idx = 0; idx < count; ++idx) { - if (void* data = field->GetClientData(idx)) { - const int stored = static_cast(reinterpret_cast(data)) - 1; - if (stored == enum_index) - return static_cast(idx); - } - } - - return -1; -} - -static int printer_agent_enum_index_for_item(const choice_ctrl* field, int item_index, int fallback) -{ - if (!field || item_index < 0) - return fallback; - - if (void* data = field->GetClientData(item_index)) - return static_cast(reinterpret_cast(data)) - 1; - - return fallback; -} - void Choice::register_dynamic_list(std::string const &optname, DynamicList *list) { dynamic_lists.emplace(optname, list); } void DynamicList::update() @@ -1518,33 +1486,7 @@ void Choice::BUILD() window = dynamic_cast(temp); if (! m_opt.enum_labels.empty() || ! m_opt.enum_values.empty()) { - if (m_opt_id == "printer_agent") { - const bool has_builtin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(), - [](const std::string& value) { return !is_plugin_printer_agent_key(value); }); - const bool has_plugin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(), - [](const std::string& value) { return is_plugin_printer_agent_key(value); }); - - auto append_agent_rows = [this, temp](bool plugins) { - for (size_t i = 0; i < m_opt.enum_values.size(); ++i) { - const bool is_plugin = is_plugin_printer_agent_key(m_opt.enum_values[i]); - if (is_plugin != plugins) - continue; - - const wxString label = i < m_opt.enum_labels.size() ? _(m_opt.enum_labels[i]) : wxString(m_opt.enum_values[i]); - const int item = temp->Append(label); - temp->SetClientData(item, reinterpret_cast(static_cast(i + 1))); - } - }; - - if (has_builtin_agents) { - temp->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); - append_agent_rows(false); - } - if (has_plugin_agents) { - temp->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); - append_agent_rows(true); - } - } else if (m_opt.enum_labels.empty()) { + if (m_opt.enum_labels.empty()) { // Append non-localized enum_values for (auto el : m_opt.enum_values) temp->Append(el); @@ -1651,7 +1593,7 @@ void Choice::set_selection() switch (m_opt.type) { case coEnum:{ const int val = m_opt.default_value->getInt(); - field->SetSelection(m_opt_id == "printer_agent" ? printer_agent_item_for_enum_index(field, val) : val); + field->SetSelection(val); break; } case coFloat: @@ -1701,12 +1643,7 @@ void Choice::set_value(const std::string& value, bool change_event) //! Redunda } choice_ctrl* field = dynamic_cast(window); - if (m_opt_id == "printer_agent") { - const int enum_index = idx == m_opt.enum_values.size() ? - (m_opt.default_value ? m_opt.default_value->getInt() : 0) : - static_cast(idx); - field->SetSelection(printer_agent_item_for_enum_index(field, enum_index)); - } else if (idx == m_opt.enum_values.size()) + if (idx == m_opt.enum_values.size()) field->SetValue(value); else field->SetSelection(idx); @@ -1772,33 +1709,11 @@ void Choice::set_value(const boost::any& value, bool change_event) case coEnum: // BBS case coEnums: { - auto printer_agent_index_from_key = [this](const std::string& key) { - auto it = std::find(m_opt.enum_values.begin(), m_opt.enum_values.end(), key); - if (it != m_opt.enum_values.end()) - return static_cast(it - m_opt.enum_values.begin()); - return m_opt.default_value ? m_opt.default_value->getInt() : 0; - }; - - int val = 0; - if (m_opt_id == "printer_agent") { - if (const int* int_value = boost::any_cast(&value)) - val = *int_value; - else if (const wxString* wx_value = boost::any_cast(&value)) - val = printer_agent_index_from_key(into_u8(*wx_value)); - else if (const std::string* string_value = boost::any_cast(&value)) - val = printer_agent_index_from_key(*string_value); - else { - m_disable_change_event = false; - return; - } - } else - val = boost::any_cast(value); + int val = boost::any_cast(value); int selection = val; - if (m_opt_id == "printer_agent") { - selection = printer_agent_item_for_enum_index(field, val); - } else if (m_opt_id == "input_shaping_type") { + if (m_opt_id == "input_shaping_type") { if (field != nullptr) { const unsigned int count = field->GetCount(); int match_index = -1; @@ -1920,12 +1835,6 @@ boost::any& Choice::get_value() { if (m_opt.nullable && field->GetSelection() == -1) m_value = ConfigOptionEnumsGenericNullable::nil_value(); - else if (m_opt_id == "printer_agent") - { - const int selection = field->GetSelection(); - const int fallback = m_opt.default_value ? m_opt.default_value->getInt() : 0; - m_value = printer_agent_enum_index_for_item(field, selection, fallback); - } else if (m_opt_id == "input_shaping_type") { int selection = field->GetSelection(); @@ -2067,6 +1976,171 @@ void Choice::msw_rescale() } +// PrinterAgentChoice + +void PrinterAgentChoice::reload_rows() +{ + auto* combo = dynamic_cast(window); // wxWidgets ComboBox + if (!combo) + return; + + // clear ComboBox + combo->Clear(); + + // helpers + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + const bool has_builtin_agents = std::any_of(agents.begin(), agents.end(), + [](const PrinterAgentInfo& a) { return !a.is_plugin(); }); + const bool has_plugin_agents = std::any_of(agents.begin(), agents.end(), + [](const PrinterAgentInfo& a) { return a.is_plugin(); }); + + auto append_agent_rows = [combo](bool is_plugin) + { + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + for (size_t i = 0; i < agents.size(); ++i) + { + if (agents[i].is_plugin() != is_plugin) + continue; + const int item = combo->Append(_(agents[i].display_name)); + // why: carry the agent-id string on the row. alias is an owned wxString (auto-freed, never rendered) + combo->SetItemAlias(item, from_u8(agents[i].id)); + } + }; + + // append rows + if (has_builtin_agents) + { + combo->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + append_agent_rows(false); // append rows for agents that are not plugins + } + if (has_plugin_agents) + { + combo->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + append_agent_rows(true); // append rows for agents that are plugins + } +} + +void PrinterAgentChoice::BUILD() +{ + wxSize size(def_width_wider() * m_em_unit, wxDefaultCoord); + if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit); + if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit); + + static Builder builder; + choice_ctrl* temp = builder.build(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, + wxCB_READONLY); + temp->Clear(); + temp->GetDropDown().SetUseContentWidth(true); + if (parent_is_custom_ctrl && m_opt.height < 0) + opt_height = (double)temp->GetTextCtrl()->GetSize().GetHeight() / m_em_unit; + temp->SetTextLabel(_L(m_opt.sidetext)); + m_combine_side_text = true; +#ifdef __WXGTK3__ + wxSize best_sz = temp->GetBestSize(); + if (best_sz.x > size.x) temp->SetSize(best_sz); +#endif + if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT); + + window = dynamic_cast(temp); + + reload_rows(); + + temp->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_change_field(); }, temp->GetId()); + temp->SetToolTip(get_tooltip_text(temp->GetValue())); +} + +// Resolve CONFIG id string to a matching row in the live REGISTRY. "" uses the vendor default. +// An unregistered id clears selection and shows " (missing)" as free text. +void PrinterAgentChoice::set_value(const std::string& value, bool change_event) +{ + m_disable_change_event = !change_event; + + auto* field = dynamic_cast(window); + + // check if any row's corresponding id matches the agent id we are attempting to set + const std::string effective_agent_id = wxGetApp().resolve_printer_agent_id(value); + const unsigned int count = field->GetCount(); + int match = wxNOT_FOUND; + for (unsigned int i = 0; i < count; ++i) + { + if (into_u8(field->GetItemAlias(i)) == effective_agent_id) // if alias == id + { + match = static_cast(i); + break; + } + } + + // based on match or not, set selection and value + // - SetSelection and SetValue are UI to manipulate the display of the ComboBox + // - SetSelection automatically calls SetValue for the same value + // - we can also SetValue separately from SetSelection + if (match == wxNOT_FOUND) + { + field->SetSelection(wxNOT_FOUND); // nothing shows as selected in the dropdown + field->SetValue(from_u8(value + " (missing)")); // set a value not in the selection (upper display field) + } + else + { + // display name of agent shows both in upper display field and appears selected in dropdown + field->SetSelection(match); + } + + m_disable_change_event = false; +} + +// Accept boost::any values from callers (usually to OptionsGroup/Field parent classes) and normalize them to an agent id. +// Then use PrinterAgentChoice::set_value(std::string& value, ...) +void PrinterAgentChoice::set_value(const boost::any& value, bool change_event) +{ + m_disable_change_event = !change_event; + + auto* field = dynamic_cast(window); + if (value.empty()) + { + field->SetValue(""); + m_value = value; + m_disable_change_event = false; + return; + } + + std::string id; + if (const std::string* s = boost::any_cast(&value)) + id = *s; + else if (const wxString* w = boost::any_cast(&value)) + id = into_u8(*w); + set_value(id, change_event); +} + +// A real row returns its alias, which is the agent id. Header rows, missing rows, +// and no selection return empty boost::any so the custom writer leaves config unchanged. +boost::any& PrinterAgentChoice::get_value() +{ + auto* field = dynamic_cast(window); + const int sel = field->GetSelection(); + const std::string id = sel < 0 ? std::string{} : into_u8(field->GetItemAlias(sel)); + if (id.empty()) + m_value = boost::any{}; + else + m_value = id; + return m_value; +} + +void PrinterAgentChoice::enable() { dynamic_cast(window)->Enable(); } +void PrinterAgentChoice::disable() { dynamic_cast(window)->Disable(); } + +void PrinterAgentChoice::msw_rescale() +{ + Field::msw_rescale(); + + auto* field = dynamic_cast(window)->GetTextCtrl(); + wxSize size(wxDefaultSize); + size.SetWidth((m_opt.width > 0 ? m_opt.width : def_width_wider()) * m_em_unit); + field->SetMinSize(wxSize(-1, int(1.5f * field->GetFont().GetPixelSize().y + 0.5f))); + field->SetSize(size); + + dynamic_cast(window)->Rescale(); +} + void PluginField::BUILD() { auto* panel = new wxPanel(m_parent, wxID_ANY); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 4e9c65da5d..e57a569561 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -469,6 +469,44 @@ public: void suppress_scroll(); }; +// printer_agent is a coString whose choices come from the live agent registry. +// PrinterAgentChoice uses a ComboBox directly because Choice expects static config enums. +// Real rows carry the stored agent id in the row alias (SetItemAlias/GetItemAlias). +class PrinterAgentChoice : public Field +{ + using Field::Field; + +public: + PrinterAgentChoice(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) + { + } + + PrinterAgentChoice(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field( + parent, opt, id) + { + } + + ~PrinterAgentChoice() + { + } + + wxWindow* window{nullptr}; + + void BUILD() override; + // Clear and repopulate rows from the live registry (grouped System agents / Plugins). + // Does not change selection; the caller follows with set_value(stored id). + void reload_rows(); + + void set_value(const std::string& value, bool change_event = false); + void set_value(const boost::any& value, bool change_event = false) override; + boost::any& get_value() override; + + void enable() override; + void disable() override; + void msw_rescale() override; + wxWindow* getWindow() override { return window; } +}; + class PluginField : public Field { using Field::Field; public: diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 4c4bdfd62c..83d4f2abaf 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3873,6 +3873,18 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } +std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) +{ + if (!stored_id.empty()) + return stored_id; + return (preset_bundle && preset_bundle->is_bbl_vendor()) ? BBL_PRINTER_AGENT_ID : ORCA_PRINTER_AGENT_ID; +} + +std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id) +{ + return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id; +} + void GUI_App::switch_printer_agent() { if (!m_agent) { @@ -3880,17 +3892,8 @@ void GUI_App::switch_printer_agent() return; } - // Read printer_agent from config, falling back to default - std::string effective_agent_id = ORCA_PRINTER_AGENT_ID; - if (preset_bundle->is_bbl_vendor()) - effective_agent_id = BBL_PRINTER_AGENT_ID; - const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; - if (config.has("printer_agent")) { - const std::string& value = config.option("printer_agent")->value; - if (!value.empty()) - effective_agent_id = value; - } + const std::string effective_agent_id = resolve_printer_agent_id(config.opt_string("printer_agent")); // Check if agent is registered const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index bda27d40ec..6a977d37fc 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -365,9 +365,14 @@ public: HMSQuery* get_hms_query() { return hms_query; } NetworkAgent* getAgent() { return m_agent; } - // Dynamic printer agent switching + // Reconcile the live printer agent with the stored preset selection. void switch_printer_agent(); + std::string resolve_printer_agent_id(const std::string& stored_id); + // ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id + // then, all resolve and canonical would just be ORCA<->"" + std::string canonical_printer_agent_id(const std::string& picked_id); + FilamentColorCodeQuery* get_filament_color_code_query(); bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 9fb4483883..25c13c4b8d 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -54,6 +54,9 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co case ConfigOptionDef::GUIType::one_string: m_fields.emplace(id, TextCtrl::Create(this->ctrl_parent(), opt, id)); break; case ConfigOptionDef::GUIType::plugin_picker: m_fields.emplace(id, PluginField::Create(this->ctrl_parent(), opt, id)); break; case ConfigOptionDef::GUIType::plugin_config: m_fields.emplace(id, PluginConfigField::Create(this->ctrl_parent(), opt, id)); break; + case ConfigOptionDef::GUIType::printer_agent_select: m_fields.emplace( + id, PrinterAgentChoice::Create(this->ctrl_parent(), opt, id)); + break; default: switch (opt.type) { case coFloatOrPercent: @@ -654,6 +657,16 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index void ConfigOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value) { + if (opt_id == "printer_agent") { + // TODO: Replace this option-specific branch with a generic value adapter if + // more fields need custom field-value to config-value conversion. + if (const std::string* id = boost::any_cast(&value)) + this->change_opt_value("printer_agent", wxGetApp().canonical_printer_agent_id(*id)); + + OptionsGroup::on_change_OG(opt_id, value); + return; + } + if (!m_opt_map.empty()) { auto it = m_opt_map.find(opt_id); if (it == m_opt_map.end()) { @@ -772,6 +785,19 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config, } } #endif + else if (opt_key == "printer_agent") + { + // why: printer_agent is a coString kept out of m_opt_map. The generic non-opt_map revert + // below restores the edited config from get_value(), but a deregistered/"(missing)" saved + // id has no selectable row, so the field yields no value and the edited config keeps the + // user's interim pick -> stuck dirty. Restore the SAVED id straight into the edited config + // (displayable or not; config is the saved or system baseline), then repaint and notify. + const std::string saved_id = config.opt_string("printer_agent"); + set_value(opt_key, saved_id); + this->change_opt_value(opt_key, saved_id); + OptionsGroup::on_change_OG(opt_key, saved_id); + return; + } else if (m_opt_map.find(opt_key) == m_opt_map.end() || // This option don't have corresponded field opt_key == "printable_area" || opt_key == "compatible_printers" || opt_key == "compatible_prints" || opt_key == "thumbnails" || diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index b40cd22697..4c9dd60d55 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -25,7 +25,6 @@ #include "GUI.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" -#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "format.hpp" #include "Tab.hpp" #include "wxExtensions.hpp" @@ -128,22 +127,8 @@ PhysicalPrinterDialog::~PhysicalPrinterDialog() void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup) { m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - // Special handling for printer_agent: convert fake enum index to string agent ID - if (opt_key == "printer_agent") { - try { - int selected_idx = boost::any_cast(value); - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - if (selected_idx >= 0 && selected_idx < static_cast(agents.size())) { - m_config->set_key_value("printer_agent", - new ConfigOptionString(agents[selected_idx].id)); - } - } catch (const boost::bad_any_cast&) { - // If value is not an int, ignore - } + if (opt_key == "host_type" || opt_key == "printhost_authorization_type") this->update(); - } else if (opt_key == "host_type" || opt_key == "printhost_authorization_type") { - this->update(); - } if (opt_key == "print_host") this->update_printhost_buttons(); if (opt_key == "printhost_port") @@ -154,47 +139,6 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr m_optgroup->append_single_option_line("host_type"); - // Build printer agent dropdown from registry (only if network agent is available) - if (wxGetApp().getAgent() != nullptr) { - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - - if (!agents.empty()) { - // Create a fake enum option to force a Choice widget instead of TextCtrl - // (printer_agent is coString in config, but we need a dropdown) - ConfigOptionDef def; - def.type = coEnum; - def.width = Field::def_width_wider(); - def.label = L("Printer Agent"); - def.tooltip = L("Select the network agent implementation for printer communication. " - "Available agents are registered at startup."); - def.mode = comAdvanced; - - // Populate enum values and labels from registered agents - for (const auto& agent : agents) { - def.enum_values.push_back(agent.id); - def.enum_labels.push_back(agent.display_name); - } - - // Resolve selected agent: use config value if valid, otherwise fall back to default - std::string selected_agent = m_config->opt_string("printer_agent"); - auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - if (it == agents.end()) { - selected_agent = ORCA_PRINTER_AGENT_ID; - it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - } - - if (it != agents.end()) { - size_t default_idx = std::distance(agents.begin(), it); - def.set_default_value(new ConfigOptionInt(static_cast(default_idx))); - } - - // Create and append the option line - auto agent_option = Option(def, "printer_agent"); - Line agent_line = m_optgroup->create_single_option_line(agent_option); - m_optgroup->append_line(agent_line); - } - } - auto create_sizer_with_btn = [](wxWindow* parent, Button** btn, const std::string& icon_name, const wxString& label) { *btn = new Button(parent, label); (*btn)->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); @@ -816,31 +760,6 @@ void PhysicalPrinterDialog::update_host_type(bool printer_change) } } -void PhysicalPrinterDialog::update_printer_agent_type() -{ - if (m_config == nullptr) - return; - - Field* agent_field = m_optgroup->get_field("printer_agent"); - if (!agent_field) - return; - - Choice* agent_choice = dynamic_cast(agent_field); - if (!agent_choice) - return; - - // Sync selection with current config value - const std::string current_agent = m_config->opt_string("printer_agent"); - - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - for (size_t i = 0; i < agents.size(); ++i) { - if (agents[i].id == current_agent) { - agent_choice->set_value(i); - return; - } - } -} - void PhysicalPrinterDialog::update_printers() { wxBusyCursor wait; @@ -894,11 +813,6 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event) { wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset("", false, false, true, m_preset_name); event.Skip(); - - // Defer printer agent switch to ensure preset save completes first - wxGetApp().CallAfter([] { - wxGetApp().switch_printer_agent(); - }); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 694e7aaf90..0ba2cad54f 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -60,7 +60,6 @@ public: void update(bool printer_change = false); void update_host_type(bool printer_change); - void update_printer_agent_type(); void update_preset_input(); void update_printhost_buttons(); void update_printers(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c8fa524ca5..857abdce67 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -33,6 +33,7 @@ #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/plugin/PluginConfig.hpp" #include "Plater.hpp" @@ -5018,6 +5019,40 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("gcode_flavor", "printer_basic_information_advanced#g-code-flavor"); optgroup->append_single_option_line("pellet_modded_printer", "printer_basic_information_advanced#pellet-modded-printer"); optgroup->append_single_option_line("bbl_use_printhost", "printer_basic_information_advanced#use-3rd-party-print-host"); + + // "Printer Agent" dropdown - printer_agent is a coString; gui_type routes it to + // PrinterAgentChoice instead of a TextCtrl. Rows and values come from the live agent + // registry, and the value is stored as the agent-id string. + if (wxGetApp().getAgent() != nullptr) + { + auto registered_printer_agents = NetworkAgentFactory::get_registered_printer_agents(); + if (!registered_printer_agents.empty()) + { + ConfigOptionDef def; + def.type = coString; + def.gui_type = ConfigOptionDef::GUIType::printer_agent_select; + def.width = 3 * Field::def_width_wider() / 2; + def.label = L("Printer Agent"); + def.tooltip = L("Select the network agent implementation for printer communication. " + "Available agents are registered at startup."); + def.mode = comAdvanced; + + // Create the field without get_option() so it is not registered in m_opt_map. + // ConfigOptionsGroup handles printer_agent before the generic mapped write path. + Line agent_line = optgroup->create_single_option_line(Option(def, "printer_agent")); + optgroup->append_line(agent_line); + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) + choice->set_value(m_config->opt_string("printer_agent"), false); + } + + // Register by hand so the UnsavedChanges dialog can render a row for it. + wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title, + optgroup->config_category()); + } + } + optgroup->append_single_option_line("use_3mf"); optgroup->append_single_option_line("scan_first_layer" , "printer_basic_information_advanced#scan-first-layer"); optgroup->append_single_option_line("enable_power_loss_recovery", "printer_basic_information_advanced#power-loss-recovery"); @@ -5884,6 +5919,16 @@ void TabPrinter::reload_config() // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) + { + const std::string selected_agent = m_config->opt_string("printer_agent"); + choice->set_value(selected_agent, false); + } + } } void TabPrinter::activate_selected_page(std::function throw_if_canceled) @@ -5894,6 +5939,16 @@ void TabPrinter::activate_selected_page(std::function throw_if_canceled) // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) + { + const std::string selected_agent = m_config->opt_string("printer_agent"); + choice->set_value(selected_agent, false); + } + } } void TabPrinter::clear_pages() From 15342681831a73e028f745c9c2d8b2efd8f44f71 Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:27:53 +0800 Subject: [PATCH 3/7] Reset device selection on agent swap or unload (#124) set_live_printer_agent centralizes the swap: deselect the machine, clear stale sidebar state and the previous agent's Other Devices, then install the new agent (or null when its provider vanished). Plugin load/unload callbacks refresh the dropdown and re-run agent selection. load_last_machine no longer falls back to the first available machine. --- src/slic3r/GUI/DeviceCore/DevManager.cpp | 70 ++++++++--------- src/slic3r/GUI/DeviceCore/DevManager.h | 8 +- src/slic3r/GUI/GUI_App.cpp | 95 +++++++++++++++++++++--- src/slic3r/GUI/GUI_App.hpp | 5 ++ src/slic3r/GUI/Tab.cpp | 18 +++++ src/slic3r/GUI/Tab.hpp | 1 + 6 files changed, 150 insertions(+), 47 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 2d54b5c85f..3c664facfd 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -496,6 +496,26 @@ namespace Slic3r OnSelectedMachineChanged(previous_selected_machine, selected_machine); } + void DeviceManager::clear_other_devices() + { + // why: on agent swap, keep "My Devices" but drop the transient "Other Devices" + // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. + const auto my = get_my_machine_list(); + for (auto it = localMachineList.begin(); it != localMachineList.end();) + { + if (my.find(it->first) == my.end()) + { + // not a "My Device" -> an "Other Device" + delete it->second; + it = localMachineList.erase(it); + } + else + { + ++it; + } + } + } + bool DeviceManager::set_selected_machine(std::string dev_id) { BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id @@ -558,7 +578,6 @@ namespace Slic3r } else { - Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning(); if (m_agent) { if (it->second->connection_type() != "lan" || it->second->connection_type().empty()) @@ -592,7 +611,6 @@ namespace Slic3r } selected_machine = dev_id; - record_user_last_machine(selected_machine); return true; } @@ -851,44 +869,26 @@ namespace Slic3r int result = m_agent->get_user_print_info(&http_code, &body, provider); if (result == 0) { - parse_user_print_info(body); + // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. + // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. + Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); } } - void DeviceManager::record_user_last_machine(const std::string& dev_id) - { - if (Slic3r::GUI::wxGetApp().app_config) { - Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id); - } - } - - std::string DeviceManager::get_user_last_machine() const - { - if (Slic3r::GUI::wxGetApp().app_config) { - const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine"); - if (!user_last_machine.empty()) { - return user_last_machine; - } else if (m_agent) { - return m_agent->get_user_selected_machine(); - } - } - - return ""; - } - void DeviceManager::load_last_machine() { - if (userMachineList.empty()) return; - else if (userMachineList.size() == 1) { - this->set_selected_machine(userMachineList.begin()->second->get_dev_id()); - } else { - const auto& last_monitor_machine = get_user_last_machine(); - if (userMachineList.find(last_monitor_machine) != userMachineList.end()) { - set_selected_machine(last_monitor_machine); - } else { - this->set_selected_machine(userMachineList.begin()->second->get_dev_id()); - } - } + // Get all available machines, include cloud machines and lan machines that have access right + auto all_machines = get_my_machine_list(); + if (all_machines.empty()) + return; + + // Reconnect the machine the user last selected, if it's still available. + // why: no first-available fallback - auto-connecting an arbitrary machine + // fights the agent-swap reset, which intentionally leaves nothing selected. + const std::string last_monitor_machine = m_agent ? m_agent->get_user_selected_machine() : ""; + const auto last_machine = all_machines.find(last_monitor_machine); + if (last_machine != all_machines.end()) + this->set_selected_machine(last_machine->second->get_dev_id()); } void DeviceManager::OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.h b/src/slic3r/GUI/DeviceCore/DevManager.h index 1f7f87b7fb..70bee613a8 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.h +++ b/src/slic3r/GUI/DeviceCore/DevManager.h @@ -48,8 +48,9 @@ public: MachineObject* get_selected_machine(); bool set_selected_machine(std::string dev_id); - void record_user_last_machine(const std::string& dev_id); - std::string get_user_last_machine() const; + // why: clears stale sidebar sync-status / AMS visuals. Public so the printer-agent + // swap path can reuse it instead of duplicating the two sidebar calls. + void OnSelectedMachineLost(); // local machine void set_local_selected_machine(std::string dev_id) { local_selected_machine = dev_id; }; @@ -70,6 +71,8 @@ public: void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void clean_user_info(bool keep_local_selection = false); + void clear_other_devices(); + void load_last_machine(); void update_user_machine_list_info(const std::string& provider); void parse_user_print_info(std::string body); @@ -110,7 +113,6 @@ private: void check_pushing(); void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state); - void OnSelectedMachineLost(); void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 83d4f2abaf..14edeb8038 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2809,16 +2809,58 @@ void GUI_App::init_plugin_gui_wiring() }); }; + // why: a newly loaded plugin only adds a selectable agent + // refresh the dropdown and leave the live agent alone + auto refresh_printer_agent_dropdown_after_load = [](const std::string&) + { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] + { + if (!app->is_closing()) + app->refresh_printer_agent_dropdown(); + }); + }; + + // why: the unloaded plugin may have been the provider of the live agent + // re-run selection, where a now-missing agent will be cleared + // refresh dropdown after + auto switch_printer_agent_after_unload = [](const std::string&) + { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] { + if (app->is_closing()) + return; + + app->switch_printer_agent(); + app->refresh_printer_agent_dropdown(); + }); + }; + plugin_mgr.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin); plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load); + plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload); plugin_mgr.subscribe_on_capability_load_callback( - [refresh_plugins_dialog](const PluginCapabilityId& capability) { + [refresh_plugins_dialog, refresh_printer_agent_dropdown_after_load](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name); refresh_plugins_dialog(); + refresh_printer_agent_dropdown_after_load(capability.plugin_key); // A newly loaded capability may satisfy a missing-plugin notification; re-validate the // current plate (on the UI thread) so the notification clears once its plugin is available. if (wxTheApp && !wxGetApp().is_closing()) @@ -2828,10 +2870,11 @@ void GUI_App::init_plugin_gui_wiring() }); }); plugin_mgr.subscribe_on_capability_unload_callback( - [refresh_plugins_dialog](const PluginCapabilityId& capability) { + [refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); refresh_plugins_dialog(); + switch_printer_agent_after_unload(capability.plugin_key); }); } @@ -3873,6 +3916,36 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } +void GUI_App::refresh_printer_agent_dropdown() +{ + if (Tab* tab = get_tab(Preset::TYPE_PRINTER)) + { + if (auto* printer_tab = dynamic_cast(tab)) + printer_tab->refresh_printer_agent_dropdown(); + } +} + +void GUI_App::set_live_printer_agent(std::shared_ptr agent) +{ + if (!m_agent) + return; + + // why: tearing down the old machine selection is only ever the prefix of setting the live + // agent (to a new one, or to null when the selection is missing) - so it lives here, not as + // a standalone helper. Pass nullptr to clear the selection. + if (DeviceManager* dev = getDeviceManager()) + { + dev->set_selected_machine(""); // why: empty id disconnects and deselects the current machine + m_agent->set_user_selected_machine(""); + // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) + dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS + dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices + } + + m_agent->set_printer_agent(agent); + sidebar().update_all_preset_comboboxes(); +} + std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) { if (!stored_id.empty()) @@ -3898,9 +3971,11 @@ void GUI_App::switch_printer_agent() // Check if agent is registered const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id); if (!agent_info_ptr) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id - << "', keeping current agent"; - // Keep current agent, don't switch + // why: the selected agent's provider is gone (e.g. plugin unloaded); leaving the old + // live agent up would keep talking to a machine the user can no longer select. + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": agent ID '" << effective_agent_id + << "' is unregistered; clearing live printer agent"; + set_live_printer_agent(nullptr); return; } const PrinterAgentInfo agent_info = *agent_info_ptr; @@ -3914,7 +3989,9 @@ void GUI_App::switch_printer_agent() NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir); if (!new_printer_agent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id + << "'; clearing live printer agent"; + set_live_printer_agent(nullptr); return; } @@ -3937,9 +4014,9 @@ void GUI_App::switch_printer_agent() return; } - // Swap the agent - m_agent->set_printer_agent(new_printer_agent); - sidebar().update_all_preset_comboboxes(); + // Swap the agent; set_live_printer_agent resets the device selection so the new + // agent starts clean (#124). + set_live_printer_agent(new_printer_agent); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 6a977d37fc..8bf32df64c 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -803,6 +803,11 @@ private: void window_pos_center(wxTopLevelWindow *window); bool select_language(); + // Dynamic printer agent selection - internal helpers for switch_printer_agent + // and the plugin load/unload callbacks (init_plugin_gui_wiring). + void refresh_printer_agent_dropdown(); + void set_live_printer_agent(std::shared_ptr agent); // null clears the selection + bool config_wizard_startup(); void check_updates(const bool verbose); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 857abdce67..bade7fee98 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -7907,6 +7907,24 @@ bool TabPrinter::apply_extruder_cnt_from_cache() return false; } +void TabPrinter::refresh_printer_agent_dropdown() const +{ + auto* choice = dynamic_cast(get_field("printer_agent")); + if (!choice || !choice->getWindow()) + return; + + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + if (agents.empty()) + return; + + // why: rows live on PrinterAgentChoice now; rebuild them from the live registry and re-select the stored id. + const std::string selected_agent = wxGetApp().preset_bundle->printers.get_edited_preset() + .config.opt_string("printer_agent"); + choice->reload_rows(); + choice->set_value(selected_agent, false); + this->GetParent()->Layout(); +} + bool Tab::validate_custom_gcodes() { if (m_type != Preset::TYPE_FILAMENT && diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 7187aff467..19eb0b849d 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -675,6 +675,7 @@ public: wxSizer* create_bed_shape_widget(wxWindow* parent); void cache_extruder_cnt(const DynamicPrintConfig* config = nullptr); bool apply_extruder_cnt_from_cache(); + void refresh_printer_agent_dropdown() const; }; class TabSLAMaterial : public Tab From 8dfc7a14b9287e4f983c8bcb37037cb5d0cb8fde Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:36:47 +0800 Subject: [PATCH 4/7] Gate agent mode behind use_printer_agents toggle Replace per-printer auto-activation (is_current_printer_agent_plugin) with a global experimental AppConfig toggle, default off: legacy print-host behavior is unchanged until the user opts in. The toggle drives device-tab routing, print button defaults, connect-button visibility and sidebar layout, and dedups machine-select dialog opens. --- src/slic3r/GUI/MainFrame.cpp | 9 ++-- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 54 +++++++++++++----------- src/slic3r/GUI/Preferences.cpp | 8 ++++ src/slic3r/Utils/NetworkAgentFactory.cpp | 20 --------- src/slic3r/Utils/NetworkAgentFactory.hpp | 2 - 6 files changed, 43 insertions(+), 52 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7b638e3316..39082a9dca 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -708,7 +708,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); if (m_print_enable) { - if (wxGetApp().preset_bundle->use_bbl_network()) + if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE)); @@ -1999,7 +1999,8 @@ wxBoxSizer* MainFrame::create_side_tools() SidePopup* p = new SidePopup(this); if (wxGetApp().preset_bundle - && !wxGetApp().preset_bundle->is_bbl_vendor()) { + && !wxGetApp().preset_bundle->is_bbl_vendor() + && !wxGetApp().app_config->get_bool("use_printer_agents")) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); export_gcode_btn->SetCornerRadius(0); @@ -2132,7 +2133,7 @@ wxBoxSizer* MainFrame::create_side_tools() const auto preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { - if (preset_bundle->use_bbl_network()) { + if (preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { // BBL network support everything } else { support_send = false; // All 3rd print hosts do not have the send options @@ -4253,7 +4254,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey) void MainFrame::load_printer_url() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin()) + if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents")) return; auto cfg = preset_bundle.printers.get_edited_preset().config; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 4c9dd60d55..989cf204e1 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -669,7 +669,7 @@ void PhysicalPrinterDialog::update(bool printer_change) } // For bbl printers, show option to control the device tab - if (wxGetApp().preset_bundle->is_bbl_vendor()) { + if (wxGetApp().preset_bundle->is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) { m_optgroup->show_field("bbl_use_print_host_webui"); const bool use_print_host_webui = !current_webui.empty(); if (Field* printhost_webui_field = m_optgroup->get_field("bbl_use_print_host_webui"); printhost_webui_field) { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 106c142fea..d83ddded34 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3246,7 +3246,7 @@ void Sidebar::update_all_preset_comboboxes() auto p_mainframe = wxGetApp().mainframe; auto cfg = preset_bundle.printers.get_edited_preset().config; - const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin(); + const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents"); if (preset_bundle.use_bbl_network()) { //only show connection button for not-BBL printer @@ -3258,7 +3258,8 @@ void Sidebar::update_all_preset_comboboxes() p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); } else { //p->btn_connect_printer->Show(); - p->m_printer_connect->Show(); + // ORCA: hide the physical-printer connection button when printer agents are enabled + p->m_printer_connect->Show(!wxGetApp().app_config->get_bool("use_printer_agents")); // ORCA: show/hide sync-ams button based on filament sync mode auto agent = wxGetApp().getAgent(); @@ -3280,7 +3281,9 @@ void Sidebar::update_all_preset_comboboxes() const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); - print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode; + print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) + ? MainFrame::PrintSelectType::ePrintPlate + : MainFrame::PrintSelectType::eSendGcode; } if (!use_native_device_tab) @@ -3439,7 +3442,10 @@ void Sidebar::update_presets(Preset::Type preset_type) bool isBBL = preset_bundle.is_bbl_vendor(); bool is_dual_extruder = extruder_variants->size() == 2; - p->layout_printer(preset_bundle.use_bbl_network(), isBBL && is_dual_extruder); + // why: agent mode drives the native device tab, so the sidebar lays out like BBL + // (no physical-printer connect button). + p->layout_printer(preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"), + isBBL && is_dual_extruder); // Update nozzle titles from printer config (e.g. "Main Nozzle" / "Auxiliary Nozzle" for N6) // UI left = DEPUTY_EXTRUDER_ID(1), UI right = MAIN_EXTRUDER_ID(0) @@ -5625,6 +5631,7 @@ struct Plater::priv void on_action_slice_all(SimpleEvent&); void on_action_publish(wxCommandEvent &evt); void on_action_print_plate(SimpleEvent&); + void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); void on_action_print_all(SimpleEvent&); void on_action_export_gcode(SimpleEvent&); void on_action_send_gcode(SimpleEvent&); @@ -11166,18 +11173,23 @@ void Plater::priv::on_action_print_plate(SimpleEvent&) } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_network()) { - // BBS - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); - m_select_machine_dlg->prepare(partplate_list.get_curr_plate_index()); - m_select_machine_dlg->ShowModal(); + if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { + open_machine_select_dialog(partplate_list.get_curr_plate_index()); } else { q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr); } } +void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print_type) +{ + // BBS + if (!m_select_machine_dlg) + m_select_machine_dlg = new SelectMachineDialog(q); + m_select_machine_dlg->set_print_type(print_type); + m_select_machine_dlg->prepare(plate_idx); + m_select_machine_dlg->ShowModal(); +} + void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&) { if (!m_send_multi_dlg) @@ -11193,10 +11205,7 @@ void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&) } //BBS - if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_SDCARD_VIEW); - m_select_machine_dlg->prepare(0); - m_select_machine_dlg->ShowModal(); + open_machine_select_dialog(0, PrintFromType::FROM_SDCARD_VIEW); } void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) @@ -11211,13 +11220,13 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; update_sidebar(); int old_sel = e.GetOldSelection(); - const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin(); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = wxGetApp().preset_bundle && - (wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin); + (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. - if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { + if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { e.Veto(); BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; if (q) { @@ -11273,13 +11282,8 @@ void Plater::priv::on_action_print_all(SimpleEvent&) } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_network()) { - // BBS - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); - m_select_machine_dlg->prepare(PLATE_ALL_IDX); - m_select_machine_dlg->ShowModal(); + if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { + open_machine_select_dialog(PLATE_ALL_IDX); } else { q->send_gcode_legacy(PLATE_ALL_IDX, nullptr); } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 1a3c6fd26a..f802ba6ecb 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1135,6 +1135,14 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT); } + if (param == "use_printer_agents") + { + // Rebuild the Device tab so the native/web-UI choice reflects the new flag + // immediately, instead of only on the next printer-preset change or restart. + if (wxGetApp().plater()) + wxGetApp().plater()->sidebar().update_all_preset_comboboxes(); + } + if (param == "enable_high_low_temp_mixed_printing") { if (checkbox->GetValue()) { const wxString warning_title = _L("Bed Temperature Difference Warning"); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 3883d99f2e..ff950d0946 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -465,25 +465,5 @@ void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plu << plugin_key << "' with agent ID '" << agent_id << "'"; } -bool NetworkAgentFactory::is_current_printer_agent_plugin() -{ - auto* preset_bundle = GUI::wxGetApp().preset_bundle; - if (!preset_bundle) - return false; - - std::string agent_key = ORCA_PRINTER_AGENT_ID; - if (preset_bundle->is_bbl_vendor()) - agent_key = BBL_PRINTER_AGENT_ID; - - const auto& cfg = preset_bundle->printers.get_edited_preset().config; - if (cfg.has("printer_agent")) { - const std::string& value = cfg.option("printer_agent")->value; - if (!value.empty()) - agent_key = value; - } - - const PrinterAgentInfo* info = get_printer_agent_info(agent_key); - return info && info->is_plugin(); -} } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index cfff6fb1c7..a055b19493 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -166,8 +166,6 @@ public: static void register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name); static void deregister_python_printer_agent(const std::string& plugin_key, const std::string& capability_name); - static bool is_current_printer_agent_plugin(); - private: // Factory is not instantiable NetworkAgentFactory() = delete; From 79dcace1acfd1d9a66a7eda20c1e7e685c091bd2 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 4 Aug 2026 21:26:50 +0800 Subject: [PATCH 5/7] Add unsupported-command feedback to the device UI --- src/slic3r/GUI/DeviceManager.cpp | 34 ++++++++++++++++++++++++++++++++ src/slic3r/GUI/DeviceManager.hpp | 2 ++ 2 files changed, 36 insertions(+) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 5499694686..ef85870461 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -4647,6 +4647,40 @@ void MachineObject::set_ctt_dlg( wxString text){ } } +void MachineObject::show_unsupported_dlg(int code) +{ + // why: a dead control invites repeat clicks, and the frame is modeless - without the guard + // every click stacks another one. Same shape as set_ctt_dlg above, including the reset on + // both hide and close so a dismissed dialog can reappear on the next attempt. + if (m_unsupported_dlg_shown) { + return; + } + m_unsupported_dlg_shown = true; + + // why: two codes so the user learns which kind of dead end this is - the slicer having no + // translation for the command, or the printer's own config lacking the hardware to run it. + const wxString text = (code == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) ? + _L("This printer is not configured with the hardware this control needs.") : + _L("This control is not supported on this printer."); + + // note: constructed directly rather than through CallAfter because every publish_json caller + // is on the UI thread - clicks come from wx handlers, and the agent marshals its own push + // callbacks back to main before parse_json runs. set_ctt_dlg relies on the same property. + auto unsupported_dlg = new GUI::SecondaryCheckDialog(nullptr, wxID_ANY, _L("Warning"), + GUI::SecondaryCheckDialog::VisibleButtons::ONLY_CONFIRM); + unsupported_dlg->update_text(text); + unsupported_dlg->Bind(wxEVT_SHOW, [this](auto& e) { + if (!e.IsShown()) { + m_unsupported_dlg_shown = false; + } + }); + unsupported_dlg->Bind(wxEVT_CLOSE_WINDOW, [this](auto& e) { + e.Skip(); + m_unsupported_dlg_shown = false; + }); + unsupported_dlg->on_show(); +} + int MachineObject::publish_gcode(std::string gcode_str) { json j; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 456901cf84..2790e37cfa 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -272,9 +272,11 @@ public: bool m_is_online; bool m_lan_mode_connection_state{false}; bool m_set_ctt_dlg{ false }; + bool m_unsupported_dlg_shown{ false }; void set_lan_mode_connection_state(bool state) {m_lan_mode_connection_state = state;}; bool get_lan_mode_connection_state() {return m_lan_mode_connection_state;}; void set_ctt_dlg( wxString text); + void show_unsupported_dlg(int code); int parse_msg_count = 0; int keep_alive_count = 0; std::chrono::system_clock::time_point last_update_time; /* last received print data from machine */ From 596cbb8b2da5199ff1f897344a16fd96f4006036 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 5 Aug 2026 11:39:37 +0800 Subject: [PATCH 6/7] Keep printer-agent error codes available to UI workflow --- src/slic3r/Utils/IPrinterAgent.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 91d271316e..0fa3616344 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -2,6 +2,13 @@ #define __I_PRINTER_AGENT_HPP__ #include "bambu_networking.hpp" +// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value +// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS. +// They live here and not in bambu_networking.hpp because that file is a vendor header replaced +// wholesale by header-sync commits (see c09252ce11), which would silently clobber them. +// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx. +#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command +#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability #include #include From 32f82b64e7da1d038d421fd28bd86b8f436c6fd8 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 5 Aug 2026 13:07:32 +0800 Subject: [PATCH 7/7] fix: enable both device tabs --- src/slic3r/GUI/MainFrame.cpp | 83 +++++++++++++++++++++++++++++++++++- src/slic3r/GUI/MainFrame.hpp | 4 +- src/slic3r/GUI/Plater.cpp | 7 +-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 39082a9dca..5ef81a32e1 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1368,9 +1368,88 @@ void MainFrame::init_tabpanel() { } // SoftFever -void MainFrame::show_device(bool bBBLPrinter) { +void MainFrame::show_device(bool should_use_native) { auto idx = -1; - if (bBBLPrinter) { + + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + + // The legacy page is appended when printer agents are enabled. Remove that + // extra page before switching back to the normal native/legacy layout. + if (!use_printer_agents) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { + m_printer_view->Show(false); + m_tabpanel->RemovePage(idx); + } + } + + if (use_printer_agents) { + if (!m_monitor) { + m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); + m_monitor->SetBackgroundColour(*wxWHITE); + } + + if (m_tabpanel->FindPage(m_monitor) == wxNOT_FOUND) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND) { + m_printer_view->Show(false); + m_tabpanel->RemovePage(idx); + } + m_monitor->Show(false); + m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), + std::string("tab_monitor_active")); + } + + if (m_printer_view == nullptr) { + m_printer_view = new PrinterWebView(m_tabpanel); + Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent& evt) { + wxString url = evt.GetString(); + wxString key = evt.GetAPIkey(); + // select_tab(MainFrame::tpMonitor); + m_printer_view->load_url(url, key); + }); + } + + if (wxGetApp().is_enable_multi_machine()) { + if (!m_multi_machine) { + m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); + m_multi_machine->SetBackgroundColour(*wxWHITE); + } + // TODO: change the bitmap + if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { + m_multi_machine->Show(false); + m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), + std::string("tab_multi_active"), false); + } + } + if (!m_calibration) { + m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); + m_calibration->SetBackgroundColour(*wxWHITE); + } + // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, + // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. + if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { + m_calibration->Show(false); + m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), + std::string("tab_calibration_active"), false); + } + + if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { + m_printer_view->Show(false); + m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"), + std::string("tab_monitor_active"), false); + } else { + m_tabpanel->SetPageText(idx, _L("Device (legacy)")); + } + +#ifdef _MSW_DARK_MODE + wxGetApp().UpdateDarkUIWin(this); +#endif // _MSW_DARK_MODE + + fit_tab_labels(); // ORCA on printer change + + return; + } + + if (should_use_native) { if (m_tabpanel->FindPage(m_monitor) != wxNOT_FOUND) { fit_tab_labels(); // ORCA on printer change - same button layout return; diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 20229a611e..a614783c31 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -358,7 +358,7 @@ public: void RunScript(wxString js); //SoftFever - void show_device(bool bBBLPrinter); + void show_device(bool should_use_native); void fit_tab_labels(); // ORCA PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; @@ -385,7 +385,7 @@ public: CalibrationPanel* m_calibration{ nullptr }; WebViewPanel* m_webview { nullptr }; PrinterWebView* m_printer_view{nullptr}; - wxLogWindow* m_log_window { nullptr }; + wxLogWindow* m_log_window { nullptr }; // BBS //wxBookCtrlBase* m_tabpanel { nullptr }; Notebook* m_tabpanel{ nullptr }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d83ddded34..3ee09fed06 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3246,7 +3246,8 @@ void Sidebar::update_all_preset_comboboxes() auto p_mainframe = wxGetApp().mainframe; auto cfg = preset_bundle.printers.get_edited_preset().config; - const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents"); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || use_printer_agents; if (preset_bundle.use_bbl_network()) { //only show connection button for not-BBL printer @@ -3259,7 +3260,7 @@ void Sidebar::update_all_preset_comboboxes() } else { //p->btn_connect_printer->Show(); // ORCA: hide the physical-printer connection button when printer agents are enabled - p->m_printer_connect->Show(!wxGetApp().app_config->get_bool("use_printer_agents")); + p->m_printer_connect->Show(!use_printer_agents); // ORCA: show/hide sync-ams button based on filament sync mode auto agent = wxGetApp().getAgent(); @@ -3286,7 +3287,7 @@ void Sidebar::update_all_preset_comboboxes() : MainFrame::PrintSelectType::eSendGcode; } - if (!use_native_device_tab) + if (!use_native_device_tab || use_printer_agents) p_mainframe->load_printer_url(url, apikey);