mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-15 15:03:49 +00:00
fix: preset overrides on sidebar
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user