fix: preset overrides on sidebar

This commit is contained in:
Ian Chua
2026-07-14 12:57:06 +08:00
parent 796080ff76
commit 86a4cec753
10 changed files with 181 additions and 71 deletions

View File

@@ -102,48 +102,22 @@ void PluginsConfigDialog::on_script_message(const nlohmann::json& payload)
}
send_capability_config(id);
show_status(_L("Configuration updated. Save the preset to persist it."), "success");
} else if (command == "restore_preset_defaults") {
} else if (command == "remove_preset_override") {
// "Restore defaults" for a preset means holding no override at all: the capability goes back
// to the global configuration, which is where an untouched preset takes its values from. The
// plugin's own get_default_config() is the global config's default, not the preset's.
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
"This writes the plugin defaults into the current preset override."),
"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 auto cap = PluginManager::instance().get_loader().get_plugin_capability_by_name(id);
if (!cap)
return;
nlohmann::json defaults;
try {
wxBusyCursor busy;
PythonGILState gil;
defaults = cap->instance->get_default_config();
} catch (const std::exception& ex) {
// A broken plugin must not be able to wipe the stored config: store nothing and say so.
send_save_error(id, into_u8(GUI::format_wxstr(_L("The plugin failed to supply its default configuration (%1%)."),
from_u8(ex.what()))));
return;
}
const MutationResult result = m_service.set_preset_override(m_overrides, id, defaults);
if (!result.ok) {
send_save_error(id, result.error);
return;
}
send_capability_config(id);
show_status(_L("Default configuration restored. Save the preset to persist it."), "success");
} else if (command == "remove_preset_override") {
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
const MutationResult result = m_service.remove_preset_override(m_overrides, id);
if (!result.ok) {
send_save_error(id, result.error);

View File

@@ -9,6 +9,7 @@
#include <slic3r/GUI/format.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PresetPluginConfig.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <stdexcept>
@@ -43,6 +44,25 @@ std::pair<std::string, std::string> capability_identity(const PluginCapabilityIn
return id;
}
// The identity above, completed with the type, which decides which preset may override the
// capability (see preset_type_for_capability). Taken from the instance rather than from the loader's
// registry: a capability calling get_config() from on_load() is not registered yet, and it must
// still see its preset's config. get_type() is the plugin's own method, so a raising one costs it
// only the preset layer — Unknown names no preset, and the base config answers as it always did.
PluginCapabilityIdentifier capability_full_identity(const PluginCapabilityInterface& capability, const char* api_name)
{
const auto [plugin_key, capability_name] = capability_identity(capability, api_name);
PluginCapabilityType type = PluginCapabilityType::Unknown;
try {
type = capability.get_type();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "Capability '" << capability_name << "' of plugin '" << plugin_key
<< "': get_type() failed (" << ex.what() << "); reading the base config only";
}
return {type, capability_name, plugin_key};
}
} // namespace
void PluginConfig::load()
@@ -178,18 +198,19 @@ bool PluginConfig::dirty() const
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability)
{
const auto [plugin_key, capability_name] = capability_identity(capability, "get_config");
const BaseConfig config = PluginManager::instance().get_config().get_config(plugin_key, capability_name);
// Never saved: hand back an empty object so a plugin can index the result unconditionally.
return config.empty() ? nlohmann::json::object() : config.config;
// The active preset's override, when it has one, is the config this run must use: it is what the
// user attached to the preset being sliced, and config.json is the fallback. The resolution is
// shared with the dialogs, so a capability reads back exactly the config its UI showed as
// effective. Stored in neither layer: an empty object, so a plugin can index it unconditionally.
return active_capability_config(capability_full_identity(capability, "get_config")).config;
}
std::string capability_get_config_version(const PluginCapabilityInterface& capability)
{
const auto [plugin_key, capability_name] = capability_identity(capability, "get_config_version");
return PluginManager::instance().get_config().get_config(plugin_key, capability_name).plugin_version;
// The version that wrote the config get_config() just handed out, whichever layer that was: the
// two must come from the same layer, or a plugin would migrate one layer's config by another's
// version stamp.
return active_capability_config(capability_full_identity(capability, "get_config_version")).stored_plugin_version;
}
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config)

View File

@@ -151,12 +151,17 @@ private:
// passed in from Python, so a capability cannot reach another capability's config.
// Throw std::runtime_error (surfacing to Python as RuntimeError) on an unmaterialized instance.
// Only the user-editable cap_config. An empty object when nothing has been saved yet.
// Only the user-editable cap_config, resolved the way the config UI presents it: the active preset's
// override when it has one, otherwise the config stored here (see active_capability_config). An
// empty object when neither layer holds one.
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability);
// The plugin version that last wrote the entry, so a plugin can migrate a stale cap_config.
// Empty when the capability has no stored config.
// The plugin version that last wrote the config get_config() returns — the same layer it came from —
// so a plugin can migrate a stale cap_config. Empty when the capability has no stored config.
std::string capability_get_config_version(const PluginCapabilityInterface& capability);
// Replaces cap_config and persists. Host-managed identity and version metadata are preserved.
// Writes the store here, never a preset: presets are the user's to edit, and a plugin saving from a
// worker thread cannot mark one dirty. A capability whose active preset overrides it will therefore
// keep reading that override back, not what it saved.
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config);
} // namespace Slic3r

View File

