mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 02:57:39 +00:00
Merge branch 'feat/plugin-feature' into feature/speed-dial
Adapt the speed dial's ActionRegistry to the collapsed get_plugin_capability(PluginCapabilityId) overload, and restore the script success/skipped status message the dialog lost when its PluginScriptRunner refactor was superseded by ActionRegistry.
This commit is contained in:
@@ -122,6 +122,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/SpeedDialDialog.hpp
|
||||
GUI/ActionRegistry.cpp
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/PluginsConfigDialog.cpp
|
||||
GUI/PluginsConfigDialog.hpp
|
||||
GUI/ProcessRunner.cpp
|
||||
GUI/ProcessRunner.hpp
|
||||
GUI/TerminalDialog.cpp
|
||||
@@ -621,6 +623,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
plugin/PluginFsUtils.hpp
|
||||
plugin/PluginConfig.cpp
|
||||
plugin/PluginConfig.hpp
|
||||
plugin/PluginLoader.cpp
|
||||
plugin/PluginLoader.hpp
|
||||
plugin/PluginDescriptor.hpp
|
||||
|
||||
@@ -114,7 +114,7 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
|
||||
if (!manager.is_plugin_loaded(plugin_key))
|
||||
return nullptr;
|
||||
// only_enabled defaults true, so a disabled capability resolves to nullptr here.
|
||||
if (!manager.get_plugin_capability(plugin_key, capability, PluginCapabilityType::Script))
|
||||
if (!manager.get_plugin_capability({PluginCapabilityType::Script, capability, plugin_key}))
|
||||
return nullptr;
|
||||
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "OG_CustomCtrl.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "BitmapComboBox.hpp"
|
||||
#include "PluginsConfigDialog.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
|
||||
// BBS
|
||||
#include "Notebook.hpp"
|
||||
@@ -1995,8 +1997,6 @@ void Choice::msw_rescale()
|
||||
|
||||
void PluginField::BUILD()
|
||||
{
|
||||
// Wrap the dynamic rows in a single container panel so the field is a proper window-field
|
||||
// (getWindow() != null). The panel owns m_main_sizer; all row controls are children of it.
|
||||
auto* panel = new wxPanel(m_parent, wxID_ANY);
|
||||
wxGetApp().UpdateDarkUI(panel);
|
||||
window = panel;
|
||||
@@ -2004,7 +2004,6 @@ void PluginField::BUILD()
|
||||
m_main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
panel->SetSizer(m_main_sizer);
|
||||
|
||||
// Initialize with default values or empty
|
||||
if (m_opt.type == coStrings) {
|
||||
const ConfigOptionStrings* vec = m_opt.get_default_value<ConfigOptionStrings>();
|
||||
if (vec != nullptr && !vec->values.empty()) {
|
||||
@@ -2095,13 +2094,11 @@ void PluginField::add_plugin_row(const wxString& value, bool is_last)
|
||||
const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1);
|
||||
auto row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// Select button with search icon
|
||||
ScalableButton* select_btn = new ScalableButton(window, wxID_ANY, "search", wxEmptyString,
|
||||
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
|
||||
wxGetApp().UpdateDarkUI(select_btn);
|
||||
select_btn->SetToolTip(_L("Select plugin"));
|
||||
|
||||
// Display text control
|
||||
wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, value,
|
||||
wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord),
|
||||
wxTE_READONLY);
|
||||
@@ -2117,7 +2114,6 @@ void PluginField::add_plugin_row(const wxString& value, bool is_last)
|
||||
remove_btn->SetToolTip(_L("Remove plugin"));
|
||||
}
|
||||
|
||||
// Add button (only on last row)
|
||||
ScalableButton* add_btn = nullptr;
|
||||
if (is_last && !m_opt.readonly) {
|
||||
add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString,
|
||||
@@ -2238,7 +2234,6 @@ void PluginField::set_value(const boost::any& value, bool change_event)
|
||||
{
|
||||
m_disable_change_event = !change_event;
|
||||
|
||||
// Handle different input types
|
||||
if (value.empty()) {
|
||||
m_values.clear();
|
||||
} else if (value.type() == typeid(std::vector<std::string>)) {
|
||||
@@ -2309,6 +2304,126 @@ void PluginField::msw_rescale()
|
||||
rebuild_ui();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The stored text as a document, or an empty array when it is absent or unparseable.
|
||||
nlohmann::json plugin_overrides_as_json(const std::string& text)
|
||||
{
|
||||
if (text.empty())
|
||||
return nlohmann::json::array();
|
||||
return nlohmann::json::parse(text, nullptr, /* allow_exceptions */ false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginConfigField::BUILD()
|
||||
{
|
||||
m_button = new ::Button(m_parent, _L("Configure"));
|
||||
// ButtonType::Parameter gives the button the same height as the parameter fields above it.
|
||||
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
|
||||
|
||||
wxSize size(def_width_wider() * m_em_unit, m_button->GetMinSize().GetHeight());
|
||||
if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit);
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
|
||||
m_button->SetMinSize(size);
|
||||
m_button->SetSize(size);
|
||||
|
||||
m_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { open_dialog(); });
|
||||
m_button->SetToolTip(get_tooltip_text(_L("Configure")));
|
||||
|
||||
window = m_button;
|
||||
|
||||
if (const ConfigOptionString* def = m_opt.get_default_value<ConfigOptionString>())
|
||||
m_json = def->value;
|
||||
|
||||
update_button_label();
|
||||
m_value = m_json;
|
||||
}
|
||||
|
||||
void PluginConfigField::update_button_label()
|
||||
{
|
||||
if (m_button == nullptr)
|
||||
return;
|
||||
|
||||
const nlohmann::json entries = plugin_overrides_as_json(m_json);
|
||||
const size_t count = entries.is_array() ? entries.size() : 0;
|
||||
|
||||
m_button->SetLabel(count == 0 ? _L("Configure")
|
||||
: wxString::Format(_L("Configure (%d)"), int(count)));
|
||||
}
|
||||
|
||||
void PluginConfigField::open_dialog()
|
||||
{
|
||||
const Preset::Type type = static_cast<Preset::Type>(m_preset_type);
|
||||
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_FILAMENT && type != Preset::TYPE_PRINTER)
|
||||
return;
|
||||
|
||||
std::string edited;
|
||||
{
|
||||
PluginsConfigDialog dlg(m_button, type, m_json);
|
||||
dlg.ShowModal();
|
||||
edited = dlg.overrides_json();
|
||||
}
|
||||
|
||||
// Compare semantically: a round-trip through the serializer can reorder keys or drop whitespace,
|
||||
// and that alone must not dirty the preset.
|
||||
if (plugin_overrides_as_json(m_json) == plugin_overrides_as_json(edited))
|
||||
return;
|
||||
|
||||
m_json = edited;
|
||||
m_value = m_json;
|
||||
update_button_label();
|
||||
on_change_field();
|
||||
}
|
||||
|
||||
void PluginConfigField::set_value(const boost::any& value, bool change_event)
|
||||
{
|
||||
m_disable_change_event = !change_event;
|
||||
|
||||
if (value.type() == typeid(wxString))
|
||||
m_json = into_u8(boost::any_cast<wxString>(value));
|
||||
else if (value.type() == typeid(std::string))
|
||||
m_json = boost::any_cast<std::string>(value);
|
||||
|
||||
m_value = m_json;
|
||||
update_button_label();
|
||||
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
|
||||
boost::any& PluginConfigField::get_value()
|
||||
{
|
||||
// std::string, not wxString: change_opt_value any_casts a coString to std::string.
|
||||
m_value = m_json;
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void PluginConfigField::enable()
|
||||
{
|
||||
if (m_button)
|
||||
m_button->Enable();
|
||||
}
|
||||
|
||||
void PluginConfigField::disable()
|
||||
{
|
||||
if (m_button)
|
||||
m_button->Disable();
|
||||
}
|
||||
|
||||
void PluginConfigField::msw_rescale()
|
||||
{
|
||||
Field::msw_rescale();
|
||||
if (m_button == nullptr)
|
||||
return;
|
||||
|
||||
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
|
||||
|
||||
wxSize size(def_width_wider() * m_em_unit, m_button->GetMinSize().GetHeight());
|
||||
if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit);
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
|
||||
m_button->SetMinSize(size);
|
||||
}
|
||||
|
||||
void ColourPicker::BUILD()
|
||||
{
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
#define wxMSW false
|
||||
#endif
|
||||
|
||||
// Orca's styled button (Widgets/Button.hpp), used by PluginConfigField. It lives at global scope.
|
||||
class Button;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class Field;
|
||||
@@ -483,9 +486,8 @@ public:
|
||||
void enable() override;
|
||||
void disable() override;
|
||||
|
||||
// Window-field: the dynamic rows live inside a single container panel (assigned to the base
|
||||
// `window` in BUILD), so the field exposes one window instead of a bare sizer. This lets focus/
|
||||
// scroll, full-width sizing and teardown operate on the whole field.
|
||||
// The rows live in one container panel (the base `window`), so the field exposes a window instead
|
||||
// of a bare sizer and focus, sizing and teardown apply to the whole field.
|
||||
wxWindow* getWindow() override { return window; }
|
||||
|
||||
void msw_rescale() override;
|
||||
@@ -517,6 +519,45 @@ private:
|
||||
std::function<std::string()> m_selector;
|
||||
};
|
||||
|
||||
// A settings row whose value is a raw JSON document nobody types by hand: the button opens
|
||||
// PluginsConfigDialog and the document it hands back becomes the field's value. The edit goes through
|
||||
// the ordinary Field value/on_change_field path, so the row gets the same dirty state and revert arrow
|
||||
// as any other setting — the dialog never touches the preset.
|
||||
class PluginConfigField : public Field {
|
||||
using Field::Field;
|
||||
public:
|
||||
PluginConfigField(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
|
||||
PluginConfigField(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
|
||||
~PluginConfigField() {}
|
||||
|
||||
void BUILD() override;
|
||||
|
||||
// Which preset's capabilities the dialog lists; set by the option group. An int for the same
|
||||
// reason OptionsGroup::m_config_type is one: it keeps Preset.hpp out of this header.
|
||||
void set_preset_type(int type) { m_preset_type = type; }
|
||||
|
||||
void set_value(const boost::any& value, bool change_event = false) override;
|
||||
boost::any& get_value() override;
|
||||
|
||||
void enable() override;
|
||||
void disable() override;
|
||||
|
||||
// The button is the whole field, so it is the window the option group sizes and positions (the
|
||||
// ColourPicker idiom). A container panel would be sized but never laid out, collapsing the row.
|
||||
wxWindow* getWindow() override { return window; }
|
||||
|
||||
void msw_rescale() override;
|
||||
|
||||
private:
|
||||
void open_dialog();
|
||||
void update_button_label();
|
||||
|
||||
wxWindow* window { nullptr }; // == m_button; the base class hands this to the option group
|
||||
::Button* m_button { nullptr };
|
||||
std::string m_json; // the option's raw text; "" when the preset overrides nothing
|
||||
int m_preset_type { -1 };
|
||||
};
|
||||
|
||||
class ColourPicker : public Field {
|
||||
using Field::Field;
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co
|
||||
break;
|
||||
case ConfigOptionDef::GUIType::one_string: m_fields.emplace(id, TextCtrl::Create<TextCtrl>(this->ctrl_parent(), opt, id)); break;
|
||||
case ConfigOptionDef::GUIType::plugin_picker: m_fields.emplace(id, PluginField::Create<PluginField>(this->ctrl_parent(), opt, id)); break;
|
||||
case ConfigOptionDef::GUIType::plugin_config: m_fields.emplace(id, PluginConfigField::Create<PluginConfigField>(this->ctrl_parent(), opt, id)); break;
|
||||
default:
|
||||
switch (opt.type) {
|
||||
case coFloatOrPercent:
|
||||
@@ -126,6 +127,12 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co
|
||||
});
|
||||
}
|
||||
|
||||
// The dialog behind the button edits one preset's overrides, so it has to know which preset. Set
|
||||
// here because fields are built lazily on activate() — too late for the Tab to reach in afterwards.
|
||||
if (auto plugin_config_field = dynamic_cast<PluginConfigField*>(field.get()))
|
||||
if (auto config_group = dynamic_cast<ConfigOptionsGroup*>(this))
|
||||
plugin_config_field->set_preset_type(config_group->config_type());
|
||||
|
||||
// assign function objects for callbacks, etc.
|
||||
return field;
|
||||
}
|
||||
@@ -714,9 +721,8 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt)
|
||||
if (selection.plugin_key.empty() || selection.name.empty())
|
||||
return {};
|
||||
|
||||
// Validate that the selected capability's plugin is resolvable, but only the bare capability
|
||||
// name is stored in the option; the full "name;uuid;capability" reference is derived lazily
|
||||
// when the preset is serialized (see ConfigBase::save_plugin_collection).
|
||||
// Only the bare capability name is stored; the full "name;uuid;capability" reference is derived when
|
||||
// the preset is serialized (see ConfigBase::save_plugin_collection). Resolve here to reject bad picks.
|
||||
Slic3r::PluginDescriptor descriptor;
|
||||
if (!manager.try_get_plugin_descriptor(selection.plugin_key, descriptor) || descriptor.name.empty())
|
||||
return {};
|
||||
|
||||
221
src/slic3r/GUI/PluginsConfigDialog.cpp
Normal file
221
src/slic3r/GUI/PluginsConfigDialog.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include "PluginsConfigDialog.hpp"
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
|
||||
#include <libslic3r/Preset.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <slic3r/plugin/PluginConfig.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/PluginResolver.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
wxString preset_type_title(Preset::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Preset::TYPE_PRINT: return _L("Process plugins");
|
||||
case Preset::TYPE_FILAMENT: return _L("Filament plugins");
|
||||
case Preset::TYPE_PRINTER: return _L("Printer plugins");
|
||||
default: return _L("Plugins");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PluginsConfigDialog::PluginsConfigDialog(wxWindow* parent, Preset::Type type, const std::string& overrides_json)
|
||||
: WebViewHostDialog(parent, wxID_ANY, preset_type_title(type))
|
||||
, m_type(type)
|
||||
{
|
||||
// On failure the document stays empty and every row goes read-only (see m_parse_error), so a preset
|
||||
// we cannot understand is never silently overwritten.
|
||||
if (!parse_plugin_overrides(overrides_json, m_overrides, m_parse_error))
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugins Config dialog: " << m_parse_error;
|
||||
|
||||
create_webview("web/dialog/PluginsConfigDialog/index.html", preset_type_title(type), wxSize(820, 660),
|
||||
wxSize(640, 520));
|
||||
}
|
||||
|
||||
PluginsConfigDialog::~PluginsConfigDialog() { m_alive->store(false, std::memory_order_release); }
|
||||
|
||||
const Preset* PluginsConfigDialog::current_preset() const
|
||||
{
|
||||
const PresetBundle* bundle = wxGetApp().preset_bundle;
|
||||
if (bundle == nullptr)
|
||||
return nullptr;
|
||||
|
||||
switch (m_type) {
|
||||
case Preset::TYPE_PRINT: return &bundle->prints.get_edited_preset();
|
||||
case Preset::TYPE_PRINTER: return &bundle->printers.get_edited_preset();
|
||||
case Preset::TYPE_FILAMENT: return &bundle->filaments.get_edited_preset();
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
PluginCapabilityId PluginsConfigDialog::identifier_from(const nlohmann::json& payload) const
|
||||
{
|
||||
return {plugin_capability_type_from_string(payload.value("capability_type", "")),
|
||||
payload.value("capability_name", ""),
|
||||
payload.value("plugin_key", "")};
|
||||
}
|
||||
|
||||
void PluginsConfigDialog::on_script_message(const nlohmann::json& payload)
|
||||
{
|
||||
if (handle_common_script_command(payload))
|
||||
return;
|
||||
|
||||
// Defer command handling out of the webview script-message callback, exactly as PluginsDialog
|
||||
// does: GTK and macOS deliver it synchronously inside the native webview callback, and window
|
||||
// work on that stack is the crash class fixed in b779a7bfed/f2ccbfc8b5. remove_preset_override
|
||||
// puts a modal message box on that stack, which is the same bug.
|
||||
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
|
||||
if (alive->load(std::memory_order_acquire))
|
||||
handle_web_command(payload);
|
||||
});
|
||||
}
|
||||
|
||||
void PluginsConfigDialog::handle_web_command(const nlohmann::json& payload)
|
||||
{
|
||||
const std::string command = payload.value("command", "");
|
||||
if (command == "request_capabilities") {
|
||||
send_capabilities();
|
||||
return;
|
||||
}
|
||||
|
||||
const PluginCapabilityId id = identifier_from(payload);
|
||||
|
||||
if (command == "get_capability_config") {
|
||||
send_capability_config(id);
|
||||
} else if (command == "save_capability_config") {
|
||||
if (!m_parse_error.empty()) {
|
||||
send_save_error(id, m_parse_error);
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json value = payload.contains("config") ? payload.at("config") : nlohmann::json::object();
|
||||
if (value.is_string()) {
|
||||
value = nlohmann::json::parse(value.get<std::string>(), nullptr, /* allow_exceptions */ false);
|
||||
if (value.is_discarded()) {
|
||||
send_save_error(id, into_u8(_L("The configuration is not valid JSON. Your changes were not saved.")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const MutationResult result = m_service.set_preset_override(m_overrides, id, value);
|
||||
if (!result.ok) {
|
||||
send_save_error(id, result.error);
|
||||
return;
|
||||
}
|
||||
send_capability_config(id);
|
||||
show_status(_L("Configuration updated. Save the preset to persist it."), "success");
|
||||
} else if (command == "remove_preset_override") {
|
||||
// "Restore defaults" for a preset means holding no override at all: the capability falls back
|
||||
// to the global configuration, not to the plugin's own get_default_config().
|
||||
if (!m_parse_error.empty()) {
|
||||
send_save_error(id, m_parse_error);
|
||||
return;
|
||||
}
|
||||
|
||||
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
|
||||
"This discards the preset's override and uses the global configuration."),
|
||||
from_u8(id.name)),
|
||||
_L("Restore defaults"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
|
||||
if (rc != wxYES)
|
||||
return;
|
||||
|
||||
const MutationResult result = m_service.remove_preset_override(m_overrides, id);
|
||||
if (!result.ok) {
|
||||
send_save_error(id, result.error);
|
||||
return;
|
||||
}
|
||||
send_capability_config(id);
|
||||
show_status(_L("Using global configuration. Save the preset to persist it."), "success");
|
||||
}
|
||||
}
|
||||
|
||||
void PluginsConfigDialog::send_capabilities()
|
||||
{
|
||||
const Preset* preset = current_preset();
|
||||
if (preset == nullptr)
|
||||
return;
|
||||
|
||||
nlohmann::json response;
|
||||
response["command"] = "list_capabilities";
|
||||
response["preset_type"] = static_cast<int>(m_type);
|
||||
response["preset_name"] = preset->name;
|
||||
response["data"] = PluginConfig::capabilities_payload(capabilities_in_use(m_type, *preset));
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Prepared " << response["data"].size() << " capability rows for the Plugins Config dialog";
|
||||
call_web_handler(response);
|
||||
}
|
||||
|
||||
void PluginsConfigDialog::send_capability_config(const PluginCapabilityId& id)
|
||||
{
|
||||
const Preset* preset = current_preset();
|
||||
|
||||
nlohmann::json response;
|
||||
response["command"] = "capability_config";
|
||||
response["plugin_key"] = id.plugin_key;
|
||||
response["capability_name"] = id.name;
|
||||
response["capability_type"] = plugin_capability_type_to_string(id.type);
|
||||
response["config"] = nlohmann::json::object();
|
||||
response["custom_html"] = "";
|
||||
response["error"] = "";
|
||||
|
||||
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
|
||||
if (!cap || preset == nullptr) {
|
||||
response["error"] = into_u8(_L("This capability is no longer available."));
|
||||
call_web_handler(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const EffectiveCapabilityConfig effective = m_service.get_effective_config(m_overrides, id);
|
||||
response["config"] = effective.config;
|
||||
response["has_preset_override"] = effective.has_preset_override;
|
||||
response["has_base_config"] = effective.has_base_config;
|
||||
response["stored_plugin_version"] = effective.stored_plugin_version;
|
||||
response["running_plugin_version"] = effective.running_plugin_version;
|
||||
response["read_only"] = !m_parse_error.empty();
|
||||
if (!m_parse_error.empty())
|
||||
response["error"] = m_parse_error;
|
||||
|
||||
if (cap->config_ui_available()) {
|
||||
try {
|
||||
wxBusyCursor busy;
|
||||
PythonGILState gil;
|
||||
response["custom_html"] = cap->get_config_ui();
|
||||
} catch (const std::exception& ex) {
|
||||
response["error"] = into_u8(GUI::format_wxstr(_L("The plugin's configuration UI failed to load (%1%). Showing the default editor."),
|
||||
from_u8(ex.what())));
|
||||
}
|
||||
}
|
||||
|
||||
call_web_handler(response);
|
||||
}
|
||||
|
||||
void PluginsConfigDialog::send_save_error(const PluginCapabilityId& id, const std::string& error)
|
||||
{
|
||||
call_web_handler({{"command", "capability_config_saved"},
|
||||
{"plugin_key", id.plugin_key},
|
||||
{"capability_name", id.name},
|
||||
{"capability_type", plugin_capability_type_to_string(id.type)},
|
||||
{"ok", false},
|
||||
{"error", error}});
|
||||
}
|
||||
|
||||
void PluginsConfigDialog::show_status(const wxString& message, const char* level)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
payload["command"] = "status_message";
|
||||
payload["level"] = level;
|
||||
payload["message"] = into_u8(message);
|
||||
call_web_handler(payload);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
52
src/slic3r/GUI/PluginsConfigDialog.hpp
Normal file
52
src/slic3r/GUI/PluginsConfigDialog.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
|
||||
#include <libslic3r/Preset.hpp>
|
||||
#include <slic3r/plugin/PluginConfig.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Lists the plugin capabilities the edited preset of `m_type` uses (see capabilities_in_use) and edits
|
||||
// each one's config, falling back to the global config where the preset has no override.
|
||||
//
|
||||
// A pure editor over a JSON document: it never writes to the preset and never writes to the base config
|
||||
// file. The caller seeds it with the preset's raw override text and reads the edited text back from
|
||||
// overrides_json(); PluginConfigField owns the value and feeds it through the normal field/dirty pipeline.
|
||||
class PluginsConfigDialog : public WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
PluginsConfigDialog(wxWindow* parent, Preset::Type type, const std::string& overrides_json);
|
||||
~PluginsConfigDialog() override;
|
||||
|
||||
// The edited overrides as compact JSON text; "" once no override remains.
|
||||
std::string overrides_json() const { return serialize_plugin_overrides(m_overrides); }
|
||||
|
||||
private:
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
// Runs one web command on a clean main-loop stack; see on_script_message.
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
|
||||
const Preset* current_preset() const;
|
||||
void send_capabilities();
|
||||
void send_capability_config(const PluginCapabilityId& id);
|
||||
void send_save_error(const PluginCapabilityId& id, const std::string& error);
|
||||
void show_status(const wxString& message, const char* level);
|
||||
|
||||
PluginCapabilityId identifier_from(const nlohmann::json& payload) const;
|
||||
|
||||
Preset::Type m_type = Preset::TYPE_INVALID;
|
||||
PresetPluginConfigService m_service;
|
||||
// The working copy the dialog edits. Seeded from the preset's raw text, read back by the caller.
|
||||
CapabilityConfigDocument m_overrides;
|
||||
// Set when the preset's stored text could not be parsed: the rows are shown read-only rather
|
||||
// than silently replacing data we did not understand.
|
||||
std::string m_parse_error;
|
||||
// Guards the deferred command handlers against the dialog being destroyed while one is queued.
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "slic3r/plugin/PluginConfig.hpp"
|
||||
#include "slic3r/plugin/PluginFsUtils.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
@@ -36,9 +37,9 @@
|
||||
namespace Slic3r { namespace GUI {
|
||||
namespace {
|
||||
|
||||
const wxString kDeletePluginTitle = _L("Delete Plugin");
|
||||
const wxString kUnsubscribeTitle = _L("Unsubscribe");
|
||||
const wxString kOverwritePluginTitle = _L("Overwrite Plugin");
|
||||
const wxString kDeletePluginTitle = _L("Delete Plugin");
|
||||
const wxString kUnsubscribeTitle = _L("Unsubscribe");
|
||||
const wxString kOverwritePluginTitle = _L("Overwrite Plugin");
|
||||
std::string s_selected_plugin_install_action = "explore";
|
||||
|
||||
struct PluginContextAction
|
||||
@@ -58,12 +59,13 @@ struct PluginAvailableActions
|
||||
|
||||
struct PluginCapabilityView
|
||||
{
|
||||
std::string name;
|
||||
std::string type_label;
|
||||
std::string type_key;
|
||||
PluginCapabilityId id;
|
||||
bool enabled = false;
|
||||
bool can_toggle = false;
|
||||
bool can_run = false;
|
||||
// Whether the capability supplies its own config UI; every capability is configurable, this only
|
||||
// picks the editor. False for descriptor-only rows: an unloaded plugin has no capabilities yet.
|
||||
bool has_config_ui = false;
|
||||
};
|
||||
|
||||
struct PluginChangelogView
|
||||
@@ -76,7 +78,6 @@ struct PluginChangelogView
|
||||
// View-model for one plugin row in the dialog
|
||||
struct PluginDialogItem
|
||||
{
|
||||
// Identity and display text
|
||||
std::string plugin_key;
|
||||
std::string plugin_id;
|
||||
std::string display_name;
|
||||
@@ -85,7 +86,7 @@ struct PluginDialogItem
|
||||
std::string version;
|
||||
std::string installed_version;
|
||||
std::string latest_version;
|
||||
std::string sort_version; // Version shown in the row (installed if installed, else latest); used by the Version sort.
|
||||
std::string sort_version; // Version shown in the row (installed if installed, else latest); used by the Version sort.
|
||||
std::string type_label;
|
||||
std::string type_key;
|
||||
std::string sharing_token;
|
||||
@@ -93,16 +94,14 @@ struct PluginDialogItem
|
||||
std::vector<std::string> type_labels;
|
||||
std::vector<PluginChangelogView> changelog;
|
||||
|
||||
// Derived UI state
|
||||
PluginSource source = PluginSource::Local;
|
||||
PluginStatus status = PluginStatus::Inactive;
|
||||
PluginUpdateStatus update_status = PluginUpdateStatus::Normal;
|
||||
std::string error_text;
|
||||
bool has_error = false;
|
||||
bool is_loaded = false;
|
||||
bool is_loaded = false;
|
||||
bool loading = false;
|
||||
|
||||
// Installation and capability flags
|
||||
bool is_cloud_plugin = false;
|
||||
bool has_local_package = false;
|
||||
bool unauthorized = false;
|
||||
@@ -112,7 +111,6 @@ struct PluginDialogItem
|
||||
// Runtime capabilities in registration order, or descriptor-only type rows when unloaded.
|
||||
std::vector<PluginCapabilityView> capabilities;
|
||||
|
||||
// Row-level actions
|
||||
PluginAvailableActions available_actions;
|
||||
};
|
||||
|
||||
@@ -172,15 +170,13 @@ void refresh_plugin_metadata_blocking(bool fetch_cloud)
|
||||
return;
|
||||
|
||||
for (const auto& uuid : not_found)
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
for (const auto& uuid : unauthorized)
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,16 +199,26 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
|
||||
nlohmann::json caps = nlohmann::json::array();
|
||||
for (const PluginCapabilityView& capability : dialog_item.capabilities) {
|
||||
nlohmann::json c;
|
||||
c["name"] = capability.name;
|
||||
c["type"] = capability.type_label;
|
||||
c["type_key"] = capability.type_key;
|
||||
c["enabled"] = capability.enabled;
|
||||
c["can_toggle"] = capability.can_toggle;
|
||||
c["can_run"] = capability.can_run;
|
||||
c["name"] = capability.id.name;
|
||||
c["type"] = plugin_capability_type_display_name(capability.id.type);
|
||||
c["type_key"] = plugin_capability_type_to_string(capability.id.type);
|
||||
c["enabled"] = capability.enabled;
|
||||
c["can_toggle"] = capability.can_toggle;
|
||||
c["can_run"] = capability.can_run;
|
||||
c["has_config_ui"] = capability.has_config_ui;
|
||||
caps.push_back(std::move(c));
|
||||
}
|
||||
payload_item["capabilities"] = std::move(caps);
|
||||
|
||||
// The Config tab's sidebar, built by the shared builder so it stays identical to PluginsConfigDialog's.
|
||||
// Not the `capabilities` array above: that is the list tab's, with enable/run state and descriptor-only
|
||||
// rows the config view has no use for.
|
||||
std::vector<PluginCapabilityId> config_ids;
|
||||
for (const PluginCapabilityView& capability : dialog_item.capabilities)
|
||||
if (!capability.id.name.empty())
|
||||
config_ids.push_back(capability.id);
|
||||
payload_item["config_capabilities"] = PluginConfig::capabilities_payload(config_ids);
|
||||
|
||||
nlohmann::json changelog = nlohmann::json::array();
|
||||
for (const PluginChangelogView& entry : dialog_item.changelog) {
|
||||
nlohmann::json c;
|
||||
@@ -304,31 +310,28 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
PluginDialogItem item;
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
item.plugin_key = descriptor.plugin_key;
|
||||
item.display_name = descriptor.name;
|
||||
item.description = !descriptor.is_metadata_valid() && descriptor.has_error() ?
|
||||
descriptor.normalized_error() :
|
||||
(descriptor.description.empty() ? "No description." : descriptor.description);
|
||||
item.author = descriptor.author;
|
||||
item.version = descriptor.version;
|
||||
// Installed version: the cloud merge overwrites `version` with the latest cloud version, so prefer
|
||||
// the preserved local version, falling back to `version` for local-only / pre-merge descriptors.
|
||||
item.plugin_key = descriptor.plugin_key;
|
||||
item.display_name = descriptor.name;
|
||||
item.description = !descriptor.is_metadata_valid() && descriptor.has_error() ?
|
||||
descriptor.normalized_error() :
|
||||
(descriptor.description.empty() ? "No description." : descriptor.description);
|
||||
item.author = descriptor.author;
|
||||
item.version = descriptor.version;
|
||||
// The cloud merge overwrites `version` with the latest cloud version, so prefer the preserved local
|
||||
// one, falling back to `version` for local-only / pre-merge descriptors.
|
||||
item.installed_version = descriptor.has_local_package() ?
|
||||
(descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version) :
|
||||
std::string{};
|
||||
item.latest_version = descriptor.latest_available_version();
|
||||
// why: sort by the same version the row displays (GetDisplayVersion in index.js) - installed when
|
||||
// installed, otherwise latest - so the Version sort matches what the user sees.
|
||||
item.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
|
||||
// A package that is not loaded has no capabilities, so its row contributes none and does not
|
||||
// expand.
|
||||
const std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities =
|
||||
manager.get_plugin_capabilities(descriptor.plugin_key, PluginCapabilityType::Unknown, /*only_enabled=*/false);
|
||||
|
||||
const PluginCapabilityType primary_type = capabilities.empty() ? PluginCapabilityType::Unknown :
|
||||
capabilities.front()->type();
|
||||
item.type_label = plugin_capability_type_to_string(primary_type);
|
||||
item.type_key = plugin_capability_type_to_string(primary_type);
|
||||
const PluginCapabilityType primary_type = capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
|
||||
item.type_label = plugin_capability_type_to_string(primary_type);
|
||||
item.type_key = plugin_capability_type_to_string(primary_type);
|
||||
// "types" is the display-only compatibility list. Cloud plugins show the raw labels the
|
||||
// service returned (which may not map to real capability types); local plugins derive them
|
||||
// from the capabilities actually loaded.
|
||||
@@ -356,8 +359,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.loading = manager.is_plugin_load_in_progress(descriptor.plugin_key);
|
||||
item.has_script_capability = false;
|
||||
for (const auto& capability : capabilities) {
|
||||
item.capabilities.push_back({capability->name(), plugin_capability_type_display_name(capability->type()),
|
||||
plugin_capability_type_to_string(capability->type()), capability->is_enabled(), true, false});
|
||||
item.capabilities.push_back({capability->identity(), capability->is_enabled(), true, false, capability->config_ui_available()});
|
||||
if (capability->type() == PluginCapabilityType::Script)
|
||||
item.has_script_capability = true;
|
||||
}
|
||||
@@ -376,12 +378,12 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.available_actions = evaluate_action_policy(item);
|
||||
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
|
||||
[](const PluginCapabilityView& capability) {
|
||||
return capability.type_key == "script" && capability.enabled;
|
||||
return capability.id.type == PluginCapabilityType::Script && capability.enabled;
|
||||
});
|
||||
item.can_run_script = descriptor.is_metadata_valid() && !descriptor.has_error() && item.has_script_capability && item.is_loaded &&
|
||||
!item.loading && has_enabled_script;
|
||||
for (PluginCapabilityView& capability : item.capabilities) {
|
||||
capability.can_run = item.can_run_script && capability.type_key == "script" && capability.enabled;
|
||||
capability.can_run = item.can_run_script && capability.id.type == PluginCapabilityType::Script && capability.enabled;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -479,8 +481,9 @@ void PluginsDialog::on_script_message(const nlohmann::json& payload)
|
||||
// Defer command handling out of the webview script-message callback: GTK and macOS
|
||||
// deliver it synchronously inside the native webview callback (see ui_create_window
|
||||
// in PluginHostUi.cpp), and window work on that stack is the crash class fixed in
|
||||
// b779a7bfed/f2ccbfc8b5. Deferring at this single entry point keeps every command
|
||||
// handler, current and future, off that stack by construction.
|
||||
// b779a7bfed/f2ccbfc8b5. Deferring here keeps every command handler in THIS dialog off
|
||||
// that stack; it is not a guarantee for other WebViewHostDialog subclasses, which each
|
||||
// have to defer for themselves (PluginsConfigDialog does; others still do not).
|
||||
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
|
||||
if (alive->load(std::memory_order_acquire))
|
||||
handle_web_command(payload);
|
||||
@@ -515,6 +518,21 @@ void PluginsDialog::handle_web_command(const nlohmann::json& payload)
|
||||
open_plugin_hub();
|
||||
} else if (command == "set_plugin_sort") {
|
||||
set_plugin_sort(payload.value("sort_key", ""), payload.value("sort_order", ""));
|
||||
} else if (command == "get_capability_config" || command == "save_capability_config" || command == "restore_capability_config") {
|
||||
const PluginCapabilityId id{plugin_capability_type_from_string(payload.value("capability_type", "")),
|
||||
payload.value("capability_name", ""), payload.value("plugin_key", "")};
|
||||
if (command == "get_capability_config") {
|
||||
send_capability_config(id);
|
||||
return;
|
||||
}
|
||||
if (command == "restore_capability_config") {
|
||||
restore_capability_config(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// `config` is a JSON string from the default editor's textarea, or an already-structured
|
||||
// value from a capability's custom UI. Both land here; save_capability_config sorts it out.
|
||||
save_capability_config(id, payload.contains("config") ? payload.at("config") : nlohmann::json::object());
|
||||
} else if (command == "set_plugin_install_action") {
|
||||
const std::string action = payload.value("action", "");
|
||||
if (action == "explore" || action == "install-local")
|
||||
@@ -549,7 +567,6 @@ nlohmann::json PluginsDialog::build_plugins_payload() const
|
||||
for (const PluginDescriptor& row : rows)
|
||||
items.push_back(build_plugin_dialog_item(row));
|
||||
|
||||
// In-place sort
|
||||
sort_plugin_items_for_dialog(items, m_plugin_sort_key, m_plugin_sort_order);
|
||||
|
||||
for (const PluginDialogItem& item : items)
|
||||
@@ -566,15 +583,6 @@ bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescript
|
||||
return manager.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_invalid_package();
|
||||
}
|
||||
|
||||
std::shared_ptr<PluginCapabilityInterface> PluginsDialog::get_capability(const std::string& plugin_key,
|
||||
PluginCapabilityType type,
|
||||
const std::string& capability_name) const
|
||||
{
|
||||
// only_enabled=false: this is an existence check used to gate both enabling and disabling a
|
||||
// capability from the dialog, so a currently-disabled capability must still resolve.
|
||||
return PluginManager::instance().get_plugin_capability(plugin_key, capability_name, type, /*only_enabled=*/false);
|
||||
}
|
||||
|
||||
void PluginsDialog::refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud)
|
||||
{
|
||||
run_with_dialog([fetch_cloud]() { refresh_plugin_metadata_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
|
||||
@@ -639,8 +647,7 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Cloud plugin installed locally from Plugins dialog: " << plugin_key;
|
||||
// download_and_install_cloud_plugin updates the descriptor with the installed
|
||||
// local package state, so no rescan or cloud fetch is needed before loading.
|
||||
// download_and_install_cloud_plugin already updated the descriptor, so no rescan is needed here.
|
||||
if (!get_descriptor(plugin_key, row_data)) {
|
||||
send_plugins();
|
||||
return;
|
||||
@@ -678,8 +685,10 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
show_status(wxString::Format(_L("Activating \"%s\"..."), plugin_display_name(plugin_key)), "info");
|
||||
}
|
||||
|
||||
void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type,
|
||||
const std::string& capability_name, bool enabled)
|
||||
void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key,
|
||||
PluginCapabilityType type,
|
||||
const std::string& capability_name,
|
||||
bool enabled)
|
||||
{
|
||||
if (plugin_key.empty() || capability_name.empty() || type == PluginCapabilityType::Unknown)
|
||||
return;
|
||||
@@ -690,7 +699,7 @@ void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, Plug
|
||||
return;
|
||||
}
|
||||
|
||||
if (!get_capability(plugin_key, type, capability_name)) {
|
||||
if (!PluginManager::instance().get_plugin_capability({type, capability_name, plugin_key}, /*only_enabled=*/false)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Cannot toggle missing plugin capability: " << plugin_key << " | " << capability_name;
|
||||
send_plugins();
|
||||
return;
|
||||
@@ -699,11 +708,11 @@ void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, Plug
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (enabled) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Enabling plugin capability: " << plugin_key << " | " << capability_name;
|
||||
manager.set_capability_enabled(plugin_key, capability_name, true);
|
||||
manager.set_capability_enabled({type, capability_name, plugin_key}, true);
|
||||
} else {
|
||||
// check if the capability is currently in use here.
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Disabling plugin capability: " << plugin_key << " | " << capability_name;
|
||||
manager.set_capability_enabled(plugin_key, capability_name, false);
|
||||
manager.set_capability_enabled({type, capability_name, plugin_key}, false);
|
||||
}
|
||||
|
||||
send_plugins();
|
||||
@@ -892,6 +901,35 @@ wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
|
||||
return from_u8(plugin_key);
|
||||
}
|
||||
|
||||
void PluginsDialog::send_capability_config(const PluginCapabilityId& id) { call_web_handler(PluginConfig::get_config_response(id)); }
|
||||
|
||||
void PluginsDialog::save_capability_config(const PluginCapabilityId& id, const nlohmann::json& config)
|
||||
{
|
||||
const nlohmann::json response = PluginConfig::save_config_response(id, config);
|
||||
call_web_handler(response);
|
||||
|
||||
if (response.value("ok", false))
|
||||
show_status(_L("Configuration saved."), "success");
|
||||
}
|
||||
|
||||
void PluginsDialog::restore_capability_config(const PluginCapabilityId& id)
|
||||
{
|
||||
// Destructive, so confirm first. The confirmation stays here rather than in PluginConfig: it needs
|
||||
// a parent window.
|
||||
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
|
||||
"This discards the settings currently saved for this capability."),
|
||||
from_u8(id.name)),
|
||||
_L("Restore defaults"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
|
||||
if (rc != wxYES)
|
||||
return;
|
||||
|
||||
const nlohmann::json response = PluginConfig::restore_config_response(id);
|
||||
call_web_handler(response);
|
||||
|
||||
if (response.value("ok", false))
|
||||
show_status(_L("Default configuration restored."), "success");
|
||||
}
|
||||
|
||||
void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
@@ -909,8 +947,8 @@ void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key,
|
||||
|
||||
send_plugins();
|
||||
|
||||
// The row now shows an "Error" status and the Diagnostics tab holds the full text, so surface
|
||||
// the outcome in the footer status bar instead of a modal box (prefer the friendlier override).
|
||||
// The row already shows "Error" and the Diagnostics tab holds the full text, so report in the
|
||||
// footer status bar instead of a modal box.
|
||||
const wxString message = status_message.empty() ? from_u8(normalized_error) : status_message;
|
||||
show_status(message, "error");
|
||||
};
|
||||
@@ -936,6 +974,11 @@ void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key,
|
||||
|
||||
manager.clear_plugin_error(plugin_key);
|
||||
send_plugins();
|
||||
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
|
||||
const wxString message = result.message.empty() ? fallback : from_u8(result.message);
|
||||
show_status(message, skipped ? "info" : "success");
|
||||
}
|
||||
|
||||
void PluginsDialog::update_plugin(const std::string& plugin_key)
|
||||
@@ -948,15 +991,13 @@ void PluginsDialog::update_plugin(const std::string& plugin_key)
|
||||
PluginDescriptor descriptor;
|
||||
const wxString name = get_descriptor(plugin_key, descriptor) ? from_u8(descriptor.name) : from_u8(plugin_key);
|
||||
|
||||
// update_cloud_plugin unloads the old plugin, deletes its local package, then downloads and
|
||||
// reinstalls the latest version. Each of those steps already runs off the main thread in the
|
||||
// delete/install paths, so run the whole operation on the worker and pump a progress dialog.
|
||||
// update_cloud_plugin unloads the old plugin, deletes its local package and reinstalls the latest
|
||||
// version; all of that is off-main-thread work, so run it on the worker behind a progress dialog.
|
||||
std::string error;
|
||||
bool updated = false;
|
||||
try {
|
||||
updated = run_with_dialog_wait(
|
||||
[plugin_key, &error]() { return PluginManager::instance().update_cloud_plugin(plugin_key, error); },
|
||||
_L("Updating plugin"), _L("Updating") + ": " + name);
|
||||
updated = run_with_dialog_wait([plugin_key, &error]() { return PluginManager::instance().update_cloud_plugin(plugin_key, error); },
|
||||
_L("Updating plugin"), _L("Updating") + ": " + name);
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
@@ -970,8 +1011,7 @@ void PluginsDialog::update_plugin(const std::string& plugin_key)
|
||||
return;
|
||||
}
|
||||
|
||||
// update_cloud_plugin installs the package and updates the in-memory descriptor
|
||||
// (installed=true, update_available=false) on success.
|
||||
// update_cloud_plugin already updated the in-memory descriptor, so a UI refresh is enough here.
|
||||
send_plugins();
|
||||
show_status(wxString::Format(_L("Updated \"%s\"."), name), "success");
|
||||
}
|
||||
@@ -1099,8 +1139,7 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
||||
|
||||
manager.load_plugin(plugin_key, false);
|
||||
std::string error;
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) ||
|
||||
!manager.is_plugin_loaded(plugin_key))
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
|
||||
return {false, error.empty() ? "Plugin failed to load." : error};
|
||||
|
||||
if (!was_loaded && !manager.unload_plugin(plugin_key))
|
||||
@@ -1132,7 +1171,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
||||
return;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
|
||||
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
|
||||
|
||||
std::string error;
|
||||
if (plugin.has_local_package()) {
|
||||
@@ -1158,8 +1197,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.load_plugin(plugin_key);
|
||||
std::string error;
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) ||
|
||||
!manager.is_plugin_loaded(plugin_key))
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
|
||||
return {false, error.empty() ? "Plugin failed to load." : error};
|
||||
return {true, {}};
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ class wxTimer;
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginCapabilityInterface;
|
||||
struct PluginCapabilityId;
|
||||
enum class PluginCapabilityType;
|
||||
|
||||
namespace GUI {
|
||||
@@ -41,7 +42,7 @@ public:
|
||||
const wxString& title = wxT(""),
|
||||
const wxPoint& pos = wxDefaultPosition,
|
||||
const wxSize& size = wxDefaultSize,
|
||||
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
|
||||
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||
|
||||
~PluginsDialog();
|
||||
|
||||
@@ -65,7 +66,6 @@ private:
|
||||
nlohmann::json build_plugins_payload() const;
|
||||
|
||||
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) const;
|
||||
std::shared_ptr<PluginCapabilityInterface> get_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name) const;
|
||||
|
||||
void refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud);
|
||||
void refresh_plugins();
|
||||
@@ -77,6 +77,12 @@ private:
|
||||
bool install_plugin_package(const std::string& package_path);
|
||||
bool install_cloud_plugin(const std::string& uuid, const std::string& version, const wxString& name);
|
||||
void run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name);
|
||||
// Config tab. Both are scoped to the full capability ID: a request naming a
|
||||
// capability that is gone or not configurable is refused rather than served from, or written
|
||||
// to, some other entry.
|
||||
void send_capability_config(const PluginCapabilityId& id);
|
||||
void save_capability_config(const PluginCapabilityId& id, const nlohmann::json& config);
|
||||
void restore_capability_config(const PluginCapabilityId& id);
|
||||
// Pushes a one-line result into the web footer status bar (level: "success" | "warn" | "error" | "info"),
|
||||
// used for every plugin/capability operation instead of a modal box so the dialog stays non-disruptive.
|
||||
void show_status(const wxString& message, const char* level);
|
||||
|
||||
@@ -270,10 +270,6 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
|
||||
ctx.host = host;
|
||||
ctx.output_name = output_name;
|
||||
ctx.full_config = &config; // no live Print here; config_value() reads this
|
||||
// Hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params (same plugin_key the
|
||||
// capability was resolved by), mirroring the in-pipeline dispatcher in GUI_App.cpp.
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ctx.params = PluginManager::instance().get_plugin_settings(plugin_key);
|
||||
|
||||
ExecutionResult exec_result;
|
||||
try {
|
||||
|
||||
@@ -1793,9 +1793,8 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so the edited preset
|
||||
// always carries resolved "name;uuid;capability" references that full_config() and save_to_json()
|
||||
// then pass downstream as-is -- no separate rebuild anywhere else.
|
||||
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so full_config() and
|
||||
// save_to_json() always find resolved "name;uuid;capability" references and rebuild it nowhere else.
|
||||
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
|
||||
opt_def && opt_def->is_plugin_backed())
|
||||
m_config->update_plugin_manifest();
|
||||
@@ -3123,6 +3122,11 @@ void TabPrint::build()
|
||||
option.opt.full_width = true;
|
||||
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
|
||||
|
||||
// Its own group: the one above hides its labels, and this row needs its label — and the revert
|
||||
// arrow beside it — to show. No label-width override either, as a 0 there means "no label column".
|
||||
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
|
||||
optgroup->append_single_option_line("plugin_config_overrides");
|
||||
|
||||
optgroup = page->new_optgroup(L("Notes"), "note", 0);
|
||||
option = optgroup->get_option("notes");
|
||||
option.opt.full_width = true;
|
||||
@@ -4524,6 +4528,9 @@ void TabFilament::build()
|
||||
option.opt.height = gcode_field_height;// 150;
|
||||
optgroup->append_single_option_line(option);
|
||||
|
||||
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
|
||||
optgroup->append_single_option_line("plugin_config_overrides");
|
||||
|
||||
page = add_options_page(L("Multimaterial"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders
|
||||
optgroup = page->new_optgroup(L("Wipe tower parameters"), "param_tower");
|
||||
optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters");
|
||||
@@ -5030,6 +5037,9 @@ void TabPrinter::build_fff()
|
||||
// optgroup->append_single_option_line("spaghetti_detector");
|
||||
optgroup->append_single_option_line("time_cost", "printer_basic_information_advanced#time-cost");
|
||||
|
||||
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
|
||||
optgroup->append_single_option_line("plugin_config_overrides");
|
||||
|
||||
optgroup = page->new_optgroup(L("Cooling Fan"), "param_cooling_fan");
|
||||
Line line = Line{ L("Fan speed-up time"), optgroup->get_option("fan_speedup_time").opt.tooltip };
|
||||
line.label_path = "printer_basic_information_cooling_fan#fan-speed-up-time";
|
||||
|
||||
@@ -21,7 +21,9 @@ public:
|
||||
const wxString& title = wxT(""),
|
||||
const wxPoint& pos = wxDefaultPosition,
|
||||
const wxSize& size = wxDefaultSize,
|
||||
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
|
||||
// wxRESIZE_BORDER is required for a resizable frame on MSW/GTK; macOS derives
|
||||
// one from wxMAXIMIZE_BOX alone, which is why these dialogs used to resize only there.
|
||||
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||
~WebViewHostDialog() override = default;
|
||||
|
||||
bool create_webview(const std::string& resource_path,
|
||||
|
||||
@@ -238,8 +238,8 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
{
|
||||
PluginManager& plugin_manager = PluginManager::instance();
|
||||
|
||||
auto cap = plugin_manager.get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::PrinterConnection,
|
||||
/*only_enabled=*/false);
|
||||
auto cap = plugin_manager.get_plugin_capability({PluginCapabilityType::PrinterConnection, capability_name, plugin_key},
|
||||
/*only_enabled=*/false);
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' not found for plugin '" << plugin_key << "'";
|
||||
return;
|
||||
@@ -305,7 +305,7 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
// Re-resolve without holding the registry mutex to avoid lock inversion and reentrancy.
|
||||
// only_enabled defaults to true here, so a capability that changed identity (reload) or was
|
||||
// merely disabled both surface the same way: current_cap no longer equals cap.
|
||||
auto current_cap = plugin_manager.get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::PrinterConnection);
|
||||
auto current_cap = plugin_manager.get_plugin_capability({PluginCapabilityType::PrinterConnection, capability_name, plugin_key});
|
||||
if (current_cap != cap) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
|
||||
<< "' changed or was disabled during registration";
|
||||
|
||||
@@ -3094,6 +3094,17 @@ static bool cloud_media_is_image(const nlohmann::json& main_image)
|
||||
return lowered(get_json_string_field(main_image, "media_type")) == "image";
|
||||
}
|
||||
|
||||
// creator_display_name degrades to the creator's username and then to their raw user id when the
|
||||
// cloud cannot resolve a profile, so a bare id is dropped rather than shown: the author then falls
|
||||
// back to the one the plugin's own manifest declares (see apply_plugin_metadata_fallbacks).
|
||||
static std::string parse_cloud_author(const nlohmann::json& item)
|
||||
{
|
||||
std::string author = get_json_string_field(item, "creator_display_name");
|
||||
if (author.empty() || is_uuid(author))
|
||||
author = get_json_string_field(item, "creator_username");
|
||||
return is_uuid(author) ? std::string() : author;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::fetch_subscribed_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors,
|
||||
std::vector<std::string>& not_found,
|
||||
std::vector<std::string>& unauthorized)
|
||||
@@ -3137,11 +3148,7 @@ int OrcaCloudServiceAgent::fetch_subscribed_manifests_into_descriptors(std::vect
|
||||
descriptor.plugin_key = uuid;
|
||||
// Cloud API "description" is intentionally not parsed: descriptions come only from the
|
||||
// plugin's Python header once installed. Cloud-only rows show a "View on OrcaCloud" link.
|
||||
descriptor.author = get_json_string_field(item, "author");
|
||||
if (descriptor.author.empty())
|
||||
descriptor.author = get_json_string_field(item, "creator_display_name");
|
||||
if (descriptor.author.empty())
|
||||
descriptor.author = get_json_string_field(item, "creator_username");
|
||||
descriptor.author = parse_cloud_author(item);
|
||||
descriptor.version = get_json_string_field(item, "version");
|
||||
descriptor.latest_version = descriptor.version;
|
||||
// Cloud "type" is cosmetic (see parse_cloud_display_types); real capability_types are
|
||||
@@ -3239,11 +3246,7 @@ int OrcaCloudServiceAgent::fetch_mine_manifests_into_descriptors(std::vector<Plu
|
||||
descriptor.plugin_key = uuid;
|
||||
// Cloud API "description" is intentionally not parsed: descriptions come only from the
|
||||
// plugin's Python header once installed. Cloud-only rows show a "View on OrcaCloud" link.
|
||||
descriptor.author = get_json_string_field(item, "author");
|
||||
if (descriptor.author.empty())
|
||||
descriptor.author = get_json_string_field(item, "creator_display_name");
|
||||
if (descriptor.author.empty())
|
||||
descriptor.author = get_json_string_field(item, "creator_username");
|
||||
descriptor.author = parse_cloud_author(item);
|
||||
descriptor.version = get_json_string_field(item, "version");
|
||||
descriptor.latest_version = descriptor.version;
|
||||
// Cloud "type" is cosmetic (see parse_cloud_display_types); real capability_types are
|
||||
|
||||
@@ -77,17 +77,22 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
|
||||
thread_local std::string PluginAuditManager::m_current_capability_name = "";
|
||||
thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading;
|
||||
thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots;
|
||||
thread_local bool PluginAuditManager::m_has_last_violation = false;
|
||||
thread_local AuditViolation PluginAuditManager::m_last_violation;
|
||||
|
||||
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, PluginAuditManager::AuditMode mode)
|
||||
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
PluginAuditManager::AuditMode mode)
|
||||
: m_previous_id(PluginAuditManager::instance().current_plugin())
|
||||
, m_previous_capability(PluginAuditManager::instance().current_capability())
|
||||
, m_previous_mode(PluginAuditManager::instance().audit_mode())
|
||||
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
|
||||
{
|
||||
PluginAuditManager::instance().set_current_plugin(plugin_key);
|
||||
PluginAuditManager::instance().set_current_capability(capability_name);
|
||||
PluginAuditManager::instance().set_audit_mode(mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots.clear();
|
||||
}
|
||||
@@ -95,6 +100,7 @@ ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key
|
||||
ScopedPluginAuditContext::~ScopedPluginAuditContext()
|
||||
{
|
||||
PluginAuditManager::instance().set_current_plugin(m_previous_id);
|
||||
PluginAuditManager::instance().set_current_capability(m_previous_capability);
|
||||
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
|
||||
}
|
||||
@@ -115,6 +121,12 @@ std::string PluginAuditManager::current_plugin() const { return m_current_plugin
|
||||
|
||||
void PluginAuditManager::clear_current_plugin() { m_current_plugin_key.clear(); }
|
||||
|
||||
void PluginAuditManager::set_current_capability(const std::string& capability_name) { m_current_capability_name = capability_name; }
|
||||
|
||||
std::string PluginAuditManager::current_capability() const { return m_current_capability_name; }
|
||||
|
||||
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
|
||||
|
||||
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root)
|
||||
{
|
||||
if (root.empty())
|
||||
|
||||
@@ -40,6 +40,14 @@ public:
|
||||
std::string current_plugin() const;
|
||||
void clear_current_plugin();
|
||||
|
||||
// --- current-capability context (thread_local) ---
|
||||
// The capability whose method is currently executing, within the current plugin. Empty
|
||||
// while a plugin-wide call runs, and during capture (get_name/get_type), where the
|
||||
// capability has no cached name yet.
|
||||
void set_current_capability(const std::string& capability_name);
|
||||
std::string current_capability() const;
|
||||
void clear_current_capability();
|
||||
|
||||
// --- allowed-roots registry ---
|
||||
void add_global_allowed_root(const boost::filesystem::path& root);
|
||||
void add_scoped_allowed_root(const boost::filesystem::path& root);
|
||||
@@ -76,6 +84,7 @@ private:
|
||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||
|
||||
static thread_local std::string m_current_plugin_key;
|
||||
static thread_local std::string m_current_capability_name;
|
||||
static thread_local AuditMode m_audit_mode;
|
||||
static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots;
|
||||
static thread_local bool m_has_last_violation;
|
||||
@@ -85,12 +94,15 @@ private:
|
||||
std::vector<boost::filesystem::path> m_global_allowed_roots;
|
||||
};
|
||||
|
||||
// RAII guard that sets the current plugin key and restores the previous one.
|
||||
// RAII guard that sets the current plugin key and capability name, restoring the previous
|
||||
// pair on scope exit. `capability_name` may be empty for calls that are not scoped to a
|
||||
// single capability.
|
||||
class ScopedPluginAuditContext
|
||||
{
|
||||
public:
|
||||
explicit ScopedPluginAuditContext(
|
||||
const std::string& plugin_key,
|
||||
const std::string& capability_name = {},
|
||||
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
|
||||
|
||||
~ScopedPluginAuditContext();
|
||||
@@ -100,6 +112,7 @@ public:
|
||||
|
||||
private:
|
||||
std::string m_previous_id;
|
||||
std::string m_previous_capability;
|
||||
PluginAuditManager::AuditMode m_previous_mode;
|
||||
std::vector<boost::filesystem::path> m_previous_scoped_roots;
|
||||
};
|
||||
|
||||
649
src/slic3r/plugin/PluginConfig.cpp
Normal file
649
src/slic3r/plugin/PluginConfig.cpp
Normal file
@@ -0,0 +1,649 @@
|
||||
#include "PluginConfig.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.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::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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
for (const auto& [key, def] : print_config_def.options) {
|
||||
if (def.plugin_type != type_key)
|
||||
continue;
|
||||
|
||||
const auto& print_options = Preset::print_options();
|
||||
if (std::find(print_options.begin(), print_options.end(), key) != print_options.end()) {
|
||||
preset = &bundle->prints.get_edited_preset();
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& printer_options = Preset::printer_options();
|
||||
if (std::find(printer_options.begin(), printer_options.end(), key) != printer_options.end()) {
|
||||
preset = &bundle->printers.get_edited_preset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preset != nullptr) {
|
||||
std::string error;
|
||||
if (!parse_plugin_overrides(plugin_overrides_of(*preset), 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
|
||||
118
src/slic3r/plugin/PluginConfig.hpp
Normal file
118
src/slic3r/plugin/PluginConfig.hpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define PLUGIN_CONFIG_DIR "config.json"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Preset;
|
||||
struct CapabilityConfigEntry
|
||||
{
|
||||
PluginCapabilityId id;
|
||||
std::string plugin_version;
|
||||
nlohmann::json 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 PluginCapabilityId& id) const;
|
||||
bool contains(const PluginCapabilityId& id) const;
|
||||
bool upsert(CapabilityConfigEntry entry);
|
||||
bool erase(const PluginCapabilityId& id);
|
||||
bool empty() const;
|
||||
nlohmann::json serialize_entries() const;
|
||||
nlohmann::json root_json() const;
|
||||
|
||||
private:
|
||||
std::map<PluginCapabilityId, nlohmann::json> m_entries;
|
||||
std::vector<nlohmann::json> m_opaque_entries;
|
||||
};
|
||||
|
||||
inline constexpr const char* PLUGIN_OVERRIDES_OPTION_KEY = "plugin_config_overrides";
|
||||
|
||||
std::string plugin_overrides_of(const Preset& preset);
|
||||
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
|
||||
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
|
||||
|
||||
struct EffectiveCapabilityConfig
|
||||
{
|
||||
PluginCapabilityId id;
|
||||
nlohmann::json config = nlohmann::json::object();
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class PresetPluginConfigService
|
||||
{
|
||||
public:
|
||||
EffectiveCapabilityConfig get_effective_config(const CapabilityConfigDocument& overrides,
|
||||
const PluginCapabilityId& id) const;
|
||||
MutationResult set_preset_override(CapabilityConfigDocument& overrides,
|
||||
const PluginCapabilityId& id,
|
||||
const nlohmann::json& value) const;
|
||||
MutationResult remove_preset_override(CapabilityConfigDocument& overrides,
|
||||
const PluginCapabilityId& id) const;
|
||||
};
|
||||
|
||||
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityId& id);
|
||||
|
||||
class PluginConfig
|
||||
{
|
||||
public:
|
||||
static const std::string plugin_config_file() { return (boost::filesystem::path(get_orca_plugins_dir()) / PLUGIN_CONFIG_DIR).string(); }
|
||||
void load();
|
||||
bool save();
|
||||
|
||||
void save_config(const CapabilityConfigEntry& config);
|
||||
|
||||
bool store_capability_config(const PluginCapabilityId& id, const nlohmann::json& config);
|
||||
bool erase_capability_config(const PluginCapabilityId& id);
|
||||
|
||||
std::optional<CapabilityConfigEntry> get_config(const PluginCapabilityId& id) const;
|
||||
bool has_config(const PluginCapabilityId& id) const;
|
||||
|
||||
bool dirty() const;
|
||||
|
||||
static nlohmann::json capabilities_payload(const std::vector<PluginCapabilityId>& caps);
|
||||
static nlohmann::json get_config_response(const PluginCapabilityId& id);
|
||||
static nlohmann::json save_config_response(const PluginCapabilityId& id, const nlohmann::json& config);
|
||||
static nlohmann::json restore_config_response(const PluginCapabilityId& id);
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
CapabilityConfigDocument m_document;
|
||||
bool m_dirty = false;
|
||||
};
|
||||
|
||||
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability);
|
||||
std::string capability_get_config_version(const PluginCapabilityInterface& capability);
|
||||
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -61,7 +61,6 @@ struct PluginDescriptor
|
||||
std::string entry_path; // Full path to the installed plugin entry file
|
||||
std::string entry_package; // Import package/module used for package-based loading
|
||||
std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata
|
||||
std::map<std::string, std::string> settings; // [tool.orcaslicer.plugin.settings] table -> per-plugin params (ctx.params)
|
||||
std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available.
|
||||
|
||||
std::string error; // Blocking error message. Non-empty means the plugin is in an error state.
|
||||
@@ -157,12 +156,6 @@ inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const Plug
|
||||
target.entry_package = fallback.entry_package;
|
||||
if (target.dependencies.empty())
|
||||
target.dependencies = fallback.dependencies;
|
||||
// [tool.orcaslicer.plugin.settings] lives only in the local package's PEP-723 header;
|
||||
// cloud catalog records never carry it. Without this, every cloud-metadata merge wipes
|
||||
// the parsed settings and plugins silently run on their built-in defaults (ctx.params
|
||||
// arrives empty).
|
||||
if (target.settings.empty())
|
||||
target.settings = fallback.settings;
|
||||
}
|
||||
|
||||
// Sanitize a value for use as a filesystem name and as a local plugin_key:
|
||||
|
||||
@@ -31,10 +31,16 @@ namespace Slic3r {
|
||||
|
||||
const char* const INSTALL_STATE_FILE = ".install_state.json";
|
||||
|
||||
std::string get_orca_plugins_dir()
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
return (fs::path(data_dir()) / "orca_plugins").string();
|
||||
}
|
||||
|
||||
std::string get_cloud_plugin_dir(const std::string& user_id)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
return (fs::path(data_dir()) / "orca_plugins" / PLUGIN_SUBSCRIBED_DIR / user_id).string();
|
||||
return (fs::path(get_orca_plugins_dir()) / PLUGIN_SUBSCRIBED_DIR / user_id).string();
|
||||
}
|
||||
|
||||
boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor)
|
||||
@@ -48,8 +54,7 @@ boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescript
|
||||
return {};
|
||||
}
|
||||
|
||||
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root,
|
||||
const std::vector<std::string>& allowed_dirs)
|
||||
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, const std::vector<std::string>& allowed_dirs)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::path resolved_root = boost::filesystem::weakly_canonical(candidate_root, ec);
|
||||
@@ -103,9 +108,7 @@ bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
||||
const std::string& plugin_id,
|
||||
std::string& error)
|
||||
bool delete_plugin_root(const boost::filesystem::path& resolved_root, const std::string& plugin_id, std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -432,7 +435,6 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
std::string& out_description,
|
||||
std::string& out_author,
|
||||
std::string& out_version,
|
||||
std::map<std::string, std::string>& out_settings,
|
||||
std::string& error)
|
||||
{
|
||||
out_deps.clear();
|
||||
@@ -441,7 +443,6 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
out_description.clear();
|
||||
out_author.clear();
|
||||
out_version.clear();
|
||||
out_settings.clear();
|
||||
|
||||
TomlSection section = TomlSection::Root;
|
||||
|
||||
@@ -466,7 +467,10 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
if (trimmed == "[tool.orcaslicer.plugin]") {
|
||||
section = TomlSection::OrcaPlugin;
|
||||
} else if (trimmed == "[tool.orcaslicer.plugin.settings]") {
|
||||
section = TomlSection::OrcaPluginSettings; // per-plugin params table
|
||||
// Legacy table, superseded by PluginConfig. Recognized so its keys are ignored
|
||||
// rather than falling through to Root, where a stray dependencies/requires-python
|
||||
// key inside it would be parsed as package metadata.
|
||||
section = TomlSection::OrcaPluginSettings;
|
||||
} else {
|
||||
section = TomlSection::Root; // Unknown section — skip.
|
||||
}
|
||||
@@ -519,10 +523,6 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
else if (key == "description") out_description = unquote_toml_string(val);
|
||||
else if (key == "author") out_author = unquote_toml_string(val);
|
||||
else if (key == "version") out_version = unquote_toml_string(val);
|
||||
} else if (section == TomlSection::OrcaPluginSettings) {
|
||||
// collect every key as a string; the plugin parses (int/float/...) what it needs.
|
||||
if (!key.empty())
|
||||
out_settings[key] = unquote_toml_string(val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,7 +920,6 @@ bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginD
|
||||
pep_desc,
|
||||
pep_author,
|
||||
pep_version,
|
||||
descriptor.settings,
|
||||
pep723_error)) {
|
||||
error = "Failed to parse PEP 723 metadata: " + pep723_error;
|
||||
return false;
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
#include "PluginDescriptor.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,6 +17,70 @@ namespace Slic3r {
|
||||
|
||||
extern const char* const INSTALL_STATE_FILE;
|
||||
|
||||
// JSON <-> Python conversion shared by the plugin bindings. The caller must hold the GIL.
|
||||
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
|
||||
// values, so both go through these.
|
||||
|
||||
inline pybind11::object json_to_py(const nlohmann::json& j)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
using json = nlohmann::json;
|
||||
|
||||
switch (j.type()) {
|
||||
case json::value_t::null: return py::none();
|
||||
case json::value_t::boolean: return py::bool_(j.get<bool>());
|
||||
case json::value_t::number_integer: return py::int_(j.get<std::int64_t>());
|
||||
case json::value_t::number_unsigned: return py::int_(j.get<std::uint64_t>());
|
||||
case json::value_t::number_float: return py::float_(j.get<double>());
|
||||
case json::value_t::string: return py::str(j.get<std::string>());
|
||||
case json::value_t::array: {
|
||||
py::list lst;
|
||||
for (const auto& e : j)
|
||||
lst.append(json_to_py(e));
|
||||
return lst;
|
||||
}
|
||||
case json::value_t::object: {
|
||||
py::dict d;
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
d[py::str(it.key())] = json_to_py(it.value());
|
||||
return d;
|
||||
}
|
||||
default: return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
inline nlohmann::json py_to_json(const pybind11::handle& o)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
using json = nlohmann::json;
|
||||
|
||||
if (o.is_none())
|
||||
return json(nullptr);
|
||||
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
|
||||
return o.cast<bool>();
|
||||
if (py::isinstance<py::int_>(o))
|
||||
return o.cast<std::int64_t>();
|
||||
if (py::isinstance<py::float_>(o))
|
||||
return o.cast<double>();
|
||||
if (py::isinstance<py::str>(o))
|
||||
return o.cast<std::string>();
|
||||
if (py::isinstance<py::bytes>(o))
|
||||
return o.cast<std::string>();
|
||||
if (py::isinstance<py::dict>(o)) {
|
||||
json obj = json::object();
|
||||
for (auto item : py::reinterpret_borrow<py::dict>(o))
|
||||
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
|
||||
return obj;
|
||||
}
|
||||
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
|
||||
json arr = json::array();
|
||||
for (auto e : o)
|
||||
arr.push_back(py_to_json(e));
|
||||
return arr;
|
||||
}
|
||||
return py::str(o).cast<std::string>(); // fallback: str()
|
||||
}
|
||||
|
||||
struct PluginInstallState {
|
||||
std::string installed_from; // "local" | "cloud"
|
||||
std::string installed_version;
|
||||
@@ -26,6 +94,8 @@ struct PluginInstallState {
|
||||
// Path: {data_dir}/orca_plugins/_subscribed/{user_id}/
|
||||
std::string get_cloud_plugin_dir(const std::string& user_id);
|
||||
|
||||
std::string get_orca_plugins_dir();
|
||||
|
||||
boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor);
|
||||
|
||||
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root,
|
||||
|
||||
@@ -70,9 +70,6 @@ void install_slicing_pipeline_hook()
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ExecutionResult r;
|
||||
try {
|
||||
// Read manager state before acquiring the GIL so this path does not take
|
||||
// m_mutex in the opposite order to plugin teardown.
|
||||
const auto plugin_settings = PluginManager::instance().get_plugin_settings(plugin_key);
|
||||
// GIL is acquired per capability (not once for the whole dispatch) so it
|
||||
// is released between capabilities.
|
||||
PythonGILState gil;
|
||||
@@ -87,9 +84,6 @@ void install_slicing_pipeline_hook()
|
||||
ctx.step = step;
|
||||
ctx.print = &print;
|
||||
ctx.object = object;
|
||||
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
|
||||
// (same plugin_key the capability was resolved by, so it always matches).
|
||||
ctx.params = plugin_settings;
|
||||
r = cap->execute(ctx);
|
||||
} catch (const CanceledException&) {
|
||||
throw; // cancellation must reach process(), never become a slicing error
|
||||
|
||||
@@ -347,6 +347,19 @@ bool load(const PluginDescriptor& descriptor,
|
||||
instance->set_resolved_identity(found.name, type);
|
||||
instance->set_enabled(enabled);
|
||||
|
||||
// Cache has_config_ui() once, under this same GIL, so the GUI can pick the
|
||||
// capability's custom UI vs. the host JSON editor without touching Python. It is
|
||||
// optional and plugin-authored: a raising or non-bool override only costs this
|
||||
// capability its custom UI, so it is caught locally rather than failing the load.
|
||||
try {
|
||||
instance->set_config_ui_available(instance->has_config_ui());
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
|
||||
<< "': has_config_ui() failed (" << ex.what() << "); falling back to the default JSON editor";
|
||||
instance->set_config_ui_available(false);
|
||||
}
|
||||
|
||||
capabilities.push_back(std::move(instance));
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <memory>
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <slic3r/plugin/PluginConfig.hpp>
|
||||
#include <slic3r/plugin/PluginLoader.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
|
||||
@@ -83,6 +85,12 @@ bool PluginManager::initialize()
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
// Bring every capability's stored config into memory. Deliberately unconditional and
|
||||
// independent of which plugins are installed: an entry outlives uninstall/unsubscribe, so a
|
||||
// plugin that comes back later finds its settings intact. A missing or malformed file just
|
||||
// leaves the store empty (see PluginConfig::load), never blocking startup.
|
||||
m_config.load();
|
||||
|
||||
// Install the libslic3r hooks (capability resolver, slicing-pipeline dispatcher).
|
||||
// Uninstalled in shutdown() before the interpreter finalizes.
|
||||
plugin_hooks::install();
|
||||
@@ -142,6 +150,12 @@ void PluginManager::shutdown()
|
||||
unload_all_plugins();
|
||||
PythonPluginBridge::instance().clear_pending_captures();
|
||||
|
||||
// Every config write already goes to disk as it happens (store_capability_config), so this
|
||||
// only catches an in-memory-only mutation. Note we flush rather than clear: unloading the
|
||||
// plugins above must never discard their stored config.
|
||||
if (m_config.dirty())
|
||||
m_config.save();
|
||||
|
||||
// Drop the lifecycle subscriptions taken out during initialize(). Without this a second
|
||||
// initialize() in the same process re-subscribes the same callbacks on top of the old ones,
|
||||
// and every load would then write the install-state sidecar once per duplicate.
|
||||
@@ -496,21 +510,18 @@ std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugi
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
PluginCapabilityType type,
|
||||
bool only_enabled) const
|
||||
std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(const PluginCapabilityId& id, bool only_enabled) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
const Plugin* plugin = find_plugin_locked(plugin_key);
|
||||
const Plugin* plugin = find_plugin_locked(id.plugin_key);
|
||||
if (plugin == nullptr)
|
||||
return nullptr;
|
||||
|
||||
for (const auto& capability : plugin->capabilities) {
|
||||
if (!capability || capability->name() != capability_name)
|
||||
if (!capability || capability->name() != id.name)
|
||||
continue;
|
||||
if (type != PluginCapabilityType::Unknown && capability->type() != type)
|
||||
if (id.type != PluginCapabilityType::Unknown && capability->type() != id.type)
|
||||
continue;
|
||||
if (only_enabled && !capability->is_enabled())
|
||||
continue;
|
||||
@@ -541,17 +552,6 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> PluginManager::get_plugin_settings(const std::string& plugin_key) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
const Plugin* plugin = find_plugin_locked(plugin_key);
|
||||
if (plugin == nullptr || !plugin->is_loaded())
|
||||
return {};
|
||||
|
||||
return plugin->descriptor.settings;
|
||||
}
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
|
||||
@@ -736,7 +736,7 @@ void PluginManager::load_plugin(const std::string& plugin_key, bool skip_deps, s
|
||||
for (const std::string& capability_name : capabilities_to_enable) {
|
||||
if (std::find(loaded_capability_names.begin(), loaded_capability_names.end(), capability_name) !=
|
||||
loaded_capability_names.end())
|
||||
set_capability_enabled(plugin_id, capability_name, true);
|
||||
set_capability_enabled({PluginCapabilityType::Unknown, capability_name, plugin_id}, true);
|
||||
}
|
||||
|
||||
run_on_load_callbacks(plugin_id);
|
||||
@@ -844,6 +844,17 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& cap : plugin.capabilities) {
|
||||
auto config = cap->get_default_config();
|
||||
if (config.empty())
|
||||
continue;
|
||||
const PluginCapabilityId id = cap->identity();
|
||||
if (m_config.has_config(id))
|
||||
continue;
|
||||
|
||||
m_config.save_config({id, plugin.descriptor.installed_version, config});
|
||||
}
|
||||
|
||||
bool committed = false;
|
||||
bool cancelled = false;
|
||||
std::string registry_error;
|
||||
@@ -993,8 +1004,7 @@ void PluginManager::unload_cloud_plugins()
|
||||
}
|
||||
|
||||
// ── Enable state ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void PluginManager::set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled)
|
||||
void PluginManager::set_capability_enabled(const PluginCapabilityId& id, bool enabled)
|
||||
{
|
||||
PluginCapabilityId changed;
|
||||
bool did_change = false;
|
||||
@@ -1002,15 +1012,18 @@ void PluginManager::set_capability_enabled(const std::string& plugin_key, const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
Plugin* plugin = find_plugin_locked(plugin_key);
|
||||
Plugin* plugin = find_plugin_locked(id.plugin_key);
|
||||
if (plugin == nullptr || !plugin->is_loaded())
|
||||
return;
|
||||
|
||||
for (const auto& capability : plugin->capabilities) {
|
||||
if (!capability || capability->name() != capability_name || capability->is_enabled() == enabled)
|
||||
if (!capability || capability->name() != id.name ||
|
||||
(id.type != PluginCapabilityType::Unknown && capability->type() != id.type) ||
|
||||
capability->is_enabled() == enabled)
|
||||
continue;
|
||||
|
||||
capability->set_enabled(enabled);
|
||||
changed = PluginCapabilityId{capability->type(), capability->name(), plugin_key};
|
||||
changed = capability->identity();
|
||||
did_change = true;
|
||||
break;
|
||||
}
|
||||
@@ -1019,7 +1032,7 @@ void PluginManager::set_capability_enabled(const std::string& plugin_key, const
|
||||
if (!did_change)
|
||||
return;
|
||||
|
||||
write_loaded_plugin_install_state(plugin_key);
|
||||
write_loaded_plugin_install_state(id.plugin_key);
|
||||
|
||||
if (enabled)
|
||||
run_on_capability_load_callbacks(changed);
|
||||
@@ -1029,6 +1042,8 @@ void PluginManager::set_capability_enabled(const std::string& plugin_key, const
|
||||
|
||||
void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
std::vector<std::pair<std::string, bool>> capabilities;
|
||||
{
|
||||
@@ -1958,7 +1973,7 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k
|
||||
return {};
|
||||
}
|
||||
|
||||
auto cap = get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::Script);
|
||||
auto cap = get_plugin_capability({PluginCapabilityType::Script, capability_name, plugin_key});
|
||||
if (!cap)
|
||||
return {};
|
||||
|
||||
|
||||
@@ -23,21 +23,12 @@
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "PluginLoader.hpp"
|
||||
#include "PluginConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class OrcaCloudServiceAgent;
|
||||
|
||||
// Identity of a single capability, as published to lifecycle subscribers. Purely a message
|
||||
// payload — capabilities are looked up by (plugin_key, name) linear scan, so unlike the registry
|
||||
// key this replaces, it needs no hash and no equality.
|
||||
struct PluginCapabilityId
|
||||
{
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown;
|
||||
std::string name;
|
||||
std::string plugin_key; // owning package
|
||||
};
|
||||
|
||||
// One discovered plugin package: one .py/.whl file -> one descriptor + one Python module +
|
||||
// N materialized capabilities.
|
||||
//
|
||||
@@ -126,6 +117,9 @@ public:
|
||||
// Manually trigger a manifest-only rescan. Blocks until discovery is complete.
|
||||
void rescan_plugins();
|
||||
|
||||
PluginConfig& get_config() { return m_config; }
|
||||
const PluginConfig& get_config() const { return m_config; }
|
||||
|
||||
bool is_discovery_complete() const;
|
||||
bool is_discovery_in_progress() const;
|
||||
std::string get_discovery_error() const;
|
||||
@@ -149,10 +143,9 @@ public:
|
||||
const std::string& plugin_key = "", // "" => all plugins
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown, // Unknown => all types
|
||||
bool only_enabled = true) const;
|
||||
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown,
|
||||
bool only_enabled = true) const;
|
||||
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const PluginCapabilityId& id, bool only_enabled = true) const;
|
||||
|
||||
// Try to resolve from just capability name and type from presets.
|
||||
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& capability_name,
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown,
|
||||
bool only_enabled = true) const;
|
||||
@@ -169,10 +162,8 @@ public:
|
||||
bool cancel_plugin_load(const std::string& plugin_key);
|
||||
std::string get_plugin_load_error(const std::string& plugin_key) const;
|
||||
|
||||
void set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled);
|
||||
void set_capability_enabled(const PluginCapabilityId& id, bool enabled);
|
||||
|
||||
// The plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown). This will be replaced once the config is merged in.
|
||||
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) const;
|
||||
// Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into.
|
||||
void set_cloud_user(const std::string& user_id);
|
||||
|
||||
@@ -264,6 +255,7 @@ private:
|
||||
|
||||
bool m_initialized = false;
|
||||
CloudPluginService m_cloud_service;
|
||||
PluginConfig m_config;
|
||||
|
||||
// Leaf lock: code holding m_mutex must not call Python, acquire the GIL, invoke lifecycle
|
||||
// callbacks, or re-enter the manager. Live plugin payloads are detached and torn down after
|
||||
@@ -283,6 +275,9 @@ private:
|
||||
std::map<std::string, std::string> m_load_errors;
|
||||
mutable std::condition_variable m_load_cv;
|
||||
|
||||
// Serialize sidecar snapshots so concurrent capability toggles cannot write stale state out of order.
|
||||
mutable std::mutex m_install_state_mutex;
|
||||
|
||||
std::map<CallbackType, std::vector<PluginLifecycleCompleteFn>> m_callbacks;
|
||||
std::map<CallbackType, std::vector<CapabilityLifecycleFn>> m_capability_callbacks;
|
||||
|
||||
@@ -339,7 +334,7 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
|
||||
// only_enabled = false so that "not loaded" and "loaded but disabled" stay distinguishable
|
||||
// and each keeps its own diagnostic.
|
||||
auto cap = plugin_mgr.get_plugin_capability(plugin_key, cap_name, type, /*only_enabled=*/false);
|
||||
auto cap = plugin_mgr.get_plugin_capability({type, cap_name, plugin_key}, /*only_enabled=*/false);
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name << "' for plugin '" << plugin_key << "'; skipping";
|
||||
continue;
|
||||
|
||||
@@ -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,35 +21,39 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
// Return the name of the tracked option in `preset` whose value references `ref`'s capability, or
|
||||
// an empty string when no active option uses it. The result doubles as the "Jump to" target and as
|
||||
// the signal that the plugin is still required: a missing plugin with no referencing option is
|
||||
// considered resolved and dropped from the missing set.
|
||||
std::string find_option_for_capability(Preset::Type type, const Preset& preset, const PluginCapabilityRef& ref)
|
||||
// The tracked option in `preset` whose value references `ref`'s capability, or "" when none does.
|
||||
// Doubles as the "Jump to" target and as the signal that the plugin is still required: a missing
|
||||
// plugin with no referencing option is dropped from the set.
|
||||
std::string find_option_for_capability(Preset::Type type,
|
||||
const Preset& preset,
|
||||
const PluginCapabilityRef& ref,
|
||||
PluginCapabilityType capability_type = PluginCapabilityType::Unknown)
|
||||
{
|
||||
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_PRINTER && type != Preset::TYPE_FILAMENT)
|
||||
return {};
|
||||
|
||||
// Plugin-bearing options opt in via ConfigOptionDef::is_plugin_backed (a non-empty plugin_type),
|
||||
// so scan the preset's definition rather than maintaining a hardcoded per-type field list. A typed
|
||||
// preset's config only contains keys for its own type, so this naturally stays scoped to `type`.
|
||||
// Options opt in via ConfigOptionDef::is_plugin_backed, so scan the definition rather than keep a
|
||||
// hardcoded per-type field list. A typed preset's config only holds keys for its own type.
|
||||
const ConfigDef* def = preset.config.def();
|
||||
if (def == nullptr)
|
||||
return {};
|
||||
|
||||
const std::string expected_type = plugin_capability_type_to_string(capability_type);
|
||||
const auto matches_ref = [&ref](const std::string& value) {
|
||||
return value == ref.capability_name;
|
||||
};
|
||||
|
||||
for (const std::string& field : preset.config.keys()) {
|
||||
const ConfigOptionDef* opt_def = def->get(field);
|
||||
if (opt_def == nullptr || !opt_def->is_plugin_backed())
|
||||
if (opt_def == nullptr || !opt_def->is_plugin_backed() ||
|
||||
(capability_type != PluginCapabilityType::Unknown && opt_def->plugin_type != expected_type))
|
||||
continue;
|
||||
|
||||
const ConfigOption* option = preset.config.option(field);
|
||||
@@ -70,16 +75,19 @@ std::string find_option_for_capability(Preset::Type type, const Preset& preset,
|
||||
|
||||
// printer_agent stores AgentInfo::id, so a missing plugin cannot be reverse-mapped through
|
||||
// the runtime registry. If no regular printer plugin field matched, assume printer_agent.
|
||||
if (type == Preset::Type::TYPE_PRINTER && preset.config.has("printer_agent"))
|
||||
if (type == Preset::Type::TYPE_PRINTER && preset.config.has("printer_agent")) {
|
||||
const ConfigOptionDef* agent_def = def->get("printer_agent");
|
||||
if (agent_def != nullptr && agent_def->is_plugin_backed() &&
|
||||
(capability_type == PluginCapabilityType::Unknown || agent_def->plugin_type == expected_type))
|
||||
return "printer_agent";
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// One missing-plugin set per tracked preset type, keyed by the full "name;uuid;capability" ref.
|
||||
// Only TYPE_PRINT (process), TYPE_PRINTER (machine) and TYPE_FILAMENT are tracked.
|
||||
// One set per tracked preset type, keyed by the full "name;uuid;capability" ref.
|
||||
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_missing;
|
||||
static std::mutex s_missing_mutex;
|
||||
// Installed-but-inactive capabilities (not loaded, or loaded-but-disabled); resolvable locally.
|
||||
@@ -92,6 +100,95 @@ 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;
|
||||
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 not in the catalog, or whose capability is not loaded, is dropped: no instance means nothing
|
||||
// to configure.
|
||||
void collect_capabilities_in_use(Preset::Type type, const Preset& preset, std::vector<PluginCapabilityId>& out)
|
||||
{
|
||||
for (const PluginCapabilityRef& ref : referenced_capabilities(type, preset)) {
|
||||
// Cloud plugins resolve by UUID, local plugins by plugin_key.
|
||||
const std::string key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
if (key.empty())
|
||||
continue;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(key, descriptor))
|
||||
continue;
|
||||
|
||||
// The manifest ref does not carry a type, so require the live capability type to match the
|
||||
// type declared by the option that references it.
|
||||
for (const auto& capability : PluginManager::instance().get_plugin_capabilities(descriptor.plugin_key, PluginCapabilityType::Unknown, false))
|
||||
if (capability && capability->type() != PluginCapabilityType::Unknown &&
|
||||
capability->name() == ref.capability_name &&
|
||||
!find_option_for_capability(type, preset, ref, capability->type()).empty())
|
||||
out.push_back(capability->identity());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<PluginCapabilityId> capabilities_in_use(const PresetBundle& preset_bundle, Preset::Type type)
|
||||
{
|
||||
if (!is_tracked_type(type))
|
||||
return {};
|
||||
|
||||
std::vector<PluginCapabilityId> 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 {
|
||||
// Each filament preset is tested against its own config; refresh_missing_plugins cannot do
|
||||
// that, as 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 PluginCapabilityId& a, const PluginCapabilityId& 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<PluginCapabilityId> capabilities_in_use(Preset::Type type, const Preset& preset)
|
||||
{
|
||||
std::vector<PluginCapabilityId> result;
|
||||
if (!is_tracked_type(type))
|
||||
return result;
|
||||
|
||||
collect_capabilities_in_use(type, preset, result);
|
||||
std::sort(result.begin(), result.end(), [](const PluginCapabilityId& a, const PluginCapabilityId& 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";
|
||||
@@ -111,9 +208,7 @@ std::string resolve_recovery_url(const PluginCapabilityRef& ref)
|
||||
return resolve_cloud_base_url() + "/app/plugins/plugin-hub?search=" + Http::url_encode(ref.name);
|
||||
}
|
||||
|
||||
// Reports whether the loaded plugin currently exposes the referenced capability (in any enabled
|
||||
// state) and whether that capability is enabled. Returns {false, false} when the plugin is not
|
||||
// loaded or does not provide the capability.
|
||||
// {present, enabled}; {false, false} when the plugin is not loaded or does not provide the capability.
|
||||
static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_key, const PluginCapabilityRef& ref)
|
||||
{
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
@@ -123,7 +218,7 @@ static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_k
|
||||
if (!mgr.is_plugin_loaded(plugin_key))
|
||||
return {false, false};
|
||||
|
||||
const auto capability = mgr.get_plugin_capability(plugin_key, ref.capability_name, PluginCapabilityType::Unknown,
|
||||
const auto capability = mgr.get_plugin_capability({PluginCapabilityType::Unknown, ref.capability_name, plugin_key},
|
||||
/*only_enabled=*/false);
|
||||
if (!capability)
|
||||
return {false, false};
|
||||
@@ -193,7 +288,7 @@ void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manif
|
||||
continue;
|
||||
|
||||
if (!installed) {
|
||||
// Not on disk — needs download/install (existing behavior).
|
||||
// Not on disk — needs download/install.
|
||||
std::string recovery_url = ref->uuid.empty() ? resolve_recovery_url(*ref) : std::string();
|
||||
missing_set.emplace(entry, MissingPlugin{*ref, std::move(recovery_url), std::move(opt), type, PluginCapabilityType::Unknown});
|
||||
} else if (!loaded || cap_present) {
|
||||
@@ -287,7 +382,6 @@ static void report_install_failure(const std::string& message)
|
||||
|
||||
void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstallProgress progress)
|
||||
{
|
||||
// Collect the unique cloud UUIDs to install; local refs are handled via the browser flow.
|
||||
std::vector<std::string> uuids;
|
||||
for (const std::string& r : refs) {
|
||||
const auto ref = parse_capability_ref(r);
|
||||
@@ -311,7 +405,6 @@ void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstall
|
||||
|
||||
const std::string& uuid = uuids[i];
|
||||
|
||||
// Use a friendly name for the progress message when the catalog already knows it.
|
||||
std::string display_name = uuid;
|
||||
PluginDescriptor known;
|
||||
if (mgr.try_get_plugin_descriptor(uuid, known) && !known.name.empty())
|
||||
@@ -345,8 +438,7 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
{
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
|
||||
// Group the requested capabilities by owning plugin so each plugin is loaded once with the full
|
||||
// set to enable.
|
||||
// Group by owning plugin so each plugin is loaded once with the full set to enable.
|
||||
std::map<std::string, std::vector<std::string>> by_plugin;
|
||||
for (const std::string& r : refs) {
|
||||
const auto ref = parse_capability_ref(r);
|
||||
@@ -361,11 +453,9 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
if (by_plugin.empty())
|
||||
return;
|
||||
|
||||
// load_plugin loads+enables a not-loaded plugin (async) and enables the listed capabilities on an
|
||||
// already-loaded one. The fresh-load path does NOT fire the capability-load callback the GUI uses
|
||||
// to clear the notification, so wait for each load off the UI thread and then re-validate once —
|
||||
// mirroring the cloud-install flow. This clears the inactive notification, or flips it to broken
|
||||
// if the loaded plugin turns out not to provide the capability.
|
||||
// The fresh-load path does NOT fire the capability-load callback the GUI uses to clear the
|
||||
// notification, so wait for each load off the UI thread and re-validate once. That clears the
|
||||
// inactive notification, or flips it to broken if the plugin does not provide the capability.
|
||||
std::vector<std::pair<std::string, std::vector<std::string>>> work(by_plugin.begin(), by_plugin.end());
|
||||
std::thread([work = std::move(work)]() {
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
@@ -383,7 +473,6 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
|
||||
void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs)
|
||||
{
|
||||
// One missing plugin: deep-link a search for it. Multiple: just open the plugin hub.
|
||||
if (local_refs.size() == 1) {
|
||||
if (const auto ref = parse_capability_ref(local_refs.front())) {
|
||||
wxLaunchDefaultBrowser(GUI::from_u8(resolve_recovery_url(*ref)), wxBROWSER_NEW_WINDOW);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <libslic3r/Config.hpp> // PluginCapabilityRef, parse_capability_ref
|
||||
#include <libslic3r/Preset.hpp> // Preset::Type
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityType
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityId, PluginCapabilityType
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -23,10 +23,9 @@ struct MissingPlugin
|
||||
PluginCapabilityType type{PluginCapabilityType::Unknown};
|
||||
};
|
||||
|
||||
// Rebuild the missing-plugin set owned by a single preset type from that preset's "plugins"
|
||||
// manifest, comparing each ref against the live plugin catalog and loaded/enabled capabilities. A
|
||||
// null/empty manifest clears the set for that type. Only TYPE_PRINT (process), TYPE_PRINTER
|
||||
// (machine) and TYPE_FILAMENT are tracked; other types are ignored.
|
||||
// Rebuild one preset type's missing-plugin set from its "plugins" manifest, comparing each ref
|
||||
// against the live catalog and loaded/enabled capabilities. A null/empty manifest clears the set.
|
||||
// Only TYPE_PRINT, TYPE_PRINTER and TYPE_FILAMENT are tracked.
|
||||
void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manifest, const Preset* preset = nullptr);
|
||||
void refresh_missing_plugins(const PresetBundle& preset_bundle);
|
||||
|
||||
@@ -36,22 +35,16 @@ std::vector<MissingPlugin> get_missing_cloud_plugins();
|
||||
std::vector<MissingPlugin> get_missing_local_plugins();
|
||||
bool has_missing_plugins();
|
||||
|
||||
// Installed-but-inactive capabilities: the plugin has a local package but the referenced capability
|
||||
// is not active because the plugin is not loaded, or it is loaded but the capability is disabled.
|
||||
// Resolved locally by loading the plugin and/or enabling the capability — no download.
|
||||
// Installed-but-inactive: the plugin has a local package but is not loaded, or is loaded with the
|
||||
// capability disabled. Resolved locally by loading and/or enabling — no download.
|
||||
std::vector<MissingPlugin> get_inactive_plugins();
|
||||
bool has_inactive_plugins();
|
||||
|
||||
// Broken references: the plugin is installed AND loaded but does not provide the referenced
|
||||
// capability at all (renamed/removed/outdated plugin). Activation cannot fix these; surfaced as an
|
||||
// informational notification pointing the user at OrcaCloud to update the plugin.
|
||||
// capability at all (renamed/removed/outdated plugin). Activation cannot fix these.
|
||||
std::vector<MissingPlugin> get_broken_plugins();
|
||||
bool has_broken_plugins();
|
||||
|
||||
// Resolution actions invoked from the missing-plugin notifications:
|
||||
// - cloud refs are subscribed/installed and loaded on a detached worker thread; failures are
|
||||
// reported through a non-blocking notification. Non-cloud refs are ignored.
|
||||
|
||||
// Optional progress hook for the cloud install worker. All three callbacks fire on the worker
|
||||
// thread; implementations must only touch thread-safe state or marshal to the UI thread.
|
||||
struct PluginInstallProgress
|
||||
@@ -64,23 +57,36 @@ struct PluginInstallProgress
|
||||
std::function<void()> on_finished;
|
||||
};
|
||||
|
||||
// Cloud refs only; local refs are handled via the browser flow. `progress` is optional — a
|
||||
// default-constructed value preserves the previous silent behavior.
|
||||
// Subscribe, install and load the cloud refs on a detached worker; failures are reported through a
|
||||
// non-blocking notification. Local refs are ignored — they go through the browser flow below.
|
||||
void resolve_missing_plugins(const std::vector<std::string>& refs,
|
||||
PluginInstallProgress progress = {});
|
||||
|
||||
// Activate inactive plugins: load each referenced plugin (passing the capabilities to enable) and/or
|
||||
// enable already-loaded-but-disabled capabilities. Local only — no network. The loads run on a
|
||||
// background worker that waits for them and then re-validates the plate, clearing the notification
|
||||
// (or reclassifying the ref as broken if the loaded plugin turns out not to provide the capability).
|
||||
// Load each referenced plugin and/or enable its disabled capabilities. Local only — no network. The
|
||||
// loads run on a background worker that waits for them and then re-validates the plate, clearing the
|
||||
// notification (or reclassifying the ref as broken if the plugin does not provide the capability).
|
||||
void resolve_inactive_plugins(const std::vector<std::string>& refs);
|
||||
|
||||
// - local refs are opened on the OrcaCloud plugin hub (search when exactly one ref, hub otherwise).
|
||||
// Opens the OrcaCloud plugin hub (a search when there is exactly one ref, the hub otherwise).
|
||||
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 catalog and loader are not consulted. Empty for untracked types.
|
||||
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset);
|
||||
std::vector<PluginCapabilityId> capabilities_in_use(Preset::Type type, const Preset& preset);
|
||||
|
||||
// The referenced capabilities of the active preset(s) of `type` 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.
|
||||
std::vector<PluginCapabilityId> capabilities_in_use(const PresetBundle& preset_bundle, Preset::Type type);
|
||||
|
||||
bool check_capability_in_use(const std::string& capability_refs);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "PythonPluginInterface.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginAuditManager.hpp"
|
||||
|
||||
// Trampoline variants of pybind11's override macros. Every C++->Python plugin call
|
||||
// crosses through a trampoline method, so this single boundary is where we (1) log the
|
||||
// full Python traceback (to sys.stderr -> session log) and rethrow the exception intact,
|
||||
// and (2) open the plugin's filesystem audit scope for the duration of the call.
|
||||
// We catch ONLY error_already_set (a Python-side raise); other pybind11_fail/runtime_error
|
||||
// like a pure-virtual-missing failure must keep their own path and are deliberately not
|
||||
// caught here.
|
||||
// Trampoline variants of pybind11's override macros. Every C++->Python plugin call crosses a
|
||||
// trampoline method, so this single boundary is where we (1) log the full Python traceback and
|
||||
// rethrow the exception intact, and (2) open the plugin's filesystem audit scope for the call.
|
||||
// We catch ONLY error_already_set (a Python-side raise); other pybind11_fail/runtime_error, such as
|
||||
// a pure-virtual-missing failure, must keep their own path and are deliberately not caught here.
|
||||
|
||||
// Logs (and rethrows) a Python exception from a pybind11 override call, preserving the
|
||||
// traceback. Internal helper shared by the public macros below and by trampolines that
|
||||
@@ -29,13 +30,13 @@
|
||||
throw; \
|
||||
}
|
||||
|
||||
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call
|
||||
// when this trampoline instance carries a non-empty audit plugin key. Declares a local
|
||||
// `_orca_audit_scope`.
|
||||
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes
|
||||
// the calling capability's cached name so host APIs invoked from Python can tell which capability
|
||||
// they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`.
|
||||
#define ORCA_PY_AUDIT_SCOPE(mode) \
|
||||
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
|
||||
if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \
|
||||
_orca_audit_scope.emplace(_orca_audit_key, mode)
|
||||
_orca_audit_scope.emplace(_orca_audit_key, this->name(), mode)
|
||||
|
||||
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
|
||||
do { \
|
||||
@@ -55,13 +56,65 @@ template<class Base> class PyPluginCommonTrampoline : public Base
|
||||
public:
|
||||
using Base::Base;
|
||||
|
||||
// get_name is required on all capabilities — Python subclass must implement it.
|
||||
std::string get_name() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
|
||||
}
|
||||
|
||||
// All plugins may define their own on_load/unload functions.
|
||||
// Config UI hooks. Available on every capability type, so they live here rather than in
|
||||
// PyPluginInterfaceTrampoline. A Python exception is rethrown; the caller decides the fallback.
|
||||
bool has_config_ui() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
bool,
|
||||
Base,
|
||||
has_config_ui);
|
||||
}
|
||||
|
||||
std::string get_config_ui() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
std::string,
|
||||
Base,
|
||||
get_config_ui);
|
||||
}
|
||||
|
||||
// Hand-rolled rather than PYBIND11_OVERRIDE: the macro casts the Python result to the return
|
||||
// type, and nlohmann::json has no pybind caster (config crosses this boundary through the
|
||||
// explicit py_to_json/json_to_py helpers). Otherwise identical — same audit scope, same rethrow.
|
||||
//
|
||||
// The hook is optional, and "not implemented" must mean an EMPTY config: no override, or an
|
||||
// override returning None or any non-object (`def get_default_config(self): pass` is the easy
|
||||
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
|
||||
nlohmann::json get_default_config() const override
|
||||
{
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
try {
|
||||
pybind11::gil_scoped_acquire gil;
|
||||
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
|
||||
if (!override)
|
||||
return Base::get_default_config();
|
||||
|
||||
nlohmann::json config = ::Slic3r::py_to_json(override());
|
||||
if (!config.is_object()) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "Plugin capability '" << this->name() << "' of plugin '" << this->audit_plugin_key()
|
||||
<< "': get_default_config() returned " << config.type_name() << ", not an object; restoring an empty config";
|
||||
return Base::get_default_config();
|
||||
}
|
||||
return config;
|
||||
} catch (pybind11::error_already_set& err) {
|
||||
::Slic3r::log_python_exception_keep(err);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void on_load() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
|
||||
@@ -83,8 +136,6 @@ class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabi
|
||||
public:
|
||||
using PyPluginCommonTrampoline<PluginCapabilityInterface>::PyPluginCommonTrampoline;
|
||||
|
||||
// get_name is implemented in PyPluginCommonTrampoline (PYBIND11_OVERRIDE_PURE).
|
||||
|
||||
PluginCapabilityType get_type() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
|
||||
@@ -1,8 +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>
|
||||
@@ -10,6 +13,8 @@
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginConfig.hpp"
|
||||
#include "host/PluginHost.hpp"
|
||||
#include "PyPluginPackage.hpp"
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
@@ -347,12 +352,63 @@ void bind_python_api(pybind11::module_& m)
|
||||
.def_static("skipped", &ExecutionResult::skipped, py::arg("message") = std::string())
|
||||
.def_static("failure", &ExecutionResult::failure, py::arg("status"), py::arg("message"), py::arg("data") = std::string());
|
||||
|
||||
// Config lives at the capability level, not as a global orca.* function: the host reads the
|
||||
// owning (plugin_key, capability) straight off the instance the call arrived on, so a
|
||||
// capability can only ever address its own config and never has to name itself.
|
||||
// Registered on the base, so every capability type (script/gcode/printer-agent) inherits it.
|
||||
py::class_<PluginCapabilityInterface, PyPluginInterfaceTrampoline, std::shared_ptr<PluginCapabilityInterface>>(m, "PythonPluginBase")
|
||||
.def(py::init<>())
|
||||
.def("get_name", &PluginCapabilityInterface::get_name)
|
||||
.def("get_type", &PluginCapabilityInterface::get_type)
|
||||
.def("on_load", &PluginCapabilityInterface::on_load)
|
||||
.def("on_unload", &PluginCapabilityInterface::on_unload);
|
||||
.def("on_unload", &PluginCapabilityInterface::on_unload)
|
||||
.def("has_config_ui", &PluginCapabilityInterface::has_config_ui,
|
||||
"Override to return True to replace the host's default JSON editor with your own HTML\n"
|
||||
"UI, returned by get_config_ui(). Every capability is configurable and appears in the\n"
|
||||
"Plugins dialog's Config tab regardless; this only chooses how its config is edited.")
|
||||
.def("get_config_ui", &PluginCapabilityInterface::get_config_ui,
|
||||
"Override to return the custom configuration UI as an HTML string. Only called when\n"
|
||||
"has_config_ui() is True; an empty result falls back to the default JSON editor.\n"
|
||||
"Inside the page, use window.orca.getConfig()/saveConfig() to reach this same config.")
|
||||
.def(
|
||||
"get_default_config",
|
||||
[](const PluginCapabilityInterface& self) {
|
||||
nlohmann::json config = self.get_default_config();
|
||||
return config.dump();
|
||||
},
|
||||
"Override to return the config that the Config tab's \"Restore defaults\" action writes\n"
|
||||
"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 config.dump();
|
||||
},
|
||||
"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); },
|
||||
"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 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 string (e.g. json.dumps(cfg)).\n"
|
||||
"The plugin key, capability name and version are supplied by the host. Returns False if\n"
|
||||
"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.
|
||||
|
||||
@@ -7,12 +7,34 @@
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
|
||||
struct PluginCapabilityId
|
||||
{
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown;
|
||||
std::string name;
|
||||
std::string plugin_key;
|
||||
|
||||
bool empty() const { return type == PluginCapabilityType::Unknown || name.empty() || plugin_key.empty(); }
|
||||
friend bool operator==(const PluginCapabilityId& lhs, const PluginCapabilityId& rhs)
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.name == rhs.name && lhs.plugin_key == rhs.plugin_key;
|
||||
}
|
||||
friend bool operator<(const PluginCapabilityId& lhs, const PluginCapabilityId& rhs)
|
||||
{
|
||||
if (lhs.plugin_key != rhs.plugin_key)
|
||||
return lhs.plugin_key < rhs.plugin_key;
|
||||
if (lhs.name != rhs.name)
|
||||
return lhs.name < rhs.name;
|
||||
return lhs.type < rhs.type;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
{
|
||||
switch (type) {
|
||||
@@ -127,6 +149,20 @@ public:
|
||||
virtual std::string get_name() const = 0; // required — overridden in Python
|
||||
virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } // optional — typed bases override
|
||||
|
||||
// Every capability is configurable and always gets the host's default JSON editor over its
|
||||
// stored config; this only says whether it supplies its own UI *instead of* that editor.
|
||||
// get_config_ui() is called only when it returns true.
|
||||
virtual bool has_config_ui() const { return false; }
|
||||
// An HTML snippet for the custom configuration UI. An empty or throwing result is treated as
|
||||
// "no custom UI" and falls back to the default JSON editor.
|
||||
virtual std::string get_config_ui() const { return ""; }
|
||||
|
||||
// The config the "Restore defaults" action writes back. Not overridden -> an empty object, which
|
||||
// is right for a capability that keeps its stored config sparse and applies its own defaults on
|
||||
// read. Override it to write an explicit starting config instead (e.g. to seed a form UI with
|
||||
// every field present). The host neither invents nor validates this value.
|
||||
virtual nlohmann::json get_default_config() const { return nlohmann::json::object(); }
|
||||
|
||||
virtual void on_load() {}
|
||||
virtual void on_unload() {}
|
||||
virtual void on_cancelled() {}
|
||||
@@ -137,9 +173,12 @@ public:
|
||||
// exactly as long as the capability does, and are discarded with it on unload. Nothing about a
|
||||
// capability outlives the capability — the durable record is the .install_state.json sidecar.
|
||||
|
||||
// Cached identity. Plain C++ reads, safe under any lock and after the interpreter is gone.
|
||||
// Cached identity. Plain C++ reads, safe under any lock and after the interpreter is gone. Also
|
||||
// doubles as the audited capability name (paired with audit_plugin_key()) so host APIs invoked
|
||||
// from Python can tell which capability they are serving.
|
||||
const std::string& name() const { return m_name; }
|
||||
PluginCapabilityType type() const { return m_type; }
|
||||
PluginCapabilityId identity() const { return {m_type, m_name, m_audit_plugin_key}; }
|
||||
void set_resolved_identity(std::string name, PluginCapabilityType type)
|
||||
{
|
||||
m_name = std::move(name);
|
||||
@@ -152,6 +191,12 @@ public:
|
||||
bool is_enabled() const { return m_enabled.load(std::memory_order_acquire); }
|
||||
void set_enabled(bool enabled) { m_enabled.store(enabled, std::memory_order_release); }
|
||||
|
||||
// Whether this capability supplies its own config UI (has_config_ui()), resolved once at
|
||||
// materialization under the GIL and cached here. Plain C++ read so the GUI can decide between
|
||||
// the capability's custom UI and the host's default JSON editor without touching Python.
|
||||
bool config_ui_available() const { return m_config_ui_available; }
|
||||
void set_config_ui_available(bool available) { m_config_ui_available = available; }
|
||||
|
||||
// The owning package (PluginDescriptor::plugin_key), the canonical runtime id. Also scopes
|
||||
// filesystem enforcement for trampoline calls.
|
||||
void set_audit_plugin_key(std::string key) { m_audit_plugin_key = std::move(key); }
|
||||
@@ -165,6 +210,7 @@ private:
|
||||
std::string m_name;
|
||||
PluginCapabilityType m_type = PluginCapabilityType::Unknown;
|
||||
std::atomic<bool> m_enabled{true};
|
||||
bool m_config_ui_available = false;
|
||||
std::string m_audit_plugin_key;
|
||||
|
||||
mutable std::atomic<int> m_refs{0};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "slic3r/plugin/PluginAuditManager.hpp"
|
||||
#include "slic3r/plugin/PythonInterpreter.hpp" // PythonGILState
|
||||
#include "slic3r/plugin/PluginFsUtils.hpp" // json_to_py / py_to_json
|
||||
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
@@ -34,63 +35,6 @@ using json = nlohmann::json;
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// JSON <-> Python conversion (caller must hold the GIL).
|
||||
// --------------------------------------------------------------------------
|
||||
py::object json_to_py(const json& j)
|
||||
{
|
||||
switch (j.type()) {
|
||||
case json::value_t::null: return py::none();
|
||||
case json::value_t::boolean: return py::bool_(j.get<bool>());
|
||||
case json::value_t::number_integer: return py::int_(j.get<std::int64_t>());
|
||||
case json::value_t::number_unsigned: return py::int_(j.get<std::uint64_t>());
|
||||
case json::value_t::number_float: return py::float_(j.get<double>());
|
||||
case json::value_t::string: return py::str(j.get<std::string>());
|
||||
case json::value_t::array: {
|
||||
py::list lst;
|
||||
for (const auto& e : j)
|
||||
lst.append(json_to_py(e));
|
||||
return lst;
|
||||
}
|
||||
case json::value_t::object: {
|
||||
py::dict d;
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
d[py::str(it.key())] = json_to_py(it.value());
|
||||
return d;
|
||||
}
|
||||
default: return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
json py_to_json(const py::handle& o)
|
||||
{
|
||||
if (o.is_none())
|
||||
return json(nullptr);
|
||||
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
|
||||
return o.cast<bool>();
|
||||
if (py::isinstance<py::int_>(o))
|
||||
return o.cast<std::int64_t>();
|
||||
if (py::isinstance<py::float_>(o))
|
||||
return o.cast<double>();
|
||||
if (py::isinstance<py::str>(o))
|
||||
return o.cast<std::string>();
|
||||
if (py::isinstance<py::bytes>(o))
|
||||
return o.cast<std::string>();
|
||||
if (py::isinstance<py::dict>(o)) {
|
||||
json obj = json::object();
|
||||
for (auto item : py::reinterpret_borrow<py::dict>(o))
|
||||
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
|
||||
return obj;
|
||||
}
|
||||
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
|
||||
json arr = json::array();
|
||||
for (auto e : o)
|
||||
arr.push_back(py_to_json(e));
|
||||
return arr;
|
||||
}
|
||||
return py::str(o).cast<std::string>(); // fallback: str()
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// GIL-safe holder for a Python callable. A std::function that captured a bare
|
||||
// py::object could be destroyed on the main thread without the GIL (a dialog
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "SlicingPipelinePluginCapabilityTrampoline.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp" // config_value_or_none
|
||||
#include "libslic3r/libslic3r.h" // unscale<>, live SCALING_FACTOR
|
||||
#include <pybind11/stl.h> // std::map<std::string,std::string> -> dict for ctx.params
|
||||
|
||||
namespace py = pybind11;
|
||||
namespace Slic3r {
|
||||
@@ -31,7 +30,7 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
|
||||
// ctx.object are None; instead ctx.gcode_path / ctx.host / ctx.output_name are set and the plugin
|
||||
// edits the file at ctx.gcode_path IN PLACE. May fire more than once per slice (file export and/or
|
||||
// upload each fire once, on separate working copies) and its output is not reflected in the G-code
|
||||
// preview (the viewer maps the pre-post-process file). ctx.config_value()/ctx.params still work.
|
||||
// preview (the viewer maps the pre-post-process file). ctx.config_value() still works.
|
||||
.value("psGCodePostProcess", SlicingPipelineStepPlugin::psGCodePostProcess)
|
||||
.export_values();
|
||||
|
||||
@@ -48,9 +47,6 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
|
||||
py::class_<SlicingPipelineContext>(slicing, "SlicingPipelineContext")
|
||||
.def_readonly("orca_version", &SlicingPipelineContext::orca_version)
|
||||
.def_readonly("step", &SlicingPipelineContext::step)
|
||||
.def_readonly("params", &SlicingPipelineContext::params,
|
||||
"read-only dict of this plugin's [tool.orcaslicer.plugin.settings] values "
|
||||
"(string->string). Parse the values you need, e.g. float(ctx.params['rate']).")
|
||||
.def_readonly("gcode_path", &SlicingPipelineContext::gcode_path,
|
||||
"Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in "
|
||||
"place; empty at every other step.")
|
||||
|
||||
@@ -18,10 +18,6 @@ struct SlicingPipelineContext {
|
||||
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
|
||||
Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
|
||||
const PrintObject* object { nullptr }; // null for print-wide steps and psGCodePostProcess
|
||||
// read-only per-plugin settings, populated by the dispatcher from the
|
||||
// plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as
|
||||
// ctx.params (dict of string->string).
|
||||
std::map<std::string, std::string> params;
|
||||
// Populated ONLY at Step.psGCodePostProcess (the GUI G-code export/post-process seam,
|
||||
// PostProcessor.cpp). gcode_path is the working G-code file on disk that the plugin edits
|
||||
// in place; host is the target ("File", "OctoPrint", ...); output_name mirrors
|
||||
|
||||
Reference in New Issue
Block a user