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,122 @@
#include "CapabilityConfigDocument.hpp"
namespace Slic3r {
namespace {
constexpr const char* KEY_PLUGIN = "plugin_key";
constexpr const char* KEY_CAPABILITY = "capability";
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, CapabilityConfigId& id)
{
if (!entry.is_object())
return false;
id.plugin_key = string_field(entry, KEY_PLUGIN);
id.capability = string_field(entry, KEY_CAPABILITY);
return !id.plugin_key.empty() && !id.capability.empty();
}
CapabilityConfigEntry decode_entry(const CapabilityConfigId& 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.cap_config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
return result;
}
} // namespace
CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::json& entries)
{
CapabilityConfigDocument document;
if (!entries.is_array())
return document;
for (const nlohmann::json& entry : entries) {
CapabilityConfigId id;
if (is_recognized_entry(entry, id))
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 CapabilityConfigId& id) const
{
const auto it = m_entries.find(id);
if (it == m_entries.end())
return std::nullopt;
return decode_entry(it->first, it->second);
}
bool CapabilityConfigDocument::contains(const CapabilityConfigId& id) const
{
return m_entries.find(id) != m_entries.end();
}
bool CapabilityConfigDocument::upsert(CapabilityConfigEntry entry)
{
if (entry.id.plugin_key.empty() || entry.id.capability.empty())
return false;
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.capability;
serialized[KEY_VERSION] = entry.plugin_version;
serialized[KEY_CAP_CONFIG] = entry.cap_config;
m_entries[entry.id] = std::move(serialized);
return true;
}
bool CapabilityConfigDocument::erase(const CapabilityConfigId& id)
{
return m_entries.erase(id) != 0;
}
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;
}
} // namespace Slic3r

View File

@@ -0,0 +1,57 @@
#pragma once
#include <nlohmann/json.hpp>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r {
struct CapabilityConfigId
{
std::string plugin_key;
std::string capability;
friend bool operator<(const CapabilityConfigId& lhs, const CapabilityConfigId& rhs)
{
return lhs.plugin_key < rhs.plugin_key ||
(lhs.plugin_key == rhs.plugin_key && lhs.capability < rhs.capability);
}
friend bool operator==(const CapabilityConfigId& lhs, const CapabilityConfigId& rhs)
{
return lhs.plugin_key == rhs.plugin_key && lhs.capability == rhs.capability;
}
};
struct CapabilityConfigEntry
{
CapabilityConfigId id;
std::string plugin_version;
nlohmann::json cap_config = nlohmann::json::object();
};
class CapabilityConfigDocument
{
public:
static constexpr const char* KeyEntries = "config";
static CapabilityConfigDocument from_root_json(const nlohmann::json& root);
static CapabilityConfigDocument from_entries(const nlohmann::json& entries);
std::optional<CapabilityConfigEntry> find(const CapabilityConfigId& id) const;
bool contains(const CapabilityConfigId& id) const;
bool upsert(CapabilityConfigEntry entry);
bool erase(const CapabilityConfigId& id);
bool empty() const;
nlohmann::json serialize_entries() const;
nlohmann::json root_json() const;
private:
std::map<CapabilityConfigId, nlohmann::json> m_entries;
std::vector<nlohmann::json> m_opaque_entries;
};
} // namespace Slic3r

View File

