diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index b22cd6009..793ef80b7 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -904,6 +904,14 @@ bool GUI_App::on_init_inner()
     }
     else
         load_current_presets();
+
+#if ENABLE_PROJECT_DIRTY_STATE
+    if (plater_ != nullptr) {
+//        plater_->reset_project_initial_presets();
+        plater_->update_project_dirty_from_presets();
+    }
+#endif // ENABLE_PROJECT_DIRTY_STATE
+
     mainframe->Show(true);
 
     obj_list()->set_min_height();
diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp
index 58560c8bd..bce64f9ea 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/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index 35b1c16d8..832ebc257 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -206,6 +206,11 @@ 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();
+#endif // ENABLE_PROJECT_DIRTY_STATE
+
         if (event.CanVeto() && !wxGetApp().check_unsaved_changes()) {
             event.Veto();
             return;
@@ -487,8 +492,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;
@@ -668,10 +679,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
 {
@@ -977,16 +1014,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();
 
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 0434d2555..9de4641c0 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -1393,7 +1393,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
@@ -1511,9 +1517,26 @@ 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);
+                if (dlg.ShowModal() == wxID_CANCEL)
+                    return false;
+                mainframe->save_project_as(wxGetApp().plater()->get_project_filename());
+            }
+        }
+        return true;
+    }
+    void reset_project_dirty_after_save() { dirty_state.reset_after_save(); }
 #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,
@@ -4216,6 +4239,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(undo_redo_stack_main(), undo_redo_stack());
+#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();
@@ -4346,6 +4374,10 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::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(undo_redo_stack_main(), undo_redo_stack());
+#endif // ENABLE_PROJECT_DIRTY_STATE
 }
 
 void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */)
@@ -4429,9 +4461,15 @@ Plater::Plater(wxWindow *parent, MainFrame *main_frame)
     // Initialization performed in the private c-tor
 }
 
+#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(); }
 #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; }
@@ -4442,12 +4480,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_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);
@@ -4471,8 +4527,16 @@ void Plater::load_project(const wxString& filename)
     std::vector<size_t> 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_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*/)
@@ -4503,7 +4567,13 @@ void Plater::add_model(bool imperial_units/* = false*/)
     }
 
     Plater::TakeSnapshot snapshot(this, snapshot_label);
+#if ENABLE_PROJECT_DIRTY_STATE
+    std::vector<size_t> 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()
@@ -5187,24 +5257,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);
@@ -5212,6 +5297,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));
@@ -5221,6 +5319,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()
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index f2d60d801..ead9679c7 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -130,9 +130,15 @@ public:
     Plater &operator=(const Plater &) = delete;
     ~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();
 #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();
@@ -201,7 +207,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;
diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp
index 9a19676b2..5cf7274bb 100644
--- a/src/slic3r/GUI/ProjectDirtyStateManager.cpp
+++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp
@@ -2,12 +2,32 @@
 #include "ProjectDirtyStateManager.hpp"
 #include "ImGuiWrapper.hpp"
 #include "GUI_App.hpp"
+#include "MainFrame.hpp"
 
 #if ENABLE_PROJECT_DIRTY_STATE
 
 namespace Slic3r {
 namespace GUI {
 
+void ProjectDirtyStateManager::update_from_undo_redo_stack(const Slic3r::UndoRedo::Stack& main_stack, const Slic3r::UndoRedo::Stack& active_stack)
+{
+    if (!wxGetApp().initialized())
+        return;
+
+    wxGetApp().mainframe->update_title();
+}
+
+void ProjectDirtyStateManager::update_from_presets()
+{
+    wxGetApp().mainframe->update_title();
+}
+
+void ProjectDirtyStateManager::reset_after_save()
+{
+    m_state.reset();
+    wxGetApp().mainframe->update_title();
+}
+
 #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
 void ProjectDirtyStateManager::render_debug_window() const
 {
diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.hpp b/src/slic3r/GUI/ProjectDirtyStateManager.hpp
index 81ac28915..b488c00bb 100644
--- a/src/slic3r/GUI/ProjectDirtyStateManager.hpp
+++ b/src/slic3r/GUI/ProjectDirtyStateManager.hpp
@@ -4,6 +4,9 @@
 #if ENABLE_PROJECT_DIRTY_STATE
 
 namespace Slic3r {
+namespace UndoRedo {
+class Stack;
+} // namespace UndoRedo
 namespace GUI {
 
 class ProjectDirtyStateManager
@@ -14,12 +17,19 @@ class ProjectDirtyStateManager
         bool presets{ false };
 
         bool is_dirty() const { return plater || presets; }
+        void reset() {
+            plater = false;
+            presets = false;
+        }
     };
 
     DirtyState m_state;
 
 public:
     bool is_dirty() const { return m_state.is_dirty(); }
+    void update_from_undo_redo_stack(const Slic3r::UndoRedo::Stack& main_stack, const Slic3r::UndoRedo::Stack& active_stack);
+    void update_from_presets();
+    void reset_after_save();
 
 #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
     void render_debug_window() const;
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 11c4875eb..97117f418 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -1215,9 +1215,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);
@@ -1235,6 +1234,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)