feat: sidebar plugin config UI

This commit is contained in:
Ian Chua
2026-07-13 20:39:41 +08:00
parent 0c69b9982d
commit 5b3e1b921c
27 changed files with 2263 additions and 290 deletions

View File

@@ -23,6 +23,8 @@
#include "OG_CustomCtrl.hpp"
#include "MsgDialog.hpp"
#include "BitmapComboBox.hpp"
#include "PluginsConfigDialog.hpp"
#include "Widgets/Button.hpp"
// BBS
#include "Notebook.hpp"
@@ -2308,6 +2310,136 @@ void PluginField::msw_rescale()
rebuild_ui();
}
namespace {
// The stored text as a document, or an empty array when it is absent or unparseable. Used to compare
// two versions of the value semantically: re-serializing an unchanged document can reorder keys or
// drop whitespace, and that alone must not be allowed to mark the preset dirty.
nlohmann::json plugin_overrides_as_json(const std::string& text)
{
if (text.empty())
return nlohmann::json::array();
return nlohmann::json::parse(text, nullptr, /* allow_exceptions */ false);
}
} // namespace
void PluginConfigField::BUILD()
{
// The button *is* the field's window (the ColourPicker idiom). Wrapping it in a panel would make
// this a container that OG_CustomCtrl sizes but never lays out, leaving the button unsized — and
// a zero-height row collapses its whole option group out of the page.
m_button = new ::Button(m_parent, _L("Configure"));
// ButtonType::Parameter is the style for a button sitting next to parameter boxes: it is what
// gives the button the same height as the fields above it, which a bare wxButton does not.
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
wxSize size(def_width_wider() * m_em_unit, m_button->GetMinSize().GetHeight());
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);
m_button->SetMinSize(size);
m_button->SetSize(size);
m_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { open_dialog(); });
m_button->SetToolTip(get_tooltip_text(_L("Configure")));
window = m_button;
if (const ConfigOptionString* def = m_opt.get_default_value<ConfigOptionString>())
m_json = def->value;
update_button_label();
m_value = m_json;
}
void PluginConfigField::update_button_label()
{
if (m_button == nullptr)
return;
const nlohmann::json entries = plugin_overrides_as_json(m_json);
const size_t count = entries.is_array() ? entries.size() : 0;
// The count is the whole state this row can show: which capabilities they belong to, and what
// they hold, is the dialog's job.
m_button->SetLabel(count == 0 ? _L("Configure")
: wxString::Format(_L("Configure (%d)"), int(count)));
}
void PluginConfigField::open_dialog()
{
const Preset::Type type = static_cast<Preset::Type>(m_preset_type);
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_FILAMENT && type != Preset::TYPE_PRINTER)
return;
std::string edited;
{
PluginsConfigDialog dlg(m_button, type, m_json);
dlg.ShowModal();
edited = dlg.overrides_json();
}
// Only a semantic change is a change: reopening the dialog and closing it must not dirty the
// preset just because the document round-tripped through the serializer.
if (plugin_overrides_as_json(m_json) == plugin_overrides_as_json(edited))
return;
m_json = edited;
m_value = m_json;
update_button_label();
// The one call that makes this behave like every other setting: it drives change_opt_value into
// the preset config, Tab::update_dirty(), and the revert arrow.
on_change_field();
}
void PluginConfigField::set_value(const boost::any& value, bool change_event)
{
m_disable_change_event = !change_event;
if (value.type() == typeid(wxString))
m_json = into_u8(boost::any_cast<wxString>(value));
else if (value.type() == typeid(std::string))
m_json = boost::any_cast<std::string>(value);
m_value = m_json;
update_button_label();
m_disable_change_event = false;
}
boost::any& PluginConfigField::get_value()
{
// std::string, not wxString: change_opt_value any_casts a coString to std::string.
m_value = m_json;
return m_value;
}
void PluginConfigField::enable()
{
if (m_button)
m_button->Enable();
}
void PluginConfigField::disable()
{
if (m_button)
m_button->Disable();
}
void PluginConfigField::msw_rescale()
{
Field::msw_rescale();
if (m_button == nullptr)
return;
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
wxSize size(def_width_wider() * m_em_unit, m_button->GetMinSize().GetHeight());
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);
m_button->SetMinSize(size);
}
void ColourPicker::BUILD()
{
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width

View File

@@ -32,6 +32,10 @@
#define wxMSW false
#endif
// Orca's styled button (Widgets/Button.hpp), used by PluginConfigField. Declared at global scope,
// which is where the widget lives.
class Button;
namespace Slic3r { namespace GUI {
class Field;
@@ -517,6 +521,47 @@ private:
std::function<std::string()> m_selector;
};
// A settings row whose value is a raw JSON document that nobody should type by hand: the row shows a
// button, the button opens PluginsConfigDialog, and the document it hands back becomes the field's
// value. Routing the edit through the ordinary Field value/on_change_field path is what earns the row
// the same dirty state and revert arrow as every other setting — the dialog itself never touches the
// preset.
class PluginConfigField : public Field {
using Field::Field;
public:
PluginConfigField(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
PluginConfigField(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~PluginConfigField() {}
void BUILD() override;
// Which preset's capabilities the dialog lists. Supplied by the option group, which already
// carries its Tab's type; an int for the same reason OptionsGroup::m_config_type is one — it
// keeps Preset.hpp out of this header.
void set_preset_type(int type) { m_preset_type = type; }
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void enable() override;
void disable() override;
// Window-field: the button is the whole field, so it is the window the option group sizes and
// positions (the ColourPicker idiom). A container panel would be sized but never laid out.
wxWindow* getWindow() override { return window; }
void msw_rescale() override;
private:
void open_dialog();
void update_button_label();
wxWindow* window { nullptr }; // == m_button; the base class hands this to the option group
::Button* m_button { nullptr };
std::string m_json; // the option's raw text; "" when the preset overrides nothing
int m_preset_type { -1 };
};
class ColourPicker : public Field {
using Field::Field;

View File

@@ -53,6 +53,7 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co
break;
case ConfigOptionDef::GUIType::one_string: m_fields.emplace(id, TextCtrl::Create<TextCtrl>(this->ctrl_parent(), opt, id)); break;
case ConfigOptionDef::GUIType::plugin_picker: m_fields.emplace(id, PluginField::Create<PluginField>(this->ctrl_parent(), opt, id)); break;
case ConfigOptionDef::GUIType::plugin_config: m_fields.emplace(id, PluginConfigField::Create<PluginConfigField>(this->ctrl_parent(), opt, id)); break;
default:
switch (opt.type) {
case coFloatOrPercent:
@@ -126,6 +127,13 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co
});
}
// The dialog behind the button edits one preset's overrides, so it has to know which preset. The
// group already carries its Tab's type, and fields are built lazily on activate() — too late for
// the Tab to reach in and set it afterwards.
if (auto plugin_config_field = dynamic_cast<PluginConfigField*>(field.get()))
if (auto config_group = dynamic_cast<ConfigOptionsGroup*>(this))
plugin_config_field->set_preset_type(config_group->config_type());
// assign function objects for callbacks, etc.
return field;
}

View File

@@ -0,0 +1,238 @@
#include "PluginsConfigDialog.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include <libslic3r/Preset.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PluginResolver.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace GUI {
namespace {
wxString preset_type_title(Preset::Type type)
{
switch (type) {
case Preset::TYPE_PRINT: return _L("Process plugins");
case Preset::TYPE_FILAMENT: return _L("Filament plugins");
case Preset::TYPE_PRINTER: return _L("Printer plugins");
default: return _L("Plugins");
}
}
} // namespace
PluginsConfigDialog::PluginsConfigDialog(wxWindow* parent, Preset::Type type, const std::string& overrides_json)
: WebViewHostDialog(parent, wxID_ANY, preset_type_title(type))
, m_type(type)
{
// Unparseable text is kept, not replaced: parse_plugin_overrides leaves the document empty and
// every row goes read-only, so a preset we cannot understand is never silently overwritten.
if (!parse_plugin_overrides(overrides_json, m_overrides, m_parse_error))
BOOST_LOG_TRIVIAL(error) << "Plugins Config dialog: " << m_parse_error;
create_webview("web/dialog/PluginsConfigDialog/index.html", preset_type_title(type), wxSize(820, 660),
wxSize(640, 520));
}
PluginsConfigDialog::~PluginsConfigDialog() = default;
const Preset* PluginsConfigDialog::current_preset() const
{
const PresetBundle* bundle = wxGetApp().preset_bundle;
if (bundle == nullptr)
return nullptr;
switch (m_type) {
case Preset::TYPE_PRINT: return &bundle->prints.get_edited_preset();
case Preset::TYPE_PRINTER: return &bundle->printers.get_edited_preset();
case Preset::TYPE_FILAMENT: return &bundle->filaments.get_edited_preset();
default: return nullptr;
}
}
PluginCapabilityIdentifier PluginsConfigDialog::identifier_from(const nlohmann::json& payload) const
{
return {plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""),
payload.value("plugin_key", "")};
}
void PluginsConfigDialog::on_script_message(const nlohmann::json& payload)
{
if (handle_common_script_command(payload))
return;
const std::string command = payload.value("command", "");
if (command == "request_capabilities") {
send_capabilities();
return;
}
const PluginCapabilityIdentifier id = identifier_from(payload);
if (command == "get_capability_config") {
send_capability_config(id);
} else if (command == "save_capability_config") {
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
nlohmann::json value = payload.contains("config") ? payload.at("config") : nlohmann::json::object();
if (value.is_string()) {
value = nlohmann::json::parse(value.get<std::string>(), nullptr, /* allow_exceptions */ false);
if (value.is_discarded()) {
send_save_error(id, into_u8(_L("The configuration is not valid JSON. Your changes were not saved.")));
return;
}
}
const MutationResult result = m_service.set_preset_override(m_overrides, id, value);
if (!result.ok) {
send_save_error(id, result.error);
return;
}
send_capability_config(id);
show_status(_L("Configuration updated. Save the preset to persist it."), "success");
} else if (command == "restore_preset_defaults") {
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
"This writes the plugin defaults into the current preset override."),
from_u8(id.name)),
_L("Restore defaults"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
if (rc != wxYES)
return;
const auto cap = PluginManager::instance().get_loader().get_plugin_capability_by_name(id);
if (!cap)
return;
nlohmann::json defaults;
try {
wxBusyCursor busy;
PythonGILState gil;
defaults = cap->instance->get_default_config();
} catch (const std::exception& ex) {
// A broken plugin must not be able to wipe the stored config: store nothing and say so.
send_save_error(id, into_u8(GUI::format_wxstr(_L("The plugin failed to supply its default configuration (%1%)."),
from_u8(ex.what()))));
return;
}
const MutationResult result = m_service.set_preset_override(m_overrides, id, defaults);
if (!result.ok) {
send_save_error(id, result.error);
return;
}
send_capability_config(id);
show_status(_L("Default configuration restored. Save the preset to persist it."), "success");
} else if (command == "remove_preset_override") {
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
const MutationResult result = m_service.remove_preset_override(m_overrides, id);
if (!result.ok) {
send_save_error(id, result.error);
return;
}
send_capability_config(id);
show_status(_L("Using global configuration. Save the preset to persist it."), "success");
}
}
void PluginsConfigDialog::send_capabilities()
{
const Preset* preset = current_preset();
if (preset == nullptr)
return;
nlohmann::json response;
response["command"] = "list_capabilities";
response["preset_type"] = static_cast<int>(m_type);
response["title"] = into_u8(preset_type_title(m_type));
response["preset_name"] = preset->name;
response["data"] = PluginConfig::capabilities_payload(capabilities_in_use(m_type, *preset));
BOOST_LOG_TRIVIAL(info) << "Prepared " << response["data"].size() << " capability rows for the Plugins Config dialog";
call_web_handler(response);
}
void PluginsConfigDialog::send_capability_config(const PluginCapabilityIdentifier& id)
{
const Preset* preset = current_preset();
nlohmann::json response;
response["command"] = "capability_config";
response["plugin_key"] = id.plugin_key;
response["capability_name"] = id.name;
response["capability_type"] = plugin_capability_type_to_string(id.type);
response["config"] = nlohmann::json::object();
response["custom_html"] = "";
response["error"] = "";
const auto cap = PluginManager::instance().get_loader().get_plugin_capability_by_name(id);
if (!cap || preset == nullptr) {
response["error"] = into_u8(_L("This capability is no longer available."));
call_web_handler(response);
return;
}
const EffectiveCapabilityConfig effective = m_service.get_effective_config(m_overrides, id);
response["config"] = effective.config;
response["source"] = plugin_config_source_to_string(effective.source);
response["has_preset_override"] = effective.has_preset_override;
response["has_base_config"] = effective.has_base_config;
response["stored_plugin_version"] = effective.stored_plugin_version;
response["running_plugin_version"] = effective.running_plugin_version;
response["read_only"] = !m_parse_error.empty();
if (!m_parse_error.empty())
response["error"] = m_parse_error;
if (cap->has_config_ui) {
try {
wxBusyCursor busy;
PythonGILState gil;
response["custom_html"] = cap->instance->get_config_ui();
} catch (const std::exception& ex) {
response["error"] = into_u8(GUI::format_wxstr(_L("The plugin's configuration UI failed to load (%1%). Showing the default editor."),
from_u8(ex.what())));
}
}
call_web_handler(response);
}
void PluginsConfigDialog::send_save_error(const PluginCapabilityIdentifier& id, const std::string& error)
{
call_web_handler({{"command", "capability_config_saved"},
{"plugin_key", id.plugin_key},
{"capability_name", id.name},
{"capability_type", plugin_capability_type_to_string(id.type)},
{"ok", false},
{"error", error}});
}
void PluginsConfigDialog::show_status(const wxString& message, const char* level)
{
nlohmann::json payload;
payload["command"] = "status_message";
payload["level"] = level;
payload["message"] = into_u8(message);
call_web_handler(payload);
}
}} // namespace Slic3r::GUI