@@ -4,54 +4,21 @@
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <slic3r/GUI/GUI.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 <utility>
#include <wx/utils.h>
namespace Slic3r {
namespace {
constexpr const char* KEY_ENTRIES = "config";
constexpr const char* KEY_PLUGIN = "plugin_key";
constexpr const char* KEY_CAPABILITY = "capability";
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();
}
// Rejects entries missing an identity, which could never be looked up again.
bool entry_to_config(const nlohmann::json& entry, BaseConfig& out)
{
if (!entry.is_object())
return false;
out.plugin_key = string_field(entry, KEY_PLUGIN);
out.capability_name = string_field(entry, KEY_CAPABILITY);
out.plugin_version = string_field(entry, KEY_VERSION);
if (out.empty())
return false;
const auto cap_config = entry.find(KEY_CAP_CONFIG);
out.config = cap_config != entry.end() ? *cap_config : nlohmann::json::object();
return true;
}
nlohmann::json config_to_entry(const BaseConfig& config)
{
return nlohmann::json{
{KEY_PLUGIN, config.plugin_key},
{KEY_CAPABILITY, config.capability_name},
{KEY_VERSION, config.plugin_version},
{KEY_CAP_CONFIG, config.config},
};
}
// The version of the plugin package currently running. PluginDescriptor::version is
// overwritten with the latest cloud version when a cloud merge happens, so it can name a
// version that is not the one on disk; installed_version is what actually loaded.
@@ -83,7 +50,7 @@ void PluginConfig::load()
const std::string path = plugin_config_file();
std::lock_guard<std::mutex> lock(m_mutex);
m_storage.clear();
m_document = CapabilityConfigDocument();
m_dirty = false;
boost::system::error_code ec;
@@ -99,20 +66,14 @@ void PluginConfig::load()
return;
}
const auto entries = root.find(KEY_ENTRIES);
const auto entries = root.find(CapabilityConfigDocument::KeyEntries);
if (entries == root.end() || !entries->is_array()) {
BOOST_LOG_TRIVIAL(warning) << "PluginConfig: " << path << " has no \"" << KEY_ENTRIES << "\" array; starting with an empty config";
BOOST_LOG_TRIVIAL(warning) << "PluginConfig: " << path << " has no \"" << CapabilityConfigDocument::KeyEntries
<< "\" array; starting with an empty config";
return;
}
for (const auto& entry : *entries) {
BaseConfig config;
if (!entry_to_config(entry, config)) {
BOOST_LOG_TRIVIAL(warning) << "PluginConfig: skipping entry without a plugin key and capability name";
continue;
}
m_storage[{config.plugin_key, config.capability_name}] = std::move(config);
}
m_document = CapabilityConfigDocument::from_root_json(root);
}
bool PluginConfig::save()
@@ -120,11 +81,10 @@ bool PluginConfig::save()
const std::string path = plugin_config_file();
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_dirty)
return true;
nlohmann::json root;
root[KEY_ENTRIES] = nlohmann::json::array();
for (const auto& [id, config] : m_storage)
root[KEY_ENTRIES].push_back(config_to_entry(config));
const nlohmann::json root = m_document.root_json();
boost::system::error_code ec;
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path(), ec);
@@ -177,21 +137,37 @@ void PluginConfig::save_config(const BaseConfig& config)
}
std::lock_guard<std::mutex> lock(m_mutex);
m_storage[{config.plugin_key, config.capability_name}] = config;
m_dirty = true;
m_dirty = m_document.upsert(CapabilityConfigEntry{{config.plugin_key, config.capability_name}, config.plugin_version, config.config}) || m_dirty;
}
bool PluginConfig::erase_capability_config(const std::string& plugin_key, const std::string& capability_name)
{
if (plugin_key.empty() || capability_name.empty())
return false;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_document.erase({plugin_key, capability_name}))
return true;
m_dirty = true;
}
return save();
}
BaseConfig PluginConfig::get_config(const std::string& plugin_key, const std::string& capability_name) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_storage.find({plugin_key, capability_name});
return it != m_storage.end() ? it->second : BaseConfig();
const auto entry = m_document.find({plugin_key, capability_name});
if (!entry)
return BaseConfig();
return BaseConfig{entry->id.plugin_key, entry->id.capability, entry->plugin_version, entry->cap_config};
}
bool PluginConfig::has_config(const std::string& plugin_key, const std::string& capability_name) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_storage.count({plugin_key, capability_name}) != 0;
return m_document.contains({plugin_key, capability_name});
}
bool PluginConfig::dirty() const
@@ -223,4 +199,194 @@ bool capability_save_config(const PluginCapabilityInterface& capability, const n
return PluginManager::instance().get_config().store_capability_config(plugin_key, capability_name, config);
}
nlohmann::json PluginConfig::capabilities_payload(const std::vector<PluginCapabilityIdentifier>& caps)
{
PluginLoader& loader = PluginManager::instance().get_loader();
nlohmann::json payload = nlohmann::json::array();
for (const PluginCapabilityIdentifier& id : caps) {
// Read has_config_ui off the live capability rather than trusting the caller's copy: a
// capability that has been unloaded since the list was built has nothing to configure.
const auto capability = loader.get_plugin_capability_by_name(id);
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->has_config_ui;
payload.push_back(std::move(entry));
}
return payload;
}
// Replies 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.
nlohmann::json PluginConfig::get_config_response(const PluginCapabilityIdentifier& 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 cannot read a different plugin's config — it just misses.
const auto cap = PluginManager::instance().get_loader().get_plugin_capability_by_name(id);
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;
}
response["config"] = PluginManager::instance().get_config().get_config(id.plugin_key, id.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=" << 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 PluginCapabilityIdentifier& 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_loader().get_plugin_capability_by_name(id);
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 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"] = 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.plugin_key, id.name, 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 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(id.plugin_key, id.name).config;
return response;
}
// The host does not invent the default: a capability that does not override get_default_config()
// restores an empty config, which is exactly right for one that applies its own defaults on read.
nlohmann::json PluginConfig::restore_config_response(const PluginCapabilityIdentifier& 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_loader().get_plugin_capability_by_name(id);
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->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=" << 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.plugin_key, id.name, 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;
// Reuses the saved reply, so both editors reload from what was actually persisted.
response["ok"] = true;
response["config"] = PluginManager::instance().get_config().get_config(id.plugin_key, id.name).config;
return response;
}
} // namespace Slic3r

