From dfd3444ae7d713b0f1ebfdbb241fbeeb4b7b0d84 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 19 Aug 2026 19:20:31 +0800 Subject: [PATCH] feat: initial draft of lifecycle events API for plugins --- src/libslic3r/GCode.cpp | 49 ++++++- src/libslic3r/LifecycleEvents.hpp | 135 ++++++++++++++++++++ src/libslic3r/Preset.cpp | 10 ++ src/libslic3r/Print.cpp | 38 +++++- src/slic3r/GUI/DeviceCore/DevManager.cpp | 27 +++- src/slic3r/GUI/DeviceManager.cpp | 28 +++- src/slic3r/GUI/GLCanvas3D.cpp | 31 ++++- src/slic3r/GUI/GUI_App.cpp | 22 ++++ src/slic3r/GUI/GUI_ObjectList.cpp | 15 +++ src/slic3r/GUI/Jobs/ArrangeJob.cpp | 8 ++ src/slic3r/GUI/Jobs/FillBedJob.cpp | 8 ++ src/slic3r/GUI/Jobs/OrientJob.cpp | 7 + src/slic3r/GUI/Plater.cpp | 58 +++++++++ src/slic3r/GUI/Tab.cpp | 15 +++ src/slic3r/Utils/PrintHost.cpp | 18 +++ src/slic3r/plugin/PluginHooks.cpp | 13 ++ src/slic3r/plugin/PluginManager.cpp | 21 +++ src/slic3r/plugin/PluginManager.hpp | 4 +- src/slic3r/plugin/PyPluginTrampoline.hpp | 6 + src/slic3r/plugin/PythonPluginBridge.cpp | 41 ++++++ src/slic3r/plugin/PythonPluginInterface.hpp | 4 + 21 files changed, 545 insertions(+), 13 deletions(-) create mode 100644 src/libslic3r/LifecycleEvents.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 18d805936e..6f5f2ad8e4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7,6 +7,7 @@ #include "I18N.hpp" #include "GCode.hpp" #include "Exception.hpp" +#include "LifecycleEvents.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" #include "Geometry/ConvexHull.hpp" @@ -2446,6 +2447,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_writer.set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); + } + // check if any custom gcode contains keywords used by the gcode processor to // produce time estimation and gcode toolpaths std::vector> validation_res = DoExport::validate_custom_gcode(*print); @@ -2481,12 +2489,22 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.set_print(print); GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) { - BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl; + std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"; + BOOST_LOG_TRIVIAL(error) << err_msg << std::endl; if (!fs::exists(folder)) { //fs::create_directory(folder); - BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl; + std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!"; + BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl; + err_msg += add_err_msg; } - throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = err_msg; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } + throw Slic3r::RuntimeError(err_msg); } try { @@ -2497,11 +2515,18 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu boost::nowide::remove(path_tmp.c_str()); throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); } - } catch (std::exception & /* ex */) { + } catch (std::exception &ex) { // Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown. // Close and remove the file. file.close(); boost::nowide::remove(path_tmp.c_str()); + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = ex.what(); + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw; } file.close(); @@ -2607,6 +2632,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu std::error_code ret = rename_file(path_tmp, path); if (ret) { + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = "Failed to rename the output G-code file: " + ret.message(); + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw Slic3r::RuntimeError( std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' + "Is " + path_tmp + " locked?" + '\n'); @@ -2617,7 +2649,14 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info(); print->set_done(psGCodeExport); - + + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } + // Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT / // M486), which is driven by exclude_object for every printer if(result != nullptr) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp new file mode 100644 index 0000000000..1528e97b46 --- /dev/null +++ b/src/libslic3r/LifecycleEvents.hpp @@ -0,0 +1,135 @@ +#pragma once + +// LifecycleEvents.hpp +// -------------------- +// Application lifecycle events (project, slicing, plate editing, preset, printer connection +// activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to +// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside +// the slicing engine itself; see fire_lifecycle_event() below. + +#include +#include +#include + +namespace Slic3r +{ + enum class LifecycleEvent { + // Project (3mf) + NewProject, + ProjectOpened, + ProjectBeforeSave, + ProjectAfterSave, + ProjectClosed, + + // Slicing pipeline + SliceStarted, + SliceGeometryFinished, + GCodeExportStarted, + GCodeExportFinished, + SlicingJobComplete, + + // Plate/model editing + ObjectAdded, + ObjectDeleted, + ObjectTransformed, + + // Preset + PresetSelected, + PresetSaved, + + // Printer/device + PrintStateChanged, + DeviceOnlineChanged, + DeviceDiscovered, + DeviceSelected, + DeviceConnected, + DeviceDisconnected, + UploadStarted, + UploadFinished, + }; + + // Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with + // Windows macros (wingdi.h) as unqualified names. + enum class LifecycleEvtCode { Ok, Error, Warn }; + + struct LifecycleEventContext + { + // The primary subject identifier for the event. This is event-specific (for example, a + // project/output path, preset name, device id, or object name), must not contain status or + // prose, and may be empty when the event has no single subject. + std::string name; + + // Outcome of the operation represented by the event. For state-change and start events, + // Ok means that the event occurred; it does not imply that a future operation succeeded. + LifecycleEvtCode code = LifecycleEvtCode::Ok; + + // Optional human-readable detail or diagnostic text. It is not a stable parsing contract; + // machine-readable data should be represented by a dedicated field or event instead. + std::string msg; + }; + + inline std::string lifecycle_event_to_string(LifecycleEvent event) + { + switch (event) { + case LifecycleEvent::NewProject: return "NewProject"; + case LifecycleEvent::ProjectOpened: return "ProjectOpened"; + case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave"; + case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave"; + case LifecycleEvent::ProjectClosed: return "ProjectClosed"; + + case LifecycleEvent::SliceStarted: return "SliceStarted"; + case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished"; + case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted"; + case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished"; + case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete"; + + case LifecycleEvent::ObjectAdded: return "ObjectAdded"; + case LifecycleEvent::ObjectDeleted: return "ObjectDeleted"; + case LifecycleEvent::ObjectTransformed: return "ObjectTransformed"; + + case LifecycleEvent::PresetSelected: return "PresetSelected"; + case LifecycleEvent::PresetSaved: return "PresetSaved"; + case LifecycleEvent::PrintStateChanged: return "PrintStateChanged"; + + case LifecycleEvent::DeviceOnlineChanged: return "DeviceOnlineChanged"; + case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered"; + case LifecycleEvent::DeviceSelected: return "DeviceSelected"; + case LifecycleEvent::DeviceConnected: return "DeviceConnected"; + case LifecycleEvent::DeviceDisconnected: return "DeviceDisconnected"; + + case LifecycleEvent::UploadStarted: return "UploadStarted"; + case LifecycleEvent::UploadFinished: return "UploadFinished"; + default: return "Unknown"; + } + } + + inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code) + { + switch (code) { + case LifecycleEvtCode::Ok: return "Ok"; + case LifecycleEvtCode::Error: return "Error"; + case LifecycleEvtCode::Warn: return "Warn"; + default: return "Unknown"; + } + } + + // Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can + // fire a lifecycle event without depending on the plugin layer above it, which installs the + // dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should + // require callers to hold a Print& just to report an event. + using LifecycleHookFn = std::function; + + inline LifecycleHookFn& lifecycle_hook_fn() + { + static LifecycleHookFn fn; + return fn; + } + + inline void set_lifecycle_hook_fn(LifecycleHookFn fn) { lifecycle_hook_fn() = std::move(fn); } + + inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) + { + if (const LifecycleHookFn& fn = lifecycle_hook_fn(); fn) + fn(event, ctx); + } +} diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9ae8b86fd8..22a0c4f6a7 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -45,6 +45,7 @@ #include #include "libslic3r.h" +#include "LifecycleEvents.hpp" #include "Utils.hpp" #include "Time.hpp" #include "PlaceholderParser.hpp" @@ -2931,6 +2932,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det // 1) Find the preset with a new_name or create a new one, // initialize it with the edited config. auto it = this->find_preset_internal(new_name); + const bool preset_existed = (it != m_presets.end() && it->name == new_name); if (it != m_presets.end() && it->name == new_name) { // Preset with the same name found. Preset &preset = *it; @@ -3038,6 +3040,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det this->get_selected_preset().save(&(parent_preset->config)); else this->get_selected_preset().save(nullptr); + + { + LifecycleEventContext ctx; + ctx.name = new_name; + ctx.msg = preset_existed ? "overwrite" : "new"; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx); + } } bool PresetCollection::delete_current_preset() diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1af28255ee..68bff0cd8e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,6 +8,7 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" +#include "LifecycleEvents.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -2252,6 +2253,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (m_objects.empty()) return; + { + LifecycleEventContext ctx; + ctx.name = output_filename(); + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx); + } + for (PrintObject *obj : m_objects) obj->clear_shared_object(); @@ -2818,6 +2826,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info(); + + { + LifecycleEventContext ctx; + ctx.code = LifecycleEvtCode::Ok; + ctx.name = output_filename(); + fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx); + } } // G-code export process, running at a background thread. @@ -4434,6 +4449,13 @@ void Print::set_gcode_file_invalidated() //BBS: add gcode file preload logic void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) { + { + LifecycleEventContext ctx; + ctx.name = file; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); + } + try { GCodeProcessor processor; GCodeProcessor::s_IsBBLPrinter = is_BBL_printer(); @@ -4453,13 +4475,27 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces *result = std::move(processor.extract_result()); result->filament_change_sequence = filament_seq_loaded; result->nozzle_change_sequence = nozzle_seq_loaded; - } catch (std::exception & /* ex */) { + } catch (std::exception &ex) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str(); + { + LifecycleEventContext ctx; + ctx.name = file; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = ex.what(); + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw Slic3r::RuntimeError( std::string("Failed to process the G-code file ") + file + " from previous 3mf\n"); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str(); + + { + LifecycleEventContext ctx; + ctx.name = file; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } } std::tuple Print::object_skirt_offset(double margin_height) const diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index edc958ec53..e3a785f430 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -10,6 +10,7 @@ #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libslic3r/Time.hpp" @@ -328,7 +329,10 @@ namespace Slic3r obj->bind_state = "free"; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->m_is_online = true; + // Route through set_online_state() (rather than writing m_is_online directly) so the + // DeviceOnlineChanged lifecycle event fires consistently; same effective value/behavior + // here since the object was already online in the common case. + obj->set_online_state(true); obj->set_dev_name(dev_name); /* if (!obj->dev_ip.empty()) { Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip); @@ -345,6 +349,10 @@ namespace Slic3r obj->bind_sec_link = sec_link; obj->dev_connection_name = connection_name; obj->bind_ssdp_version = ssdp_version; + // Discovery establishes the initial reachability state. Do not report it as an + // online transition; DeviceDiscovered below is the lifecycle event for a new + // device. Subsequent updates route through set_online_state(), so a known device + // still emits DeviceOnlineChanged when its reachability actually changes. obj->m_is_online = true; //load access code @@ -361,6 +369,15 @@ namespace Slic3r BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id << ", ip = " << dev_ip <<", printer_name = " << dev_name << ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state; + + // First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to + // an already-known one, which is handled in the branch above). + { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx); + } } update_local_machine(*obj); } @@ -935,8 +952,14 @@ namespace Slic3r } void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/, - const std::string& /*new_dev_id*/) + const std::string& new_dev_id) { + { + LifecycleEventContext ctx; + ctx.name = new_dev_id; // empty string is a valid deselection + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx); + } if (MachineObject* obj_ = get_selected_machine()) { GUI::wxGetApp().sidebar().update_sync_status(obj_); if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index f4befe78c1..ba818aefd6 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "GuiColor.hpp" #include "GUI_App.hpp" @@ -2591,7 +2592,15 @@ void MachineObject::reset() void MachineObject::set_print_state(std::string status) { + const bool changed = (print_status != status); print_status = status; + if (changed) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = print_status; + fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx); + } } int MachineObject::connect(bool use_openssl) @@ -2613,6 +2622,10 @@ int MachineObject::connect(bool use_openssl) int MachineObject::disconnect() { if (m_agent) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); return m_agent->disconnect_printer(); } return -1; @@ -2643,8 +2656,16 @@ bool MachineObject::is_connecting() void MachineObject::set_online_state(bool on_off) { + const bool changed = (m_is_online != on_off); m_is_online = on_off; if (!on_off) m_active_state = NotActive; + if (changed) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = on_off ? "online" : "offline"; + fire_lifecycle_event(LifecycleEvent::DeviceOnlineChanged, ctx); + } } bool MachineObject::is_info_ready(bool check_version) const @@ -4566,8 +4587,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ try { if (j.contains("event")) { if (j["event"].contains("event")) { - if (j["event"]["event"].get() == "client.disconnected") + if (j["event"]["event"].get() == "client.disconnected") { set_online_state(false); + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); + } else if (j["event"]["event"].get() == "client.connected") set_online_state(true); } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index f6ffb75405..469ab8120e 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -39,6 +39,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/MacDarkMode.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include @@ -5061,8 +5062,16 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) //BBS: nofity object list to update wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); - if (object_moved) + if (object_moved) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "moved"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); + } // BBS: support wipe-tower for multi-plates for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) { @@ -5183,8 +5192,16 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) //BBS: nofity object list to update wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); - if (!done.empty()) + if (!done.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "rotated"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); + } m_dirty = true; } @@ -5275,8 +5292,16 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) //BBS: notify object info update wxGetApp().plater()->show_object_info(); - if (!done.empty()) + if (!done.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "scaled"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); + } m_dirty = true; } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 0d4e3f35cf..83b2b2303d 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2169,6 +2169,11 @@ void GUI_App::init_networking_callbacks() obj->command_get_access_code(); if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); + + LifecycleEventContext ctx; + ctx.name = obj->get_dev_id(); + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx); } }); }); @@ -2207,6 +2212,11 @@ void GUI_App::init_networking_callbacks() obj->command_get_version(); event.SetInt(0); event.SetString(obj->get_dev_id()); + + LifecycleEventContext ctx; + ctx.name = obj->get_dev_id(); + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx); } else if (state == ConnectStatus::ConnectStatusFailed) { // Orca: only update status if same device id if (m_device_manager->selected_machine != dev_id) return; @@ -2222,10 +2232,22 @@ void GUI_App::init_networking_callbacks() wxGetApp().show_dialog(text); } event.SetInt(-1); + + { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); + } } else if (state == ConnectStatus::ConnectStatusLost) { m_device_manager->set_selected_machine(""); event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost"; + + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); } else { event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index dc89f5f9bb..e786b7241b 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -9,6 +9,7 @@ #include "BitmapComboBox.hpp" #include "MainFrame.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "OptionsGroup.hpp" #include "Tab.hpp" @@ -3530,7 +3531,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx) obj->delete_connectors(); if (obj->volumes.empty() || !obj->has_solid_mesh()) { + const std::string deleted_obj_name = obj->name; model.delete_object(idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = deleted_obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } m_objects_model->Delete(m_objects_model->GetItemById(idx)); continue; } @@ -4099,6 +4107,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed, const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut()); Expand(m_objects_model->GetParent(item)); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = model_object->name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx); + } + if (!do_info_update) return; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 6b74b71af1..71a5e58344 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -13,6 +13,7 @@ #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libnest2d/common.hpp" @@ -699,6 +700,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale(ap.translation(X)) % unscale(ap.translation(Y)) % ap.name; } + { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "arranged"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } + m_plater->update(); // BBS //wxGetApp().obj_manipul()->set_dirty(); diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index a9a66cb96f..502282c307 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libnest2d/common.hpp" #include @@ -347,6 +348,13 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) m_plater->arrange(); } m_plater->update(); + + { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "arranged"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } } m_plater->mark_plate_toolbar_image_dirty(); diff --git a/src/slic3r/GUI/Jobs/OrientJob.cpp b/src/slic3r/GUI/Jobs/OrientJob.cpp index ee8ea875c0..ed9117e8bf 100644 --- a/src/slic3r/GUI/Jobs/OrientJob.cpp +++ b/src/slic3r/GUI/Jobs/OrientJob.cpp @@ -5,6 +5,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libslic3r/PresetBundle.hpp" @@ -254,6 +255,12 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr) mesh.apply(); } + if (!m_selected.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "auto_oriented"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } m_plater->update(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2225812ff6..197ece0823 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8375,7 +8375,14 @@ void Plater::priv::remove(size_t obj_idx) view3D->enable_layers_editing(false); m_worker.cancel_all(); + std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx); model.delete_object(obj_idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); update(); @@ -8408,7 +8415,14 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia if (obj->is_cut()) sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); + std::string obj_name = obj->name; model.delete_object(obj_idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); @@ -8458,6 +8472,14 @@ void Plater::priv::reset(bool apply_presets_change) clear_warnings(); + const std::string closed_project_name = into_u8(get_project_filename()); + if (!closed_project_name.empty() || !model.objects.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.name = closed_project_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx); + } + set_project_filename(""); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: empty"; @@ -10868,11 +10890,15 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) notification_manager->set_slicing_progress_export_possible(); // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. + const std::string lifecycle_job_name = this->background_process.fff_print() ? + this->background_process.fff_print()->output_filename() : std::string(); this->background_process.reset_export(); // This bool stops showing export finished notification even when process_completed_with_error is false bool has_error = false; + std::string lifecycle_error_msg; if (evt.error()) { auto message = evt.format_error_message(); + lifecycle_error_msg = message.first; if (evt.critical_error()) { if (q->m_tracking_popup_menu) { // We don't want to pop-up a message box when tracking a pop-up menu. @@ -10910,6 +10936,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) is_finished = true; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = lifecycle_job_name; + ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok); + ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string()); + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx); + } + //BBS: set the current plater's slice result to valid if (!this->background_process.empty()) this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); @@ -13068,6 +13102,11 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ //get_partplate_list().reinit(); //get_partplate_list().update_slice_context_to_current_plate(p->background_process); //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + if (!silent) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx); + } reset(transfer_preset_changes); reset_project_dirty_after_save(); reset_project_dirty_initial_presets(); @@ -13199,6 +13238,13 @@ void Plater::load_project(wxString const& filename2, p->set_project_name(_L("Untitled")); } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(load_restore ? originfile : filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx); + } + } else { if (using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; @@ -13261,11 +13307,23 @@ int Plater::save_project(bool saveAs) if (full_pathnames) { save_strategy = save_strategy | SaveStrategy::FullPathSources; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx); + } if (export_3mf(into_path(filename), save_strategy) < 0) { MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."), _L("Save project"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx); + } Slic3r::remove_backup(model(), false); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1a31355d0e..9d6e2299c0 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -36,6 +36,7 @@ #include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/plugin/PluginConfig.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "Plater.hpp" #include "MainFrame.hpp" #include "format.hpp" @@ -6900,6 +6901,20 @@ bool Tab::select_preset( } load_current_preset(); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = preset_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + switch (m_type) { + case Preset::TYPE_PRINT: ctx.msg = "print"; break; + case Preset::TYPE_SLA_PRINT: ctx.msg = "sla_print"; break; + case Preset::TYPE_FILAMENT: ctx.msg = "filament"; break; + case Preset::TYPE_SLA_MATERIAL: ctx.msg = "sla_material"; break; + case Preset::TYPE_PRINTER: ctx.msg = "printer"; break; + default: break; + } + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::PresetSelected, ctx); + } if (delete_third_printer) { wxGetApp().CallAfter([filament_presets, process_presets]() { diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 7f69d5e087..0952019b10 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -23,6 +23,7 @@ #include "CrealityPrint.hpp" #include "../GUI/PrintHostDialogs.hpp" #include "../GUI/MainFrame.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "Obico.hpp" #include "Flashforge.hpp" #include "SimplyPrint.hpp" @@ -358,12 +359,29 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) { emit_progress(0); // Indicate the upload is starting + // Captured before upload_data is moved into upload() below. + const std::string upload_filename = the_job.upload_data.source_path.filename().string(); + + { + LifecycleEventContext ctx; + ctx.name = upload_filename; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx); + } + bool success = the_job.printhost->upload(std::move(the_job.upload_data), [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, [this](wxString error) { this->error_fn(std::move(error)); }, [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } ); + { + LifecycleEventContext ctx; + ctx.name = upload_filename; + ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error; + fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx); + } + if (success) { emit_progress(100); if (the_job.switch_to_device_tab) { diff --git a/src/slic3r/plugin/PluginHooks.cpp b/src/slic3r/plugin/PluginHooks.cpp index c6aaaf803c..2d84df5fdb 100644 --- a/src/slic3r/plugin/PluginHooks.cpp +++ b/src/slic3r/plugin/PluginHooks.cpp @@ -7,6 +7,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/Exception.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/Print.hpp" #include "libslic3r_version.h" @@ -46,6 +47,16 @@ void install_capability_resolver() }); } +// Global libslic3r-side seam (Slic3r::fire_lifecycle_event, in libslic3r/LifecycleEvents.hpp): +// broadcasts to every loaded, enabled capability regardless of type, unlike the SlicingPipeline +// hook below which only targets picker-selected SlicingPipeline capabilities. +void install_lifecycle_event_hook() +{ + set_lifecycle_hook_fn([](LifecycleEvent event, const LifecycleEventContext& ctx) { + PluginManager::instance().dispatch_lifecycle_event(event, ctx); + }); +} + // Print::process() fires this hook at each pipeline seam on the slicing worker // thread; here we run the picker-selected SlicingPipeline capabilities. Per // capability we acquire the GIL, honor cancellation, and convert a plugin @@ -122,12 +133,14 @@ void install() { install_capability_resolver(); install_slicing_pipeline_hook(); + install_lifecycle_event_hook(); } void uninstall() { ConfigBase::set_resolve_capability_fn(nullptr); Print::set_slicing_pipeline_hook_fn(nullptr); + set_lifecycle_hook_fn(nullptr); } } // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 3587c0a824..3675f0e466 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -1,5 +1,6 @@ #include "PluginManager.hpp" +#include #include #include #include @@ -1987,4 +1988,24 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k return result; } +void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) { + for (const auto& cap : get_plugin_capabilities()) { + if (!cap || !cap->is_enabled()) continue; + try { + cap->on_lifecycle_event(evt, ctx); + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name() + << "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what() + << " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code) + << ", msg='" << ctx.msg << "']"; + } catch (...) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name() + << "' on_lifecycle_event(" << lifecycle_event_to_string(evt) + << ") threw a non-standard exception" + << " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code) + << ", msg='" << ctx.msg << "']"; + } + } +} + } // namespace Slic3r diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index ddb0bf9dce..3d6be9981a 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -20,7 +21,6 @@ #include #include "CloudPluginService.hpp" -#include "PluginFsUtils.hpp" #include "PluginDescriptor.hpp" #include "PluginLoader.hpp" #include "PluginConfig.hpp" @@ -204,6 +204,8 @@ public: ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error); + void dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx); + private: PluginManager() = default; PluginManager(const PluginManager&) = delete; diff --git a/src/slic3r/plugin/PyPluginTrampoline.hpp b/src/slic3r/plugin/PyPluginTrampoline.hpp index e36c0b375c..a36fd8100a 100644 --- a/src/slic3r/plugin/PyPluginTrampoline.hpp +++ b/src/slic3r/plugin/PyPluginTrampoline.hpp @@ -129,6 +129,12 @@ public: { ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); } + + void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx); + } }; class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 328ebbace1..264b72a24c 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -338,6 +338,44 @@ void bind_python_api(pybind11::module_& m) .value("FatalError", PluginResult::FatalError) .export_values(); + py::enum_(m, "LifecycleEvent", "Application lifecycle moment passed to on_lifecycle_event") + .value("NewProject", LifecycleEvent::NewProject) + .value("ProjectOpened", LifecycleEvent::ProjectOpened) + .value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave) + .value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave) + .value("ProjectClosed", LifecycleEvent::ProjectClosed) + .value("SliceStarted", LifecycleEvent::SliceStarted) + .value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished) + .value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted) + .value("GCodeExportFinished", LifecycleEvent::GCodeExportFinished) + .value("SlicingJobComplete", LifecycleEvent::SlicingJobComplete) + .value("ObjectAdded", LifecycleEvent::ObjectAdded) + .value("ObjectDeleted", LifecycleEvent::ObjectDeleted) + .value("ObjectTransformed", LifecycleEvent::ObjectTransformed) + .value("PresetSelected", LifecycleEvent::PresetSelected) + .value("PresetSaved", LifecycleEvent::PresetSaved) + .value("PrintStateChanged", LifecycleEvent::PrintStateChanged) + .value("DeviceOnlineChanged", LifecycleEvent::DeviceOnlineChanged) + .value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered) + .value("DeviceSelected", LifecycleEvent::DeviceSelected) + .value("DeviceConnected", LifecycleEvent::DeviceConnected) + .value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected) + .value("UploadStarted", LifecycleEvent::UploadStarted) + .value("UploadFinished", LifecycleEvent::UploadFinished) + .export_values(); + + py::enum_(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext") + .value("Ok", LifecycleEvtCode::Ok) + .value("Error", LifecycleEvtCode::Error) + .value("Warn", LifecycleEvtCode::Warn) + .export_values(); + + py::class_(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent") + .def(py::init<>()) + .def_readonly("name", &LifecycleEventContext::name) + .def_readonly("code", &LifecycleEventContext::code) + .def_readonly("msg", &LifecycleEventContext::msg); + py::class_(m, "PluginContext", "Context shared with plugin entry points") .def(py::init<>()) .def_readwrite("orca_version", &PluginContext::orca_version); @@ -362,6 +400,9 @@ void bind_python_api(pybind11::module_& m) .def("get_type", &PluginCapabilityInterface::get_type) .def("on_load", &PluginCapabilityInterface::on_load) .def("on_unload", &PluginCapabilityInterface::on_unload) + .def("on_lifecycle_event", &PluginCapabilityInterface::on_lifecycle_event, + "Override to react to an application lifecycle moment (LifecycleEvent) and its\n" + "LifecycleEventContext payload. Available on every capability type.") .def("has_config_ui", &PluginCapabilityInterface::has_config_ui, "Override to return True to replace the host's default JSON editor with your own HTML\n" "UI, returned by get_config_ui(). Every capability is configurable and appears in the\n" diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index 4a6df06441..c1e0b17aac 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -10,6 +10,8 @@ #include #include +#include + namespace Slic3r { enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; @@ -167,6 +169,8 @@ public: virtual void on_unload() {} virtual void on_cancelled() {} + virtual void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { (void) event; (void) ctx; } + // ── C++-only host state, never exposed to Python. Set by the loader at materialization. ── // // The capability owns its own identity and enable flag: they are read once under the GIL, live