@@ -184,6 +184,36 @@ std::vector<PluginCapabilityIdentifier> capabilities_in_use(Preset::Type type, c
return result;
}
Preset::Type preset_type_for_capability(PluginCapabilityType type)
{
if (type == PluginCapabilityType::Unknown)
return Preset::TYPE_INVALID;
// The three typed option lists are disjoint, so the first that holds the key names its owner.
const auto owner_of_option = [](const std::string& key) {
const auto holds = [&key](const std::vector<std::string>& keys) {
return std::find(keys.begin(), keys.end(), key) != keys.end();
};
if (holds(Preset::print_options()))
return Preset::TYPE_PRINT;
if (holds(Preset::filament_options()))
return Preset::TYPE_FILAMENT;
if (holds(Preset::printer_options()))
return Preset::TYPE_PRINTER;
return Preset::TYPE_INVALID;
};
const std::string type_key = plugin_capability_type_to_string(type);
for (const auto& [key, def] : print_config_def.options) {
if (def.plugin_type != type_key)
continue;
const Preset::Type owner = owner_of_option(key);
if (owner != Preset::TYPE_INVALID)
return owner;
}
return Preset::TYPE_INVALID;
}
static std::string resolve_cloud_base_url()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";

View File

@@ -89,6 +89,13 @@ std::string resolve_recovery_url(const PluginCapabilityRef& ref);
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset);
std::vector<PluginCapabilityIdentifier> capabilities_in_use(Preset::Type type, const Preset& preset);
// The preset type that owns capabilities of `type` — the one whose presets can reference them and
// therefore carry their overrides. Derived from the ConfigDef rather than hardcoded: a plugin-backed
// option names the capability type it accepts (ConfigOptionDef::plugin_type) and belongs to exactly
// one preset type, so declaring the option is all it takes to map a new capability type.
// TYPE_INVALID when no option accepts the type (nothing can reference it, so no preset owns it).
Preset::Type preset_type_for_capability(PluginCapabilityType type);
// The capabilities the active preset(s) of `type` reference (see referenced_capabilities) and that
// are loaded right now: the set that can actually be configured. Missing and broken refs are absent,
// having no instance to ask for a config UI or defaults. A loaded-but-disabled capability IS listed —

View File

@@ -1,6 +1,13 @@
#include "PresetPluginConfig.hpp"
#include "PluginManager.hpp"
#include "PluginResolver.hpp"
#include <slic3r/GUI/GUI_App.hpp>
#include <boost/log/trivial.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <wx/app.h>
namespace Slic3r {
@@ -19,6 +26,48 @@ std::string running_plugin_version(const std::string& plugin_key)
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
}
// Null wherever the plugin host runs without the GUI app (the unit tests): there are no presets
// then, only the base config. wxGetApp() dereferences the app unconditionally, so ask wxWidgets.
const PresetBundle* active_preset_bundle()
{
const auto* app = dynamic_cast<const GUI::GUI_App*>(wxApp::GetInstance());
return app == nullptr ? nullptr : app->preset_bundle;
}
// The active preset that can carry an override for a capability of `type`. Edited, not selected: an
// override typed into the tab configures the next slice the same way every other unsaved setting does.
const Preset* active_preset_for(Preset::Type type)
{
const PresetBundle* bundle = active_preset_bundle();
if (bundle == nullptr)
return nullptr;
switch (type) {
case Preset::TYPE_PRINT: return &bundle->prints.get_edited_preset();
case Preset::TYPE_PRINTER: return &bundle->printers.get_edited_preset();
// Deliberately unimplemented, not forgotten.
//
// There is no single active filament preset: one is selected per extruder, and a capability
// calls get_config() without saying which extruder it is running for, so we cannot tell which
// of them owns the config. Guessing (first override wins, or extruder 0) would hand a plugin
// another extruder's settings and look like a plugin bug, so it reads the base config instead —
// the value it had before presets could override anything.
//
// Nothing reaches this today: preset_type_for_capability only names TYPE_FILAMENT once a
// filament option declares a plugin_type, and none does (the two plugin-backed options are
// print and printer ones). Solve it with the first filament-backed capability, by pushing the
// extruder onto the plugin call context the trampoline already maintains
// (ScopedPluginAuditContext) along with the override text snapshotted off the preset, and
// resolving the preset here from that instead of from the bundle. The extruder must be optional:
// whole slicing steps (posSlice, psGCodePostProcess) span every extruder and have no current
// filament, and this fallback is the honest answer for them.
case Preset::TYPE_FILAMENT: return nullptr;
default: return nullptr;
}
}
} // namespace
std::string plugin_overrides_of(const Preset& preset)
@@ -125,4 +174,22 @@ MutationResult PresetPluginConfigService::remove_preset_override(CapabilityConfi
return result;
}
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityIdentifier& id)
{
const PresetPluginConfigService service;
CapabilityConfigDocument overrides;
if (const Preset* preset = active_preset_for(preset_type_for_capability(id.type))) {
std::string error;
if (!parse_plugin_overrides(plugin_overrides_of(*preset), overrides, error)) {
// Text we cannot read is not an override. Say so and resolve against the base config,
// which is what the capability ran with before anything was written into the preset.
BOOST_LOG_TRIVIAL(error) << "Preset \"" << preset->name << "\": " << error;
overrides = CapabilityConfigDocument();
}
}
return service.get_effective_config(overrides, id);
}
} // namespace Slic3r

View File

@@ -67,4 +67,15 @@ public:
const PluginCapabilityIdentifier& id) const;
};
// The same `preset override -> base config -> none` resolution, against the preset that is active
// right now instead of a document the caller holds: this is what a running capability reads through
// the Python config API, so a preset that overrides a capability configures the slice it drives.
//
// Only one preset type can reference a given capability type (preset_type_for_capability), so there
// is exactly one preset to consult. Filament capabilities are the exception and are not supported:
// the active filament preset is per-extruder and a capability does not say which extruder it runs
// for, so they read the base config. See active_preset_for() for why, and for what it will take to
// lift that. Base config also in a host with no preset bundle (the plugin unit tests).
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityIdentifier& id);
} // namespace Slic3r