View File

@@ -3,11 +3,14 @@
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <slic3r/plugin/CapabilityConfigDocument.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#define PLUGIN_CONFIG_DIR "config.json"
@@ -15,6 +18,12 @@ namespace Slic3r {
class PluginCapabilityInterface;
enum class PluginConfigSource {
None,
Base,
Preset,
};
/*
Example config.json shape
{
@@ -95,6 +104,7 @@ public:
// untouched, so saving one capability cannot disturb another's config.
// The single mutation entry point for both the Plugins dialog and the Python binding.
bool store_capability_config(const std::string& plugin_key, const std::string& capability_name, const nlohmann::json& config);
bool erase_capability_config(const std::string& plugin_key, const std::string& capability_name);
// Returns a default-constructed BaseConfig (see BaseConfig::empty) when the capability has
// no stored config.
@@ -103,12 +113,35 @@ public:
bool dirty() const;
private:
// (plugin_key, capability_name) -> entry. Ordered, so config.json serializes stably.
using CapabilityId = std::pair<std::string, std::string>;
// ---- Webview-facing helpers, shared by PluginsDialog's Config tab and PluginsConfigDialog ----
//
// These build the payloads both dialogs' config views speak, so the two pages stay in step and
// neither dialog owns the config protocol. They are static because a capability's config is
// addressed globally by (plugin_key, capability_name) through PluginManager's store, not through
// any one PluginConfig instance.
//
// The caller owns the UI: it confirms destructive restores and shows status toasts. These only
// touch the store, the loaded capability, and the payload.
// The config sidebar's rows: one entry per capability, in the order given. Capabilities that are
// no longer loaded are skipped — the sidebar only offers what can actually be configured.
static nlohmann::json capabilities_payload(const std::vector<PluginCapabilityIdentifier>& caps);
// One capability's stored config, plus its custom HTML UI when it provides one.
static nlohmann::json get_config_response(const PluginCapabilityIdentifier& id);
// Persists one capability's config. `config` is either text straight from the default editor
// (re-parsed here, so malformed JSON can never reach config.json) or a structured value from a
// custom UI.
static nlohmann::json save_config_response(const PluginCapabilityIdentifier& id, const nlohmann::json& config);
// Overwrites one capability's stored config with its get_default_config(). The caller must have
// confirmed with the user first — this does not ask.
static nlohmann::json restore_config_response(const PluginCapabilityIdentifier& id);
private:
mutable std::mutex m_mutex;
std::map<CapabilityId, BaseConfig> m_storage;
CapabilityConfigDocument m_document;
bool m_dirty = false;
};

View File

@@ -12,6 +12,7 @@
#include <boost/log/trivial.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <vector>
#include <wx/utils.h>
@@ -20,6 +21,7 @@
#include <map>
#include <mutex>
#include <thread>
#include <tuple>
#include <unordered_map>
namespace Slic3r {
@@ -92,6 +94,96 @@ static bool is_tracked_type(Preset::Type type)
return type == Preset::TYPE_PRINT || type == Preset::TYPE_PRINTER || type == Preset::TYPE_FILAMENT;
}
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset)
{
if (!is_tracked_type(type))
return {};
const auto* manifest = dynamic_cast<const ConfigOptionStrings*>(preset.config.option("plugins"));
if (manifest == nullptr)
return {};
std::vector<PluginCapabilityRef> refs;
for (const std::string& entry : manifest->values) {
const auto ref = parse_capability_ref(entry);
if (!ref)
continue;
// The same test the missing-plugin path uses: an entry no option points at is not in use.
if (find_option_for_capability(type, preset, *ref).empty())
continue;
refs.push_back(*ref);
}
return refs;
}
namespace {
// Resolve one preset's referenced capabilities to loaded capability identifiers, appending to `out`.
// A ref whose plugin is not in the catalog, or whose capability is not currently loaded, is dropped:
// there is no instance to ask for a config UI or defaults, so there is nothing to configure.
void collect_capabilities_in_use(Preset::Type type, const Preset& preset, std::vector<PluginCapabilityIdentifier>& out)
{
PluginLoader& loader = PluginManager::instance().get_loader();
const PluginCatalog& catalog = PluginManager::instance().get_catalog();
for (const PluginCapabilityRef& ref : referenced_capabilities(type, preset)) {
// Cloud plugins resolve by UUID, local plugins by plugin_key — the rule refresh_missing_plugins uses.
const std::string key = ref.uuid.empty() ? ref.name : ref.uuid;
if (key.empty())
continue;
PluginDescriptor descriptor;
if (!catalog.try_get_plugin_descriptor(key, descriptor))
continue;
// The loaded capability is also what supplies the type: the manifest ref does not carry one.
for (const auto& capability : loader.get_loaded_plugin_capabilities(descriptor.plugin_key))
if (capability && capability->name == ref.capability_name)
out.push_back(PluginCapabilityIdentifier{capability->type, capability->name, capability->plugin_key});
}
}
} // namespace
std::vector<PluginCapabilityIdentifier> capabilities_in_use(const PresetBundle& preset_bundle, Preset::Type type)
{
if (!is_tracked_type(type))
return {};
std::vector<PluginCapabilityIdentifier> result;
if (type == Preset::TYPE_PRINT) {
collect_capabilities_in_use(type, preset_bundle.prints.get_edited_preset(), result);
} else if (type == Preset::TYPE_PRINTER) {
collect_capabilities_in_use(type, preset_bundle.printers.get_edited_preset(), result);
} else {
// Filament: every selected filament preset, tested against its own config. (Note that
// refresh_missing_plugins cannot do this — it unions the manifests and loses the preset.)
for (const std::string& filament_name : preset_bundle.filament_presets)
if (const Preset* filament = preset_bundle.filaments.find_preset(filament_name))
collect_capabilities_in_use(type, *filament, result);
}
std::sort(result.begin(), result.end(), [](const PluginCapabilityIdentifier& a, const PluginCapabilityIdentifier& b) {
return std::tie(a.plugin_key, a.name, a.type) < std::tie(b.plugin_key, b.name, b.type);
});
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
std::vector<PluginCapabilityIdentifier> capabilities_in_use(Preset::Type type, const Preset& preset)
{
std::vector<PluginCapabilityIdentifier> result;
if (!is_tracked_type(type))
return result;
collect_capabilities_in_use(type, preset, result);
std::sort(result.begin(), result.end(), [](const PluginCapabilityIdentifier& a, const PluginCapabilityIdentifier& b) {
return std::tie(a.plugin_key, a.name, a.type) < std::tie(b.plugin_key, b.name, b.type);
});
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
static std::string resolve_cloud_base_url()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";

View File

@@ -4,6 +4,7 @@
#include <libslic3r/Config.hpp> // PluginCapabilityRef, parse_capability_ref
#include <libslic3r/Preset.hpp> // Preset::Type
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PluginLoader.hpp> // PluginCapabilityIdentifier
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityType
#include <cstddef>
#include <functional>
@@ -81,6 +82,21 @@ void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs);
std::string create_full_ref(const PluginCapabilityRef& ref);
std::string resolve_recovery_url(const PluginCapabilityRef& ref);
// The capabilities `preset`'s "plugins" manifest declares AND that one of its plugin-backed options
// (ConfigOptionDef::is_plugin_backed) currently references. A manifest entry nobody points at is not
// in use. Pure preset logic — the plugin catalog and loader are not consulted. Empty for untracked
// preset types.
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset);
std::vector<PluginCapabilityIdentifier> capabilities_in_use(Preset::Type type, const Preset& preset);
// The capabilities the active preset(s) of `type` reference (see referenced_capabilities) and that
// are loaded right now: the set that can actually be configured. Missing and broken refs are absent,
// having no instance to ask for a config UI or defaults. A loaded-but-disabled capability IS listed —
// it still has stored config worth editing, and disabling it is not a reason to hide that.
// TYPE_FILAMENT unions every selected filament preset; a capability used by two extruders is listed
// once. Deduped on the full identity, so two plugins exposing a same-named capability stay distinct.
std::vector<PluginCapabilityIdentifier> capabilities_in_use(const PresetBundle& preset_bundle, Preset::Type type);
bool check_capability_in_use(const std::string& capability_refs);
} // namespace Slic3r

View File

@@ -0,0 +1,128 @@
#include "PresetPluginConfig.hpp"
#include "PluginManager.hpp"
namespace Slic3r {
namespace {
CapabilityConfigId make_id(const PluginCapabilityIdentifier& id)
{
return CapabilityConfigId{id.plugin_key, id.name};
}
std::string running_plugin_version(const std::string& plugin_key)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().get_catalog().try_get_valid_plugin_descriptor(plugin_key, descriptor))
return {};
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
}
} // namespace
std::string plugin_overrides_of(const Preset& preset)
{
const auto* opt = dynamic_cast<const ConfigOptionString*>(preset.config.option(PLUGIN_OVERRIDES_OPTION_KEY));
return opt == nullptr ? std::string() : opt->value;
}
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();
}
std::string plugin_config_source_to_string(PluginConfigSource source)
{
switch (source) {
case PluginConfigSource::Preset: return "preset";
case PluginConfigSource::Base: return "base";
default: return "none";
}
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const
{
EffectiveCapabilityConfig result;
result.id = make_id(id);
result.running_plugin_version = running_plugin_version(id.plugin_key);
const BaseConfig base = PluginManager::instance().get_config().get_config(id.plugin_key, id.name);
result.has_base_config = !base.empty();
if (const auto entry = overrides.find(result.id)) {
result.has_preset_override = true;
result.source = PluginConfigSource::Preset;
result.config = entry->cap_config;
result.stored_plugin_version = entry->plugin_version;
return result;
}
if (result.has_base_config) {
result.source = PluginConfigSource::Base;
result.config = base.config;
result.stored_plugin_version = base.plugin_version;
}
return result;
}
MutationResult PresetPluginConfigService::set_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id,
const nlohmann::json& value) const
{
MutationResult result;
const CapabilityConfigId config_id = make_id(id);
const std::string version = running_plugin_version(id.plugin_key);
// A no-op is a successful unchanged result, not a reason to rewrite the preset: re-saving the
// displayed value must not be able to mark it dirty.
const auto existing = overrides.find(config_id);
if (existing && existing->cap_config == value && existing->plugin_version == version) {
result.ok = true;
result.effective = get_effective_config(overrides, id);
return result;
}
overrides.upsert({config_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 PluginCapabilityIdentifier& id) const
{
MutationResult result;
result.ok = true;
result.changed = overrides.erase(make_id(id));
// Reads back as the base value now that the override is gone.
result.effective = get_effective_config(overrides, id);
return result;
}
} // namespace Slic3r

View File

@@ -0,0 +1,70 @@
#pragma once
#include "CapabilityConfigDocument.hpp"
#include "PluginConfig.hpp"
#include <libslic3r/Preset.hpp>
#include <nlohmann/json.hpp>
#include <string>
namespace Slic3r {
// A preset keeps its plugin capability overrides as one raw JSON string in this ordinary
// ConfigOptionString, so the whole preset lifecycle — load, save, diff/dirty, inheritance, 3MF,
// sync — carries it for free. The plugin layer is the only thing that gives that string meaning.
inline constexpr const char* PLUGIN_OVERRIDES_OPTION_KEY = "plugin_preference_overrides";
// The preset's raw override text, or "" when it stores none.
std::string plugin_overrides_of(const Preset& preset);
// An empty string is a valid, empty document. Returns false and fills `error` when the text is
// present but is not a JSON array of entries; the caller then shows it and edits nothing.
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
// The document as compact JSON text, and "" once it holds no entries. Empty text — rather than a
// removed option — is what records "cleared here" against an inheriting parent that has overrides.
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
struct EffectiveCapabilityConfig
{
CapabilityConfigId id;
nlohmann::json config = nlohmann::json::object();
PluginConfigSource source = PluginConfigSource::None;
bool has_preset_override = false;
bool has_base_config = false;
std::string stored_plugin_version;
std::string running_plugin_version;
};
struct MutationResult
{
bool ok = false;
bool changed = false;
std::string error;
EffectiveCapabilityConfig effective;
};
std::string plugin_config_source_to_string(PluginConfigSource source);
// Resolves a capability's effective config as `preset override -> base config -> none`, and mutates
// the override layer.
//
// It works on a CapabilityConfigDocument the caller owns, never on a Preset and never on the base
// config file. That is what keeps the two layers from writing to each other: PluginConfigField holds
// the document, and feeds the edited text back through the normal field/dirty pipeline, so the
// preset is written exactly the way every other setting is.
class PresetPluginConfigService
{
public:
EffectiveCapabilityConfig get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const;
MutationResult set_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id,
const nlohmann::json& value) const;
MutationResult remove_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const;
};
} // namespace Slic3r

View File

@@ -1,9 +1,11 @@
#include "PythonPluginBridge.hpp"
#include <boost/log/trivial.hpp>
#include <exception>
#include <memory>
#include <mutex>
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <string>
#include <unordered_map>
#include <pybind11/embed.h>
@@ -343,33 +345,41 @@ void bind_python_api(pybind11::module_& m)
"get_default_config",
[](const PluginCapabilityInterface& self) {
nlohmann::json config = self.get_default_config();
return json_to_py(config); // GIL held (binding body)
return config.dump();
},
"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.")
"back, as a dict. Optional: without it the action stores an empty config, which already\n"
"restores the defaults of a capability that keeps its stored config sparse and applies\n"
"its own defaults on read. Override it to write an explicit starting config instead.\n"
"Calling it returns that config as a JSON string.")
.def(
"get_config",
[](const PluginCapabilityInterface& self) {
nlohmann::json config = capability_get_config(self);
return json_to_py(config); // GIL held (binding body)
return config.dump();
},
"Return this capability's stored config. An empty dict if it has never been saved.")
"Return this capability's stored config as a JSON string — json.loads() it to use.\n"
"\"{}\" if it has never been saved, so the parsed result is always indexable.")
.def(
"get_config_version",
[](const PluginCapabilityInterface& self) { return capability_get_config_version(self); },
"get_config_version", [](const PluginCapabilityInterface& self) { return capability_get_config_version(self); },
"Return the plugin version that last wrote this capability's config, so a newer\n"
"release can spot a stale config and migrate it. Empty string if never saved.")
.def(
"save_config",
[](const PluginCapabilityInterface& self, const py::object& config) {
return capability_save_config(self, py_to_json(config)); // GIL held (binding body)
[](const PluginCapabilityInterface& self, const std::string& config_str) {
nlohmann::json config = nlohmann::json::parse(config_str, nullptr, /* allow_exceptions */ false);
if (config.is_discarded()) {
// Refused rather than stored: the caller gets False, and the previously stored
// config is left alone. Logged because False alone does not say why.
BOOST_LOG_TRIVIAL(error) << "save_config: capability '" << self.get_name() << "' passed a config that is not valid JSON";
return false;
}
return capability_save_config(self, config);
},
py::arg("config"),
"Persist this capability's config, given as a JSON-compatible value (usually a dict).\n"
"Persist this capability's config, given as a JSON string (e.g. json.dumps(cfg)).\n"
"The plugin key, capability name and version are supplied by the host. Returns False if\n"
"the config file could not be written.");
"the string is not valid JSON, or if the config file could not be written.");
// Expose the package marker base as orca.base. @orca.plugin later verifies that the
// decorated class derives from this exact pybind-registered C++ type.