mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
1069 lines
48 KiB
C++
1069 lines
48 KiB
C++
#include "PublishSettingsDialog.hpp"
|
||
|
||
#include "GUI_App.hpp"
|
||
#include "MainFrame.hpp"
|
||
#include "MsgDialog.hpp"
|
||
#include "I18N.hpp"
|
||
#include "Tab.hpp"
|
||
#include "ConfigValueFormatter.hpp"
|
||
#include "Widgets/Label.hpp"
|
||
#include "Widgets/TextInput.hpp"
|
||
#include "Widgets/DialogButtons.hpp"
|
||
#include "Widgets/StaticLine.hpp"
|
||
#include "Widgets/StateColor.hpp"
|
||
|
||
#include "libslic3r/PrintConfig.hpp"
|
||
#include "libslic3r/Preset.hpp"
|
||
#include "libslic3r/PublishSettings.hpp"
|
||
|
||
#include <boost/algorithm/string/trim.hpp>
|
||
#include <set>
|
||
#include <algorithm>
|
||
|
||
namespace Slic3r { namespace GUI {
|
||
namespace {
|
||
|
||
// Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level
|
||
// bindings (e.g. MainFrame's recent-files wxID_FILE1.. range).
|
||
enum {
|
||
kPublishSelectAll = wxID_HIGHEST + 1,
|
||
kPublishDeselectAll,
|
||
kPublishSelectVisible,
|
||
kPublishDeselectVisible,
|
||
kPublishFilterSelected,
|
||
kPublishFilterNonSelected
|
||
};
|
||
|
||
// Tree-style flags shared by the outer tabs and every group's inner tabs.
|
||
constexpr long s_tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS |
|
||
wxTR_FULL_ROW_HIGHLIGHT;
|
||
|
||
// The author's hex colour for a filament slot (empty when the slot is out of range); shared by
|
||
// the header chip, the inner-tab chip and the DPI rescale paths.
|
||
std::string filament_color_hex(const DynamicPrintConfig& full, size_t slot)
|
||
{
|
||
if (const auto* colours = full.opt<ConfigOptionStrings>("filament_colour"))
|
||
if (slot < colours->size())
|
||
return colours->get_at(slot);
|
||
return std::string();
|
||
}
|
||
|
||
PublishMaterialIdentity material_identity(size_t slot, const DynamicPrintConfig& full)
|
||
{
|
||
PublishMaterialIdentity identity;
|
||
if (const auto* types = full.opt<ConfigOptionStrings>("filament_type"))
|
||
if (slot < types->size())
|
||
identity.type = types->get_at(slot);
|
||
if (const auto* vendors = full.opt<ConfigOptionStrings>("filament_vendor"))
|
||
if (slot < vendors->size())
|
||
identity.vendor = vendors->get_at(slot);
|
||
if (const auto* ids = full.opt<ConfigOptionStrings>("filament_ids"))
|
||
if (slot < ids->size())
|
||
identity.id = ids->get_at(slot);
|
||
return identity;
|
||
}
|
||
|
||
// "Generic PLA @System" -> "Generic PLA"; mirrors the alias derivation in PresetBundle.cpp.
|
||
std::string material_display_name(const std::string& preset_name)
|
||
{
|
||
const size_t at = preset_name.find_first_of('@');
|
||
if (at == std::string::npos)
|
||
return preset_name;
|
||
std::string bare = preset_name.substr(0, at);
|
||
boost::trim_right(bare);
|
||
return bare.empty() ? preset_name : bare;
|
||
}
|
||
|
||
// Section title for a filament slot: the resolved preset name, then the filament type, then
|
||
// the generic "Material".
|
||
wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPrintConfig& full)
|
||
{
|
||
if (slot < bundle->filament_presets.size()) {
|
||
const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[slot]);
|
||
if (preset != nullptr && !preset->name.empty())
|
||
return from_u8(material_display_name(preset->name));
|
||
}
|
||
const PublishMaterialIdentity identity = material_identity(slot, full);
|
||
if (!identity.type.empty())
|
||
return from_u8(identity.type);
|
||
return _L("Material");
|
||
}
|
||
|
||
} // namespace
|
||
|
||
PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent)
|
||
: DPIDialog(parent ? parent : static_cast<wxWindow*>(wxGetApp().mainframe),
|
||
wxID_ANY,
|
||
_L("Publish 3MF..."),
|
||
wxDefaultPosition,
|
||
wxDefaultSize,
|
||
wxCAPTION | wxCLOSE_BOX | wxRESIZE_BORDER)
|
||
, m_search(this, "search", 16)
|
||
, m_menu(this, "filter", 16)
|
||
{
|
||
SetBackgroundColour(*wxWHITE);
|
||
|
||
// --- filter bar: search box, All/None, menu button ---
|
||
wxPanel* f_bar = new wxPanel(this, wxID_ANY);
|
||
f_bar->SetBackgroundColour(GetBackgroundColour());
|
||
wxBoxSizer* f_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||
|
||
m_filter_box = new TextInput(f_bar, "", "", "", wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER);
|
||
m_filter_box->SetIcon(m_search.bmp());
|
||
m_filter_box->SetMinSize(FromDIP(wxSize(200, 24)));
|
||
m_filter_box->SetSize(FromDIP(wxSize(-1, 24)));
|
||
m_filter_box->SetFocus();
|
||
m_filter_ctrl = m_filter_box->GetTextCtrl();
|
||
m_filter_ctrl->SetFont(Label::Body_13);
|
||
m_filter_ctrl->SetSize(wxSize(-1, FromDIP(16))); // centers text vertically
|
||
m_filter_ctrl->SetHint(_L("Type to filter..."));
|
||
m_filter_ctrl->Bind(wxEVT_TEXT, [this](auto&) { apply_filter(m_filter_ctrl->GetValue()); });
|
||
m_filter_ctrl->Bind(wxEVT_TEXT_ENTER, [this](auto&) { apply_filter(m_filter_ctrl->GetValue()); });
|
||
m_filter_ctrl->Bind(wxEVT_SET_FOCUS, [this](auto& e) {
|
||
apply_filter(m_filter_ctrl->GetValue());
|
||
e.Skip();
|
||
});
|
||
m_filter_ctrl->Bind(wxEVT_KILL_FOCUS, [this](auto& e) {
|
||
apply_filter(m_filter_ctrl->GetValue());
|
||
e.Skip();
|
||
});
|
||
f_sizer->Add(m_filter_box, 1, wxEXPAND);
|
||
|
||
m_fb_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||
auto create_btn = [this, f_bar](wxString title, bool select) {
|
||
auto btn = new wxStaticText(f_bar, wxID_ANY, title);
|
||
btn->SetForegroundColour("#009687");
|
||
btn->SetCursor(wxCURSOR_HAND);
|
||
btn->SetFont(Label::Body_13);
|
||
btn->Bind(wxEVT_LEFT_DOWN, [this, select](wxMouseEvent&) { select_all(select); });
|
||
m_fb_sizer->Add(btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10));
|
||
};
|
||
f_sizer->Add(m_fb_sizer, 0, wxALIGN_CENTER_VERTICAL);
|
||
create_btn(_L("All"), true);
|
||
create_btn(_L("None"), false);
|
||
|
||
// Indicator for the menu's pseudo filters: sits where All/None go while they are hidden.
|
||
// Labels reuse the menu entries (no new strings); clicking the chip returns to text
|
||
// filtering, keeping the search box contents.
|
||
m_pseudo_chip = new wxStaticText(f_bar, wxID_ANY, "");
|
||
m_pseudo_chip->SetForegroundColour("#009687");
|
||
m_pseudo_chip->SetCursor(wxCURSOR_HAND);
|
||
m_pseudo_chip->SetFont(Label::Body_13);
|
||
m_pseudo_chip->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { apply_filter(m_filter_ctrl->GetValue()); });
|
||
m_pseudo_chip->Hide();
|
||
f_sizer->Add(m_pseudo_chip, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10));
|
||
|
||
m_menu_button = new wxStaticBitmap(f_bar, wxID_ANY, m_menu.bmp());
|
||
m_menu_button->SetCursor(wxCURSOR_HAND);
|
||
m_menu_button->Bind(wxEVT_LEFT_DOWN, &PublishSettingsDialog::show_menu, this);
|
||
f_sizer->Add(m_menu_button, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10));
|
||
|
||
f_bar->SetSizerAndFit(f_sizer);
|
||
|
||
m_outer_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
|
||
m_outer_tabs->SetFont(Label::Body_14);
|
||
m_outer_tabs->SetBackgroundColour(GetBackgroundColour());
|
||
|
||
m_outer_host = new wxPanel(this, wxID_ANY);
|
||
m_outer_host->SetBackgroundColour(GetBackgroundColour());
|
||
m_outer_host_sizer = new wxBoxSizer(wxVERTICAL);
|
||
m_outer_host->SetSizer(m_outer_host_sizer);
|
||
|
||
wxBoxSizer* w_sizer = new wxBoxSizer(wxVERTICAL);
|
||
|
||
wxStaticText* msg = new wxStaticText(this, wxID_ANY, _L("Select which settings to embed in the 3MF file"));
|
||
msg->SetFont(Label::Body_13);
|
||
msg->Wrap(-1);
|
||
w_sizer->Add(msg, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10));
|
||
|
||
w_sizer->Add(f_bar, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
|
||
w_sizer->Add(m_outer_tabs, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
|
||
w_sizer->Add(m_outer_host, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
|
||
|
||
build_option_model();
|
||
|
||
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
|
||
|
||
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||
// Publish is always allowed: no settings selected means no settings override.
|
||
EndModal(wxID_OK);
|
||
});
|
||
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||
|
||
w_sizer->Add(dlg_btns, 0, wxEXPAND);
|
||
|
||
SetSizerAndFit(w_sizer);
|
||
SetMinSize(FromDIP(wxSize(600, 500)));
|
||
SetSize(FromDIP(wxSize(600, 500))); // initial size only; the dialog is resizable
|
||
wxGetApp().UpdateDlgDarkUI(this);
|
||
}
|
||
|
||
PublishSettingsDialog::~PublishSettingsDialog() {}
|
||
|
||
void PublishSettingsDialog::build_option_model()
|
||
{
|
||
// Structural / non-publishable keys, shared with the published-3MF overlay path.
|
||
const std::set<std::string>& denylist = publish_structural_keys();
|
||
// Base keys already added in the print/printer sections. Printer rows share this set:
|
||
// per-extruder "#N" variants collapse to the first occurrence (acceptable MVP; the
|
||
// per-extruder context is lost in the UI).
|
||
std::set<std::string> added;
|
||
|
||
PresetBundle* bundle = wxGetApp().preset_bundle;
|
||
DynamicPrintConfig full = bundle->full_config();
|
||
|
||
m_info_nonsel = _L("No selected items...");
|
||
m_info_allsel = _L("All items selected...");
|
||
m_info_empty = _L("No matching items...");
|
||
|
||
// Tab order differs from Section's enum order (Print, Printer, Material): the dialog
|
||
// presents Printer, Filament, Process.
|
||
m_sections.reserve(3);
|
||
const Section tab_order[] = {Section::Printer, Section::Material, Section::Print};
|
||
for (Section kind : tab_order)
|
||
section_group_for(kind);
|
||
bind_tab_events();
|
||
|
||
// Shared per-option label/value computation; returns false when the option must be skipped
|
||
// (denylisted / unknown / empty label). value is the stringified value; unit the translated
|
||
// sidetext (may be empty).
|
||
auto option_text = [&denylist, &full](const std::string& opt_id, const std::string& pure_key, wxString& label, wxString& value,
|
||
wxString& unit) -> bool {
|
||
if (denylist.count(pure_key) > 0)
|
||
return false;
|
||
const ConfigOptionDef* def = print_config_def.get(pure_key);
|
||
if (def == nullptr)
|
||
return false;
|
||
label = _(def->full_label.empty() ? def->label : def->full_label);
|
||
if (label.IsEmpty())
|
||
return false;
|
||
value = get_string_value(opt_id, full);
|
||
unit = _(def->sidetext);
|
||
return true;
|
||
};
|
||
|
||
// --- Phase 1: printer per-extruder retraction settings (first, mirroring the sidebar's
|
||
// Printer group), from the printer tab's "Extruder"/"Extruder N" pages.
|
||
{
|
||
size_t g = section_group_for(Section::Printer);
|
||
category_index_for(_L("Extruder"), Section::Printer, g, 0);
|
||
for (Tab* tab : wxGetApp().tabs_list) {
|
||
if (tab->m_type != Preset::TYPE_PRINTER)
|
||
continue;
|
||
for (const PageShp& page : tab->m_pages) {
|
||
if (!page->title().StartsWith("Extruder"))
|
||
continue;
|
||
const wxString page_title = Tab::translate_category(page->title(), tab->m_type);
|
||
for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) {
|
||
// Allowlist on the untranslated optgroup title; the "Retraction when
|
||
// switching material" group is intentionally skipped.
|
||
if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop")
|
||
continue;
|
||
const wxString subcategory = _(optgroup->title);
|
||
for (const auto& opt : optgroup->opt_map()) {
|
||
const std::string& opt_id = opt.first;
|
||
const std::string& pure_key = opt.second.first;
|
||
// Per-extruder "#N" variants collapse to the first base key. The row
|
||
// stores the base key; GetPublishedKeys() later expands it back to one
|
||
// "#N" entry per extruder so the load side can apply per-extruder values.
|
||
if (!added.insert(pure_key).second)
|
||
continue;
|
||
wxString label, value, unit;
|
||
if (!option_text(opt_id, pure_key, label, value, unit))
|
||
continue;
|
||
size_t cat_index = category_index_for(_L("Extruder"), Section::Printer, g, 0);
|
||
size_t sub_index = subcategory_index_for(cat_index, subcategory, optgroup->icon);
|
||
add_row_ui(pure_key, label, value, unit, cat_index, sub_index);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Phase 2: per-material sections synthesized from the filament tab's "Setting
|
||
// Overrides" page, under the Filament group.
|
||
{
|
||
size_t g = section_group_for(Section::Material);
|
||
Tab* filament_tab = nullptr;
|
||
for (Tab* tab : wxGetApp().tabs_list)
|
||
if (tab->m_type == Preset::TYPE_FILAMENT) {
|
||
filament_tab = tab;
|
||
break;
|
||
}
|
||
if (filament_tab != nullptr) {
|
||
const Page* overrides_page = nullptr;
|
||
for (const PageShp& page : filament_tab->m_pages)
|
||
if (page->title() == "Setting Overrides") {
|
||
overrides_page = page.get();
|
||
break;
|
||
}
|
||
|
||
if (overrides_page != nullptr) {
|
||
// One section per filament slot (a 4-slot printer shows 4 pages), each
|
||
// disambiguated by its colour chip and slot identity while showing the bare name.
|
||
for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) {
|
||
const PublishMaterialIdentity identity = material_identity(slot, full);
|
||
const wxString title = material_title(slot, bundle, full);
|
||
const size_t category_index = category_index_for(title, Section::Material, g, slot, identity);
|
||
|
||
// Material requirement rows: an optional filament colour and/or a
|
||
// vendor-agnostic material type for this slot, in their own optgroup so they
|
||
// stay visually separated from the setting rows.
|
||
{
|
||
const size_t req_sub = subcategory_index_for(category_index, _L("Material"), "custom-gcode_filament");
|
||
add_row_ui("filament_colour", _L("Color"), from_u8(filament_color_hex(full, slot)), wxString(), category_index,
|
||
req_sub, RowKind::Color);
|
||
std::string type;
|
||
if (const auto* types = full.opt<ConfigOptionStrings>("filament_type"))
|
||
if (slot < types->size())
|
||
type = types->get_at(slot);
|
||
add_row_ui("filament_type", _L("Type"), from_u8(normalize_filament_type(type)), wxString(), category_index, req_sub,
|
||
RowKind::Type);
|
||
}
|
||
|
||
// A material section must not repeat a key; the same key may appear in other
|
||
// material sections - that is intended.
|
||
std::set<std::string> material_added;
|
||
|
||
for (const ConfigOptionsGroupShp& optgroup : overrides_page->m_optgroups) {
|
||
// Allowlist on the untranslated optgroup title; "Ironing" is skipped.
|
||
if (optgroup->title != "Retraction" && optgroup->title != "Retraction when switching material")
|
||
continue;
|
||
for (const auto& opt : optgroup->opt_map()) {
|
||
// Row keys are base keys; the load side applies them positionally.
|
||
const std::string& opt_id = opt.first;
|
||
std::string base = opt_id.substr(0, opt_id.find('#'));
|
||
if (!material_added.insert(base).second)
|
||
continue;
|
||
// Show the value of this slot; fall back to slot 0 if out of range.
|
||
std::string value_opt_id = base + "#" + std::to_string(slot);
|
||
if (const ConfigOption* opt_cfg = full.option(base))
|
||
if (const auto* vec = dynamic_cast<const ConfigOptionVectorBase*>(opt_cfg))
|
||
if (vec->size() > 0 && slot >= vec->size())
|
||
value_opt_id = base + "#0";
|
||
wxString label, value, unit;
|
||
if (!option_text(value_opt_id, base, label, value, unit))
|
||
continue;
|
||
size_t sub_index = subcategory_index_for(category_index, _(optgroup->title), optgroup->icon);
|
||
add_row_ui(base, label, value, unit, category_index, sub_index);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Phase 3: process (print) settings; grouping matches the process tab.
|
||
{
|
||
size_t g = section_group_for(Section::Print);
|
||
for (Tab* tab : wxGetApp().tabs_list) {
|
||
if (tab->m_type != Preset::TYPE_PRINT)
|
||
continue;
|
||
size_t page_index = 0;
|
||
for (const PageShp& page : tab->m_pages) {
|
||
wxString category = Tab::translate_category(page->title(), tab->m_type);
|
||
const size_t category_index = category_index_for(category, Section::Print, g, page_index);
|
||
|
||
for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) {
|
||
for (const auto& opt : optgroup->opt_map()) {
|
||
// opt_map() key is the opt_id (may carry "#N"); the value is
|
||
// (pure_opt_key, opt_index).
|
||
const std::string& opt_id = opt.first;
|
||
const std::string& pure_key = opt.second.first;
|
||
// A key may appear in more than one page/group; keep the first.
|
||
if (!added.insert(pure_key).second)
|
||
continue;
|
||
wxString label, value, unit;
|
||
if (!option_text(opt_id, pure_key, label, value, unit))
|
||
continue;
|
||
size_t sub_index = subcategory_index_for(category_index, _(optgroup->title), optgroup->icon);
|
||
add_row_ui(opt_id, label, value, unit, category_index, sub_index);
|
||
}
|
||
}
|
||
++page_index;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pre-check the dirty (modified) settings and mark them bold (base-key match, across all
|
||
// sections; collect_dirty_settings_keys unions the prints, printers and filaments).
|
||
std::set<std::string> dirty_base;
|
||
for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) {
|
||
auto n = key.find('#');
|
||
dirty_base.insert(n == std::string::npos ? key : key.substr(0, n));
|
||
}
|
||
for (Row& row : m_rows) {
|
||
// The Color/Type requirement rows are not "dirty overrides": never auto-checked.
|
||
if (row.kind != RowKind::Setting)
|
||
continue;
|
||
std::string base = row.key.substr(0, row.key.find('#'));
|
||
row.dirty = dirty_base.count(base) > 0;
|
||
if (row.dirty) {
|
||
row.check->SetValue(true);
|
||
set_row_bold(row, true);
|
||
}
|
||
}
|
||
|
||
// Wire the "Full Publish" checkboxes: toggling one disables/enables the material's rows.
|
||
// Bind by index so the lambda stays valid even if the vector is reallocated later.
|
||
for (size_t c = 0; c < m_categories.size(); ++c)
|
||
if (m_categories[c].full_check != nullptr)
|
||
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); });
|
||
|
||
// No filter is active at startup: every row matches until the user types.
|
||
for (Row& row : m_rows)
|
||
row.matches_filter = true;
|
||
apply_visibility();
|
||
|
||
for (Category& category : m_categories) {
|
||
category.scroll->FitInside();
|
||
category.list_sizer->Layout();
|
||
}
|
||
for (SectionGroup& section : m_sections)
|
||
if (!section.categories.empty())
|
||
section.tabs->SelectItem(0);
|
||
if (!m_sections.empty()) {
|
||
m_outer_tabs->SelectItem(0);
|
||
show_outer_page(0);
|
||
}
|
||
}
|
||
|
||
size_t PublishSettingsDialog::section_group_for(Section kind)
|
||
{
|
||
for (size_t i = 0; i < m_sections.size(); ++i)
|
||
if (m_sections[i].kind == kind)
|
||
return i;
|
||
|
||
SectionGroup section;
|
||
section.kind = kind;
|
||
const size_t new_index = m_sections.size();
|
||
switch (kind) {
|
||
case Section::Printer:
|
||
section.title = _L("Printer");
|
||
section.icon_name = "printer";
|
||
break;
|
||
case Section::Material:
|
||
section.title = _L("Filament");
|
||
section.icon_name = "filament";
|
||
break;
|
||
case Section::Print:
|
||
section.title = _L("Process");
|
||
section.icon_name = "process";
|
||
break;
|
||
}
|
||
section.icon_bmp = ScalableBitmap(this, section.icon_name, 16);
|
||
|
||
section.page = new wxPanel(m_outer_host, wxID_ANY);
|
||
section.page->SetBackgroundColour(GetBackgroundColour());
|
||
auto* page_sizer = new wxBoxSizer(wxVERTICAL);
|
||
section.tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
|
||
section.tabs->SetFont(Label::Body_14);
|
||
section.tabs->SetBackgroundColour(GetBackgroundColour());
|
||
page_sizer->Add(section.tabs, 0, wxEXPAND);
|
||
section.page_host = new wxPanel(section.page, wxID_ANY);
|
||
section.page_host->SetBackgroundColour(GetBackgroundColour());
|
||
section.page_host_sizer = new wxBoxSizer(wxVERTICAL);
|
||
section.page_host->SetSizer(section.page_host_sizer);
|
||
page_sizer->Add(section.page_host, 1, wxEXPAND | wxTOP, FromDIP(4));
|
||
section.page->SetSizer(page_sizer);
|
||
|
||
if (section.icon_bmp.bmp().IsOk())
|
||
m_outer_tabs->AppendItem(section.title, section.icon_bmp.bmp());
|
||
else
|
||
m_outer_tabs->AppendItem(section.title);
|
||
m_outer_host_sizer->Add(section.page, 1, wxEXPAND);
|
||
section.page->Hide();
|
||
m_sections.push_back(std::move(section));
|
||
return new_index;
|
||
}
|
||
|
||
size_t PublishSettingsDialog::category_index_for(
|
||
const wxString& title, Section section, size_t group, size_t source_index, const PublishMaterialIdentity& identity)
|
||
{
|
||
for (size_t i : m_sections[group].categories) {
|
||
Category& existing = m_categories[i];
|
||
if (existing.title == title && existing.section == section && existing.source_index == source_index &&
|
||
existing.filament_id == identity.id && existing.filament_type == identity.type && existing.filament_vendor == identity.vendor)
|
||
return i;
|
||
}
|
||
|
||
Category category;
|
||
category.title = title;
|
||
category.section = section;
|
||
category.group = group;
|
||
category.source_index = source_index;
|
||
category.filament_type = identity.type;
|
||
category.filament_vendor = identity.vendor;
|
||
category.filament_id = identity.id;
|
||
category.filament_slot = source_index;
|
||
category.page = new wxPanel(m_sections[group].page_host, wxID_ANY);
|
||
category.page->SetBackgroundColour(GetBackgroundColour());
|
||
auto* page_sizer = new wxBoxSizer(wxVERTICAL);
|
||
|
||
// The slot's colour chip decorates both the section header and the inner tab.
|
||
std::string hex;
|
||
if (section == Section::Material)
|
||
hex = filament_color_hex(wxGetApp().preset_bundle->full_config(), source_index);
|
||
|
||
if (section == Section::Material) {
|
||
auto* header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||
if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) {
|
||
category.filament_color_chip = new wxStaticBitmap(category.page, wxID_ANY, *chip);
|
||
header_sizer->Add(category.filament_color_chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
|
||
}
|
||
category.title_label = new wxStaticText(category.page, wxID_ANY, title);
|
||
category.title_label->SetFont(Label::Head_14);
|
||
header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL);
|
||
category.full_check = new wxCheckBox(category.page, wxID_ANY, _L("Full Publish"));
|
||
category.full_check->SetFont(Label::Body_13);
|
||
category.full_check->SetToolTip(_L("Embed the entire filament of this slot in the 3MF file"));
|
||
header_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||
page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
|
||
}
|
||
|
||
category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||
category.scroll->SetScrollRate(0, 10);
|
||
category.scroll->SetBackgroundColour(GetBackgroundColour());
|
||
category.list_sizer = new wxBoxSizer(wxVERTICAL);
|
||
category.scroll->SetSizer(category.list_sizer);
|
||
category.scroll->DisableFocusFromKeyboard();
|
||
category.scroll->Bind(wxEVT_RIGHT_DOWN, &PublishSettingsDialog::show_menu, this);
|
||
category.info = new wxStaticText(category.scroll, wxID_ANY, m_info_empty);
|
||
category.info->SetFont(Label::Body_13);
|
||
category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10));
|
||
category.info->Hide();
|
||
page_sizer->Add(category.scroll, 1, wxEXPAND | wxALL, FromDIP(4));
|
||
category.page->SetSizer(page_sizer);
|
||
category.page->Hide();
|
||
|
||
const size_t category_index = m_categories.size();
|
||
m_categories.push_back(std::move(category));
|
||
m_sections[group].categories.push_back(category_index);
|
||
if (section == Section::Material) {
|
||
if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12)))
|
||
m_sections[group].tabs->AppendItem(title, *chip);
|
||
else
|
||
m_sections[group].tabs->AppendItem(title);
|
||
} else {
|
||
m_sections[group].tabs->AppendItem(title);
|
||
}
|
||
m_sections[group].page_host_sizer->Add(m_categories[category_index].page, 1, wxEXPAND);
|
||
if (m_sections[group].selected_inner < 0)
|
||
m_sections[group].selected_inner = 0;
|
||
return category_index;
|
||
}
|
||
|
||
size_t PublishSettingsDialog::subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon)
|
||
{
|
||
Category& category = m_categories[category_index];
|
||
for (size_t i = 0; i < category.subs.size(); ++i)
|
||
if (category.subs[i].title == title)
|
||
return i;
|
||
|
||
Subcategory sub;
|
||
sub.title = title;
|
||
if (!title.IsEmpty()) {
|
||
sub.header = new ::StaticLine(category.scroll, false, title, icon);
|
||
sub.header->SetFont(Label::Head_14);
|
||
sub.header->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||
auto* wrap = new wxBoxSizer(wxVERTICAL);
|
||
wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6));
|
||
sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(22));
|
||
}
|
||
category.subs.push_back(std::move(sub));
|
||
return category.subs.size() - 1;
|
||
}
|
||
|
||
void PublishSettingsDialog::add_row_ui(const std::string& key,
|
||
const wxString& label,
|
||
const wxString& value,
|
||
const wxString& unit,
|
||
size_t category_index,
|
||
size_t subcategory_index,
|
||
RowKind kind)
|
||
{
|
||
Category& category = m_categories[category_index];
|
||
Row row;
|
||
row.key = key;
|
||
row.label = label;
|
||
row.value = value;
|
||
row.unit = unit;
|
||
row.kind = kind;
|
||
row.category = category.title;
|
||
row.subcategory = category.subs[subcategory_index].title;
|
||
row.section = category.section;
|
||
row.section_title = m_sections[category.group].title;
|
||
row.outer_index = category.group;
|
||
row.inner_index = category_index;
|
||
const size_t row_index = m_rows.size();
|
||
m_rows.push_back(std::move(row));
|
||
Row& current = m_rows[row_index];
|
||
current.check = new wxCheckBox(category.scroll, wxID_ANY, label);
|
||
current.check->SetFont(Label::Body_13);
|
||
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
|
||
// The value is read-only text (incl. the Type row: the published type is the slot's
|
||
// normalized type, not author-editable).
|
||
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
|
||
current.value_label->SetFont(Label::Body_13);
|
||
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||
current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit);
|
||
if (kind == RowKind::Color && !value.IsEmpty()) {
|
||
if (wxBitmap* chip = get_extruder_color_icon(value.ToStdString(), "", FromDIP(12), FromDIP(12))) {
|
||
current.color_chip = new wxStaticBitmap(category.scroll, wxID_ANY, *chip);
|
||
row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||
}
|
||
}
|
||
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||
if (!unit.IsEmpty()) {
|
||
current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit);
|
||
current.unit_label->SetFont(Label::Body_13);
|
||
current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||
row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
|
||
}
|
||
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38));
|
||
category.rows.push_back(row_index);
|
||
category.subs[subcategory_index].rows.push_back(row_index);
|
||
}
|
||
|
||
void PublishSettingsDialog::on_full_toggle(size_t category_index)
|
||
{
|
||
Category& cat = m_categories[category_index];
|
||
const bool full = cat.full_check->GetValue();
|
||
for (size_t r : cat.rows)
|
||
m_rows[r].check->Enable(!full);
|
||
}
|
||
|
||
void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
|
||
{
|
||
// Rebase on the dialog's body font so clearing bold restores the exact original font.
|
||
row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
|
||
}
|
||
|
||
void PublishSettingsDialog::save_scroll_position(Category& category)
|
||
{
|
||
if (category.scroll != nullptr)
|
||
category.scroll->GetViewStart(&category.scroll_pos.x, &category.scroll_pos.y);
|
||
}
|
||
|
||
void PublishSettingsDialog::show_outer_page(size_t section_index)
|
||
{
|
||
if (section_index >= m_sections.size())
|
||
return;
|
||
if (m_selected_outer >= 0 && m_selected_outer < static_cast<int>(m_sections.size())) {
|
||
SectionGroup& old_section = m_sections[m_selected_outer];
|
||
if (old_section.selected_inner >= 0 && old_section.selected_inner < static_cast<int>(old_section.categories.size()))
|
||
save_scroll_position(m_categories[old_section.categories[old_section.selected_inner]]);
|
||
m_sections[m_selected_outer].page->Hide();
|
||
}
|
||
m_selected_outer = static_cast<int>(section_index);
|
||
SectionGroup& section = m_sections[section_index];
|
||
section.page->Show();
|
||
if (section.selected_inner >= 0)
|
||
show_inner_page(section_index, section.selected_inner);
|
||
m_outer_host_sizer->Layout();
|
||
}
|
||
|
||
void PublishSettingsDialog::show_inner_page(size_t section_index, int inner_index)
|
||
{
|
||
if (section_index >= m_sections.size())
|
||
return;
|
||
SectionGroup& section = m_sections[section_index];
|
||
if (inner_index < 0 || inner_index >= static_cast<int>(section.categories.size()))
|
||
return;
|
||
if (section.selected_inner >= 0 && section.selected_inner < static_cast<int>(section.categories.size())) {
|
||
save_scroll_position(m_categories[section.categories[section.selected_inner]]);
|
||
m_categories[section.categories[section.selected_inner]].page->Hide();
|
||
}
|
||
section.selected_inner = inner_index;
|
||
Category& category = m_categories[section.categories[inner_index]];
|
||
category.page->Show();
|
||
category.scroll->FitInside();
|
||
category.scroll->Scroll(category.scroll_pos.x, category.scroll_pos.y);
|
||
section.page_host_sizer->Layout();
|
||
}
|
||
|
||
void PublishSettingsDialog::on_outer_tab_changed(wxCommandEvent& event)
|
||
{
|
||
const int selection = event.GetInt();
|
||
if (selection >= 0 && selection < static_cast<int>(m_sections.size()))
|
||
show_outer_page(static_cast<size_t>(selection));
|
||
}
|
||
|
||
void PublishSettingsDialog::on_inner_tab_changed(size_t section_index, wxCommandEvent& event)
|
||
{
|
||
const int selection = event.GetInt();
|
||
if (section_index < m_sections.size() && selection >= 0 && selection < static_cast<int>(m_sections[section_index].categories.size()))
|
||
show_inner_page(section_index, selection);
|
||
}
|
||
|
||
void PublishSettingsDialog::bind_tab_events()
|
||
{
|
||
m_outer_tabs->Bind(wxEVT_TAB_SEL_CHANGED, &PublishSettingsDialog::on_outer_tab_changed, this);
|
||
for (size_t section_index = 0; section_index < m_sections.size(); ++section_index)
|
||
m_sections[section_index].tabs->Bind(wxEVT_TAB_SEL_CHANGED,
|
||
[this, section_index](wxCommandEvent& event) { on_inner_tab_changed(section_index, event); });
|
||
}
|
||
|
||
void PublishSettingsDialog::apply_filter(const wxString& filter_text)
|
||
{
|
||
m_filter_mode = FilterMode::Text;
|
||
refresh_filter(filter_text.Lower());
|
||
}
|
||
|
||
void PublishSettingsDialog::apply_pseudo_filter(bool selected_only)
|
||
{
|
||
m_filter_mode = selected_only ? FilterMode::SelectedOnly : FilterMode::UnselectedOnly;
|
||
refresh_filter(wxString()); // the pseudo modes ignore the search text
|
||
}
|
||
|
||
void PublishSettingsDialog::refresh_filter(const wxString& filter)
|
||
{
|
||
Freeze();
|
||
const bool pseudo = m_filter_mode != FilterMode::Text;
|
||
const bool want_checked = m_filter_mode == FilterMode::SelectedOnly;
|
||
m_fb_sizer->Show(!pseudo);
|
||
if (pseudo) {
|
||
// "×" marks the chip as a dismissible filter state (U+00D7, present in all UI fonts).
|
||
m_pseudo_chip->SetLabel((want_checked ? _L("Filter selected") : _L("Filter non-selected")) + " " + wxString::FromUTF8("\u00d7"));
|
||
}
|
||
m_pseudo_chip->Show(pseudo);
|
||
|
||
// Row matches are computed first; page and optgroup visibility is applied below.
|
||
if (pseudo) {
|
||
for (Row& row : m_rows)
|
||
row.matches_filter = row.check->IsEnabled() && row.check->GetValue() == want_checked;
|
||
} else {
|
||
const bool clear = filter.IsEmpty();
|
||
for (Row& row : m_rows) {
|
||
row.matches_filter = clear || (row.section_title + " " + row.category + " " + row.subcategory + " " + row.label + " " +
|
||
row.value + " " + row.unit)
|
||
.Lower()
|
||
.Contains(filter);
|
||
}
|
||
}
|
||
|
||
size_t first_outer = 0;
|
||
int first_inner = -1;
|
||
bool active_has_match = false;
|
||
for (size_t s = 0; s < m_sections.size(); ++s) {
|
||
for (size_t inner = 0; inner < m_sections[s].categories.size(); ++inner) {
|
||
Category& category = m_categories[m_sections[s].categories[inner]];
|
||
bool has_match = false;
|
||
for (size_t r : category.rows)
|
||
has_match = has_match || m_rows[r].matches_filter;
|
||
category.info->Show(!has_match);
|
||
if (!has_match)
|
||
category.info->SetLabel(pseudo ? (want_checked ? m_info_nonsel : m_info_allsel) : m_info_empty);
|
||
if (has_match && first_inner < 0) {
|
||
first_outer = s;
|
||
first_inner = static_cast<int>(inner);
|
||
}
|
||
if (static_cast<int>(s) == m_selected_outer && static_cast<int>(inner) == m_sections[s].selected_inner)
|
||
active_has_match = has_match;
|
||
}
|
||
}
|
||
if (!active_has_match && first_inner >= 0 &&
|
||
(m_selected_outer != static_cast<int>(first_outer) || m_sections[first_outer].selected_inner != first_inner)) {
|
||
if (m_selected_outer != static_cast<int>(first_outer)) {
|
||
m_outer_tabs->SelectItem(static_cast<int>(first_outer));
|
||
show_outer_page(first_outer);
|
||
}
|
||
m_sections[first_outer].tabs->SelectItem(first_inner);
|
||
show_inner_page(first_outer, first_inner);
|
||
}
|
||
|
||
apply_visibility();
|
||
for (Category& category : m_categories) {
|
||
save_scroll_position(category);
|
||
category.scroll->FitInside();
|
||
category.list_sizer->Layout();
|
||
category.scroll->Scroll(category.scroll_pos.x, category.scroll_pos.y);
|
||
}
|
||
Layout();
|
||
Thaw();
|
||
}
|
||
|
||
void PublishSettingsDialog::apply_visibility()
|
||
{
|
||
Freeze();
|
||
for (Category& category : m_categories) {
|
||
bool category_any = false;
|
||
for (size_t r : category.rows)
|
||
category_any = category_any || m_rows[r].matches_filter;
|
||
category.info->Show(!category_any);
|
||
for (Subcategory& sub : category.subs) {
|
||
bool sub_any = false;
|
||
for (size_t r : sub.rows)
|
||
sub_any = sub_any || m_rows[r].matches_filter;
|
||
if (sub.header != nullptr)
|
||
sub.item->Show(sub_any);
|
||
for (size_t r : sub.rows)
|
||
m_rows[r].item->Show(m_rows[r].matches_filter);
|
||
}
|
||
category.list_sizer->Layout();
|
||
category.scroll->FitInside();
|
||
}
|
||
Thaw();
|
||
}
|
||
|
||
void PublishSettingsDialog::select_all(bool value)
|
||
{
|
||
// "All" skips disabled (gated) rows; "None" leaves a gated row's preserved value.
|
||
for (Row& row : m_rows)
|
||
if (row.check->IsEnabled())
|
||
row.check->SetValue(value);
|
||
}
|
||
|
||
bool PublishSettingsDialog::row_is_visible(const Row& row) const
|
||
{
|
||
if (m_selected_outer < 0 || m_selected_outer >= static_cast<int>(m_sections.size()) ||
|
||
row.outer_index != static_cast<size_t>(m_selected_outer) || m_sections[m_selected_outer].selected_inner < 0 ||
|
||
m_sections[m_selected_outer].selected_inner >= static_cast<int>(m_sections[m_selected_outer].categories.size()) ||
|
||
row.inner_index != m_sections[m_selected_outer].categories[m_sections[m_selected_outer].selected_inner] || !row.matches_filter ||
|
||
!row.check->IsEnabled())
|
||
return false;
|
||
return row.item->IsShown();
|
||
}
|
||
|
||
void PublishSettingsDialog::select_visible(bool value)
|
||
{
|
||
// In a pseudo-filter view the rows being toggled would all disappear; drop the filter
|
||
// afterwards so the result stays visible.
|
||
const bool clear_pseudo = (m_filter_mode == FilterMode::UnselectedOnly && !value) ||
|
||
(m_filter_mode == FilterMode::SelectedOnly && value);
|
||
|
||
// Toggle the rows visible under the *current* filter.
|
||
for (Row& row : m_rows)
|
||
if (row_is_visible(row))
|
||
row.check->SetValue(value);
|
||
|
||
if (clear_pseudo) {
|
||
// Note: SetValue() may fire wxEVT_TEXT on some platforms, re-entering apply_filter() -
|
||
// that is fine; the rows above were already toggled and the trailing call is idempotent.
|
||
m_filter_ctrl->ChangeValue("");
|
||
apply_filter(""); // resync visibility and the All/None bar
|
||
}
|
||
}
|
||
|
||
void PublishSettingsDialog::show_menu(wxMouseEvent& evt)
|
||
{
|
||
bool filtering = !m_filter_ctrl->GetValue().IsEmpty() || m_filter_mode != FilterMode::Text;
|
||
bool list_empty = true;
|
||
if (m_selected_outer >= 0) {
|
||
for (const Row& row : m_rows)
|
||
if (row_is_visible(row)) {
|
||
list_empty = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
wxMenu m;
|
||
m.Append(kPublishSelectAll, _L("Select All"))->Enable(!filtering);
|
||
m.Append(kPublishDeselectAll, _L("Deselect All"))->Enable(!filtering);
|
||
m.AppendSeparator();
|
||
m.Append(kPublishSelectVisible, _L("Select visible"))->Enable(!list_empty && filtering);
|
||
m.Append(kPublishDeselectVisible, _L("Deselect visible"))->Enable(!list_empty && filtering);
|
||
m.AppendSeparator();
|
||
m.AppendCheckItem(kPublishFilterSelected, _L("Filter selected"));
|
||
m.AppendCheckItem(kPublishFilterNonSelected, _L("Filter non-selected"));
|
||
m.Check(kPublishFilterSelected, m_filter_mode == FilterMode::SelectedOnly);
|
||
m.Check(kPublishFilterNonSelected, m_filter_mode == FilterMode::UnselectedOnly);
|
||
|
||
m.Bind(
|
||
wxEVT_MENU,
|
||
[this](wxCommandEvent& e) {
|
||
switch (e.GetId()) {
|
||
case kPublishSelectAll: select_all(true); break;
|
||
case kPublishDeselectAll: select_all(false); break;
|
||
case kPublishSelectVisible: select_visible(true); break;
|
||
case kPublishDeselectVisible: select_visible(false); break;
|
||
case kPublishFilterSelected:
|
||
// Clicking the active entry again clears the pseudo filter.
|
||
if (m_filter_mode == FilterMode::SelectedOnly)
|
||
apply_filter(m_filter_ctrl->GetValue());
|
||
else
|
||
apply_pseudo_filter(true);
|
||
break;
|
||
case kPublishFilterNonSelected:
|
||
if (m_filter_mode == FilterMode::UnselectedOnly)
|
||
apply_filter(m_filter_ctrl->GetValue());
|
||
else
|
||
apply_pseudo_filter(false);
|
||
break;
|
||
default: break;
|
||
}
|
||
},
|
||
kPublishSelectAll, kPublishFilterNonSelected);
|
||
|
||
wxWindow* src = dynamic_cast<wxWindow*>(evt.GetEventObject());
|
||
if (!src)
|
||
return;
|
||
wxPoint screen_pos = src->ClientToScreen(evt.GetPosition());
|
||
wxPoint local_pos = ScreenToClient(screen_pos);
|
||
PopupMenu(&m, local_pos);
|
||
}
|
||
|
||
std::vector<std::string> PublishSettingsDialog::GetPublishedKeys() const
|
||
{
|
||
std::vector<std::string> out;
|
||
// Process and printer sections both travel through published_keys (the load-side overlay
|
||
// applies process keys to the prints edited preset and the allowlisted printer keys to the
|
||
// printers edited preset); material keys use a separate API.
|
||
const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
|
||
for (const Row& row : m_rows) {
|
||
if ((row.section != Section::Print && row.section != Section::Printer) || !row.check->GetValue())
|
||
continue;
|
||
if (row.section == Section::Printer) {
|
||
// Printer rows store the base key (per-extruder "#N" variants collapsed during
|
||
// build). Publish every extruder element so the load side can apply per-extruder
|
||
// values even when the receiver has a different extruder count; a scalar printer
|
||
// key is published as-is.
|
||
const std::string base_key = row.key.substr(0, row.key.find('#'));
|
||
if (const ConfigOption* opt = full.option(base_key)) {
|
||
if (const auto* vec = dynamic_cast<const ConfigOptionVectorBase*>(opt)) {
|
||
for (size_t i = 0; i < vec->size(); ++i)
|
||
out.push_back(base_key + "#" + std::to_string(i));
|
||
} else {
|
||
out.push_back(base_key);
|
||
}
|
||
}
|
||
} else {
|
||
out.push_back(row.key);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::vector<Slic3r::PublishedMaterialEntry> PublishSettingsDialog::GetPublishedMaterialKeys() const
|
||
{
|
||
std::vector<Slic3r::PublishedMaterialEntry> out;
|
||
for (const Category& cat : m_categories) {
|
||
if (cat.section != Section::Material)
|
||
continue;
|
||
Slic3r::PublishedMaterialEntry entry;
|
||
entry.filament_type = cat.filament_type;
|
||
entry.filament_vendor = cat.filament_vendor;
|
||
entry.filament_id = cat.filament_id;
|
||
entry.slot = static_cast<int>(cat.filament_slot);
|
||
// The author's preset id distinguishes exact variants that share filament_id
|
||
// ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely; the
|
||
// preset name is the most direct identity and is matched first on load.
|
||
PresetBundle* bundle = wxGetApp().preset_bundle;
|
||
if (bundle != nullptr && cat.filament_slot < bundle->filament_presets.size()) {
|
||
if (const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) {
|
||
entry.setting_id = preset->setting_id;
|
||
entry.preset_name = preset->name;
|
||
}
|
||
}
|
||
// "Full Publish": the whole filament preset is embedded; type and colour are implicitly
|
||
// published, and the per-key rows are disabled / their state ignored.
|
||
if (cat.full_check != nullptr && cat.full_check->GetValue()) {
|
||
entry.full = true;
|
||
entry.full_keys = full_keys_for_slot();
|
||
entry.publish_type = true;
|
||
entry.publish_type_value = normalize_filament_type(cat.filament_type);
|
||
for (size_t r : cat.rows) {
|
||
const Row& row = m_rows[r];
|
||
if (row.kind == RowKind::Color && !row.value.IsEmpty()) {
|
||
entry.publish_color = true;
|
||
entry.color = row.value.ToStdString();
|
||
}
|
||
}
|
||
out.push_back(std::move(entry));
|
||
continue;
|
||
}
|
||
for (size_t r : cat.rows) {
|
||
const Row& row = m_rows[r];
|
||
if (!row.check->GetValue())
|
||
continue;
|
||
if (row.kind == RowKind::Color) {
|
||
entry.publish_color = true;
|
||
entry.color = row.value.ToStdString();
|
||
} else if (row.kind == RowKind::Type) {
|
||
entry.publish_type = true;
|
||
entry.publish_type_value = row.value.ToStdString();
|
||
} else {
|
||
entry.keys.push_back(row.key);
|
||
}
|
||
}
|
||
// Nothing checked at all -> nothing to write.
|
||
if (!entry.keys.empty() || entry.publish_type || entry.publish_color)
|
||
out.push_back(std::move(entry));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::vector<std::string> PublishSettingsDialog::full_keys_for_slot() const
|
||
{
|
||
// The canonical filament preset keys minus the structural keys the published overlay must
|
||
// never touch (inherits, compatibility, *_settings_id, ...), plus filament_colour (not a
|
||
// member of Preset::filament_options). Values travel in the exported config, masked to
|
||
// this slot, and are applied on load onto the receiver's slot.
|
||
const std::set<std::string>& denylist = publish_structural_keys();
|
||
std::vector<std::string> keys;
|
||
for (const std::string& key : Preset::filament_options())
|
||
if (denylist.count(key) == 0)
|
||
keys.emplace_back(key);
|
||
keys.emplace_back("filament_colour");
|
||
return keys;
|
||
}
|
||
|
||
void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||
{
|
||
// Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint.
|
||
m_search.msw_rescale();
|
||
m_menu.msw_rescale();
|
||
m_filter_box->SetIcon(m_search.bmp());
|
||
m_menu_button->SetBitmap(m_menu.bmp());
|
||
m_outer_tabs->Rescale();
|
||
|
||
const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
|
||
|
||
for (Category& cat : m_categories) {
|
||
if (cat.full_check != nullptr)
|
||
cat.full_check->Refresh();
|
||
if (cat.title_label != nullptr)
|
||
cat.title_label->Refresh();
|
||
if (cat.filament_color_chip != nullptr) {
|
||
if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, cat.filament_slot), "", FromDIP(12), FromDIP(12)))
|
||
cat.filament_color_chip->SetBitmap(*chip);
|
||
}
|
||
cat.scroll->FitInside();
|
||
cat.list_sizer->Layout();
|
||
}
|
||
|
||
for (size_t s = 0; s < m_sections.size(); ++s) {
|
||
SectionGroup& section = m_sections[s];
|
||
section.icon_bmp.msw_rescale();
|
||
if (section.icon_bmp.bmp().IsOk())
|
||
m_outer_tabs->SetItemBitmap(s, section.icon_bmp.bmp());
|
||
section.tabs->Rescale();
|
||
}
|
||
|
||
// Refresh the per-row Color chips at the new DPI.
|
||
for (Row& row : m_rows) {
|
||
if (row.color_chip != nullptr && !row.value.IsEmpty()) {
|
||
if (wxBitmap* chip = get_extruder_color_icon(row.value.ToStdString(), "", FromDIP(12), FromDIP(12)))
|
||
row.color_chip->SetBitmap(*chip);
|
||
}
|
||
}
|
||
|
||
for (size_t category_index = 0; category_index < m_categories.size(); ++category_index) {
|
||
const Category& category = m_categories[category_index];
|
||
if (category.section != Section::Material)
|
||
continue;
|
||
if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), "", FromDIP(12), FromDIP(12))) {
|
||
const SectionGroup& section = m_sections[category.group];
|
||
const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index);
|
||
if (iter != section.categories.end())
|
||
m_sections[category.group].tabs->SetItemBitmap(static_cast<unsigned int>(iter - section.categories.begin()), *chip);
|
||
}
|
||
}
|
||
|
||
SetMinSize(FromDIP(wxSize(600, 500)));
|
||
Refresh();
|
||
}
|
||
|
||
}} // namespace Slic3r::GUI
|