diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 92aa979e4..7a258182e 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -624,11 +624,17 @@ const std::vector& Preset::sla_printer_options() PresetCollection::PresetCollection(Preset::Type type, const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name) : m_type(type), m_edited_preset(type, "", false), +#if ENABLE_PROJECT_DIRTY_STATE + m_saved_preset(type, "", false), +#endif // ENABLE_PROJECT_DIRTY_STATE m_idx_selected(0) { // Insert just the default preset. this->add_default_preset(keys, defaults, default_name); m_edited_preset.config.apply(m_presets.front().config); +#if ENABLE_PROJECT_DIRTY_STATE + update_saved_preset_from_current_preset(); +#endif // ENABLE_PROJECT_DIRTY_STATE } void PresetCollection::reset(bool delete_files) @@ -805,7 +811,10 @@ std::pair PresetCollection::load_external_preset( // The source config may contain keys from many possible preset types. Just copy those that relate to this preset. this->get_edited_preset().config.apply_only(combined_config, keys, true); this->update_dirty(); - assert(this->get_edited_preset().is_dirty); +#if ENABLE_PROJECT_DIRTY_STATE + update_saved_preset_from_current_preset(); +#endif // ENABLE_PROJECT_DIRTY_STATE + assert(this->get_edited_preset().is_dirty); return std::make_pair(&(*it), this->get_edited_preset().is_dirty); } if (inherits.empty()) { @@ -1215,6 +1224,9 @@ Preset& PresetCollection::select_preset(size_t idx) idx = first_visible_idx(); m_idx_selected = idx; m_edited_preset = m_presets[idx]; +#if ENABLE_PROJECT_DIRTY_STATE + update_saved_preset_from_current_preset(); +#endif // ENABLE_PROJECT_DIRTY_STATE bool default_visible = ! m_default_suppressed || m_idx_selected < m_num_default_presets; for (size_t i = 0; i < m_num_default_presets; ++i) m_presets[i].is_visible = default_visible; diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 6e56ad911..8d407fb64 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -346,6 +346,11 @@ public: Preset& get_edited_preset() { return m_edited_preset; } const Preset& get_edited_preset() const { return m_edited_preset; } +#if ENABLE_PROJECT_DIRTY_STATE + // Return the last saved preset. + const Preset& get_saved_preset() const { return m_saved_preset; } +#endif // ENABLE_PROJECT_DIRTY_STATE + // Return vendor of the first parent profile, for which the vendor is defined, or null if such profile does not exist. PresetWithVendorProfile get_preset_with_vendor_profile(const Preset &preset) const; PresetWithVendorProfile get_edited_preset_with_vendor_profile() const { return this->get_preset_with_vendor_profile(this->get_edited_preset()); } @@ -365,8 +370,16 @@ public: // Return a preset by an index. If the preset is active, a temporary copy is returned. Preset& preset(size_t idx) { return (idx == m_idx_selected) ? m_edited_preset : m_presets[idx]; } const Preset& preset(size_t idx) const { return const_cast(this)->preset(idx); } +#if ENABLE_PROJECT_DIRTY_STATE + void discard_current_changes() { + m_presets[m_idx_selected].reset_dirty(); + m_edited_preset = m_presets[m_idx_selected]; + update_saved_preset_from_current_preset(); + } +#else void discard_current_changes() { m_presets[m_idx_selected].reset_dirty(); m_edited_preset = m_presets[m_idx_selected]; } - +#endif // ENABLE_PROJECT_DIRTY_STATE + // Return a preset by its name. If the preset is active, a temporary copy is returned. // If a preset is not found by its name, null is returned. Preset* find_preset(const std::string &name, bool first_visible_if_not_found = false); @@ -440,6 +453,16 @@ public: std::vector current_different_from_parent_options(const bool deep_compare = false) const { return dirty_options(&this->get_edited_preset(), this->get_selected_preset_parent(), deep_compare); } +#if ENABLE_PROJECT_DIRTY_STATE + // Compare the content of get_saved_preset() with get_edited_preset() configs, return true if they differ. + bool saved_is_dirty() const { return !this->saved_dirty_options().empty(); } + // Compare the content of get_saved_preset() with get_edited_preset() configs, return the list of keys where they differ. + std::vector saved_dirty_options(const bool deep_compare = false) const + { return dirty_options(&this->get_edited_preset(), &this->get_saved_preset(), deep_compare); } + // Copy edited preset into saved preset. + void update_saved_preset_from_current_preset() { m_saved_preset = m_edited_preset; } +#endif // ENABLE_PROJECT_DIRTY_STATE + // Return a sorted list of system preset names. // Used for validating the "inherits" flag when importing user's config bundles. // Returns names of all system presets including the former names of these presets. @@ -527,6 +550,11 @@ private: std::map m_map_system_profile_renamed; // Initially this preset contains a copy of the selected preset. Later on, this copy may be modified by the user. Preset m_edited_preset; +#if ENABLE_PROJECT_DIRTY_STATE + // Contains a copy of the last saved selected preset. + Preset m_saved_preset; +#endif // ENABLE_PROJECT_DIRTY_STATE + // Selected preset. size_t m_idx_selected; // Is the "- default -" preset suppressed? diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index edb6e6bcb..99684e93e 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -66,5 +66,10 @@ // Enable visualization of seams in preview #define ENABLE_SEAMS_VISUALIZATION (1 && ENABLE_2_4_0_ALPHA0) +// Enable project dirty state manager +#define ENABLE_PROJECT_DIRTY_STATE (1 && ENABLE_2_4_0_ALPHA0) +// Enable project dirty state manager debug window +#define ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW (0 && ENABLE_PROJECT_DIRTY_STATE) + #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 7f4b87439..aa3bf01c4 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -189,6 +189,8 @@ set(SLIC3R_GUI_SOURCES GUI/UnsavedChangesDialog.hpp GUI/ExtraRenderers.cpp GUI/ExtraRenderers.hpp + GUI/ProjectDirtyStateManager.hpp + GUI/ProjectDirtyStateManager.cpp GUI/DesktopIntegrationDialog.cpp GUI/DesktopIntegrationDialog.hpp Utils/Http.cpp diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d4f037998..39214221f 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1723,6 +1723,11 @@ void GLCanvas3D::render() } #endif // ENABLE_RENDER_STATISTICS +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + if (wxGetApp().is_editor() && wxGetApp().plater()->is_view3D_shown()) + wxGetApp().plater()->render_project_state_debug_window(); +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + #if ENABLE_CAMERA_STATISTICS camera.debug_render(); #endif // ENABLE_CAMERA_STATISTICS diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index cb7c2d24e..803ab5a14 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -916,6 +916,14 @@ bool GUI_App::on_init_inner() } else load_current_presets(); + +#if ENABLE_PROJECT_DIRTY_STATE + if (plater_ != nullptr) { + plater_->reset_project_dirty_initial_presets(); + plater_->update_project_dirty_from_presets(); + } +#endif // ENABLE_PROJECT_DIRTY_STATE + mainframe->Show(true); obj_list()->set_min_height(); @@ -1686,7 +1694,11 @@ void GUI_App::add_config_menu(wxMenuBar *menu) #endif case ConfigMenuTakeSnapshot: // Take a configuration snapshot. +#if ENABLE_PROJECT_DIRTY_STATE + if (check_and_save_current_preset_changes()) { +#else if (check_unsaved_changes()) { +#endif // ENABLE_PROJECT_DIRTY_STATE wxTextEntryDialog dlg(nullptr, _L("Taking configuration snapshot"), _L("Snapshot name")); // set current normal font for dialog children, @@ -1701,7 +1713,11 @@ void GUI_App::add_config_menu(wxMenuBar *menu) } break; case ConfigMenuSnapshots: +#if ENABLE_PROJECT_DIRTY_STATE + if (check_and_save_current_preset_changes()) { +#else if (check_unsaved_changes()) { +#endif // ENABLE_PROJECT_DIRTY_STATE std::string on_snapshot; if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) on_snapshot = app_config->get("on_snapshot"); @@ -1802,8 +1818,57 @@ void GUI_App::add_config_menu(wxMenuBar *menu) menu->Append(local_menu, _L("&Configuration")); } +#if ENABLE_PROJECT_DIRTY_STATE +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector> GUI_App::get_selected_presets() const +{ + std::vector> ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) { + const PresetCollection* presets = tab->get_presets(); + ret.push_back({ static_cast(presets->type()), presets->get_selected_preset_name() }); + } + } + return ret; +} +#endif // ENABLE_PROJECT_DIRTY_STATE + // This is called when closing the application, when loading a config file or when starting the config wizard // to notify the user whether he is aware that some preset changes will be lost. +#if ENABLE_PROJECT_DIRTY_STATE +bool GUI_App::check_and_save_current_preset_changes(const wxString& header) +{ + if (this->plater()->model().objects.empty() && has_current_preset_changes()) { +#else bool GUI_App::check_unsaved_changes(const wxString &header) { PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); @@ -1815,8 +1880,8 @@ bool GUI_App::check_unsaved_changes(const wxString &header) break; } - if (has_unsaved_changes) - { + if (has_unsaved_changes) { +#endif // ENABLE_PROJECT_DIRTY_STATE UnsavedChangesDialog dlg(header); if (wxGetApp().app_config->get("default_action_on_close_application") == "none" && dlg.ShowModal() == wxID_CANCEL) return false; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index bc030a1bf..2613c51b1 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -210,7 +210,15 @@ public: void update_mode(); void add_config_menu(wxMenuBar *menu); - bool check_unsaved_changes(const wxString &header = wxString()); +#if ENABLE_PROJECT_DIRTY_STATE + bool has_unsaved_preset_changes() const; + bool has_current_preset_changes() const; + void update_saved_preset_from_current_preset(); + std::vector> get_selected_presets() const; + bool check_and_save_current_preset_changes(const wxString& header = wxString()); +#else + bool check_unsaved_changes(const wxString& header = wxString()); +#endif // ENABLE_PROJECT_DIRTY_STATE bool check_print_host_queue(); bool checked_tab(Tab* tab); void load_current_presets(bool check_printer_presets = true); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 2b72e901a..4bf3bdfd1 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -7,6 +7,9 @@ #include "GUI_App.hpp" #include "I18N.hpp" #include "Plater.hpp" +#if ENABLE_PROJECT_DIRTY_STATE +#include "MainFrame.hpp" +#endif // ENABLE_PROJECT_DIRTY_STATE #include "OptionsGroup.hpp" #include "Tab.hpp" @@ -1457,12 +1460,15 @@ void ObjectList::load_shape_object(const std::string& type_name) if (obj_idx < 0) return; - take_snapshot(_(L("Add Shape"))); + take_snapshot(_L("Add Shape")); // Create mesh BoundingBoxf3 bb; TriangleMesh mesh = create_mesh(type_name, bb); - load_mesh_object(mesh, _(L("Shape")) + "-" + _(type_name)); + load_mesh_object(mesh, _L("Shape") + "-" + _(type_name)); +#if ENABLE_PROJECT_DIRTY_STATE + wxGetApp().mainframe->update_title(); +#endif // ENABLE_PROJECT_DIRTY_STATE } void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 91aef75d9..3a932c598 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -32,6 +32,42 @@ GLGizmoPainterBase::GLGizmoPainterBase(GLCanvas3D& parent, const std::string& ic +#if ENABLE_PROJECT_DIRTY_STATE +// port of 948bc382655993721d93d3b9fce9b0186fcfb211 +void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate) +{ + Plater* plater = wxGetApp().plater(); + + // Following is needed to prevent taking an extra snapshot when the activation of + // the internal stack happens when the gizmo is already active (such as open gizmo, + // close gizmo, undo, start painting). The internal stack does not activate on the + // undo, because that would obliterate all future of the main stack (user would + // have to close the gizmo himself, he has no access to main undo/redo after the + // internal stack opens). We don't want the "entering" snapshot taken in this case, + // because there already is one. + std::string last_snapshot_name; + plater->undo_redo_topmost_string_getter(plater->can_undo(), last_snapshot_name); + + if (activate && !m_internal_stack_active) { + std::string str = get_painter_type() == PainterGizmoType::FDM_SUPPORTS + ? _u8L("Entering Paint-on supports") + : _u8L("Entering Seam painting"); + if (last_snapshot_name != str) + Plater::TakeSnapshot(plater, str); + plater->enter_gizmos_stack(); + m_internal_stack_active = true; + } + if (!activate && m_internal_stack_active) { + plater->leave_gizmos_stack(); + std::string str = get_painter_type() == PainterGizmoType::SEAM + ? _u8L("Leaving Seam painting") + : _u8L("Leaving Paint-on supports"); + if (last_snapshot_name != str) + Plater::TakeSnapshot(plater, str); + m_internal_stack_active = false; + } +} +#else void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate) { if (activate && ! m_internal_stack_active) { @@ -51,6 +87,7 @@ void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate) m_internal_stack_active = false; } } +#endif // ENABLE_PROJECT_DIRTY_STATE diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index b200623f4..7ff274aac 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -45,18 +45,18 @@ bool GLGizmoSlaSupports::on_init() { m_shortcut_key = WXK_CONTROL_L; - m_desc["head_diameter"] = _(L("Head diameter")) + ": "; - m_desc["lock_supports"] = _(L("Lock supports under new islands")); - m_desc["remove_selected"] = _(L("Remove selected points")); - m_desc["remove_all"] = _(L("Remove all points")); - m_desc["apply_changes"] = _(L("Apply changes")); - m_desc["discard_changes"] = _(L("Discard changes")); - m_desc["minimal_distance"] = _(L("Minimal points distance")) + ": "; - m_desc["points_density"] = _(L("Support points density")) + ": "; - m_desc["auto_generate"] = _(L("Auto-generate points")); - m_desc["manual_editing"] = _(L("Manual editing")); - m_desc["clipping_of_view"] = _(L("Clipping of view"))+ ": "; - m_desc["reset_direction"] = _(L("Reset direction")); + m_desc["head_diameter"] = _L("Head diameter") + ": "; + m_desc["lock_supports"] = _L("Lock supports under new islands"); + m_desc["remove_selected"] = _L("Remove selected points"); + m_desc["remove_all"] = _L("Remove all points"); + m_desc["apply_changes"] = _L("Apply changes"); + m_desc["discard_changes"] = _L("Discard changes"); + m_desc["minimal_distance"] = _L("Minimal points distance") + ": "; + m_desc["points_density"] = _L("Support points density") + ": "; + m_desc["auto_generate"] = _L("Auto-generate points"); + m_desc["manual_editing"] = _L("Manual editing"); + m_desc["clipping_of_view"] = _L("Clipping of view")+ ": "; + m_desc["reset_direction"] = _L("Reset direction"); return true; } @@ -372,7 +372,7 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous if (m_selection_empty) { std::pair pos_and_normal; if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add support point"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Add support point")); m_editing_cache.emplace_back(sla::SupportPoint(pos_and_normal.first, m_new_point_head_diameter/2.f, false), false, pos_and_normal.second); m_parent.set_as_dirty(); m_wait_for_up_event = true; @@ -512,7 +512,7 @@ void GLGizmoSlaSupports::delete_selected_points(bool force) std::abort(); } - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Delete support point"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Delete support point")); for (unsigned int idx=0; idxconfig.set("support_points_minimal_distance", m_minimal_point_distance_stash); mo->config.set("support_points_density_relative", (int)m_density_stash); - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Support parameter change"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Support parameter change")); mo->config.set("support_points_minimal_distance", minimal_point_distance); mo->config.set("support_points_density_relative", (int)density); wxGetApp().obj_list()->update_and_show_object_settings_item(); @@ -867,10 +867,9 @@ bool GLGizmoSlaSupports::on_is_selectable() const std::string GLGizmoSlaSupports::on_get_name() const { - return (_(L("SLA Support Points")) + " [L]").ToUTF8().data(); + return (_L("SLA Support Points") + " [L]").ToUTF8().data(); } - CommonGizmosDataID GLGizmoSlaSupports::on_get_requirements() const { return CommonGizmosDataID( @@ -895,7 +894,11 @@ void GLGizmoSlaSupports::on_set_state() // data are not yet available, the CallAfter will postpone taking the // snapshot until they are. No, it does not feel right. wxGetApp().CallAfter([]() { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned on"))); +#if ENABLE_PROJECT_DIRTY_STATE + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Entering SLA gizmo")); +#else + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("SLA gizmo turned on")); +#endif // ENABLE_PROJECT_DIRTY_STATE }); } @@ -909,8 +912,8 @@ void GLGizmoSlaSupports::on_set_state() wxGetApp().CallAfter([this]() { // Following is called through CallAfter, because otherwise there was a problem // on OSX with the wxMessageDialog being shown several times when clicked into. - wxMessageDialog dlg(GUI::wxGetApp().mainframe, _(L("Do you want to save your manually " - "edited support points?")) + "\n",_(L("Save changes?")), wxICON_QUESTION | wxYES | wxNO); + wxMessageDialog dlg(GUI::wxGetApp().mainframe, _L("Do you want to save your manually " + "edited support points?") + "\n",_L("Save changes?"), wxICON_QUESTION | wxYES | wxNO); if (dlg.ShowModal() == wxID_YES) editing_mode_apply_changes(); else @@ -922,7 +925,11 @@ void GLGizmoSlaSupports::on_set_state() else { // we are actually shutting down disable_editing_mode(); // so it is not active next time the gizmo opens - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned off"))); +#if ENABLE_PROJECT_DIRTY_STATE + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Leaving SLA gizmo")); +#else + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("SLA gizmo turned off")); +#endif // ENABLE_PROJECT_DIRTY_STATE m_normal_cache.clear(); m_old_mo_id = -1; } @@ -953,7 +960,7 @@ void GLGizmoSlaSupports::on_stop_dragging() && backup.support_point.pos != m_point_before_drag.support_point.pos) // and it was moved, not just selected { m_editing_cache[m_hover_id] = m_point_before_drag; - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Move support point"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move support point")); m_editing_cache[m_hover_id] = backup; } } @@ -1046,7 +1053,7 @@ void GLGizmoSlaSupports::editing_mode_apply_changes() disable_editing_mode(); // this leaves the editing mode undo/redo stack and must be done before the snapshot is taken if (unsaved_changes()) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Support points edit"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Support points edit")); m_normal_cache.clear(); for (const CacheEntry& ce : m_editing_cache) @@ -1125,14 +1132,14 @@ void GLGizmoSlaSupports::get_data_from_backend() void GLGizmoSlaSupports::auto_generate() { wxMessageDialog dlg(GUI::wxGetApp().plater(), - _(L("Autogeneration will erase all manually edited points.")) + "\n\n" + - _(L("Are you sure you want to do it?")) + "\n", - _(L("Warning")), wxICON_WARNING | wxYES | wxNO); + _L("Autogeneration will erase all manually edited points.") + "\n\n" + + _L("Are you sure you want to do it?") + "\n", + _L("Warning"), wxICON_WARNING | wxYES | wxNO); ModelObject* mo = m_c->selection_info()->model_object(); if (mo->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Autogenerate support points"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); wxGetApp().CallAfter([this]() { reslice_SLA_supports(); }); mo->sla_points_status = sla::PointsStatus::Generating; } @@ -1180,7 +1187,7 @@ bool GLGizmoSlaSupports::unsaved_changes() const } SlaGizmoHelpDialog::SlaGizmoHelpDialog() -: wxDialog(nullptr, wxID_ANY, _(L("SLA gizmo keyboard shortcuts")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) +: wxDialog(nullptr, wxID_ANY, _L("SLA gizmo keyboard shortcuts"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); const wxString ctrl = GUI::shortkey_ctrl_prefix(); @@ -1191,7 +1198,7 @@ SlaGizmoHelpDialog::SlaGizmoHelpDialog() const wxFont& font = wxGetApp().small_font(); const wxFont& bold_font = wxGetApp().bold_font(); - auto note_text = new wxStaticText(this, wxID_ANY, _(L("Note: some shortcuts work in (non)editing mode only."))); + auto note_text = new wxStaticText(this, wxID_ANY, _L("Note: some shortcuts work in (non)editing mode only.")); note_text->SetFont(font); auto vsizer = new wxBoxSizer(wxVERTICAL); @@ -1209,21 +1216,21 @@ SlaGizmoHelpDialog::SlaGizmoHelpDialog() vsizer->AddSpacer(20); std::vector> shortcuts; - shortcuts.push_back(std::make_pair(_(L("Left click")), _(L("Add point")))); - shortcuts.push_back(std::make_pair(_(L("Right click")), _(L("Remove point")))); - shortcuts.push_back(std::make_pair(_(L("Drag")), _(L("Move point")))); - shortcuts.push_back(std::make_pair(ctrl+_(L("Left click")), _(L("Add point to selection")))); - shortcuts.push_back(std::make_pair(alt+_(L("Left click")), _(L("Remove point from selection")))); - shortcuts.push_back(std::make_pair(wxString("Shift+")+_(L("Drag")), _(L("Select by rectangle")))); - shortcuts.push_back(std::make_pair(alt+_(L("Drag")), _(L("Deselect by rectangle")))); - shortcuts.push_back(std::make_pair(ctrl+"A", _(L("Select all points")))); - shortcuts.push_back(std::make_pair("Delete", _(L("Remove selected points")))); - shortcuts.push_back(std::make_pair(ctrl+_(L("Mouse wheel")), _(L("Move clipping plane")))); - shortcuts.push_back(std::make_pair("R", _(L("Reset clipping plane")))); - shortcuts.push_back(std::make_pair("Enter", _(L("Apply changes")))); - shortcuts.push_back(std::make_pair("Esc", _(L("Discard changes")))); - shortcuts.push_back(std::make_pair("M", _(L("Switch to editing mode")))); - shortcuts.push_back(std::make_pair("A", _(L("Auto-generate points")))); + shortcuts.push_back(std::make_pair(_L("Left click"), _L("Add point"))); + shortcuts.push_back(std::make_pair(_L("Right click"), _L("Remove point"))); + shortcuts.push_back(std::make_pair(_L("Drag"), _L("Move point"))); + shortcuts.push_back(std::make_pair(ctrl+_L("Left click"), _L("Add point to selection"))); + shortcuts.push_back(std::make_pair(alt+_L("Left click"), _L("Remove point from selection"))); + shortcuts.push_back(std::make_pair(wxString("Shift+")+_L("Drag"), _L("Select by rectangle"))); + shortcuts.push_back(std::make_pair(alt+_(L("Drag")), _L("Deselect by rectangle"))); + shortcuts.push_back(std::make_pair(ctrl+"A", _L("Select all points"))); + shortcuts.push_back(std::make_pair("Delete", _L("Remove selected points"))); + shortcuts.push_back(std::make_pair(ctrl+_L("Mouse wheel"), _L("Move clipping plane"))); + shortcuts.push_back(std::make_pair("R", _L("Reset clipping plane"))); + shortcuts.push_back(std::make_pair("Enter", _L("Apply changes"))); + shortcuts.push_back(std::make_pair("Esc", _L("Discard changes"))); + shortcuts.push_back(std::make_pair("M", _L("Switch to editing mode"))); + shortcuts.push_back(std::make_pair("A", _L("Auto-generate points"))); for (const auto& pair : shortcuts) { auto shortcut = new wxStaticText(this, wxID_ANY, pair.first); diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index db7af046b..5726477ab 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -488,8 +488,7 @@ bool ImGuiWrapper::undo_redo_list(const ImVec2& size, const bool is_undo, bool ( int i=0; const char* item_text; - while (items_getter(is_undo, i, &item_text)) - { + while (items_getter(is_undo, i, &item_text)) { ImGui::Selectable(item_text, i < hovered); if (ImGui::IsItemHovered()) { diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 666e3327b..8c54e10f8 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -206,7 +206,14 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S // declare events Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) { +#if ENABLE_PROJECT_DIRTY_STATE + if (m_plater != nullptr) + m_plater->save_project_if_dirty(); + + if (event.CanVeto() && !wxGetApp().check_and_save_current_preset_changes()) { +#else if (event.CanVeto() && !wxGetApp().check_unsaved_changes()) { +#endif // ENABLE_PROJECT_DIRTY_STATE event.Veto(); return; } @@ -487,8 +494,14 @@ void MainFrame::update_title() // m_plater->get_project_filename() produces file name including path, but excluding extension. // Don't try to remove the extension, it would remove part of the file name after the last dot! wxString project = from_path(into_path(m_plater->get_project_filename()).filename()); +#if ENABLE_PROJECT_DIRTY_STATE + wxString dirty_marker = (!m_plater->model().objects.empty() && m_plater->is_project_dirty()) ? "*" : ""; + if (!dirty_marker.empty() || !project.empty()) + title = dirty_marker + project + " - "; +#else if (!project.empty()) title += (project + " - "); +#endif // ENABLE_PROJECT_DIRTY_STATE } std::string build_id = wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID; @@ -672,10 +685,36 @@ bool MainFrame::can_start_new_project() const return (m_plater != nullptr) && (!m_plater->get_project_filename(".3mf").IsEmpty() || !m_plater->model().objects.empty()); } +#if ENABLE_PROJECT_DIRTY_STATE +bool MainFrame::can_save() const +{ + return (m_plater != nullptr) && !m_plater->model().objects.empty() && !m_plater->get_project_filename().empty() && m_plater->is_project_dirty(); +} + +bool MainFrame::can_save_as() const +{ + return (m_plater != nullptr) && !m_plater->model().objects.empty(); +} + +void MainFrame::save_project() +{ + save_project_as(m_plater->get_project_filename(".3mf")); +} + +void MainFrame::save_project_as(const wxString& filename) +{ + bool ret = (m_plater != nullptr) ? m_plater->export_3mf(into_path(filename)) : false; + if (ret) { +// wxGetApp().update_saved_preset_from_current_preset(); + m_plater->reset_project_dirty_after_save(); + } +} +#else bool MainFrame::can_save() const { return (m_plater != nullptr) && !m_plater->model().objects.empty(); } +#endif // ENABLE_PROJECT_DIRTY_STATE bool MainFrame::can_export_model() const { @@ -984,16 +1023,27 @@ void MainFrame::init_menubar_as_editor() Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(m_recent_projects.GetCount() > 0); }, recent_projects_submenu->GetId()); +#if ENABLE_PROJECT_DIRTY_STATE + append_menu_item(fileMenu, wxID_ANY, _L("&Save Project") + "\tCtrl+S", _L("Save current project file"), + [this](wxCommandEvent&) { save_project(); }, "save", nullptr, + [this](){return m_plater != nullptr && can_save(); }, this); +#else append_menu_item(fileMenu, wxID_ANY, _L("&Save Project") + "\tCtrl+S", _L("Save current project file"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_3mf(into_path(m_plater->get_project_filename(".3mf"))); }, "save", nullptr, [this](){return m_plater != nullptr && can_save(); }, this); +#endif // ENABLE_PROJECT_DIRTY_STATE #ifdef __APPLE__ append_menu_item(fileMenu, wxID_ANY, _L("Save Project &as") + dots + "\tCtrl+Shift+S", _L("Save current project file as"), #else append_menu_item(fileMenu, wxID_ANY, _L("Save Project &as") + dots + "\tCtrl+Alt+S", _L("Save current project file as"), #endif // __APPLE__ +#if ENABLE_PROJECT_DIRTY_STATE + [this](wxCommandEvent&) { save_project_as(); }, "save", nullptr, + [this](){return m_plater != nullptr && can_save_as(); }, this); +#else [this](wxCommandEvent&) { if (m_plater) m_plater->export_3mf(); }, "save", nullptr, [this](){return m_plater != nullptr && can_save(); }, this); +#endif // ENABLE_PROJECT_DIRTY_STATE fileMenu->AppendSeparator(); @@ -1518,7 +1568,11 @@ void MainFrame::export_config() // Load a config file containing a Print, Filament & Printer preset. void MainFrame::load_config_file() { +#if ENABLE_PROJECT_DIRTY_STATE + if (!wxGetApp().check_and_save_current_preset_changes()) +#else if (!wxGetApp().check_unsaved_changes()) +#endif // ENABLE_PROJECT_DIRTY_STATE return; wxFileDialog dlg(this, _L("Select configuration to load:"), !m_last_config.IsEmpty() ? get_dir_name(m_last_config) : wxGetApp().app_config->get_last_dir(), @@ -1547,7 +1601,11 @@ bool MainFrame::load_config_file(const std::string &path) void MainFrame::export_configbundle(bool export_physical_printers /*= false*/) { +#if ENABLE_PROJECT_DIRTY_STATE + if (!wxGetApp().check_and_save_current_preset_changes()) +#else if (!wxGetApp().check_unsaved_changes()) +#endif // ENABLE_PROJECT_DIRTY_STATE return; // validate current configuration in case it's dirty auto err = wxGetApp().preset_bundle->full_config().validate(); @@ -1579,7 +1637,11 @@ void MainFrame::export_configbundle(bool export_physical_printers /*= false*/) // but that behavior was not documented and likely buggy. void MainFrame::load_configbundle(wxString file/* = wxEmptyString, const bool reset_user_profile*/) { +#if ENABLE_PROJECT_DIRTY_STATE + if (!wxGetApp().check_and_save_current_preset_changes()) +#else if (!wxGetApp().check_unsaved_changes()) +#endif // ENABLE_PROJECT_DIRTY_STATE return; if (file.IsEmpty()) { wxFileDialog dlg(this, _L("Select configuration to load:"), diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 0971fdc77..307cdf1ae 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -91,7 +91,9 @@ class MainFrame : public DPIFrame void on_value_changed(wxCommandEvent&); bool can_start_new_project() const; +#if !ENABLE_PROJECT_DIRTY_STATE bool can_save() const; +#endif // !ENABLE_PROJECT_DIRTY_STATE bool can_export_model() const; bool can_export_toolpaths() const; bool can_export_supports() const; @@ -184,6 +186,13 @@ public: // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig void on_config_changed(DynamicPrintConfig* cfg) const ; +#if ENABLE_PROJECT_DIRTY_STATE + bool can_save() const; + bool can_save_as() const; + void save_project(); + void save_project_as(const wxString& filename = wxString()); +#endif // ENABLE_PROJECT_DIRTY_STATE + void add_to_recent_projects(const wxString& filename); PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 13601396f..b2f335a54 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -81,6 +81,9 @@ #include "InstanceCheck.hpp" #include "NotificationManager.hpp" #include "PresetComboBoxes.hpp" +#if ENABLE_PROJECT_DIRTY_STATE +#include "ProjectDirtyStateManager.hpp" +#endif // ENABLE_PROJECT_DIRTY_STATE #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -1396,7 +1399,13 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi this->MSWUpdateDragImageOnLeave(); #endif // WIN32 +#if ENABLE_PROJECT_DIRTY_STATE + bool res = (m_plater != nullptr) ? m_plater->load_files(filenames) : false; + wxGetApp().mainframe->update_title(); + return res; +#else return (m_plater != nullptr) ? m_plater->load_files(filenames) : false; +#endif // ENABLE_PROJECT_DIRTY_STATE } // State to manage showing after export notifications and device ejecting @@ -1440,6 +1449,10 @@ struct Plater::priv Preview *preview; NotificationManager* notification_manager { nullptr }; +#if ENABLE_PROJECT_DIRTY_STATE + ProjectDirtyStateManager dirty_state; +#endif // ENABLE_PROJECT_DIRTY_STATE + BackgroundSlicingProcess background_process; bool suppressed_backround_processing_update { false }; @@ -1510,6 +1523,31 @@ struct Plater::priv priv(Plater *q, MainFrame *main_frame); ~priv(); +#if ENABLE_PROJECT_DIRTY_STATE + bool is_project_dirty() const { return dirty_state.is_dirty(); } + void update_project_dirty_from_presets() { dirty_state.update_from_presets(); } + bool save_project_if_dirty() { + if (dirty_state.is_dirty()) { + MainFrame* mainframe = wxGetApp().mainframe; + if (mainframe->can_save_as()) { + wxMessageDialog dlg(mainframe, _L("Do you want to save the changes to the current project ?"), wxString(SLIC3R_APP_NAME), wxYES_NO | wxCANCEL); + int res = dlg.ShowModal(); + if (res == wxID_YES) + mainframe->save_project_as(wxGetApp().plater()->get_project_filename()); + else if (res == wxID_CANCEL) + return false; + } + } + return true; + } + void reset_project_dirty_after_save() { dirty_state.reset_after_save(); } + void reset_project_dirty_initial_presets() { dirty_state.reset_initial_presets(); } + +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + void render_project_state_debug_window() const { dirty_state.render_debug_window(); } +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +#endif // ENABLE_PROJECT_DIRTY_STATE + enum class UpdateParams { FORCE_FULL_SCREEN_REFRESH = 1, FORCE_BACKGROUND_PROCESSING_UPDATE = 2, @@ -4231,6 +4269,11 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name) } this->undo_redo_stack().take_snapshot(snapshot_name, model, view3D->get_canvas3d()->get_selection(), view3D->get_canvas3d()->get_gizmos_manager(), snapshot_data); this->undo_redo_stack().release_least_recently_used(); + +#if ENABLE_PROJECT_DIRTY_STATE + dirty_state.update_from_undo_redo_stack(ProjectDirtyStateManager::UpdateType::TakeSnapshot); +#endif // ENABLE_PROJECT_DIRTY_STATE + // Save the last active preset name of a particular printer technology. ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); @@ -4271,8 +4314,13 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator if (printer_technology_changed) { // Switching the printer technology when jumping forwards / backwards in time. Switch to the last active printer profile of the other type. std::string s_pt = (it_snapshot->snapshot_data.printer_technology == ptFFF) ? "FFF" : "SLA"; +#if ENABLE_PROJECT_DIRTY_STATE + if (!wxGetApp().check_and_save_current_preset_changes(format_wxstr(_L( + "%1% printer was active at the time the target Undo / Redo snapshot was taken. Switching to %1% printer requires reloading of %1% presets."), s_pt))) +#else if (! wxGetApp().check_unsaved_changes(format_wxstr(_L( "%1% printer was active at the time the target Undo / Redo snapshot was taken. Switching to %1% printer requires reloading of %1% presets."), s_pt))) +#endif // ENABLE_PROJECT_DIRTY_STATE // Don't switch the profiles. return; } @@ -4361,6 +4409,10 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator if (! view3D->is_layers_editing_enabled() && this->layers_height_allowed() && new_variable_layer_editing_active) view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting")); } + +#if ENABLE_PROJECT_DIRTY_STATE + dirty_state.update_from_undo_redo_stack(ProjectDirtyStateManager::UpdateType::UndoRedoTo); +#endif // ENABLE_PROJECT_DIRTY_STATE } void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */) @@ -4444,9 +4496,16 @@ Plater::Plater(wxWindow *parent, MainFrame *main_frame) // Initialization performed in the private c-tor } -Plater::~Plater() -{ -} +#if ENABLE_PROJECT_DIRTY_STATE +bool Plater::is_project_dirty() const { return p->is_project_dirty(); } +void Plater::update_project_dirty_from_presets() { p->update_project_dirty_from_presets(); } +bool Plater::save_project_if_dirty() { return p->save_project_if_dirty(); } +void Plater::reset_project_dirty_after_save() { p->reset_project_dirty_after_save(); } +void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_initial_presets(); } +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +void Plater::render_project_state_debug_window() const { p->render_project_state_debug_window(); } +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +#endif // ENABLE_PROJECT_DIRTY_STATE Sidebar& Plater::sidebar() { return *p->sidebar; } Model& Plater::model() { return p->model; } @@ -4457,12 +4516,30 @@ SLAPrint& Plater::sla_print() { return p->sla_print; } void Plater::new_project() { +#if ENABLE_PROJECT_DIRTY_STATE + if (!p->save_project_if_dirty()) + return; +#endif // ENABLE_PROJECT_DIRTY_STATE + p->select_view_3D("3D"); +#if ENABLE_PROJECT_DIRTY_STATE + take_snapshot(_L("New Project")); + Plater::SuppressSnapshots suppress(this); + reset(); + reset_project_dirty_initial_presets(); + update_project_dirty_from_presets(); +#else wxPostEvent(p->view3D->get_wxglcanvas(), SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); +#endif // ENABLE_PROJECT_DIRTY_STATE } void Plater::load_project() { +#if ENABLE_PROJECT_DIRTY_STATE + if (!p->save_project_if_dirty()) + return; +#endif // ENABLE_PROJECT_DIRTY_STATE + // Ask user for a project file name. wxString input_file; wxGetApp().load_project(this, input_file); @@ -4486,8 +4563,16 @@ void Plater::load_project(const wxString& filename) std::vector res = load_files(input_paths); // if res is empty no data has been loaded +#if ENABLE_PROJECT_DIRTY_STATE + if (!res.empty()) { + p->set_project_filename(filename); + reset_project_dirty_initial_presets(); + update_project_dirty_from_presets(); + } +#else if (!res.empty()) p->set_project_filename(filename); +#endif // ENABLE_PROJECT_DIRTY_STATE } void Plater::add_model(bool imperial_units/* = false*/) @@ -4518,7 +4603,13 @@ void Plater::add_model(bool imperial_units/* = false*/) } Plater::TakeSnapshot snapshot(this, snapshot_label); +#if ENABLE_PROJECT_DIRTY_STATE + std::vector res = load_files(paths, true, false, imperial_units); + if (!res.empty()) + wxGetApp().mainframe->update_title(); +#else load_files(paths, true, false, imperial_units); +#endif // ENABLE_PROJECT_DIRTY_STATE } void Plater::import_sl1_archive() @@ -5233,24 +5324,39 @@ void Plater::export_amf() } } +#if ENABLE_PROJECT_DIRTY_STATE +bool Plater::export_3mf(const boost::filesystem::path& output_path) +#else void Plater::export_3mf(const boost::filesystem::path& output_path) +#endif // ENABLE_PROJECT_DIRTY_STATE { if (p->model.objects.empty() || canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) +#if ENABLE_PROJECT_DIRTY_STATE + return false; +#else return; +#endif // ENABLE_PROJECT_DIRTY_STATE wxString path; bool export_config = true; - if (output_path.empty()) - { + if (output_path.empty()) { path = p->get_export_file(FT_3MF); +#if ENABLE_PROJECT_DIRTY_STATE + if (path.empty()) { return false; } +#else if (path.empty()) { return; } +#endif // ENABLE_PROJECT_DIRTY_STATE } else path = from_path(output_path); if (!path.Lower().EndsWith(".3mf")) +#if ENABLE_PROJECT_DIRTY_STATE + return false; +#else return; +#endif // ENABLE_PROJECT_DIRTY_STATE DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); @@ -5258,6 +5364,19 @@ void Plater::export_3mf(const boost::filesystem::path& output_path) bool full_pathnames = wxGetApp().app_config->get("export_sources_full_pathnames") == "1"; ThumbnailData thumbnail_data; p->generate_thumbnail(thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, false, true, true, true); +#if ENABLE_PROJECT_DIRTY_STATE + bool ret = Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames, &thumbnail_data); + if (ret) { + // Success + p->statusbar()->set_status_text(format_wxstr(_L("3MF file exported to %s"), path)); + p->set_project_filename(path); + } + else { + // Failure + p->statusbar()->set_status_text(format_wxstr(_L("Error exporting 3MF file %s"), path)); + } + return ret; +#else if (Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames, &thumbnail_data)) { // Success p->statusbar()->set_status_text(format_wxstr(_L("3MF file exported to %s"), path)); @@ -5267,6 +5386,7 @@ void Plater::export_3mf(const boost::filesystem::path& output_path) // Failure p->statusbar()->set_status_text(format_wxstr(_L("Error exporting 3MF file %s"), path)); } +#endif // ENABLE_PROJECT_DIRTY_STATE } void Plater::reload_from_disk() @@ -6127,6 +6247,9 @@ bool Plater::can_mirror() const { return p->can_mirror(); } bool Plater::can_split(bool to_objects) const { return p->can_split(to_objects); } const UndoRedo::Stack& Plater::undo_redo_stack_main() const { return p->undo_redo_stack_main(); } void Plater::clear_undo_redo_stack_main() { p->undo_redo_stack_main().clear(); } +#if ENABLE_PROJECT_DIRTY_STATE +const UndoRedo::Stack& Plater::undo_redo_stack_active() const { return p->undo_redo_stack(); } +#endif // ENABLE_PROJECT_DIRTY_STATE void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); } void Plater::leave_gizmos_stack() { p->leave_gizmos_stack(); } bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e99feee82..e6a5160f3 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -128,7 +128,18 @@ public: Plater(const Plater &) = delete; Plater &operator=(Plater &&) = delete; Plater &operator=(const Plater &) = delete; - ~Plater(); + ~Plater() = default; + +#if ENABLE_PROJECT_DIRTY_STATE + bool is_project_dirty() const; + void update_project_dirty_from_presets(); + bool save_project_if_dirty(); + void reset_project_dirty_after_save(); + void reset_project_dirty_initial_presets(); +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + void render_project_state_debug_window() const; +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +#endif // ENABLE_PROJECT_DIRTY_STATE Sidebar& sidebar(); Model& model(); @@ -198,7 +209,11 @@ public: void export_gcode(bool prefer_removable); void export_stl(bool extended = false, bool selection_only = false); void export_amf(); +#if ENABLE_PROJECT_DIRTY_STATE + bool export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path()); +#else void export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path()); +#endif // ENABLE_PROJECT_DIRTY_STATE void reload_from_disk(); void reload_all_from_disk(); bool has_toolpaths_to_export() const; @@ -228,6 +243,9 @@ public: // For the memory statistics. const Slic3r::UndoRedo::Stack& undo_redo_stack_main() const; void clear_undo_redo_stack_main(); +#if ENABLE_PROJECT_DIRTY_STATE + const Slic3r::UndoRedo::Stack& undo_redo_stack_active() const; +#endif // ENABLE_PROJECT_DIRTY_STATE // Enter / leave the Gizmos specific Undo / Redo stack. To be used by the SLA support point editing gizmo. void enter_gizmos_stack(); void leave_gizmos_stack(); diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp new file mode 100644 index 000000000..30d41f808 --- /dev/null +++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp @@ -0,0 +1,415 @@ +#include "libslic3r/libslic3r.h" + +#include "ProjectDirtyStateManager.hpp" +#include "ImGuiWrapper.hpp" +#include "GUI_App.hpp" +#include "MainFrame.hpp" +#include "I18N.hpp" +#include "Plater.hpp" +#include "../Utils/UndoRedo.hpp" + +#include + +#include +#include + +#if ENABLE_PROJECT_DIRTY_STATE + +namespace Slic3r { +namespace GUI { + +enum class EStackType +{ + Main, + Gizmo +}; + +// returns the current active snapshot (the topmost snapshot in the undo part of the stack) in the given stack +static const UndoRedo::Snapshot* get_active_snapshot(const UndoRedo::Stack& stack) { + const std::vector& snapshots = stack.snapshots(); + const size_t active_snapshot_time = stack.active_snapshot_time(); + const auto it = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(active_snapshot_time)); + const int idx = it - snapshots.begin() - 1; + const Slic3r::UndoRedo::Snapshot* ret = (0 <= idx && (size_t)idx < snapshots.size() - 1) ? + &snapshots[idx] : nullptr; + + assert(ret != nullptr); + return ret; +} + +// returns the last saveable snapshot (the topmost snapshot in the undo part of the stack that can be saved) in the given stack +static const UndoRedo::Snapshot* get_last_saveable_snapshot(EStackType type, const UndoRedo::Stack& stack, + const ProjectDirtyStateManager::DirtyState::Gizmos& gizmos, size_t last_save_main) { + + // returns true if the given snapshot is not saveable + auto skip_main = [&gizmos, last_save_main, &stack](const UndoRedo::Snapshot& snapshot) { + auto is_gizmo_with_modifications = [&gizmos, &stack](const UndoRedo::Snapshot& snapshot) { + if (boost::starts_with(snapshot.name, _utf8("Entering"))) { + if (gizmos.current) + return true; + + std::string topmost_redo; + wxGetApp().plater()->undo_redo_topmost_string_getter(false, topmost_redo); + if (boost::starts_with(topmost_redo, _utf8("Leaving"))) { + const std::vector& snapshots = stack.snapshots(); + const UndoRedo::Snapshot* leaving_snapshot = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(snapshot.timestamp + 1))); + if (gizmos.is_used_and_modified(*leaving_snapshot)) + return true; + } + } + return false; + }; + + if (snapshot.name == _utf8("New Project")) + return true; + else if (snapshot.name == _utf8("Reset Project")) + return true; + else if (boost::starts_with(snapshot.name, _utf8("Load Project:"))) + return true; + else if (boost::starts_with(snapshot.name, _utf8("Selection"))) + return true; + else if (boost::starts_with(snapshot.name, _utf8("Entering"))) { + if (last_save_main != snapshot.timestamp + 1 && !is_gizmo_with_modifications(snapshot)) + return true; + } + else if (boost::starts_with(snapshot.name, _utf8("Leaving"))) { + if (last_save_main != snapshot.timestamp && !gizmos.is_used_and_modified(snapshot)) + return true; + } + + return false; + }; + + // returns true if the given snapshot is not saveable + auto skip_gizmo = [](const UndoRedo::Snapshot& snapshot) { + // put here any needed condition to skip the snapshot + return false; + }; + + const UndoRedo::Snapshot* curr = get_active_snapshot(stack); + const std::vector& snapshots = stack.snapshots(); + size_t shift = 1; + while (curr->timestamp > 0 && ((type == EStackType::Main && skip_main(*curr)) || (type == EStackType::Gizmo && skip_gizmo(*curr)))) { + const UndoRedo::Snapshot* temp = curr; + curr = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(curr->timestamp - shift))); + shift = (curr == temp) ? shift + 1 : 1; + } + if (type == EStackType::Main) { + if (boost::starts_with(curr->name, _utf8("Entering")) && last_save_main == curr->timestamp + 1) { + std::string topmost_redo; + wxGetApp().plater()->undo_redo_topmost_string_getter(false, topmost_redo); + if (boost::starts_with(topmost_redo, _utf8("Leaving"))) + curr = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(curr->timestamp + 1))); + } + } + return curr->timestamp > 0 ? curr : nullptr; +} + +// returns the name of the gizmo contained in the given string +static std::string extract_gizmo_name(const std::string& s) { + static const std::array prefixes = { _utf8("Entering"), _utf8("Leaving") }; + + std::string ret; + for (const std::string& prefix : prefixes) { + if (boost::starts_with(s, prefix)) + ret = s.substr(prefix.length() + 1); + + if (!ret.empty()) + break; + } + return ret; +} + +void ProjectDirtyStateManager::DirtyState::Gizmos::add_used(const UndoRedo::Snapshot& snapshot) +{ + const std::string name = extract_gizmo_name(snapshot.name); + auto it = used.find(name); + if (it == used.end()) + it = used.insert({ name, { {} } }).first; + + it->second.modified_timestamps.push_back(snapshot.timestamp); +} + +void ProjectDirtyStateManager::DirtyState::Gizmos::remove_obsolete_used(const Slic3r::UndoRedo::Stack& main_stack) +{ + const std::vector& snapshots = main_stack.snapshots(); + for (auto& [name, gizmo] : used) { + auto it = gizmo.modified_timestamps.begin(); + while (it != gizmo.modified_timestamps.end()) { + size_t timestamp = *it; + auto snapshot_it = std::find_if(snapshots.begin(), snapshots.end(), [timestamp](const Slic3r::UndoRedo::Snapshot& snapshot) { return snapshot.timestamp == timestamp; }); + if (snapshot_it == snapshots.end()) + it = gizmo.modified_timestamps.erase(it); + else + ++it; + } + } +} + +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +bool ProjectDirtyStateManager::DirtyState::Gizmos::any_used_modified() const +{ + for (auto& [name, gizmo] : used) { + if (!gizmo.modified_timestamps.empty()) + return true; + } + return false; +} +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + +// returns true if the given snapshot is contained in any of the gizmos caches +bool ProjectDirtyStateManager::DirtyState::Gizmos::is_used_and_modified(const UndoRedo::Snapshot& snapshot) const +{ + for (auto& [name, gizmo] : used) { + for (size_t i : gizmo.modified_timestamps) { + if (i == snapshot.timestamp) + return true; + } + } + return false; +} + +void ProjectDirtyStateManager::DirtyState::Gizmos::reset() +{ + used.clear(); +} + +void ProjectDirtyStateManager::update_from_undo_redo_stack(UpdateType type) +{ + if (!wxGetApp().initialized()) + return; + + const Plater* plater = wxGetApp().plater(); + const UndoRedo::Stack& main_stack = plater->undo_redo_stack_main(); + const UndoRedo::Stack& active_stack = plater->undo_redo_stack_active(); + + if (&main_stack == &active_stack) + update_from_undo_redo_main_stack(type, main_stack); + else + update_from_undo_redo_gizmo_stack(type, active_stack); + + wxGetApp().mainframe->update_title(); +} + +void ProjectDirtyStateManager::update_from_presets() +{ + m_state.presets = false; + std::vector> selected_presets = wxGetApp().get_selected_presets(); + for (const auto& [type, name] : selected_presets) { + m_state.presets |= !m_initial_presets[type].empty() && m_initial_presets[type] != name; + } + m_state.presets |= wxGetApp().has_unsaved_preset_changes(); + wxGetApp().mainframe->update_title(); +} + +void ProjectDirtyStateManager::reset_after_save() +{ + const Plater* plater = wxGetApp().plater(); + const UndoRedo::Stack& main_stack = plater->undo_redo_stack_main(); + const UndoRedo::Stack& active_stack = plater->undo_redo_stack_active(); + + if (&main_stack == &active_stack) { + const UndoRedo::Snapshot* saveable_snapshot = get_last_saveable_snapshot(EStackType::Main, main_stack, m_state.gizmos, m_last_save.main); + m_last_save.main = (saveable_snapshot != nullptr) ? saveable_snapshot->timestamp : 0; + } + else { + const UndoRedo::Snapshot* main_active_snapshot = get_active_snapshot(main_stack); + if (boost::starts_with(main_active_snapshot->name, _utf8("Entering"))) { + if (m_state.gizmos.current) + m_last_save.main = main_active_snapshot->timestamp + 1; + } + const UndoRedo::Snapshot* saveable_snapshot = get_last_saveable_snapshot(EStackType::Gizmo, active_stack, m_state.gizmos, m_last_save.main); + m_last_save.gizmo = saveable_snapshot->timestamp; + } + + reset_initial_presets(); + m_state.reset(); + wxGetApp().mainframe->update_title(); +} + +void ProjectDirtyStateManager::reset_initial_presets() +{ + m_initial_presets = std::array(); + std::vector> selected_presets = wxGetApp().get_selected_presets(); + for (const auto& [type, name] : selected_presets) { + m_initial_presets[type] = name; + } +} + +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +void ProjectDirtyStateManager::render_debug_window() const +{ + ImGuiWrapper& imgui = *wxGetApp().imgui(); + + auto color = [](bool value) { + return value ? ImVec4(1.0f, 0.49f, 0.216f, 1.0f) /* orange */: ImVec4(1.0f, 1.0f, 1.0f, 1.0f) /* white */; + }; + auto bool_to_text = [](bool value) { + return value ? "true" : "false"; + }; + auto append_bool_item = [color, bool_to_text, &imgui](const std::string& name, bool value) { + imgui.text_colored(color(value), name); + ImGui::SameLine(); + imgui.text_colored(color(value), bool_to_text(value)); + }; + auto append_int_item = [&imgui](const std::string& name, int value) { + imgui.text(name); + ImGui::SameLine(); + imgui.text(std::to_string(value)); + }; + auto append_snapshot_item = [&imgui](const std::string& label, const UndoRedo::Snapshot* snapshot) { + imgui.text(label); + ImGui::SameLine(100); + if (snapshot != nullptr) + imgui.text(snapshot->name + " (" + std::to_string(snapshot->timestamp) + ")"); + else + imgui.text("-"); + }; + + imgui.begin(std::string("Project dirty state statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + if (ImGui::CollapsingHeader("Dirty state", ImGuiTreeNodeFlags_DefaultOpen)) { + append_bool_item("Overall:", is_dirty()); + ImGui::Separator(); + append_bool_item("Plater:", m_state.plater); + append_bool_item("Presets:", m_state.presets); + append_bool_item("Current gizmo:", m_state.gizmos.current); + } + + if (ImGui::CollapsingHeader("Last save timestamps", ImGuiTreeNodeFlags_DefaultOpen)) { + append_int_item("Main:", m_last_save.main); + append_int_item("Current gizmo:", m_last_save.gizmo); + } + + const UndoRedo::Stack& main_stack = wxGetApp().plater()->undo_redo_stack_main(); + const UndoRedo::Snapshot* main_active_snapshot = get_active_snapshot(main_stack); + const UndoRedo::Snapshot* main_last_saveable_snapshot = get_last_saveable_snapshot(EStackType::Main, main_stack, m_state.gizmos, m_last_save.main); + const std::vector& main_snapshots = main_stack.snapshots(); + + if (ImGui::CollapsingHeader("Main snapshots", ImGuiTreeNodeFlags_DefaultOpen)) { + append_snapshot_item("Active:", main_active_snapshot); + append_snapshot_item("Last saveable:", main_last_saveable_snapshot); + } + + if (ImGui::CollapsingHeader("Main undo/redo stack", ImGuiTreeNodeFlags_DefaultOpen)) { + for (const UndoRedo::Snapshot& snapshot : main_snapshots) { + bool active = main_active_snapshot->timestamp == snapshot.timestamp; + imgui.text_colored(color(active), snapshot.name); + ImGui::SameLine(150); + imgui.text_colored(color(active), " (" + std::to_string(snapshot.timestamp) + ")"); + if (&snapshot == main_last_saveable_snapshot) { + ImGui::SameLine(); + imgui.text_colored(color(active), " (S)"); + } + if (m_last_save.main > 0 && m_last_save.main == snapshot.timestamp) { + ImGui::SameLine(); + imgui.text_colored(color(active), " (LS)"); + } + } + } + + const UndoRedo::Stack& active_stack = wxGetApp().plater()->undo_redo_stack_active(); + if (&active_stack != &main_stack) { + if (ImGui::CollapsingHeader("Gizmo undo/redo stack", ImGuiTreeNodeFlags_DefaultOpen)) { + const UndoRedo::Snapshot* active_active_snapshot = get_active_snapshot(active_stack); + const std::vector& active_snapshots = active_stack.snapshots(); + for (const UndoRedo::Snapshot& snapshot : active_snapshots) { + bool active = active_active_snapshot->timestamp == snapshot.timestamp; + imgui.text_colored(color(active), snapshot.name); + ImGui::SameLine(150); + imgui.text_colored(color(active), " (" + std::to_string(snapshot.timestamp) + ")"); + } + } + } + + if (m_state.gizmos.any_used_modified()) { + if (ImGui::CollapsingHeader("Gizmos", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(10.0f); + for (const auto& [name, gizmo] : m_state.gizmos.used) { + if (!gizmo.modified_timestamps.empty()) { + if (ImGui::CollapsingHeader(name.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + std::string modified_timestamps; + for (size_t i = 0; i < gizmo.modified_timestamps.size(); ++i) { + if (i > 0) + modified_timestamps += " | "; + modified_timestamps += std::to_string(gizmo.modified_timestamps[i]); + } + imgui.text(modified_timestamps); + } + } + } + ImGui::Unindent(10.0f); + } + } + + imgui.end(); +} +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + +void ProjectDirtyStateManager::update_from_undo_redo_main_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack) +{ + m_state.plater = false; + + if (type == UpdateType::TakeSnapshot) { + if (m_last_save.main != 0) { + const std::vector& snapshots = stack.snapshots(); + auto snapshot_it = std::find_if(snapshots.begin(), snapshots.end(), [this](const Slic3r::UndoRedo::Snapshot& snapshot) { return snapshot.timestamp == m_last_save.main; }); + if (snapshot_it == snapshots.end()) + m_last_save.main = 0; + } + m_state.gizmos.remove_obsolete_used(stack); + } + + const UndoRedo::Snapshot* active_snapshot = get_active_snapshot(stack); + if (active_snapshot->name == _utf8("New Project") || + active_snapshot->name == _utf8("Reset Project") || + boost::starts_with(active_snapshot->name, _utf8("Load Project:"))) + return; + + size_t search_timestamp = 0; + if (boost::starts_with(active_snapshot->name, _utf8("Entering"))) { + if (type == UpdateType::UndoRedoTo) { + std::string topmost_redo; + wxGetApp().plater()->undo_redo_topmost_string_getter(false, topmost_redo); + if (boost::starts_with(topmost_redo, _utf8("Leaving"))) { + const std::vector& snapshots = stack.snapshots(); + const UndoRedo::Snapshot* leaving_snapshot = &(*std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(active_snapshot->timestamp + 1))); + if (m_state.gizmos.is_used_and_modified(*leaving_snapshot)) { + m_state.plater = (leaving_snapshot != nullptr && leaving_snapshot->timestamp != m_last_save.main); + return; + } + } + } + m_state.gizmos.current = false; + m_last_save.gizmo = 0; + search_timestamp = m_last_save.main; + } + else if (boost::starts_with(active_snapshot->name, _utf8("Leaving"))) { + if (m_state.gizmos.current) + m_state.gizmos.add_used(*active_snapshot); + m_state.gizmos.current = false; + m_last_save.gizmo = 0; + search_timestamp = m_last_save.main; + } + + const UndoRedo::Snapshot* last_saveable_snapshot = get_last_saveable_snapshot(EStackType::Main, stack, m_state.gizmos, m_last_save.main); + m_state.plater = (last_saveable_snapshot != nullptr && last_saveable_snapshot->timestamp != m_last_save.main); +} + +void ProjectDirtyStateManager::update_from_undo_redo_gizmo_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack) +{ + m_state.gizmos.current = false; + + const UndoRedo::Snapshot* active_snapshot = get_active_snapshot(stack); + if (active_snapshot->name == "Gizmos-Initial") + return; + + const UndoRedo::Snapshot* last_saveable_snapshot = get_last_saveable_snapshot(EStackType::Gizmo, stack, m_state.gizmos, m_last_save.main); + m_state.gizmos.current = (last_saveable_snapshot != nullptr && last_saveable_snapshot->timestamp != m_last_save.gizmo); +} + +} // namespace GUI +} // namespace Slic3r + +#endif // ENABLE_PROJECT_DIRTY_STATE + diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.hpp b/src/slic3r/GUI/ProjectDirtyStateManager.hpp new file mode 100644 index 000000000..f7ce81a62 --- /dev/null +++ b/src/slic3r/GUI/ProjectDirtyStateManager.hpp @@ -0,0 +1,96 @@ +#ifndef slic3r_ProjectDirtyStateManager_hpp_ +#define slic3r_ProjectDirtyStateManager_hpp_ + +#include "libslic3r/Preset.hpp" + +#if ENABLE_PROJECT_DIRTY_STATE + +namespace Slic3r { +namespace UndoRedo { +class Stack; +struct Snapshot; +} // namespace UndoRedo + +namespace GUI { +class ProjectDirtyStateManager +{ +public: + enum class UpdateType : unsigned char + { + TakeSnapshot, + UndoRedoTo + }; + + struct DirtyState + { + struct Gizmos + { + struct Gizmo + { + std::vector modified_timestamps; + }; + + bool current{ false }; + std::map used; + + void add_used(const UndoRedo::Snapshot& snapshot); + void remove_obsolete_used(const Slic3r::UndoRedo::Stack& main_stack); +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + bool any_used_modified() const; +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + bool is_used_and_modified(const UndoRedo::Snapshot& snapshot) const; + void reset(); + }; + + bool plater{ false }; + bool presets{ false }; + Gizmos gizmos; + + bool is_dirty() const { return plater || presets || gizmos.current; } + void reset() { + plater = false; + presets = false; + gizmos.current = false; + } + }; + +private: + struct LastSaveTimestamps + { + size_t main{ 0 }; + size_t gizmo{ 0 }; + + void reset() { + main = 0; + gizmo = 0; + } + }; + + DirtyState m_state; + LastSaveTimestamps m_last_save; + + // keeps track of initial selected presets + std::array m_initial_presets; + +public: + bool is_dirty() const { return m_state.is_dirty(); } + void update_from_undo_redo_stack(UpdateType type); + void update_from_presets(); + void reset_after_save(); + void reset_initial_presets(); +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + void render_debug_window() const; +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + +private: + void update_from_undo_redo_main_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack); + void update_from_undo_redo_gizmo_stack(UpdateType type, const Slic3r::UndoRedo::Stack& stack); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // ENABLE_PROJECT_DIRTY_STATE + +#endif // slic3r_ProjectDirtyStateManager_hpp_ + diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index a037a3b47..5920ab5e3 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1217,9 +1217,8 @@ void Tab::apply_config_from_cache() // to update number of "filament" selection boxes when the number of extruders change. void Tab::on_presets_changed() { - if (wxGetApp().plater() == nullptr) { + if (wxGetApp().plater() == nullptr) return; - } // Instead of PostEvent (EVT_TAB_PRESETS_CHANGED) just call update_presets wxGetApp().plater()->sidebar().update_presets(m_type); @@ -1237,6 +1236,10 @@ void Tab::on_presets_changed() // clear m_dependent_tabs after first update from select_preset() // to avoid needless preset loading from update() function m_dependent_tabs.clear(); + +#if ENABLE_PROJECT_DIRTY_STATE + wxGetApp().plater()->update_project_dirty_from_presets(); +#endif // ENABLE_PROJECT_DIRTY_STATE } void Tab::build_preset_description_line(ConfigOptionsGroup* optgroup) @@ -2113,10 +2116,16 @@ wxSizer* Tab::description_line_widget(wxWindow* parent, ogStaticText* *StaticTex return sizer; } +#if ENABLE_PROJECT_DIRTY_STATE +bool Tab::saved_preset_is_dirty() const { return m_presets->saved_is_dirty(); } +void Tab::update_saved_preset_from_current_preset() { m_presets->update_saved_preset_from_current_preset(); } +bool Tab::current_preset_is_dirty() const { return m_presets->current_is_dirty(); } +#else bool Tab::current_preset_is_dirty() { return m_presets->current_is_dirty(); } +#endif // ENABLE_PROJECT_DIRTY_STATE void TabPrinter::build() { diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 9ccbcda28..601995ea7 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -270,7 +270,11 @@ public: Preset::Type type() const { return m_type; } // The tab is already constructed. bool completed() const { return m_completed; } - virtual bool supports_printer_technology(const PrinterTechnology tech) = 0; +#if ENABLE_PROJECT_DIRTY_STATE + virtual bool supports_printer_technology(const PrinterTechnology tech) const = 0; +#else + virtual bool supports_printer_technology(const PrinterTechnology tech) = 0; +#endif // ENABLE_PROJECT_DIRTY_STATE void create_preset_tab(); void add_scaled_button(wxWindow* parent, ScalableButton** btn, const std::string& icon_name, @@ -333,7 +337,13 @@ public: Field* get_field(const t_config_option_key &opt_key, Page** selected_page, int opt_index = -1); void toggle_option(const std::string& opt_key, bool toggle, int opt_index = -1); wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText, wxString text = wxEmptyString); +#if ENABLE_PROJECT_DIRTY_STATE + bool current_preset_is_dirty() const; + bool saved_preset_is_dirty() const; + void update_saved_preset_from_current_preset(); +#else bool current_preset_is_dirty(); +#endif // ENABLE_PROJECT_DIRTY_STATE DynamicPrintConfig* get_config() { return m_config; } PresetCollection* get_presets() { return m_presets; } @@ -377,8 +387,8 @@ class TabPrint : public Tab { public: TabPrint(wxNotebook* parent) : -// Tab(parent, _(L("Print Settings")), L("print")) {} - Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_PRINT) {} +// Tab(parent, _L("Print Settings"), L("print")) {} + Tab(parent, _L("Print Settings"), Slic3r::Preset::TYPE_PRINT) {} ~TabPrint() {} void build() override; @@ -387,7 +397,11 @@ public: void toggle_options() override; void update() override; void clear_pages() override; - bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; } +#if ENABLE_PROJECT_DIRTY_STATE + bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } +#else + bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; } +#endif // ENABLE_PROJECT_DIRTY_STATE private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; @@ -407,8 +421,8 @@ private: std::map m_overrides_options; public: TabFilament(wxNotebook* parent) : -// Tab(parent, _(L("Filament Settings")), L("filament")) {} - Tab(parent, _(L("Filament Settings")), Slic3r::Preset::TYPE_FILAMENT) {} +// Tab(parent, _L("Filament Settings"), L("filament")) {} + Tab(parent, _L("Filament Settings"), Slic3r::Preset::TYPE_FILAMENT) {} ~TabFilament() {} void build() override; @@ -417,7 +431,11 @@ public: void toggle_options() override; void update() override; void clear_pages() override; - bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; } +#if ENABLE_PROJECT_DIRTY_STATE + bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } +#else + bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptFFF; } +#endif // ENABLE_PROJECT_DIRTY_STATE }; class TabPrinter : public Tab @@ -450,7 +468,7 @@ public: // TabPrinter(wxNotebook* parent) : Tab(parent, _(L("Printer Settings")), L("printer")) {} TabPrinter(wxNotebook* parent) : - Tab(parent, _(L("Printer Settings")), Slic3r::Preset::TYPE_PRINTER) {} + Tab(parent, _L("Printer Settings"), Slic3r::Preset::TYPE_PRINTER) {} ~TabPrinter() {} void build() override; @@ -472,7 +490,11 @@ public: void init_options_list() override; void msw_rescale() override; void sys_color_changed() override; - bool supports_printer_technology(const PrinterTechnology /* tech */) override { return true; } +#if ENABLE_PROJECT_DIRTY_STATE + bool supports_printer_technology(const PrinterTechnology /* tech */) const override { return true; } +#else + bool supports_printer_technology(const PrinterTechnology /* tech */) override { return true; } +#endif // ENABLE_PROJECT_DIRTY_STATE wxSizer* create_bed_shape_widget(wxWindow* parent); void cache_extruder_cnt(); @@ -483,8 +505,8 @@ class TabSLAMaterial : public Tab { public: TabSLAMaterial(wxNotebook* parent) : -// Tab(parent, _(L("Material Settings")), L("sla_material")) {} - Tab(parent, _(L("Material Settings")), Slic3r::Preset::TYPE_SLA_MATERIAL) {} +// Tab(parent, _L("Material Settings"), L("sla_material")) {} + Tab(parent, _L("Material Settings"), Slic3r::Preset::TYPE_SLA_MATERIAL) {} ~TabSLAMaterial() {} void build() override; @@ -492,15 +514,19 @@ public: void toggle_options() override {}; void update() override; void init_options_list() override; - bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; } +#if ENABLE_PROJECT_DIRTY_STATE + bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptSLA; } +#else + bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; } +#endif // ENABLE_PROJECT_DIRTY_STATE }; class TabSLAPrint : public Tab { public: TabSLAPrint(wxNotebook* parent) : -// Tab(parent, _(L("Print Settings")), L("sla_print")) {} - Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {} +// Tab(parent, _L("Print Settings"), L("sla_print")) {} + Tab(parent, _L("Print Settings"), Slic3r::Preset::TYPE_SLA_PRINT) {} ~TabSLAPrint() {} ogStaticText* m_support_object_elevation_description_line = nullptr; @@ -511,7 +537,11 @@ public: void toggle_options() override; void update() override; void clear_pages() override; - bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; } +#if ENABLE_PROJECT_DIRTY_STATE + bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptSLA; } +#else + bool supports_printer_technology(const PrinterTechnology tech) override { return tech == ptSLA; } +#endif // ENABLE_PROJECT_DIRTY_STATE }; } // GUI