feat: plugins config APIs

This commit is contained in:
peachismomo
2026-07-12 16:55:04 +08:00
parent a00fac9b72
commit ce21a09cb1
8 changed files with 224 additions and 10 deletions

View File

@@ -536,6 +536,10 @@ void PluginsDialog::on_script_message(const nlohmann::json& payload)
plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""),
payload.contains("config") ? payload.at("config") : nlohmann::json::object());
} else if (command == "restore_capability_config") {
restore_capability_config(payload.value("plugin_key", ""),
plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""));
} else if (command == "set_plugin_install_action") {
const std::string action = payload.value("action", "");
if (action == "explore" || action == "install-local")
@@ -1035,6 +1039,84 @@ void PluginsDialog::save_capability_config(const std::string& plugin_key,
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.
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)),
_L("Restore defaults"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
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;
call_web_handler(response);
show_status(_L("Default configuration restored."), "success");
}
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
{
if (plugin_key.empty() || capability_name.empty()) {

View File

@@ -75,6 +75,7 @@ private:
PluginCapabilityType type,
const std::string& capability_name,
const nlohmann::json& config);
void restore_capability_config(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name);
// Pushes a one-line result into the web footer status bar (level: "success" | "warn" | "error" | "info"),
// used for every plugin/capability operation instead of a modal box so the dialog stays non-disruptive.
void show_status(const wxString& message, const char* level);

View File

@@ -3,10 +3,13 @@
#include <pybind11/embed.h>
#include <boost/log/trivial.hpp>
#include <optional>
#include "PythonPluginInterface.hpp"
#include "PythonInterpreter.hpp"
#include "PythonJsonUtils.hpp"
#include "PluginAuditManager.hpp"
// Trampoline variants of pybind11's override macros. Every C++->Python plugin call
@@ -90,6 +93,40 @@ public:
get_config_ui);
}
// Hand-rolled rather than PYBIND11_OVERRIDE: the macro casts the Python result to the return
// type, and nlohmann::json has no pybind caster (config crosses this boundary through the
// explicit py_to_json/json_to_py helpers instead). Otherwise identical — same audit scope, and
// a Python exception is logged with its traceback and rethrown for the caller to handle.
//
// The hook is optional, and "not implemented" must mean an EMPTY config, never a null or a
// stray scalar landing in cap_config. Two ways to not implement it, both resolved here:
// - no override at all -> the base's empty object
// - an override that returns None, or any -> likewise. `def get_default_config(self): pass`
// non-object (a list, a string, a number) is the easy mistake, and it must not be able to
// write `"cap_config": null` to config.json.
nlohmann::json get_default_config() const override
{
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
try {
pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
if (!override)
return Base::get_default_config();
nlohmann::json config = ::Slic3r::py_to_json(override());
if (!config.is_object()) {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << this->audit_capability_name() << "' of plugin '" << this->audit_plugin_key()
<< "': get_default_config() returned " << config.type_name() << ", not an object; restoring an empty config";
return Base::get_default_config();
}
return config;
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
}
// All plugins may define their own on_load/unload functions.
void on_load() override
{

View File

@@ -339,6 +339,16 @@ void bind_python_api(pybind11::module_& m)
"Override to return the custom configuration UI as an HTML string. Only called when\n"
"has_config_ui() is True; an empty result falls back to the default JSON editor.\n"
"Inside the page, use window.orca.getConfig()/saveConfig() to reach this same config.")
.def(
"get_default_config",
[](const PluginCapabilityInterface& self) {
nlohmann::json config = self.get_default_config();
return json_to_py(config); // GIL held (binding body)
},
"Override to return the config that the Config tab's \"Restore defaults\" action writes\n"
"back. Optional: without it the action stores an empty dict, which already restores the\n"
"defaults of a capability that keeps its stored config sparse and applies its own\n"
"defaults on read. Override it to write an explicit starting config instead.")
.def(
"get_config",
[](const PluginCapabilityInterface& self) {

View File

@@ -6,6 +6,7 @@
#include <string_view>
#include <utility>
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
namespace Slic3r {
@@ -116,6 +117,16 @@ public:
// treated as "no custom UI" and falls back to the default JSON editor.
virtual std::string get_config_ui() const { return ""; }
// The config the Config tab's "Restore defaults" action writes back. Optional.
//
// Not overridden -> an empty object, which is the right answer for a capability that keeps
// its stored config sparse and applies its own defaults on read: clearing the overrides
// *is* restoring the defaults, and it keeps a later release free to change them.
// Override it to write an explicit starting config instead (e.g. to seed a form UI with
// every field present). The host neither invents nor validates this value; it only stores
// whatever comes back, so a throwing override leaves the stored config untouched.
virtual nlohmann::json get_default_config() const { return nlohmann::json::object(); }
virtual void on_load() {}
virtual void on_unload() {}