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

@@ -41,12 +41,8 @@
<div id="configFooter" class="config-view-footer" hidden>
<span id="configValidation" class="config-validation" role="status" aria-live="polite"></span>
<div class="config-actions">
<button id="configUseGlobalBtn" class="ButtonStyleRegular ButtonTypeChoice" type="button"
title="Remove the preset override and use the global configuration again">
Use global
</button>
<button id="configRestoreBtn" class="ButtonStyleRegular ButtonTypeChoice" type="button"
title="Write the plugin defaults into this preset override">
title="Discard this preset's override and use the global configuration again">
Restore defaults
</button>
<button id="configSaveBtn" class="ButtonStyleConfirm ButtonTypeChoice" type="button">Save</button>

View File

@@ -233,21 +233,19 @@ function UpdateConfigMeta(payload) {
const meta = document.getElementById("configMeta");
const badge = document.getElementById("configSourceBadge");
const useGlobal = document.getElementById("configUseGlobalBtn");
const save = document.getElementById("configSaveBtn");
const restore = document.getElementById("configRestoreBtn");
if (!meta || !badge || !useGlobal)
if (!meta || !badge)
return;
const source = String(payload?.source || "none");
badge.textContent = source === "preset" ? "Preset override" :
(source === "base" ? "Inherited from global configuration" : "No saved configuration");
useGlobal.hidden = !selectedHasPresetOverride;
useGlobal.disabled = selectedReadOnly;
if (save)
save.disabled = selectedReadOnly;
// There is nothing to discard when the preset holds no override of its own.
if (restore)
restore.disabled = selectedReadOnly;
restore.disabled = selectedReadOnly || !selectedHasPresetOverride;
meta.hidden = false;
}
@@ -361,22 +359,11 @@ function SaveCapabilityConfig() {
});
}
// Asks the native side to write the capability's default config over whatever is stored. The
// defaults come from the capability's get_default_config(), never from this page — the host does not
// know what a given plugin considers default. The native side confirms before discarding anything,
// and replies with the same "saved" payload, so both editors reload from what was persisted.
// Drops the preset's override, which is what "default" means here: the capability falls back to the
// global configuration, the same as a preset that was never overridden. The native side confirms
// before discarding it and then re-sends the capability's config, so the editors reload from what is
// now effective rather than from what was typed.
function RestoreCapabilityConfig() {
if (!selectedPluginKey || !selectedCapabilityName)
return;
SendMessage("restore_preset_defaults", {
plugin_key: selectedPluginKey,
capability_name: selectedCapabilityName,
capability_type: selectedCapabilityType
});
}
function UseGlobalCapabilityConfig() {
if (!selectedPluginKey || !selectedCapabilityName || !selectedHasPresetOverride)
return;
@@ -482,10 +469,6 @@ document.addEventListener("DOMContentLoaded", () => {
if (restoreBtn)
restoreBtn.addEventListener("click", RestoreCapabilityConfig);
const useGlobalBtn = document.getElementById("configUseGlobalBtn");
if (useGlobalBtn)
useGlobalBtn.addEventListener("click", UseGlobalCapabilityConfig);
const text = document.getElementById("configText");
if (text)
text.addEventListener("input", ValidateConfigText);

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

View File

@@ -73,3 +73,19 @@ TEST_CASE("referenced_capabilities skips malformed manifest entries", "[PluginRe
CHECK(capability_names(referenced_capabilities(Preset::TYPE_PRINT, preset)) == std::vector<std::string>{"CapA"});
}
TEST_CASE("preset_type_for_capability names the preset type whose options reference the capability", "[PluginResolver]")
{
// Read out of the ConfigDef: slicing_pipeline_plugin is a print option, printer_agent a printer
// one. Declaring a plugin_type on an option is what puts its capability type on this map.
CHECK(preset_type_for_capability(PluginCapabilityType::SlicingPipeline) == Preset::TYPE_PRINT);
CHECK(preset_type_for_capability(PluginCapabilityType::PrinterConnection) == Preset::TYPE_PRINTER);
}
TEST_CASE("preset_type_for_capability leaves capability types no option accepts unowned", "[PluginResolver]")
{
// Nothing can reference these from a preset, so no preset can override them either: they read
// their config from the base config.json alone.
CHECK(preset_type_for_capability(PluginCapabilityType::Automation) == Preset::TYPE_INVALID);
CHECK(preset_type_for_capability(PluginCapabilityType::Unknown) == Preset::TYPE_INVALID);
}