View File

@@ -0,0 +1,50 @@
#pragma once
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
#include <libslic3r/Preset.hpp>
#include <slic3r/plugin/PresetPluginConfig.hpp>
#include <string>
namespace Slic3r { namespace GUI {
// The config half of the Plugins dialog, scoped to one preset instead of one plugin: it lists the
// capabilities the edited preset of `m_type` actually uses (see capabilities_in_use) and edits each
// one's stored config, falling back to the global config where the preset has no override.
//
// It is a pure editor over a JSON document: it never writes to the preset and never writes to the
// base config file. The caller seeds it with the preset's raw override text and reads the edited
// text back from overrides_json(). PluginConfigField, which owns the value, then feeds that through
// the normal field/dirty pipeline — which is what makes the revert arrow behave like any other
// setting. The capability list and config payloads are shared with PluginsDialog's Config tab
// through PluginConfig's statics.
class PluginsConfigDialog : public WebViewHostDialog
{
public:
PluginsConfigDialog(wxWindow* parent, Preset::Type type, const std::string& overrides_json);
~PluginsConfigDialog() override;
// The edited overrides as compact JSON text; "" once no override remains.
std::string overrides_json() const { return serialize_plugin_overrides(m_overrides); }
private:
void on_script_message(const nlohmann::json& payload) override;
const Preset* current_preset() const;
void send_capabilities();
void send_capability_config(const PluginCapabilityIdentifier& id);
void send_save_error(const PluginCapabilityIdentifier& id, const std::string& error);
void show_status(const wxString& message, const char* level);
PluginCapabilityIdentifier identifier_from(const nlohmann::json& payload) const;
Preset::Type m_type = Preset::TYPE_INVALID;
PresetPluginConfigService m_service;
// The working copy the dialog edits. Seeded from the preset's raw text, read back by the caller.
CapabilityConfigDocument m_overrides;
// Set when the preset's stored text could not be parsed: the rows are shown read-only rather
// than silently replacing data we did not understand.
std::string m_parse_error;
};
}} // namespace Slic3r::GUI

