Merge branch 'main' into feat/plugin-feature

Resolve five conflicts, all of which needed both sides rather than a pick:

- BackgroundSlicingProcess: ours was a pure tabs->spaces reformat of base, so
  keep main's per-filament volume/nozzle map read-back (its only change here).
- GUI_App: main's #12506 else-if attached to an `if` this branch deleted;
  re-expressed onto the same-agent early-return path (the agent factory caches
  per id, so pointer equality is the same predicate).
- MainFrame: both sides relocated Sync Presets independently; keep main's
  push_notification plus the branch's Plugins menu items.
- Tab: the "TODO: Orca: Support hybrid" blocks were unchanged base, not a branch
  decision; take main's enabled Hybrid to match the already auto-merged siblings.
- test_config: union of both sides' cases (6 plugin + 9 multi-nozzle).
This commit is contained in:
SoftFever
2026-07-15 17:18:46 +08:00
986 changed files with 210610 additions and 18572 deletions

View File

@@ -2931,10 +2931,11 @@ bool GUI_App::on_init_inner()
//update_label_colours_from_appconfig();
}
if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1";
init_sys_menu_enabled != new_sys_menu_enabled)
init_sys_menu_enabled != new_sys_menu_enabled) {
#ifdef __WINDOWS__
NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled);
#endif
}
#endif
// Orca: we allow user to pin the version of plugin, so we don't need to remove old networking plugins when the app version is updated
@@ -3748,8 +3749,24 @@ void GUI_App::switch_printer_agent()
return;
}
if (m_agent->get_printer_agent() == new_printer_agent)
// The factory caches agents per ID, so an identical pointer means the agent type is unchanged.
if (m_agent->get_printer_agent() == new_printer_agent) {
// Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper
// printer presets), so the selected machine and the agent's cached device_info still
// point at the previously active printer preset. Re-select the machine when the new
// preset targets a different host, otherwise filament sync keeps hitting the old
// printer. (#12506)
if (effective_agent_id != BBL_PRINTER_AGENT_ID && m_device_manager) {
const std::string print_host = config.opt_string("print_host");
if (!print_host.empty()) {
const std::string dev_id = MachineObject::dev_id_from_address(print_host, config.opt_string("printhost_port"));
MachineObject* sel = m_device_manager->get_selected_machine();
if (!sel || sel->get_dev_id() != dev_id)
select_machine(effective_agent_id);
}
}
return;
}
// Swap the agent
m_agent->set_printer_agent(new_printer_agent);
@@ -3760,10 +3777,8 @@ void GUI_App::switch_printer_agent()
// Start discovery so Python agents can populate the device list via SSDP callback
m_agent->start_discovery(true, false);
{
// Auto-switch MachineObject
select_machine(agent_info.id);
}
// Auto-switch MachineObject (new agent has empty device_info, so always re-select)
select_machine(agent_info.id);
}
void GUI_App::select_machine(const std::string& agent_id)
@@ -5236,21 +5251,28 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
if (plater != nullptr && wxGetApp().imgui()->display_initialized()) {
std::string text;
// Name the specific preset in the header so the user knows which one conflicts.
// The agent injects the local preset name into every 409 conflict body, so this is
// normally populated; fall back to a generic header if it is somehow missing.
const std::string header = conflict_preset_name.empty()
? _u8L("Cloud sync conflict:")
: format(_u8L("Cloud sync conflict for preset \"%s\":"), conflict_preset_name);
switch (conflict_code) {
case -1:
text = _u8L("Cloud sync conflict: this preset has a newer version in OrcaCloud.\n"
text = header + " " + _u8L("This preset has a newer version in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
break;
case -2:
text = _u8L("Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n"
text = header + " " + _u8L("A preset with this name already exists in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
break;
case -3:
text = _u8L("Cloud sync conflict: a preset with the same name was previously deleted from the cloud.\n"
text = header + " " + _u8L("A preset with the same name was previously deleted from the cloud.\n"
"Delete will delete your local preset. Force push overwrites it with your local preset.");
break;
default:
text = _u8L("Cloud sync conflict: there was an unexpected or unidentified preset conflict.\n"
text = header + " " + _u8L("There was an unexpected or unidentified preset conflict.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
break;
};
@@ -5269,10 +5291,12 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
[this, conflict_setting_id, conflict_preset_name, conflict_user_id](wxEvtHandler*) {
if (mainframe == nullptr)
return false;
MessageDialog
dlg(mainframe,
_L("Force push will overwrite the cloud copy with your local preset changes.\nDo you want to continue?"),
_L("Resolve cloud sync conflict"), wxCENTER | wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
const wxString confirm_msg = conflict_preset_name.empty()
? _L("Force push will overwrite the cloud copy with your local preset changes.\nDo you want to continue?")
: format_wxstr(_L("Force push will overwrite the cloud copy of preset \"%s\" with your local changes.\nDo you want to continue?"),
conflict_preset_name);
MessageDialog dlg(mainframe, confirm_msg, _L("Resolve cloud sync conflict"),
wxCENTER | wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
if (dlg.ShowModal() != wxID_YES)
return false;
@@ -6693,14 +6717,14 @@ void GUI_App::sync_preset(Preset* preset, bool force)
result = 0; // Set to 0 so the sync_info gets saved below
// Show user notification
CallAfter([this] {
CallAfter([this, name = preset->name] {
static bool size_limit_dialog_notified = false;
if (size_limit_dialog_notified)
return;
size_limit_dialog_notified = true;
if (mainframe == nullptr)
return;
auto msg = _L("The preset content is too large to sync to the cloud (exceeds 1MB). Please reduce the preset size by removing custom configurations or use it locally only.");
auto msg = format_wxstr(_L("The preset \"%s\" is too large to sync to the cloud (exceeds 1MB). Please reduce the preset size by removing custom configurations or use it locally only."), name);
MessageDialog(mainframe, msg, _L("Sync user presets"), wxICON_WARNING | wxOK).ShowModal();
});
// NOTE: Don't return here - let execution continue to save the sync_info
@@ -7077,6 +7101,10 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// finishFn tears down the progress dialog (and clears the re-entrancy guard), so it
// must run on every exit path — otherwise an early bail-out would leak the modal
// dialog and leave the guard stuck, blocking all later manual syncs.
// Guard the whole thread body: an uncaught exception here (e.g. a transient
// boost::filesystem error while scanning the preset folder) would otherwise
// propagate out of the thread and terminate the entire application.
try {
if (!m_agent) { finishFn(false); return; }
// One-time scan for orphaned .info files left over from offline deletions; queues HTTP DELETEs.
@@ -7252,7 +7280,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
.plater()
->get_notification_manager()
->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel, "There is an update available. Open the preset bundle dialog to update it.");
NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("There is an update available. Open the preset bundle dialog to update it."));
update_available = false;
}
@@ -7318,6 +7346,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
boost::this_thread::sleep_for(boost::chrono::milliseconds(500));
}
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "user preset sync thread terminated by exception: " << e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "user preset sync thread terminated by unknown exception";
}
});
}
@@ -7825,14 +7858,14 @@ bool GUI_App::load_language(wxString language, bool initial)
if (!wxLocale::IsAvailable(locale_language_info->Language)) {
// Loading the language dictionary failed.
wxString message = "Switching Orca Slicer to language " + requested_language_code + " failed.";
wxString message = wxString::Format(_L("Switching Orca Slicer to language %s failed."), requested_language_code);
#if !defined(_WIN32) && !defined(__APPLE__)
// likely some linux system
message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n";
message += _L("\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n");
#endif
if (initial)
message + "\n\nApplication will close.";
wxMessageBox(message, "Orca Slicer - Switching language failed", wxOK | wxICON_ERROR);
wxMessageBox(message, _L("Orca Slicer - Switching language failed"), wxOK | wxICON_ERROR);
if (initial)
std::exit(EXIT_FAILURE);
else
@@ -8696,8 +8729,13 @@ void GUI_App::scan_orphaned_info_files()
if (!fs::exists(type_dir))
continue;
// Iterate through all .info files
for (auto& entry : boost::filesystem::directory_iterator(type_dir)) {
// Iterate through all .info files. Use the error_code-based iterator so a transient
// directory-read failure (e.g. macOS readdir returning ENOTSUP) is logged and skipped
// instead of throwing an uncaught exception that would terminate the app from the
// background sync thread this runs on.
boost::system::error_code ec;
for (boost::filesystem::directory_iterator it(type_dir, ec), end; !ec && it != end; it.increment(ec)) {
const auto& entry = *it;
if (entry.path().extension() != ".info")
continue;
@@ -8716,6 +8754,8 @@ void GUI_App::scan_orphaned_info_files()
}
}
}
if (ec)
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
}
}