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

@@ -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