feat: introduce plugin APIs for mutable presets

This commit is contained in:
Ian Chua committed 2026-10-15 20:32:49 +08:00
1 parent aacc36b9d2
commit 193cfc6dbf
7 files changed
+1369

No files matched your search

+15
View File
@@ -1332,6 +1332,21 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi
return ret;
}
bool ConfigOptionsGroup::commit_config_value(const t_config_option_key& opt_key, int opt_index)
{
if (m_config == nullptr)
return false;
// m_opt_map is filled while the option lines are created, so it still resolves the option when
// the group's controls (and thus its fields) have not been built yet.
const std::string opt_id = option_id_in_map(m_opt_map, opt_key, opt_index);
if (opt_id.empty())
return false;
on_change_OG(opt_id, get_config_value2(*m_config, opt_key, opt_index));
return true;
}
Field* ConfigOptionsGroup::get_fieldc(const t_config_option_key& opt_key, int opt_index)
{
Field* field = get_field(opt_key);
+40
View File
@@ -119,6 +119,37 @@ using column_t = std::function<wxWindow*(wxWindow* parent, const Line&)>;
using t_optionfield_map = std::map<t_config_option_key, t_field>;
using t_opt_map = std::map< std::string, std::pair<std::string, int> >;
// The option id a group's option map holds for opt_key, and the index it carries. `opt_index` is
// an in/out parameter: a negative value asks for the option's single entry, an explicit one selects
// that index. Returns an empty string when the map does not own the option, or when a negative index
// matches several entries -- a vector option the GUI edits one index at a time, which has no single
// value to commit. A group fills the map while its option lines are created (OptionsGroup::append_line
// / ConfigOptionsGroup::get_option), so this resolves an option even before its controls (and its
// field) have been built.
inline std::string option_id_in_map(const t_opt_map& options, const t_config_option_key& opt_key, int& opt_index)
{
if (opt_index >= 0) {
for (const auto& opt : options) {
if (opt.second.first == opt_key && opt.second.second == opt_index) {
opt_index = opt.second.second;
return opt.first;
}
}
return {};
}
std::string opt_id;
for (const auto& opt : options) {
if (opt.second.first != opt_key)
continue;
if (!opt_id.empty())
return {}; // several entries: a vector option, not one value
opt_id = opt.first;
opt_index = opt.second.second;
}
return opt_id;
}
class OptionsGroup {
public:
const bool staticbox{true};
@@ -332,6 +363,15 @@ public:
boost::any get_config_value(const DynamicPrintConfig& config, const std::string& opt_key, int opt_index = -1);
// BBS: restore all pages in preset
boost::any get_config_value2(const DynamicPrintConfig& config, const std::string& opt_key, int opt_index = -1);
// Re-run this group's committed-change path for one of its options without a widget event:
// build the option's value from the config this group edits (get_config_value2, the same form
// the option's field reports) and dispatch on_change_OG(), so the group's option-specific
// conversion (e.g. the printer_agent canonicalization) and its registered change handler run
// exactly as for a hand edit. The option map is filled while the option lines are created, so
// this resolves an option whose field is not built yet (a lazy page, or an option the GUI has
// no field for). Returns false when this group does not own the option, or when opt_index < 0
// matches several entries (a vector option the GUI edits index by index has no single value).
bool commit_config_value(const t_config_option_key& opt_key, int opt_index = -1);
Field* get_fieldc(const t_config_option_key& opt_key, int opt_index);
std::pair<OG_CustomCtrl*, bool*> get_custom_ctrl_with_blinking_ptr(const t_config_option_key& opt_key, int opt_index/* = -1*/);
+96
View File
@@ -46,6 +46,7 @@
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
#include <deque>
#include <exception>
#include <stdexcept>
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Point.hpp"
#include "slic3r/GUI/SettingsIndex.hpp"
@@ -1645,6 +1646,101 @@ void Tab::load_config(const DynamicPrintConfig& config)
}
}
// The option keys a preset of `type` may hold. The tab's config shares the global
// print_config_def with every other preset type, so membership has to be checked against the
// preset type's own key list instead of the config definition, or a printer setting could be
// written into the process preset.
static const std::vector<std::string>& preset_type_options(Preset::Type type)
{
static const std::vector<std::string> none;
switch (type) {
case Preset::TYPE_PRINT: return Preset::print_options();
case Preset::TYPE_FILAMENT: return Preset::filament_options();
case Preset::TYPE_PRINTER: return Preset::printer_options();
default: return none;
}
}
static const char* preset_type_name(Preset::Type type)
{
switch (type) {
case Preset::TYPE_PRINT: return "process";
case Preset::TYPE_FILAMENT: return "filament";
case Preset::TYPE_PRINTER: return "printer";
default: return "preset";
}
}
// Set one option of the currently edited preset from its serialized string form, then run the
// same update path a hand edit runs. The edited preset is not saved: it only becomes dirty,
// exactly as if the field had been edited by hand.
void Tab::apply_preset_config_value(const std::string& opt_key, const std::string& value)
{
if (m_config == nullptr)
throw std::runtime_error("The preset configuration is not available");
if (wxGetApp().plater() == nullptr)
throw std::runtime_error("OrcaSlicer main window is not available");
const std::vector<std::string>& options = preset_type_options(m_type);
if (std::find(options.begin(), options.end(), opt_key) == options.end())
throw std::invalid_argument(format("Unknown %1% preset setting \"%2%\"", preset_type_name(m_type), opt_key));
// Parse strictly, keeping a backup: the strict deserializer can already have mutated the option
// when it rejects a bad value, so a failed update has to restore the preset it was given.
const ConfigOption* previous_option = m_config->option(opt_key);
std::unique_ptr<ConfigOption> previous(previous_option != nullptr ? previous_option->clone() : nullptr);
try {
m_config->set_deserialize_strict(opt_key, value);
} catch (const ConfigurationError& err) {
if (previous)
m_config->set_key_value(opt_key, previous.release());
else
m_config->erase(opt_key);
throw std::invalid_argument(err.what());
}
// Run the same commit path the GUI runs for this setting, so its key-specific behaviour
// (ConfigOptionsGroup::on_change_OG, including the printer_agent canonicalization, the group's
// pre-checks and Tab::on_value_change()) runs exactly as for a hand edit. A built field supplies
// the field's typed/clamped value; otherwise the owning ConfigOptionsGroup builds the value from
// the config it edits and dispatches the same on_change_OG(). The field is refreshed from the
// config just committed first, so it reports the committed value rather than a stale one.
bool committed = false;
if (Page* field_page = nullptr; Field* field = get_field(opt_key, &field_page)) {
field_page->reload_config();
field->field_changed();
committed = true;
} else {
for (const PageShp& page : m_pages) {
for (const ConfigOptionsGroupShp& group : page->m_optgroups) {
if (group->commit_config_value(opt_key)) {
committed = true;
break;
}
}
if (committed)
break;
}
}
if (committed) {
// The commit path already ran update_dirty() (dirty flag, preset combo and, on the printer
// tab, the printer-agent switch); refresh the visible page and push the config to the plater.
reload_config();
update();
} else {
// The GUI edits this setting through a custom widget rather than a group (e.g.
// compatible_printers), so take the same shared no-field commit path the widget uses. Saved
// mode skips the value write (the strict parse above already did it) and keeps the path's
// compatibility recompute, dirty flag, preset-combo and plater updates.
load_key_value(opt_key, boost::any(), /*saved_value=*/true);
// The widget itself normally re-reads its value after load_key_value(); with no widget to do
// that, sync the visible page so a custom widget (the compatible-printers row) reflects the
// new value.
reload_config();
}
}
// Reload current $self->{config} (aka $self->{presets}->edited_preset->config) into the UI fields.
void Tab::reload_config()
{
+11
View File
@@ -424,6 +424,17 @@ public:
//BBS update plater presets if update_plater_presets = true
void update_tab_ui(bool update_plater_presets = false);
void load_config(const DynamicPrintConfig& config);
// Host/plugin API: set one option of this tab's currently edited preset from its serialized
// string value, then run the same committed-change path a field edit runs. `opt_key` must be a
// setting of this tab's preset type, so a key of another preset type (e.g. a printer setting
// passed to the process tab) is rejected instead of written silently. `value` is parsed by the
// strict config deserializer, which raises std::invalid_argument for an unknown key or a value
// the option cannot parse, and leaves the preset unchanged. The key-specific behaviour runs
// through its ConfigOptionsGroup::on_change_OG(): with the option's built field it uses the
// field's typed value, otherwise the owning group builds the value and dispatches the same call,
// so a not-yet-built field still gets it. A key the GUI edits through a custom widget instead
// takes the shared no-field commit path, Tab::load_key_value() in saved mode.
void apply_preset_config_value(const std::string& opt_key, const std::string& value);
virtual void reload_config();
void update_mode();
void update_visibility();