diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 20ea4e33a9..cd28d6eb20 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -163,6 +163,8 @@ set(SLIC3R_GUI_SOURCES
GUI/InstanceCheck.hpp
GUI/Search.cpp
GUI/Search.hpp
+ GUI/UnsavedChangesDialog.cpp
+ GUI/UnsavedChangesDialog.hpp
Utils/Http.cpp
Utils/Http.hpp
Utils/FixModelByWin10.cpp
diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp
index db551610b7..6fa9429157 100644
--- a/src/slic3r/GUI/GUI_App.hpp
+++ b/src/slic3r/GUI/GUI_App.hpp
@@ -86,6 +86,12 @@ class ConfigWizard;
static wxString dots("…", wxConvUTF8);
+// Does our wxWidgets version support markup?
+// https://github.com/prusa3d/PrusaSlicer/issues/4282#issuecomment-634676371
+#if wxUSE_MARKUP && wxCHECK_VERSION(3, 1, 1)
+ #define SUPPORTS_MARKUP
+#endif
+
class GUI_App : public wxApp
{
bool m_initialized { false };
diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp
index 2a2af5336b..6eab6b72ac 100644
--- a/src/slic3r/GUI/Search.cpp
+++ b/src/slic3r/GUI/Search.cpp
@@ -28,12 +28,6 @@ using GUI::into_u8;
namespace Search {
-// Does our wxWidgets version support markup?
-// https://github.com/prusa3d/PrusaSlicer/issues/4282#issuecomment-634676371
-#if wxUSE_MARKUP && wxCHECK_VERSION(3, 1, 1)
- #define SEARCH_SUPPORTS_MARKUP
-#endif
-
static char marker_by_type(Preset::Type type, PrinterTechnology pt)
{
switch(type) {
@@ -264,7 +258,7 @@ bool OptionsSearcher::search(const std::string& search, bool force/* = false*/)
std::string label_u8 = into_u8(label);
std::string label_plain = label_u8;
-#ifdef SEARCH_SUPPORTS_MARKUP
+#ifdef SUPPORTS_MARKUP
boost::replace_all(label_plain, std::string(1, char(ImGui::ColorMarkerStart)), "");
boost::replace_all(label_plain, std::string(1, char(ImGui::ColorMarkerEnd)), "");
#else
@@ -442,7 +436,7 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher)
wxDataViewTextRenderer* const markupRenderer = new wxDataViewTextRenderer();
-#ifdef SEARCH_SUPPORTS_MARKUP
+#ifdef SUPPORTS_MARKUP
markupRenderer->EnableMarkup();
#endif
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 9d37362ba4..49317f802f 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -36,6 +36,7 @@
#include "MainFrame.hpp"
#include "format.hpp"
#include "PhysicalPrinterDialog.hpp"
+#include "UnsavedChangesDialog.hpp"
namespace Slic3r {
namespace GUI {
@@ -3131,6 +3132,10 @@ void Tab::select_preset(std::string preset_name, bool delete_current /*=false*/,
// if the current preset was not dirty, or the user agreed to discard the changes, 1 is returned.
bool Tab::may_discard_current_dirty_preset(PresetCollection* presets /*= nullptr*/, const std::string& new_printer_name /*= ""*/)
{
+ UnsavedChangesDialog dlg(m_type);
+ dlg.ShowModal();
+
+
if (presets == nullptr) presets = m_presets;
// Display a dialog showing the dirty options in a human readable form.
const Preset& old_preset = presets->get_edited_preset();
diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp
new file mode 100644
index 0000000000..21da295d40
--- /dev/null
+++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp
@@ -0,0 +1,271 @@
+#include "UnsavedChangesDialog.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "wx/dataview.h"
+
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/PresetBundle.hpp"
+#include "GUI_App.hpp"
+#include "Plater.hpp"
+#include "Tab.hpp"
+
+#define FTS_FUZZY_MATCH_IMPLEMENTATION
+#include "fts_fuzzy_match.h"
+
+#include "imgui/imconfig.h"
+
+using boost::optional;
+
+namespace Slic3r {
+
+namespace GUI {
+
+// ----------------------------------------------------------------------------
+// ModelNode: a node inside UnsavedChangesModel
+// ----------------------------------------------------------------------------
+
+// preset(root) node
+ModelNode::ModelNode(const wxString& text, Preset::Type preset_type) :
+ m_parent(nullptr),
+ m_preset_type(preset_type),
+ m_text(text)
+{
+}
+
+// group node
+ModelNode::ModelNode(ModelNode* parent, const wxString& text, const std::string& icon_name) :
+ m_parent(parent),
+ m_text(text)
+{
+}
+
+// group node
+ModelNode::ModelNode(ModelNode* parent, const wxString& text, bool is_option) :
+ m_parent(parent),
+ m_text(text),
+ m_container(!is_option)
+{
+}
+
+
+// ----------------------------------------------------------------------------
+// UnsavedChangesModel
+// ----------------------------------------------------------------------------
+
+UnsavedChangesModel::UnsavedChangesModel(wxWindow* parent)
+{
+ int icon_id = 0;
+ for (const std::string& icon : { "cog", "printer", "sla_printer", "spool", "resin" })
+ m_icon[icon_id++] = ScalableBitmap(parent, icon);
+
+ m_root = new ModelNode("Preset", Preset::TYPE_INVALID);
+}
+
+UnsavedChangesModel::~UnsavedChangesModel()
+{
+ delete m_root;
+}
+
+void UnsavedChangesModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const
+{
+ wxASSERT(item.IsOk());
+
+ ModelNode* node = (ModelNode*)item.GetID();
+ switch (col)
+ {
+ case colToggle:
+ variant = node->m_toggle;
+ break;
+ case colTypeIcon:
+ variant << node->m_type_icon;
+ break;
+ case colGroupIcon:
+ variant << node->m_group_icon;
+ break;
+ case colMarkedText:
+ variant =node->m_text;
+ break;
+ case colOldValue:
+ variant =node->m_text;
+ break;
+ case colNewValue:
+ variant =node->m_text;
+ break;
+
+ default:
+ wxLogError("UnsavedChangesModel::GetValue: wrong column %d", col);
+ }
+}
+
+bool UnsavedChangesModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col)
+{
+ assert(item.IsOk());
+
+ ModelNode* node = (ModelNode*)item.GetID();
+ switch (col)
+ {
+ case colToggle:
+ node->m_toggle = variant.GetBool();
+ return true;
+ case colTypeIcon:
+ node->m_type_icon << variant;
+ return true;
+ case colGroupIcon:
+ node->m_group_icon << variant;
+ return true;
+ case colMarkedText:
+ node->m_text = variant.GetString();
+ return true;
+ case colOldValue:
+ node->m_text = variant.GetString();
+ return true;
+ case colNewValue:
+ node->m_text = variant.GetString();
+ return true;
+ default:
+ wxLogError("UnsavedChangesModel::SetValue: wrong column");
+ }
+ return false;
+}
+
+bool UnsavedChangesModel::IsEnabled(const wxDataViewItem& item, unsigned int col) const
+{
+ assert(item.IsOk());
+
+ ModelNode* node = (ModelNode*)item.GetID();
+
+ // disable unchecked nodes
+ return !node->IsToggle();
+}
+
+wxDataViewItem UnsavedChangesModel::GetParent(const wxDataViewItem& item) const
+{
+ // the invisible root node has no parent
+ if (!item.IsOk())
+ return wxDataViewItem(nullptr);
+
+ ModelNode* node = (ModelNode*)item.GetID();
+
+ // "MyMusic" also has no parent
+ if (node == m_root)
+ return wxDataViewItem(nullptr);
+
+ return wxDataViewItem((void*)node->GetParent());
+}
+
+bool UnsavedChangesModel::IsContainer(const wxDataViewItem& item) const
+{
+ // the invisble root node can have children
+ if (!item.IsOk())
+ return true;
+
+ ModelNode* node = (ModelNode*)item.GetID();
+ return node->IsContainer();
+}
+
+unsigned int UnsavedChangesModel::GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const
+{
+ ModelNode* node = (ModelNode*)parent.GetID();
+ if (!node) {
+ array.Add(wxDataViewItem((void*)m_root));
+ return 1;
+ }
+
+ if (node->GetChildCount() == 0)
+ return 0;
+
+ unsigned int count = node->GetChildren().GetCount();
+ for (unsigned int pos = 0; pos < count; pos++) {
+ ModelNode* child = node->GetChildren().Item(pos);
+ array.Add(wxDataViewItem((void*)child));
+ }
+
+ return count;
+}
+
+
+wxString UnsavedChangesModel::GetColumnType(unsigned int col) const
+{
+ if (col == colToggle)
+ return "bool";
+
+ if (col < colMarkedText)
+ return "wxBitmap";
+
+ return "string";
+}
+
+
+//------------------------------------------
+// UnsavedChangesDialog
+//------------------------------------------
+
+UnsavedChangesDialog::UnsavedChangesDialog(Preset::Type type)
+ : DPIDialog(NULL, wxID_ANY, _L("Unsaved Changes"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+{
+ wxColour bgr_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
+ SetBackgroundColour(bgr_clr);
+
+ int border = 10;
+ int em = em_unit();
+
+ changes_tree = new wxDataViewCtrl(this, wxID_ANY, wxDefaultPosition, wxSize(em * 80, em * 60), wxBORDER_SIMPLE);
+ changes_tree_model = new UnsavedChangesModel(this);
+ changes_tree->AssociateModel(changes_tree_model);
+
+ changes_tree->AppendToggleColumn(L"\u2610", UnsavedChangesModel::colToggle);//2610,11,12 //2714
+ changes_tree->AppendBitmapColumn("", UnsavedChangesModel::colTypeIcon);
+ changes_tree->AppendBitmapColumn("", UnsavedChangesModel::colGroupIcon);
+
+ wxDataViewTextRenderer* const markupRenderer = new wxDataViewTextRenderer();
+
+#ifdef SUPPORTS_MARKUP
+ markupRenderer->EnableMarkup();
+#endif
+
+ changes_tree->AppendColumn(new wxDataViewColumn("", markupRenderer, UnsavedChangesModel::colMarkedText, wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT));
+ changes_tree->AppendColumn(new wxDataViewColumn("Old value", markupRenderer, UnsavedChangesModel::colOldValue, wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT));
+ changes_tree->AppendColumn(new wxDataViewColumn("New value", markupRenderer, UnsavedChangesModel::colNewValue, wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT));
+
+ wxStdDialogButtonSizer* cancel_btn = this->CreateStdDialogButtonSizer(wxCANCEL);
+
+ wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL);
+
+ topSizer->Add(new wxStaticText(this, wxID_ANY, _L("There is unsaved changes for the current preset") + ":"), 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border);
+ topSizer->Add(changes_tree, 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border);
+ topSizer->Add(cancel_btn, 0, wxEXPAND | wxALL, border);
+
+ SetSizer(topSizer);
+ topSizer->SetSizeHints(this);
+}
+
+void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect)
+{
+ const int& em = em_unit();
+
+ msw_buttons_rescale(this, em, { wxID_CANCEL });
+
+ const wxSize& size = wxSize(80 * em, 60 * em);
+ SetMinSize(size);
+
+ Fit();
+ Refresh();
+}
+
+void UnsavedChangesDialog::on_sys_color_changed()
+{
+ // msw_rescale updates just icons, so use it
+// changes_tree_model->msw_rescale();
+
+ Refresh();
+}
+
+
+}
+
+} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp
new file mode 100644
index 0000000000..a3ee7d984f
--- /dev/null
+++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp
@@ -0,0 +1,151 @@
+#ifndef slic3r_UnsavedChangesDialog_hpp_
+#define slic3r_UnsavedChangesDialog_hpp_
+
+#include
+#include