Compare commits

...
Author SHA1 Message Date
Hanif Koh 86941859fa Keep a Scalar Config Option's Value When Deserializing Fails
A failed stream extraction writes 0 into its target, so Float, Int, Percent and FloatOrPercent options were overwritten before deserialize reported the error, and Point/Point3 kept a partially scanned value. Parse into a temporary and assign only on success; FloatOrPercent sets its percent flag only then too.
2026-10-19 16:41:04 +08:00
HanifKoh d3699dd9f6 Keep Python Bytecode out of the Bundled Python Home (#16550)
* Keep Python Bytecode out of the Bundled Python Home

The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.

* Drop Cached Bytecode Through the Interpreter in the Module Release Test
2026-10-19 13:48:53 +08:00
HanifKoh a315a96c5c Show "No Available Printers" Instead of an Empty Printer Dropdown (#16611)
* Show "No Available Printers" Instead of an Empty Printer Dropdown

A ComboBox with no items opened a one-row-high popup that DropDown never
paints, leaving a blank strip with stale pixels under the box. Don't open
the list when it is empty, and label the printer box in the print and
send-to-printer dialogs when no printer is available.

* Add "No Available Printers" to the Translation Template
2026-10-19 13:30:36 +08:00
HanifKoh 24cf1c6bcc Fix Crash Selecting a User Printer Whose Parent Preset Is Not Loaded (#16579)
A user preset can inherit a system preset from a vendor that is not
installed. get_preset_base() returns null for it, and
get_selected_preset_base() dereferenced that unconditionally, so picking
the printer crashed while building the shared profiles notification URL.
Fall back to the selected preset itself. The create printer and create
filament dialogs had the same null dereference on such presets.
2026-10-19 13:29:26 +08:00
11 changed files with 151 additions and 23 deletions

No files matched your search

+3
View File
@@ -10657,6 +10657,9 @@ msgstr ""
msgid "Send Failed" msgid "Send Failed"
msgstr "" msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "" msgstr ""
+37 -13
View File
@@ -841,8 +841,12 @@ public:
{ {
UNUSED(append); UNUSED(append);
std::istringstream iss(str); std::istringstream iss(str);
iss >> this->value; double value;
return !iss.fail(); iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
} }
bool operator==(const ConfigOption &rhs) const override bool operator==(const ConfigOption &rhs) const override
@@ -1033,8 +1037,12 @@ public:
{ {
UNUSED(append); UNUSED(append);
std::istringstream iss(str); std::istringstream iss(str);
iss >> this->value; int value;
return !iss.fail(); iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
} }
ConfigOptionInt& operator=(const ConfigOption *opt) ConfigOptionInt& operator=(const ConfigOption *opt)
@@ -1252,8 +1260,12 @@ public:
UNUSED(append); UNUSED(append);
// don't try to parse the trailing % since it's optional // don't try to parse the trailing % since it's optional
std::istringstream iss(str); std::istringstream iss(str);
iss >> this->value; double value;
return !iss.fail(); iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
} }
private: private:
@@ -1365,10 +1377,14 @@ public:
bool deserialize(const std::string &str, bool append = false) override bool deserialize(const std::string &str, bool append = false) override
{ {
UNUSED(append); UNUSED(append);
this->percent = str.find_first_of("%") != std::string::npos;
std::istringstream iss(str); std::istringstream iss(str);
iss >> this->value; double value;
return !iss.fail(); iss >> value;
if (iss.fail())
return false;
this->value = value;
this->percent = str.find_first_of("%") != std::string::npos;
return true;
} }
private: private:
@@ -1538,8 +1554,12 @@ public:
{ {
UNUSED(append); UNUSED(append);
char dummy; char dummy;
return sscanf(str.data(), " %lf , %lf %c", &this->value(0), &this->value(1), &dummy) == 2 || Vec2d value;
sscanf(str.data(), " %lf x %lf %c", &this->value(0), &this->value(1), &dummy) == 2; if (sscanf(str.data(), " %lf , %lf %c", &value(0), &value(1), &dummy) != 2 &&
sscanf(str.data(), " %lf x %lf %c", &value(0), &value(1), &dummy) != 2)
return false;
this->value = value;
return true;
} }
private: private:
@@ -1658,8 +1678,12 @@ public:
{ {
UNUSED(append); UNUSED(append);
char dummy; char dummy;
return sscanf(str.data(), " %lf , %lf , %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3 || Vec3d value;
sscanf(str.data(), " %lf x %lf x %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3; if (sscanf(str.data(), " %lf , %lf , %lf %c", &value(0), &value(1), &value(2), &dummy) != 3 &&
sscanf(str.data(), " %lf x %lf x %lf %c", &value(0), &value(1), &value(2), &dummy) != 3)
return false;
this->value = value;
return true;
} }
private: private:
+8 -1
View File
@@ -756,7 +756,14 @@ public:
Preset& get_edited_preset() { return m_edited_preset; } Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { return m_edited_preset; } const Preset& get_edited_preset() const { return m_edited_preset; }
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); } // The root of the selected preset's inherits chain, or the selected preset itself when
// the chain names a preset that is not loaded (e.g. a vendor that is not installed).
const Preset& get_selected_preset_base() const
{
const Preset &selected = m_presets[m_idx_selected];
const Preset *base = get_preset_base(selected);
return base ? *base : selected;
}
// Return the last saved preset. // Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; } // const Preset& get_saved_preset() const { return m_saved_preset; }
+2 -2
View File
@@ -3151,7 +3151,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
m_printer_name_to_preset.clear(); m_printer_name_to_preset.clear();
for (const Preset &printer_preset : printer_presets) { for (const Preset &printer_preset : printer_presets) {
if (!printer_preset.is_visible) continue; if (!printer_preset.is_visible) continue;
if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue; if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue;
if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) { if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) {
if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) { if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) {
printer_choice.push_back(from_u8(printer_model->value)); printer_choice.push_back(from_u8(printer_model->value));
@@ -4962,7 +4962,7 @@ void CreatePresetForPrinterDialog::get_visible_printer_and_compatible_filament_p
if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue; if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue;
ConfigOptionStrings *filament_types; ConfigOptionStrings *filament_types;
const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset); const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset);
if (filament_preset_base == &filament_preset) { if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type")); filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type"));
} else { } else {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type")); filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type"));
+1 -1
View File
@@ -4087,7 +4087,7 @@ void SelectMachineDialog::update_user_printer()
{ {
m_printer_last_select = ""; m_printer_last_select = "";
update_select_layout(nullptr); update_select_layout(nullptr);
m_printer_box->GetPrinterComboBox()->SetTextLabel(""); m_printer_box->GetPrinterComboBox()->SetValue(_L("No available printers"));
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl;
+1 -1
View File
@@ -1229,7 +1229,7 @@ void SendToPrinterDialog::update_user_printer()
} }
else { else {
m_printer_last_select = ""; m_printer_last_select = "";
m_comboBox_printer->SetTextLabel(""); m_comboBox_printer->SetValue(_L("No available printers"));
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl;
+3 -3
View File
@@ -287,7 +287,7 @@ void ComboBox::DoDeleteOneItem(unsigned int pos)
void ComboBox::ForceDropdownOpen() void ComboBox::ForceDropdownOpen()
{ {
if (!IsEnabled()) if (!IsEnabled() || items.empty())
return; return;
if (!drop_down) { if (!drop_down) {
@@ -398,7 +398,7 @@ void ComboBox::mouseDown(wxMouseEvent &event)
SetFocus(); SetFocus();
if (drop_down) { if (drop_down) {
drop.Hide(); drop.Hide();
} else if (drop.HasDismissLongTime()) { } else if (!items.empty() && drop.HasDismissLongTime()) { // DropDown paints nothing for an empty list
drop.autoPosition(); drop.autoPosition();
drop_down = true; drop_down = true;
drop.Popup(&drop); drop.Popup(&drop);
@@ -426,7 +426,7 @@ void ComboBox::keyDown(wxKeyEvent& event)
case WXK_SPACE: case WXK_SPACE:
if (drop_down) { if (drop_down) {
drop.DismissAndNotify(); drop.DismissAndNotify();
} else if (drop.HasDismissLongTime()) { } else if (!items.empty() && drop.HasDismissLongTime()) {
drop.autoPosition(); drop.autoPosition();
drop_down = true; drop_down = true;
drop.Popup(); drop.Popup();
+13
View File
@@ -584,6 +584,19 @@ bool PythonInterpreter::initialize()
} }
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully"; BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
// Keep bytecode out of the Python home. The shipped .pyc files are stale against their
// sources, so a writable install would rewrite them inside the app (breaking a signed
// macOS bundle's seal) and a read-only one would recompile them on every launch.
const std::string pycache_prefix = data_dir() + "/python/pycache";
#ifdef _WIN32
status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str());
#else
status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str());
#endif
if (PyStatus_Exception(status))
BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": "
<< (status.err_msg ? status.err_msg : "unknown error");
// Use pybind11's scoped_interpreter which properly initializes // Use pybind11's scoped_interpreter which properly initializes
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig // pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
// does not set up thread state tracking that pybind11 requires. // does not set up thread state tracking that pybind11 requires.
+49
View File
@@ -1263,6 +1263,55 @@ TEST_CASE("read_cli rejects an invalid scalar numeric value", "[Config]") {
REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys));
} }
TEST_CASE("A scalar option keeps its value when deserializing fails", "[Config]") {
using Catch::Matchers::WithinAbs;
SECTION("float") {
ConfigOptionFloat opt(7.5);
REQUIRE_FALSE(opt.deserialize("abc"));
CHECK_THAT(opt.value, WithinAbs(7.5, 1e-9));
}
SECTION("int") {
ConfigOptionInt opt(3);
REQUIRE_FALSE(opt.deserialize("abc"));
CHECK(opt.value == 3);
}
SECTION("percent") {
ConfigOptionPercent opt(40.);
REQUIRE_FALSE(opt.deserialize("abc%"));
CHECK_THAT(opt.value, WithinAbs(40., 1e-9));
}
SECTION("float or percent") {
ConfigOptionFloatOrPercent opt(0.4, false);
REQUIRE_FALSE(opt.deserialize("abc%"));
CHECK_THAT(opt.value, WithinAbs(0.4, 1e-9));
CHECK_FALSE(opt.percent);
}
SECTION("point") {
ConfigOptionPoint opt(Vec2d(1., 2.));
REQUIRE_FALSE(opt.deserialize("5,abc"));
CHECK(opt.value == Vec2d(1., 2.));
}
SECTION("point3") {
ConfigOptionPoint3 opt(Vec3d(1., 2., 3.));
REQUIRE_FALSE(opt.deserialize("5,6,abc"));
CHECK(opt.value == Vec3d(1., 2., 3.));
}
}
TEST_CASE("A scalar option still takes a valid value", "[Config]") {
using Catch::Matchers::WithinAbs;
ConfigOptionFloatOrPercent opt(0.4, false);
REQUIRE(opt.deserialize("50%"));
CHECK_THAT(opt.value, WithinAbs(50., 1e-9));
CHECK(opt.percent);
ConfigOptionPoint point;
REQUIRE(point.deserialize("3x4"));
CHECK(point.value == Vec2d(3., 4.));
}
TEST_CASE("read_cli appends values when a vector option is repeated", "[Config]") { TEST_CASE("read_cli appends values when a vector option is repeated", "[Config]") {
Slic3r::DynamicPrintConfig config; Slic3r::DynamicPrintConfig config;
t_config_option_keys extra, keys; t_config_option_keys extra, keys;
@@ -1219,6 +1219,24 @@ TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the libr
CHECK(bundle.filaments.find_preset2("BrandX Generic PETG", /*auto_match=*/true) == nullptr); CHECK(bundle.filaments.find_preset2("BrandX Generic PETG", /*auto_match=*/true) == nullptr);
} }
TEST_CASE("The selected preset is its own base when its parent is not loaded", "[Preset]")
{
PresetCollection printers(Preset::TYPE_PRINTER, Preset::printer_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
add_inmemory_preset(printers, "Printer");
add_inmemory_preset(printers, "User Printer", "Printer");
add_inmemory_preset(printers, "Orphan Printer", "Uninstalled Vendor Printer");
printers.select_preset_by_name("User Printer", true);
REQUIRE(printers.get_selected_preset_name() == "User Printer");
CHECK(printers.get_selected_preset_base().name == "Printer");
printers.select_preset_by_name("Orphan Printer", true);
REQUIRE(printers.get_selected_preset_name() == "Orphan Printer");
REQUIRE(printers.get_preset_base(printers.get_selected_preset()) == nullptr);
CHECK(printers.get_selected_preset_base().name == "Orphan Printer");
}
TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]") TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]")
{ {
ScopedTemporaryDir temp_dir; ScopedTemporaryDir temp_dir;
+16 -2
View File
@@ -615,8 +615,22 @@ TEST_CASE("Python module release removes package submodules and owned sys.path",
// Ensure the next import executes the new submodule rather than reusing a stale package child. // Ensure the next import executes the new submodule rather than reusing a stale package child.
write_package("'new'"); write_package("'new'");
boost::system::error_code ec; {
fs::remove_all(package_root / "__pycache__", ec); // The rewrite can keep the old mtime and size, so drop the bytecode wherever the
// interpreter caches it.
PythonGILState gil;
REQUIRE(gil);
PyObjectPtr util(PyImport_ImportModule("importlib.util"));
REQUIRE(util);
for (const char* file : {"__init__.py", "sub.py"}) {
PyObjectPtr source(PyUnicode_DecodeFSDefault((package_root / file).string().c_str()));
REQUIRE(source);
PyObjectPtr cached(PyObject_CallMethod(util.get(), "cache_from_source", "O", source.get()));
REQUIRE(cached);
boost::system::error_code ec;
fs::remove(fs::path(PyUnicode_AsUTF8(cached.get())), ec);
}
}
paths.clear(); paths.clear();
modules.clear(); modules.clear();