View File

@@ -4,6 +4,7 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "slic3r/plugin/PluginConfig.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
@@ -16,11 +17,8 @@
#include <slic3r/GUI/format.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <pybind11/embed.h>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
@@ -222,6 +220,17 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
}
payload_item["capabilities"] = std::move(caps);
// The Config tab's sidebar, built by the shared builder so it stays identical to
// PluginsConfigDialog's. Deliberately not the `capabilities` array above: that one is the list
// tab's, carrying enable/run state and descriptor-only rows for plugins that are not activated
// yet — neither of which the config view has any use for.
std::vector<PluginCapabilityIdentifier> config_ids;
for (const PluginCapabilityView& capability : dialog_item.capabilities)
if (!capability.name.empty())
config_ids.push_back(PluginCapabilityIdentifier{plugin_capability_type_from_string(capability.type_key),
capability.name, dialog_item.plugin_key});
payload_item["config_capabilities"] = PluginConfig::capabilities_payload(config_ids);
nlohmann::json changelog = nlohmann::json::array();
for (const PluginChangelogView& entry : dialog_item.changelog) {
nlohmann::json c;
@@ -918,153 +927,32 @@ wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
return from_u8(plugin_key);
}
// Replies to the web page with one capability's stored config, plus the custom HTML UI when the
// capability provides one. Config is sent as a JSON value, not text: the default editor
// pretty-prints it into its textarea, and a custom UI receives it as-is through window.orca.
void PluginsDialog::send_capability_config(const std::string& plugin_key,
PluginCapabilityType type,
const std::string& capability_name)
{
nlohmann::json response;
response["command"] = "capability_config";
response["plugin_key"] = plugin_key;
response["capability_name"] = capability_name;
response["capability_type"] = plugin_capability_type_to_string(type);
response["config"] = nlohmann::json::object();
response["custom_html"] = "";
response["error"] = "";
// Scoped to (plugin_key, type, capability_name), so a stale request from a page that has not
// caught up with a refresh cannot read a different plugin's config — it just misses.
auto cap = get_capability(plugin_key, type, capability_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring config request for a capability that is no longer loaded. plugin_key="
<< plugin_key << " capability_name=" << capability_name;
response["error"] = into_u8(_L("This capability is no longer available."));
call_web_handler(response);
return;
}
response["config"] = PluginManager::instance().get_config().get_config(plugin_key, capability_name).config;
if (cap->has_config_ui) {
// Plugin-authored HTML. A raising or empty get_config_ui() costs the capability only its
// custom UI: we report the failure and let the page fall back to the default JSON editor,
// which edits the very same stored config.
std::string html;
std::string error;
{
wxBusyCursor busy;
try {
PythonGILState gil;
html = cap->instance->get_config_ui();
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
}
if (!error.empty()) {
BOOST_LOG_TRIVIAL(error) << "Plugin capability get_config_ui() failed. plugin_key=" << plugin_key
<< " capability_name=" << capability_name << " error=" << error;
response["error"] = into_u8(format_wxstr(_L("The plugin's configuration UI failed to load (%1%). Showing the default editor."),
from_u8(error)));
} else if (html.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Plugin capability reports a config UI but returned no HTML. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
response["error"] = into_u8(_L("The plugin's configuration UI was empty. Showing the default editor."));
} else {
response["custom_html"] = html;
}
}
call_web_handler(response);
call_web_handler(PluginConfig::get_config_response({type, capability_name, plugin_key}));
}
// Persists one capability's config. `config` arrives either as text straight from the default
// editor (validated here, so invalid JSON can never reach the file) or as a structured value from
// a custom UI. Only cap_config is written; identity and version metadata stay host-managed.
void PluginsDialog::save_capability_config(const std::string& plugin_key,
PluginCapabilityType type,
const std::string& capability_name,
const nlohmann::json& config)
{
nlohmann::json response;
response["command"] = "capability_config_saved";
response["plugin_key"] = plugin_key;
response["capability_name"] = capability_name;
response["capability_type"] = plugin_capability_type_to_string(type);
response["ok"] = false;
response["error"] = "";
auto cap = get_capability(plugin_key, type, capability_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Refusing to save config for a capability that is no longer loaded. plugin_key="
<< plugin_key << " capability_name=" << capability_name;
response["error"] = into_u8(_L("This capability is no longer available. Your changes were not saved."));
call_web_handler(response);
return;
}
nlohmann::json parsed = config;
if (config.is_string()) {
// The page validates as the user types, but it is not the authority: re-parse here so a
// malformed document is rejected before it can reach config.json.
parsed = nlohmann::json::parse(config.get<std::string>(), nullptr, /* allow_exceptions */ false);
if (parsed.is_discarded()) {
response["error"] = into_u8(_L("The configuration is not valid JSON. Your changes were not saved."));
call_web_handler(response);
return;
}
}
if (!PluginManager::instance().get_config().store_capability_config(plugin_key, capability_name, parsed)) {
BOOST_LOG_TRIVIAL(error) << "Failed to write the plugin config file. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
response["error"] = into_u8(_L("The configuration could not be written to disk. Your changes were not saved."));
call_web_handler(response);
return;
}
BOOST_LOG_TRIVIAL(info) << "Saved plugin capability config. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
// Echo the persisted value back so the editor reloads from what is actually stored rather
// than from what the user typed.
response["ok"] = true;
response["config"] = PluginManager::instance().get_config().get_config(plugin_key, capability_name).config;
const nlohmann::json response = PluginConfig::save_config_response({type, capability_name, plugin_key}, config);
call_web_handler(response);
show_status(_L("Configuration saved."), "success");
if (response.value("ok", false))
show_status(_L("Configuration saved."), "success");
}
// Overwrites one capability's stored config with the value its get_default_config() hands back.
// The host does not invent that value: a capability that does not override the hook restores an
// empty config, which is exactly right for one that applies its own defaults on read.
void PluginsDialog::restore_capability_config(const std::string& plugin_key,
PluginCapabilityType type,
const std::string& capability_name)
{
nlohmann::json response;
response["command"] = "capability_config_saved";
response["plugin_key"] = plugin_key;
response["capability_name"] = capability_name;
response["capability_type"] = plugin_capability_type_to_string(type);
response["ok"] = false;
response["error"] = "";
auto cap = get_capability(plugin_key, type, capability_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Refusing to restore config for a capability that is no longer loaded. plugin_key="
<< plugin_key << " capability_name=" << capability_name;
response["error"] = into_u8(_L("This capability is no longer available."));
call_web_handler(response);
return;
}
// Discards whatever the user had stored, so confirm first — same as the other destructive
// actions in this dialog.
// actions in this dialog. The confirmation stays here rather than in PluginConfig: it needs a
// parent window, and it is the dialog's business to ask.
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
"This discards the settings currently saved for this capability."),
from_u8(capability_name)),
@@ -1072,49 +960,11 @@ void PluginsDialog::restore_capability_config(const std::string& plugin_key,
if (rc != wxYES)
return;
nlohmann::json defaults;
std::string error;
{
wxBusyCursor busy;
try {
PythonGILState gil;
defaults = cap->instance->get_default_config();
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
}
// A raising hook leaves the stored config exactly as it was: better to restore nothing than to
// wipe the user's settings on the strength of a broken plugin.
if (!error.empty()) {
BOOST_LOG_TRIVIAL(error) << "Plugin capability get_default_config() failed. plugin_key=" << plugin_key
<< " capability_name=" << capability_name << " error=" << error;
response["error"] = into_u8(format_wxstr(_L("The plugin could not supply a default configuration (%1%). "
"Nothing was changed."),
from_u8(error)));
call_web_handler(response);
return;
}
if (!PluginManager::instance().get_config().store_capability_config(plugin_key, capability_name, defaults)) {
BOOST_LOG_TRIVIAL(error) << "Failed to write the plugin config file while restoring defaults. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
response["error"] = into_u8(_L("The configuration could not be written to disk. Nothing was changed."));
call_web_handler(response);
return;
}
BOOST_LOG_TRIVIAL(info) << "Restored default plugin capability config. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
// Reuses the saved reply, so both editors reload from what was actually persisted.
response["ok"] = true;
response["config"] = PluginManager::instance().get_config().get_config(plugin_key, capability_name).config;
const nlohmann::json response = PluginConfig::restore_config_response({type, capability_name, plugin_key});
call_web_handler(response);
show_status(_L("Default configuration restored."), "success");
if (response.value("ok", false))
show_status(_L("Default configuration restored."), "success");
}
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)

