mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 01:02:08 +00:00
feat: sidebar plugin config UI
This commit is contained in:
122
src/slic3r/plugin/CapabilityConfigDocument.cpp
Normal file
122
src/slic3r/plugin/CapabilityConfigDocument.cpp
Normal 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
|
||||
57
src/slic3r/plugin/CapabilityConfigDocument.hpp
Normal file
57
src/slic3r/plugin/CapabilityConfigDocument.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
128
src/slic3r/plugin/PresetPluginConfig.cpp
Normal file
128
src/slic3r/plugin/PresetPluginConfig.cpp
Normal 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
|
||||
70
src/slic3r/plugin/PresetPluginConfig.hpp
Normal file
70
src/slic3r/plugin/PresetPluginConfig.hpp
Normal 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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user