mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 09:07:39 +00:00
# Description Changing a slicing plugin's configuration had no effect on the sliced result until you forced a re-slice some other way; it now applies immediately. Print, printer and filament presets also keep their plugin configuration separately, so configuring a plugin on one no longer wipes out what you set on another. A plugin's custom configuration page gets the same round of improvements in both the Plugins dialog and the per-preset dialog: it follows the app's light/dark theme, keeps its state while you edit instead of resetting under the cursor, and can tell whether it is being edited globally or for a preset, so "Restore defaults" can be labeled for what it will actually do. The two bundled examples show this off — Twistify now ships a custom configuration UI, and Inspector is themed, groups # Screenshots/Recordings/Graphs https://github.com/user-attachments/assets/02ca062a-5143-49a3-abe0-a2a040b3a928 ## Tests <!-- > Please describe the tests that you have conducted to verify the changes made in this PR. --> <!-- > A guide for users on how to download the artifacts from this PR. --> [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
705 lines
27 KiB
C++
705 lines
27 KiB
C++
#include "PluginConfig.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <boost/format.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/nowide/fstream.hpp>
|
|
|
|
#include <libslic3r/Config.hpp>
|
|
#include <libslic3r/PresetBundle.hpp>
|
|
#include <libslic3r/PrintConfig.hpp>
|
|
#include <slic3r/GUI/GUI.hpp>
|
|
#include <slic3r/GUI/GUI_App.hpp>
|
|
#include <slic3r/GUI/I18N.hpp>
|
|
#include <slic3r/GUI/format.hpp>
|
|
#include <slic3r/plugin/PluginLoader.hpp>
|
|
#include <slic3r/plugin/PluginManager.hpp>
|
|
#include <slic3r/plugin/PythonInterpreter.hpp>
|
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
|
#include <stdexcept>
|
|
|
|
#include <wx/app.h>
|
|
#include <wx/utils.h>
|
|
|
|
namespace Slic3r {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* KEY_PLUGIN = "plugin_key";
|
|
constexpr const char* KEY_CAPABILITY = "capability";
|
|
constexpr const char* KEY_TYPE = "capability_type";
|
|
constexpr const char* KEY_VERSION = "plugin_version";
|
|
constexpr const char* KEY_CAP_CONFIG = "cap_config";
|
|
|
|
std::string string_field(const nlohmann::json& entry, const char* key)
|
|
{
|
|
const auto it = entry.find(key);
|
|
return it != entry.end() && it->is_string() ? it->get<std::string>() : std::string();
|
|
}
|
|
|
|
bool is_recognized_entry(const nlohmann::json& entry, PluginCapabilityId& id)
|
|
{
|
|
if (!entry.is_object())
|
|
return false;
|
|
|
|
id.plugin_key = string_field(entry, KEY_PLUGIN);
|
|
id.name = string_field(entry, KEY_CAPABILITY);
|
|
id.type = plugin_capability_type_from_string(string_field(entry, KEY_TYPE));
|
|
return !id.empty();
|
|
}
|
|
|
|
CapabilityConfigEntry decode_entry(const PluginCapabilityId& id, const nlohmann::json& entry)
|
|
{
|
|
CapabilityConfigEntry result;
|
|
result.id = id;
|
|
result.plugin_version = string_field(entry, KEY_VERSION);
|
|
const auto cap_it = entry.find(KEY_CAP_CONFIG);
|
|
result.config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
|
|
return result;
|
|
}
|
|
|
|
// PluginDescriptor::version is overwritten with the latest cloud version on a cloud merge, so it can
|
|
// name a version that is not the one on disk; installed_version is what actually loaded.
|
|
std::string running_plugin_version(const std::string& plugin_key)
|
|
{
|
|
PluginDescriptor descriptor;
|
|
if (!PluginManager::instance().try_get_valid_plugin_descriptor(plugin_key, descriptor))
|
|
return {};
|
|
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
|
|
}
|
|
|
|
// Null wherever the plugin host runs without the GUI app (the unit tests). wxGetApp() dereferences
|
|
// the app unconditionally, so ask wxWidgets instead.
|
|
const PresetBundle* active_preset_bundle()
|
|
{
|
|
const auto* app = dynamic_cast<const GUI::GUI_App*>(wxApp::GetInstance());
|
|
return app == nullptr ? nullptr : app->preset_bundle;
|
|
}
|
|
|
|
// PluginLoader stamps both halves onto the instance when it materializes the capability, so the
|
|
// caller never supplies them and cannot name another capability's entry. Empty means the instance
|
|
// was never materialized: refuse rather than read or clobber a wrong entry.
|
|
PluginCapabilityId capability_identity(const PluginCapabilityInterface& capability, const char* api_name)
|
|
{
|
|
const PluginCapabilityId id = capability.identity();
|
|
if (id.empty())
|
|
throw std::runtime_error(std::string(api_name) + "() is only available on a capability loaded by the plugin host");
|
|
|
|
return id;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::json& entries)
|
|
{
|
|
CapabilityConfigDocument document;
|
|
if (!entries.is_array())
|
|
return document;
|
|
|
|
for (const nlohmann::json& entry : entries) {
|
|
PluginCapabilityId id;
|
|
if (is_recognized_entry(entry, id) || (!id.plugin_key.empty() && !id.name.empty()))
|
|
document.m_entries[id] = entry;
|
|
else
|
|
document.m_opaque_entries.push_back(entry);
|
|
}
|
|
|
|
return document;
|
|
}
|
|
|
|
CapabilityConfigDocument CapabilityConfigDocument::from_root_json(const nlohmann::json& root)
|
|
{
|
|
const auto entries = root.find(KeyEntries);
|
|
return entries != root.end() ? from_entries(*entries) : CapabilityConfigDocument();
|
|
}
|
|
|
|
std::optional<CapabilityConfigEntry> CapabilityConfigDocument::find(const PluginCapabilityId& id) const
|
|
{
|
|
const auto it = m_entries.find(id);
|
|
if (it != m_entries.end())
|
|
return decode_entry(it->first, it->second);
|
|
|
|
// Legacy config.json entries have no capability type. Keep them addressable by the new
|
|
// typed API until that capability is saved again.
|
|
if (id.type != PluginCapabilityType::Unknown) {
|
|
const auto legacy = m_entries.find({PluginCapabilityType::Unknown, id.name, id.plugin_key});
|
|
if (legacy != m_entries.end())
|
|
return decode_entry(id, legacy->second);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool CapabilityConfigDocument::contains(const PluginCapabilityId& id) const
|
|
{
|
|
return find(id).has_value();
|
|
}
|
|
|
|
bool CapabilityConfigDocument::upsert(CapabilityConfigEntry entry)
|
|
{
|
|
if (entry.id.empty())
|
|
return false;
|
|
|
|
if (entry.id.type != PluginCapabilityType::Unknown)
|
|
m_entries.erase({PluginCapabilityType::Unknown, entry.id.name, entry.id.plugin_key});
|
|
|
|
nlohmann::json serialized = nlohmann::json::object();
|
|
const auto existing = m_entries.find(entry.id);
|
|
if (existing != m_entries.end() && existing->second.is_object())
|
|
serialized = existing->second;
|
|
|
|
serialized[KEY_PLUGIN] = entry.id.plugin_key;
|
|
serialized[KEY_CAPABILITY] = entry.id.name;
|
|
serialized[KEY_TYPE] = plugin_capability_type_to_string(entry.id.type);
|
|
serialized[KEY_VERSION] = entry.plugin_version;
|
|
serialized[KEY_CAP_CONFIG] = entry.config;
|
|
|
|
m_entries[entry.id] = std::move(serialized);
|
|
return true;
|
|
}
|
|
|
|
bool CapabilityConfigDocument::erase(const PluginCapabilityId& id)
|
|
{
|
|
bool erased = m_entries.erase(id) != 0;
|
|
if (id.type != PluginCapabilityType::Unknown)
|
|
erased = m_entries.erase({PluginCapabilityType::Unknown, id.name, id.plugin_key}) != 0 || erased;
|
|
return erased;
|
|
}
|
|
|
|
bool CapabilityConfigDocument::prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced)
|
|
{
|
|
bool changed = false;
|
|
for (auto it = m_entries.begin(); it != m_entries.end();) {
|
|
if (referenced.count({it->first.type, it->first.name}) != 0) {
|
|
++it;
|
|
} else {
|
|
it = m_entries.erase(it);
|
|
changed = true;
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
bool CapabilityConfigDocument::empty() const
|
|
{
|
|
return m_entries.empty() && m_opaque_entries.empty();
|
|
}
|
|
|
|
nlohmann::json CapabilityConfigDocument::serialize_entries() const
|
|
{
|
|
nlohmann::json result = nlohmann::json::array();
|
|
for (const auto& item : m_entries)
|
|
result.push_back(item.second);
|
|
for (const nlohmann::json& entry : m_opaque_entries)
|
|
result.push_back(entry);
|
|
return result;
|
|
}
|
|
|
|
nlohmann::json CapabilityConfigDocument::root_json() const
|
|
{
|
|
nlohmann::json root = nlohmann::json::object();
|
|
root[KeyEntries] = serialize_entries();
|
|
return root;
|
|
}
|
|
|
|
void PluginConfig::load()
|
|
{
|
|
const std::string path = plugin_config_file();
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
m_document = CapabilityConfigDocument();
|
|
m_dirty = false;
|
|
|
|
boost::system::error_code ec;
|
|
if (!boost::filesystem::exists(path, ec))
|
|
return;
|
|
|
|
nlohmann::json root;
|
|
try {
|
|
boost::nowide::ifstream ifs(path.c_str());
|
|
ifs >> root;
|
|
} catch (const std::exception& err) {
|
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: cannot read " << path << ": " << err.what() << "; starting with an empty config";
|
|
return;
|
|
}
|
|
|
|
const auto entries = root.find(CapabilityConfigDocument::KeyEntries);
|
|
if (entries == root.end() || !entries->is_array()) {
|
|
BOOST_LOG_TRIVIAL(warning) << "PluginConfig: " << path << " has no \"" << CapabilityConfigDocument::KeyEntries
|
|
<< "\" array; starting with an empty config";
|
|
return;
|
|
}
|
|
|
|
m_document = CapabilityConfigDocument::from_root_json(root);
|
|
}
|
|
|
|
bool PluginConfig::save()
|
|
{
|
|
const std::string path = plugin_config_file();
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!m_dirty)
|
|
return true;
|
|
|
|
const nlohmann::json root = m_document.root_json();
|
|
|
|
boost::system::error_code ec;
|
|
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path(), ec);
|
|
if (ec) {
|
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: cannot create the plugin directory: " << ec.message();
|
|
return false;
|
|
}
|
|
|
|
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
|
|
// existing config. Same approach as AppConfig::save().
|
|
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
|
|
boost::nowide::ofstream file;
|
|
file.open(path_pid, std::ios::out | std::ios::trunc);
|
|
file << root.dump(1, '\t') << std::endl;
|
|
file.close();
|
|
if (file.fail()) {
|
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path_pid << "; keeping the existing config";
|
|
return false;
|
|
}
|
|
|
|
if (const std::error_code rename_ec = rename_file(path_pid, path)) {
|
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to move " << path_pid << " onto " << path << ": " << rename_ec.message();
|
|
return false;
|
|
}
|
|
|
|
m_dirty = false;
|
|
return true;
|
|
}
|
|
|
|
bool PluginConfig::store_capability_config(const PluginCapabilityId& id, const nlohmann::json& config)
|
|
{
|
|
if (id.empty())
|
|
return false;
|
|
|
|
save_config({id, running_plugin_version(id.plugin_key), config});
|
|
return save();
|
|
}
|
|
|
|
void PluginConfig::save_config(const CapabilityConfigEntry& config)
|
|
{
|
|
if (config.id.empty()) {
|
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: refusing to store a config without a complete capability identity";
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
m_dirty = m_document.upsert(config) || m_dirty;
|
|
}
|
|
|
|
bool PluginConfig::erase_capability_config(const PluginCapabilityId& id)
|
|
{
|
|
if (id.empty())
|
|
return false;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!m_document.erase(id))
|
|
return true;
|
|
m_dirty = true;
|
|
}
|
|
|
|
return save();
|
|
}
|
|
|
|
std::optional<CapabilityConfigEntry> PluginConfig::get_config(const PluginCapabilityId& id) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
return m_document.find(id);
|
|
}
|
|
|
|
bool PluginConfig::has_config(const PluginCapabilityId& id) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
return m_document.contains(id);
|
|
}
|
|
|
|
bool PluginConfig::dirty() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
return m_dirty;
|
|
}
|
|
|
|
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error)
|
|
{
|
|
document = CapabilityConfigDocument();
|
|
error.clear();
|
|
|
|
if (raw.empty())
|
|
return true;
|
|
|
|
const nlohmann::json parsed = nlohmann::json::parse(raw, nullptr, /* allow_exceptions */ false);
|
|
if (parsed.is_discarded()) {
|
|
error = "The preset stores invalid plugin capability configuration JSON.";
|
|
return false;
|
|
}
|
|
if (!parsed.is_array()) {
|
|
error = "The preset's plugin capability configuration is not an array and cannot be edited.";
|
|
return false;
|
|
}
|
|
|
|
document = CapabilityConfigDocument::from_entries(parsed);
|
|
return true;
|
|
}
|
|
|
|
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
|
|
{
|
|
return document.empty() ? std::string() : document.serialize_entries().dump();
|
|
}
|
|
|
|
bool prune_stale_plugin_overrides(DynamicConfig& config, const std::string& overrides_key)
|
|
{
|
|
const auto* overrides_opt = dynamic_cast<const ConfigOptionString*>(config.option(overrides_key));
|
|
if (overrides_opt == nullptr || overrides_opt->value.empty())
|
|
return false;
|
|
|
|
CapabilityConfigDocument overrides;
|
|
std::string error;
|
|
if (!parse_plugin_overrides(overrides_opt->value, overrides, error)) {
|
|
// Malformed text is not ours to fix up here: leave it untouched rather than risk
|
|
// discarding data the user might still be able to recover.
|
|
BOOST_LOG_TRIVIAL(error) << "prune_stale_plugin_overrides: " << error;
|
|
return false;
|
|
}
|
|
|
|
// Capability names currently referenced by a plugin-backed option's value(s) — e.g.
|
|
// slicing_pipeline_plugin's ConfigOptionStrings entries name SlicingPipeline capabilities
|
|
// directly, the same raw values save_plugin_collection() resolves into the "plugins" manifest.
|
|
std::set<std::pair<PluginCapabilityType, std::string>> referenced;
|
|
const ConfigDef* def = config.def();
|
|
for (const std::string& opt_key : config.keys()) {
|
|
const ConfigOptionDef* opt_def = def != nullptr ? def->get(opt_key) : nullptr;
|
|
if (opt_def == nullptr || !opt_def->is_plugin_backed())
|
|
continue;
|
|
|
|
const ConfigOption* opt = config.option(opt_key);
|
|
const PluginCapabilityType type = plugin_capability_type_from_string(opt_def->plugin_type);
|
|
if (const auto* string_opt = dynamic_cast<const ConfigOptionString*>(opt)) {
|
|
if (!string_opt->value.empty())
|
|
referenced.emplace(type, string_opt->value);
|
|
} else if (const auto* vector_opt = dynamic_cast<const ConfigOptionVectorBase*>(opt)) {
|
|
for (const std::string& value : vector_opt->vserialize())
|
|
if (!value.empty())
|
|
referenced.emplace(type, value);
|
|
}
|
|
}
|
|
|
|
if (!overrides.prune_unreferenced(referenced))
|
|
return false;
|
|
|
|
config.set_key_value(overrides_key, new ConfigOptionString(serialize_plugin_overrides(overrides)));
|
|
return true;
|
|
}
|
|
|
|
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
|
|
const PluginCapabilityId& id) const
|
|
{
|
|
EffectiveCapabilityConfig result;
|
|
result.id = id;
|
|
result.running_plugin_version = running_plugin_version(id.plugin_key);
|
|
|
|
const auto base = PluginManager::instance().get_config().get_config(id);
|
|
result.has_base_config = base.has_value();
|
|
|
|
if (const auto entry = overrides.find(result.id)) {
|
|
result.has_preset_override = true;
|
|
result.config = entry->config;
|
|
result.stored_plugin_version = entry->plugin_version;
|
|
return result;
|
|
}
|
|
|
|
if (result.has_base_config) {
|
|
result.config = base->config;
|
|
result.stored_plugin_version = base->plugin_version;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
MutationResult PresetPluginConfigService::set_preset_override(CapabilityConfigDocument& overrides,
|
|
const PluginCapabilityId& id,
|
|
const nlohmann::json& value) const
|
|
{
|
|
MutationResult result;
|
|
const std::string version = running_plugin_version(id.plugin_key);
|
|
|
|
// A no-op is a successful unchanged result: re-saving the displayed value must not mark the
|
|
// preset dirty.
|
|
const auto existing = overrides.find(id);
|
|
if (existing && existing->config == value && existing->plugin_version == version) {
|
|
result.ok = true;
|
|
result.effective = get_effective_config(overrides, id);
|
|
return result;
|
|
}
|
|
|
|
overrides.upsert({id, version, value});
|
|
|
|
result.ok = true;
|
|
result.changed = true;
|
|
result.effective = get_effective_config(overrides, id);
|
|
return result;
|
|
}
|
|
|
|
MutationResult PresetPluginConfigService::remove_preset_override(CapabilityConfigDocument& overrides,
|
|
const PluginCapabilityId& id) const
|
|
{
|
|
MutationResult result;
|
|
result.ok = true;
|
|
result.changed = overrides.erase(id);
|
|
result.effective = get_effective_config(overrides, id);
|
|
return result;
|
|
}
|
|
|
|
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityId& id)
|
|
{
|
|
const PresetPluginConfigService service;
|
|
|
|
CapabilityConfigDocument overrides;
|
|
|
|
const PresetBundle* bundle = active_preset_bundle();
|
|
const Preset* preset = nullptr;
|
|
|
|
if (bundle != nullptr) {
|
|
const std::string type_key = plugin_capability_type_to_string(id.type);
|
|
// The edited preset of each type that can hold plugin-backed options, keyed by its option list.
|
|
const std::pair<const std::vector<std::string>*, const Preset*> scopes[] = {
|
|
{&Preset::print_options(), &bundle->prints.get_edited_preset()},
|
|
{&Preset::printer_options(), &bundle->printers.get_edited_preset()},
|
|
{&Preset::filament_options(), &bundle->filaments.get_edited_preset()},
|
|
};
|
|
for (const auto& [key, def] : print_config_def.options) {
|
|
if (def.plugin_type != type_key)
|
|
continue;
|
|
for (const auto& [options, edited] : scopes)
|
|
if (contains(*options, key)) {
|
|
preset = edited;
|
|
break;
|
|
}
|
|
if (preset != nullptr)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (preset != nullptr) {
|
|
const auto* stored = dynamic_cast<const ConfigOptionString*>(preset->config.option(Preset::plugin_overrides_key(preset->type)));
|
|
std::string error;
|
|
if (!parse_plugin_overrides(stored == nullptr ? std::string() : stored->value, overrides, error)) {
|
|
// Text we cannot read is not an override: log it and resolve against the base config.
|
|
BOOST_LOG_TRIVIAL(error) << "Preset \"" << preset->name << "\": " << error;
|
|
overrides = CapabilityConfigDocument();
|
|
}
|
|
}
|
|
|
|
return service.get_effective_config(overrides, id);
|
|
}
|
|
|
|
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability)
|
|
{
|
|
// Shares its resolution with the dialogs, so a capability reads back exactly the config its UI
|
|
// showed as effective. Stored in neither layer: an empty object, indexable unconditionally.
|
|
return active_capability_config(capability_identity(capability, "get_config")).config;
|
|
}
|
|
|
|
std::string capability_get_config_version(const PluginCapabilityInterface& capability)
|
|
{
|
|
// Must resolve through the same layer as get_config(), or a plugin would migrate one layer's
|
|
// config by another's version stamp.
|
|
return active_capability_config(capability_identity(capability, "get_config_version")).stored_plugin_version;
|
|
}
|
|
|
|
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config)
|
|
{
|
|
return PluginManager::instance().get_config().store_capability_config(capability_identity(capability, "save_config"), config);
|
|
}
|
|
|
|
nlohmann::json PluginConfig::capabilities_payload(const std::vector<PluginCapabilityId>& caps)
|
|
{
|
|
nlohmann::json payload = nlohmann::json::array();
|
|
for (const PluginCapabilityId& id : caps) {
|
|
// A capability unloaded since the list was built has nothing to configure.
|
|
const auto capability = PluginManager::instance().get_plugin_capability(id, false);
|
|
if (!capability)
|
|
continue;
|
|
|
|
nlohmann::json entry;
|
|
entry["plugin_key"] = id.plugin_key;
|
|
entry["name"] = id.name;
|
|
entry["type"] = plugin_capability_type_display_name(id.type);
|
|
entry["type_key"] = plugin_capability_type_to_string(id.type);
|
|
entry["has_config_ui"] = capability->config_ui_available();
|
|
payload.push_back(std::move(entry));
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
// 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.
|
|
nlohmann::json PluginConfig::get_config_response(const PluginCapabilityId& id)
|
|
{
|
|
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"] = "";
|
|
|
|
// Scoped to the full identity, so a stale request from a page that has not caught up with a
|
|
// refresh misses rather than reading a different plugin's config.
|
|
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
|
|
if (!cap) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Ignoring config request for a capability that is no longer loaded. plugin_key="
|
|
<< id.plugin_key << " capability_name=" << id.name;
|
|
response["error"] = GUI::into_u8(_L("This capability is no longer available."));
|
|
return response;
|
|
}
|
|
|
|
if (const auto stored = PluginManager::instance().get_config().get_config(id))
|
|
response["config"] = stored->config;
|
|
|
|
if (cap->config_ui_available()) {
|
|
// A raising or empty get_config_ui() costs the capability only its custom UI: report the
|
|
// failure and let the page fall back to the default JSON editor over the same stored config.
|
|
std::string html;
|
|
std::string error;
|
|
{
|
|
wxBusyCursor busy;
|
|
try {
|
|
PythonGILState gil;
|
|
html = cap->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=" << id.plugin_key
|
|
<< " capability_name=" << id.name << " error=" << error;
|
|
response["error"] = GUI::into_u8(GUI::format_wxstr(_L("The plugin's configuration UI failed to load (%1%). Showing the default editor."),
|
|
GUI::from_u8(error)));
|
|
} else if (html.empty()) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Plugin capability reports a config UI but returned no HTML. plugin_key=" << id.plugin_key
|
|
<< " capability_name=" << id.name;
|
|
response["error"] = GUI::into_u8(_L("The plugin's configuration UI was empty. Showing the default editor."));
|
|
} else {
|
|
response["custom_html"] = html;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
nlohmann::json PluginConfig::save_config_response(const PluginCapabilityId& id, const nlohmann::json& config)
|
|
{
|
|
nlohmann::json response;
|
|
response["command"] = "capability_config_saved";
|
|
response["plugin_key"] = id.plugin_key;
|
|
response["capability_name"] = id.name;
|
|
response["capability_type"] = plugin_capability_type_to_string(id.type);
|
|
response["ok"] = false;
|
|
response["error"] = "";
|
|
|
|
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
|
|
if (!cap) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Refusing to save config for a capability that is no longer loaded. plugin_key="
|
|
<< id.plugin_key << " capability_name=" << id.name;
|
|
response["error"] = GUI::into_u8(_L("This capability is no longer available. Your changes were not saved."));
|
|
return response;
|
|
}
|
|
|
|
nlohmann::json parsed = config;
|
|
if (config.is_string()) {
|
|
// The page validates as the user types, but it is not the authority: re-parse 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"] = GUI::into_u8(_L("The configuration is not valid JSON. Your changes were not saved."));
|
|
return response;
|
|
}
|
|
}
|
|
|
|
if (!PluginManager::instance().get_config().store_capability_config(id, parsed)) {
|
|
BOOST_LOG_TRIVIAL(error) << "Failed to write the plugin config file. plugin_key=" << id.plugin_key
|
|
<< " capability_name=" << id.name;
|
|
response["error"] = GUI::into_u8(_L("The configuration could not be written to disk. Your changes were not saved."));
|
|
return response;
|
|
}
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Saved plugin capability config. plugin_key=" << id.plugin_key << " capability_name=" << id.name;
|
|
|
|
// Echo back what was persisted, not what the user typed, so the editor reloads from the store.
|
|
response["ok"] = true;
|
|
if (const auto stored = PluginManager::instance().get_config().get_config(id))
|
|
response["config"] = stored->config;
|
|
return response;
|
|
}
|
|
|
|
// The host never invents the default: a capability that does not override get_default_config()
|
|
// restores an empty config, which is right for one that applies its own defaults on read.
|
|
nlohmann::json PluginConfig::restore_config_response(const PluginCapabilityId& id)
|
|
{
|
|
nlohmann::json response;
|
|
response["command"] = "capability_config_saved";
|
|
response["plugin_key"] = id.plugin_key;
|
|
response["capability_name"] = id.name;
|
|
response["capability_type"] = plugin_capability_type_to_string(id.type);
|
|
response["ok"] = false;
|
|
response["error"] = "";
|
|
|
|
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
|
|
if (!cap) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Refusing to restore config for a capability that is no longer loaded. plugin_key="
|
|
<< id.plugin_key << " capability_name=" << id.name;
|
|
response["error"] = GUI::into_u8(_L("This capability is no longer available."));
|
|
return response;
|
|
}
|
|
|
|
nlohmann::json defaults;
|
|
std::string error;
|
|
{
|
|
wxBusyCursor busy;
|
|
try {
|
|
PythonGILState gil;
|
|
defaults = cap->get_default_config();
|
|
} catch (const std::exception& ex) {
|
|
error = ex.what();
|
|
} catch (...) {
|
|
error = "Unknown error";
|
|
}
|
|
}
|
|
|
|
// A raising hook leaves the stored config 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=" << id.plugin_key
|
|
<< " capability_name=" << id.name << " error=" << error;
|
|
response["error"] = GUI::into_u8(GUI::format_wxstr(_L("The plugin could not supply a default configuration (%1%). "
|
|
"Nothing was changed."),
|
|
GUI::from_u8(error)));
|
|
return response;
|
|
}
|
|
|
|
if (!PluginManager::instance().get_config().store_capability_config(id, defaults)) {
|
|
BOOST_LOG_TRIVIAL(error) << "Failed to write the plugin config file while restoring defaults. plugin_key="
|
|
<< id.plugin_key << " capability_name=" << id.name;
|
|
response["error"] = GUI::into_u8(_L("The configuration could not be written to disk. Nothing was changed."));
|
|
return response;
|
|
}
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Restored default plugin capability config. plugin_key=" << id.plugin_key
|
|
<< " capability_name=" << id.name;
|
|
|
|
response["ok"] = true;
|
|
if (const auto stored = PluginManager::instance().get_config().get_config(id))
|
|
response["config"] = stored->config;
|
|
return response;
|
|
}
|
|
|
|
} // namespace Slic3r
|