View File

@@ -3086,6 +3086,13 @@ void TabPrint::build()
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
// Its own group rather than the one above: that one hides its labels, and this row needs its
// label — and so the revert arrow beside it — to show. No label-width override either: a 0
// there means "no label column", which is what the neighbouring full-width groups want and
// what would collapse this one.
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
optgroup->append_single_option_line("plugin_preference_overrides");
optgroup = page->new_optgroup(L("Notes"), "note", 0);
option = optgroup->get_option("notes");
option.opt.full_width = true;
@@ -4481,6 +4488,9 @@ void TabFilament::build()
option.opt.height = gcode_field_height;// 150;
optgroup->append_single_option_line(option);
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
optgroup->append_single_option_line("plugin_preference_overrides");
page = add_options_page(L("Multimaterial"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders
optgroup = page->new_optgroup(L("Wipe tower parameters"), "param_tower");
optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters");
@@ -4923,6 +4933,7 @@ void TabPrinter::build_fff()
optgroup->append_single_option_line("use_firmware_retraction", "printer_basic_information_advanced#use-firmware-retraction");
// optgroup->append_single_option_line("spaghetti_detector");
optgroup->append_single_option_line("time_cost", "printer_basic_information_advanced#time-cost");
optgroup->append_single_option_line("plugin_preference_overrides");
optgroup = page->new_optgroup(L("Cooling Fan"), "param_cooling_fan");
Line line = Line{ L("Fan speed-up time"), optgroup->get_option("fan_speedup_time").opt.tooltip };