From 64d04a75f3365fa72a89d22170433e659aaa2be5 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 24 Aug 2026 11:36:37 +0800 Subject: [PATCH] feat: additional events --- src/libslic3r/LifecycleEvents.hpp | 50 ++++++++++++++++- src/slic3r/GUI/GUI_ObjectList.cpp | 25 ++++++++- src/slic3r/GUI/Jobs/PrintJob.cpp | 22 ++++++++ src/slic3r/GUI/Jobs/PrintJob.hpp | 3 + src/slic3r/GUI/Jobs/SendJob.cpp | 21 +++++++ src/slic3r/GUI/Jobs/SendJob.hpp | 2 + src/slic3r/GUI/PartPlate.cpp | 35 +++++++++++- src/slic3r/GUI/Plater.cpp | 61 ++++++++++++++++----- src/slic3r/GUI/ProjectDirtyStateManager.cpp | 47 +++++++++++++++- src/slic3r/GUI/ProjectDirtyStateManager.hpp | 23 +++++++- src/slic3r/GUI/TaskManager.cpp | 18 ++++++ src/slic3r/plugin/PythonPluginBridge.cpp | 20 ++++++- 12 files changed, 307 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp index 1528e97b46..9954836b3e 100644 --- a/src/libslic3r/LifecycleEvents.hpp +++ b/src/libslic3r/LifecycleEvents.hpp @@ -2,8 +2,8 @@ // 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 +// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and +// job 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. @@ -20,6 +20,7 @@ namespace Slic3r ProjectBeforeSave, ProjectAfterSave, ProjectClosed, + ProjectDirtyChanged, // Slicing pipeline SliceStarted, @@ -32,6 +33,12 @@ namespace Slic3r ObjectAdded, ObjectDeleted, ObjectTransformed, + ObjectChanged, + ObjectRenamed, + PlateCreated, + PlateDeleted, + PlateSelected, + PlateRenamed, // Preset PresetSelected, @@ -46,6 +53,12 @@ namespace Slic3r DeviceDisconnected, UploadStarted, UploadFinished, + + // Print/send jobs + PrintJobStarted, + PrintJobFinished, + SendJobStarted, + SendJobFinished, }; // Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with @@ -66,6 +79,28 @@ namespace Slic3r // 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; + + // Stable subject/object identifier, when the source model provides one. + std::string id; + + // Previous value for rename and other before/after events. + std::string previous_name; + + // Device identifier for printer and job events. + std::string device_id; + + // Job identifier when the originating queue/task provides one. + std::string job_id; + + // Source subsystem or operation detail, suitable for filtering but not guaranteed to be + // exhaustive across versions. + std::string source; + + // Plate, object, or volume index when the source uses an index rather than a stable id. + int index = -1; + + // Aggregate project dirty state for ProjectDirtyChanged. + bool dirty = false; }; inline std::string lifecycle_event_to_string(LifecycleEvent event) @@ -76,6 +111,7 @@ namespace Slic3r case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave"; case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave"; case LifecycleEvent::ProjectClosed: return "ProjectClosed"; + case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged"; case LifecycleEvent::SliceStarted: return "SliceStarted"; case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished"; @@ -86,6 +122,12 @@ namespace Slic3r case LifecycleEvent::ObjectAdded: return "ObjectAdded"; case LifecycleEvent::ObjectDeleted: return "ObjectDeleted"; case LifecycleEvent::ObjectTransformed: return "ObjectTransformed"; + case LifecycleEvent::ObjectChanged: return "ObjectChanged"; + case LifecycleEvent::ObjectRenamed: return "ObjectRenamed"; + case LifecycleEvent::PlateCreated: return "PlateCreated"; + case LifecycleEvent::PlateDeleted: return "PlateDeleted"; + case LifecycleEvent::PlateSelected: return "PlateSelected"; + case LifecycleEvent::PlateRenamed: return "PlateRenamed"; case LifecycleEvent::PresetSelected: return "PresetSelected"; case LifecycleEvent::PresetSaved: return "PresetSaved"; @@ -99,6 +141,10 @@ namespace Slic3r case LifecycleEvent::UploadStarted: return "UploadStarted"; case LifecycleEvent::UploadFinished: return "UploadFinished"; + case LifecycleEvent::PrintJobStarted: return "PrintJobStarted"; + case LifecycleEvent::PrintJobFinished: return "PrintJobFinished"; + case LifecycleEvent::SendJobStarted: return "SendJobStarted"; + case LifecycleEvent::SendJobFinished: return "SendJobFinished"; default: return "Unknown"; } } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index e786b7241b..2d492be812 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1,5 +1,6 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Factories.hpp" //#include "GUI_ObjectLayers.hpp" @@ -1200,17 +1201,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const if (m_objects_model->GetItemType(item) & itObject) { std::string name = m_objects_model->GetName(item).ToUTF8().data(); if (obj->name != name) { + const std::string previous_name = obj->name; obj->name = name; // if object has just one volume, rename this volume too if (obj->volumes.size() == 1) obj->volumes[0]->name = obj->name; Slic3r::save_object_mesh(*obj); + + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.id = std::to_string(obj->id().id); + ctx.index = obj_idx; + ctx.source = "object"; + fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx); } return; } if (volume_id < 0) return; - obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data(); + std::string name = m_objects_model->GetName(item).ToUTF8().data(); + if (obj->volumes[volume_id]->name == name) + return; + + const std::string previous_name = obj->volumes[volume_id]->name; + obj->volumes[volume_id]->name = name; + + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.id = std::to_string(obj->volumes[volume_id]->id().id); + ctx.index = volume_id; + ctx.source = "volume"; + fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx); } void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index d79deb955b..cd9b851b2a 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -1,4 +1,5 @@ #include "PrintJob.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -133,6 +134,13 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body) void PrintJob::process(Ctl &ctl) { + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "print_job"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + m_lifecycle_started = true; + /* display info */ std::string msg; wxString error_str; @@ -689,6 +697,7 @@ void PrintJob::process(Ctl &ctl) } wxQueueEvent(m_plater, evt); m_job_finished = true; + m_lifecycle_success = true; } } @@ -701,6 +710,19 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) { eptr = std::current_exception(); } + if (m_lifecycle_started && !m_lifecycle_finished) { + LifecycleEventContext finish_ctx; + finish_ctx.name = m_project_name; + finish_ctx.device_id = m_dev_id; + finish_ctx.source = "print_job"; + finish_ctx.code = canceled ? LifecycleEvtCode::Warn : + (eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok); + finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" : + (m_lifecycle_success ? "" : "failed")); + fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx); + m_lifecycle_finished = true; + } + if (canceled || eptr) return; } diff --git a/src/slic3r/GUI/Jobs/PrintJob.hpp b/src/slic3r/GUI/Jobs/PrintJob.hpp index 9a23b04054..4be68031d9 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.hpp +++ b/src/slic3r/GUI/Jobs/PrintJob.hpp @@ -43,6 +43,9 @@ class PrintJob : public Job std::function m_success_fun{nullptr}; std::string m_dev_id; bool m_job_finished{ false }; + bool m_lifecycle_started{ false }; + bool m_lifecycle_finished{ false }; + bool m_lifecycle_success{ false }; int m_print_job_completed_id = 0; wxString m_completed_evt_data; std::function m_enter_ip_address_fun_fail{ nullptr }; diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index bb52367188..5bf9010201 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -1,4 +1,5 @@ #include "SendJob.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -146,6 +147,13 @@ void SendJob::process(Ctl &ctl) } } + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "send_job"; + fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx); + m_lifecycle_started = true; + int total_plate_num = m_plater->get_partplate_list().get_plate_count(); PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx); @@ -423,6 +431,19 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr) eptr = std::current_exception(); } + if (m_lifecycle_started && !m_lifecycle_finished) { + LifecycleEventContext finish_ctx; + finish_ctx.name = m_project_name; + finish_ctx.device_id = m_dev_id; + finish_ctx.source = "send_job"; + finish_ctx.code = canceled ? LifecycleEvtCode::Warn : + (eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error)); + finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" : + (m_job_finished ? "" : "failed")); + fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx); + m_lifecycle_finished = true; + } + if (canceled || eptr) return; } diff --git a/src/slic3r/GUI/Jobs/SendJob.hpp b/src/slic3r/GUI/Jobs/SendJob.hpp index 1d4e025d6e..86d180ac8c 100644 --- a/src/slic3r/GUI/Jobs/SendJob.hpp +++ b/src/slic3r/GUI/Jobs/SendJob.hpp @@ -22,6 +22,8 @@ class SendJob : public Job PrintPrepareData job_data; std::string m_dev_id; bool m_job_finished{ false }; + bool m_lifecycle_started{ false }; + bool m_lifecycle_finished{ false }; int m_print_job_completed_id = 0; bool m_is_check_mode{false}; bool m_check_and_continue{false}; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 910c761c06..a2466bc4a7 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -25,6 +25,7 @@ #include "libslic3r/Tesselate.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "I18N.hpp" #include "GUI_App.hpp" @@ -2527,11 +2528,20 @@ void PartPlate::set_plate_name(const std::string& name) if (boost::equals(m_name, name)) return; + const std::string previous_name = m_name; m_name = name; if (m_print != nullptr) m_print->set_plate_name(name); invalidate_plate_name_texture(); + + if (m_plater != nullptr && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.index = m_plate_index; + fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx); + } } //get the print's object, result and index @@ -4638,9 +4648,16 @@ int PartPlateList::create_plate(bool adjust_position) if (m_plater) { // In GUI mode - wxGetApp().obj_list()->on_plate_added(plate); + wxGetApp().obj_list()->on_plate_added(plate); } + if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = plate->get_plate_name(); + ctx.index = new_index; + fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx); + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index; return new_index; } @@ -4739,6 +4756,7 @@ int PartPlateList::delete_plate(int index) BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index(); return -1; } + const std::string plate_name = plate->get_plate_name(); if (m_plater) { // In GUI mode @@ -4821,6 +4839,13 @@ int PartPlateList::delete_plate(int index) delete plate; + if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = plate_name; + ctx.index = index; + fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx); + } + // FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates(). #if 0 if (m_plater != nullptr) { @@ -4929,6 +4954,7 @@ int PartPlateList::select_plate(int index) if (m_plate_list.empty() || index >= m_plate_list.size()) { return -1; } + const int previous_index = m_current_plate; // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { @@ -4955,6 +4981,13 @@ int PartPlateList::select_plate(int index) //wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT)); } + if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = m_plate_list[index]->get_plate_name(); + ctx.index = index; + fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx); + } + return 0; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d9ad76b51f..4da3370681 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -61,6 +61,7 @@ #include #include "libslic3r/libslic3r.h" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/STEP.hpp" @@ -8470,6 +8471,11 @@ void Plater::priv::delete_all_objects_from_model() void Plater::priv::reset(bool apply_presets_change) { + // TakeSnapshot below and load_current_presets() further down each re-evaluate the + // aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle + // is_dirty() back and forth several times before it settles; coalesce those into one event. + ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state); + Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator); clear_warnings(); @@ -13118,21 +13124,28 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ 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(); - wxGetApp().update_saved_preset_from_current_preset(); - update_project_dirty_from_presets(); + { + // Same rationale as in Plater::priv::reset(): the whole reset + preset-reload + + // baseline-reset sequence below settles into its final dirty state only once it + // completes, so hold notifications until then to avoid firing on transient flips. + ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state); - //reset project - p->project.reset(); - //set project name - if (project_name.empty()) - p->set_project_name(_L("Untitled")); - else - p->set_project_name(project_name); + reset(transfer_preset_changes); + reset_project_dirty_after_save(); + reset_project_dirty_initial_presets(); + wxGetApp().update_saved_preset_from_current_preset(); + update_project_dirty_from_presets(); - Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + //reset project + p->project.reset(); + //set project name + if (project_name.empty()) + p->set_project_name(_L("Untitled")); + else + p->set_project_name(project_name); + + Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + } Model m; model().load_from(m); // new id avoid same path name @@ -18339,6 +18352,14 @@ void Plater::changed_object(ModelObject &object){ // Check outside bed get_current_canvas3D()->requires_check_outside_state(); + + if (!is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = object.name; + ctx.id = std::to_string(object.id().id); + ctx.source = "geometry"; + fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx); + } } void Plater::changed_object(int obj_idx) @@ -18375,6 +18396,20 @@ void Plater::changed_objects(const std::vector& object_idxs) // update print this->p->schedule_background_process(); + + if (!is_loading_project()) { + for (size_t obj_idx : object_idxs) { + if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr) + continue; + + LifecycleEventContext ctx; + ctx.name = p->model.objects[obj_idx]->name; + ctx.id = std::to_string(p->model.objects[obj_idx]->id().id); + ctx.index = static_cast(obj_idx); + ctx.source = "geometry"; + fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx); + } + } } void Plater::schedule_background_process(bool schedule/* = true*/) diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp index c986328f71..53eb3cd353 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.cpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp @@ -6,6 +6,7 @@ #include "MainFrame.hpp" #include "I18N.hpp" #include "Plater.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include @@ -17,13 +18,23 @@ namespace GUI { void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty) { + const bool was_dirty = is_dirty(); m_plater_dirty = dirty; + notify_dirty_change(was_dirty, "undo_redo"); if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized()) wxGetApp().mainframe->update_title(); } +void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty) +{ + const bool was_dirty = this->is_dirty(); + m_plater_dirty = is_dirty; + notify_dirty_change(was_dirty, "plater"); +} + void ProjectDirtyStateManager::update_from_presets() { + const bool was_dirty = is_dirty(); m_presets_dirty = false; // check switching of the presets only for exist/loaded project, but not for new GUI_App &app = wxGetApp(); @@ -45,18 +56,53 @@ void ProjectDirtyStateManager::update_from_presets() } m_presets_dirty |= app.has_unsaved_preset_changes(); m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config; + notify_dirty_change(was_dirty, "presets"); app.mainframe->update_title(); } void ProjectDirtyStateManager::reset_after_save() { + const bool was_dirty = is_dirty(); this->reset_initial_presets(); m_plater_dirty = false; m_presets_dirty = false; m_project_config_dirty = false; + notify_dirty_change(was_dirty, "save"); wxGetApp().mainframe->update_title(); } +void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source) +{ + if (m_suppress_depth > 0) + return; + + const bool dirty = is_dirty(); + if (was_dirty == dirty) + return; + + LifecycleEventContext ctx; + ctx.code = LifecycleEvtCode::Ok; + ctx.dirty = dirty; + ctx.source = source; + fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx); +} + +void ProjectDirtyStateManager::begin_suppress_notifications() +{ + if (m_suppress_depth == 0) + m_suppress_entry_dirty = is_dirty(); + ++m_suppress_depth; +} + +void ProjectDirtyStateManager::end_suppress_notifications() +{ + assert(m_suppress_depth > 0); + if (m_suppress_depth > 0) + --m_suppress_depth; + if (m_suppress_depth == 0) + notify_dirty_change(m_suppress_entry_dirty, "batch"); +} + void ProjectDirtyStateManager::reset_initial_presets() { m_initial_presets.fill(std::string{}); @@ -168,4 +214,3 @@ void ProjectDirtyStateManager::render_debug_window() const } // namespace GUI } // namespace Slic3r - diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.hpp b/src/slic3r/GUI/ProjectDirtyStateManager.hpp index 171d19e219..fd121d1d85 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.hpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.hpp @@ -14,21 +14,42 @@ public: void reset_after_save(); void reset_initial_presets(); - void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; } + void set_plater_dirty(bool is_dirty); bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; } bool is_presets_dirty() const { return m_presets_dirty; } + // RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged + // notifications are held back; when the outermost guard is destroyed, at most one + // notification fires, reflecting only the net change across the whole guarded scope. + class NotificationSuppressor + { + public: + explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); } + ~NotificationSuppressor() { m_owner.end_suppress_notifications(); } + NotificationSuppressor(const NotificationSuppressor &) = delete; + NotificationSuppressor &operator=(const NotificationSuppressor &) = delete; + private: + ProjectDirtyStateManager &m_owner; + }; + #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW void render_debug_window() const; #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW private: + void notify_dirty_change(bool was_dirty, const char *source); + void begin_suppress_notifications(); + void end_suppress_notifications(); + // Does the Undo / Redo stack indicate the project is dirty? bool m_plater_dirty { false }; // Do the presets indicate the project is dirty? bool m_presets_dirty { false }; // Is the project config dirty? bool m_project_config_dirty { false }; + // NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began. + int m_suppress_depth { 0 }; + bool m_suppress_entry_dirty { false }; // Keeps track of preset names selected at the time of last project save. std::array m_initial_presets; DynamicPrintConfig m_initial_project_config; diff --git a/src/slic3r/GUI/TaskManager.cpp b/src/slic3r/GUI/TaskManager.cpp index d294d140c5..b0f6f43f58 100644 --- a/src/slic3r/GUI/TaskManager.cpp +++ b/src/slic3r/GUI/TaskManager.cpp @@ -1,6 +1,7 @@ #include "TaskManager.hpp" #include "libslic3r/Thread.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "nlohmann/json.hpp" #include "MainFrame.hpp" #include "GUI_App.hpp" @@ -210,6 +211,13 @@ int TaskManager::schedule(TaskStateInfo* task) assert(task->state() == TaskState::TS_PENDING); task->set_state(TaskState::TS_SENDING); + LifecycleEventContext start_ctx; + start_ctx.name = task->params().project_name; + start_ctx.device_id = task->params().dev_id; + start_ctx.job_id = std::to_string(task->task_info_id); + start_ctx.source = "task_manager"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + BOOST_LOG_TRIVIAL(trace) << "task_manager: schedule a task to dev_id = " << task->params().dev_id; boost::thread* new_sending_thread = new boost::thread(); *new_sending_thread = Slic3r::create_thread( @@ -237,6 +245,16 @@ int TaskManager::schedule(TaskStateInfo* task) task->set_state(TaskState::TS_SEND_CANCELED); } } + + LifecycleEventContext finish_ctx; + finish_ctx.name = task->params().project_name; + finish_ctx.device_id = task->params().dev_id; + finish_ctx.job_id = std::to_string(task->task_info_id); + finish_ctx.source = "task_manager"; + finish_ctx.code = result == 0 ? LifecycleEvtCode::Ok : + (task->is_canceled() ? LifecycleEvtCode::Warn : LifecycleEvtCode::Error); + finish_ctx.msg = result == 0 ? "" : (task->is_canceled() ? "cancelled" : "failed"); + fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx); /* remove from sending task list */ m_scedule_mutex.lock(); diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 34a91a543e..2b4ca52fd8 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -345,6 +345,7 @@ void bind_python_api(pybind11::module_& m) .value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave) .value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave) .value("ProjectClosed", LifecycleEvent::ProjectClosed) + .value("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged) .value("SliceStarted", LifecycleEvent::SliceStarted) .value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished) .value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted) @@ -353,6 +354,12 @@ void bind_python_api(pybind11::module_& m) .value("ObjectAdded", LifecycleEvent::ObjectAdded) .value("ObjectDeleted", LifecycleEvent::ObjectDeleted) .value("ObjectTransformed", LifecycleEvent::ObjectTransformed) + .value("ObjectChanged", LifecycleEvent::ObjectChanged) + .value("ObjectRenamed", LifecycleEvent::ObjectRenamed) + .value("PlateCreated", LifecycleEvent::PlateCreated) + .value("PlateDeleted", LifecycleEvent::PlateDeleted) + .value("PlateSelected", LifecycleEvent::PlateSelected) + .value("PlateRenamed", LifecycleEvent::PlateRenamed) .value("PresetSelected", LifecycleEvent::PresetSelected) .value("PresetSaved", LifecycleEvent::PresetSaved) .value("PrintStateChanged", LifecycleEvent::PrintStateChanged) @@ -363,6 +370,10 @@ void bind_python_api(pybind11::module_& m) .value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected) .value("UploadStarted", LifecycleEvent::UploadStarted) .value("UploadFinished", LifecycleEvent::UploadFinished) + .value("PrintJobStarted", LifecycleEvent::PrintJobStarted) + .value("PrintJobFinished", LifecycleEvent::PrintJobFinished) + .value("SendJobStarted", LifecycleEvent::SendJobStarted) + .value("SendJobFinished", LifecycleEvent::SendJobFinished) .export_values(); py::enum_(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext") @@ -375,7 +386,14 @@ void bind_python_api(pybind11::module_& m) .def(py::init<>()) .def_readonly("name", &LifecycleEventContext::name) .def_readonly("code", &LifecycleEventContext::code) - .def_readonly("msg", &LifecycleEventContext::msg); + .def_readonly("msg", &LifecycleEventContext::msg) + .def_readonly("id", &LifecycleEventContext::id) + .def_readonly("previous_name", &LifecycleEventContext::previous_name) + .def_readonly("device_id", &LifecycleEventContext::device_id) + .def_readonly("job_id", &LifecycleEventContext::job_id) + .def_readonly("source", &LifecycleEventContext::source) + .def_readonly("index", &LifecycleEventContext::index) + .def_readonly("dirty", &LifecycleEventContext::dirty); py::class_(m, "PluginContext", "Context shared with plugin entry points") .def(py::init<>())