Implement a dialog for unsaved changes (#1241)

* Track unsaved scene changes

* Implement a dialog for unsaved changes

* Exclude all selection actions from scene change tracking
This commit is contained in:
Stanislav Denisov
2025-10-15 17:39:15 +02:00
committed by GitHub
parent 7318056235
commit a00fb8efc2
8 changed files with 190 additions and 16 deletions
+1 -1
View File
@@ -177,7 +177,7 @@ void ArmatureWindow::Create(EditorComponent* _editor)
if (args.iValue < 0)
return;
wi::Archive& archive = editor->AdvanceHistory();
wi::Archive& archive = editor->AdvanceHistory(true);
archive << EditorComponent::HISTORYOP_SELECTION;
// record PREVIOUS selection state...
editor->RecordSelection(archive);
+1 -1
View File
@@ -91,7 +91,7 @@ void ComponentsWindow::Create(EditorComponent* _editor)
if (args.iValue < 0)
return;
wi::Archive& archive = editor->AdvanceHistory();
wi::Archive& archive = editor->AdvanceHistory(true);
archive << EditorComponent::HISTORYOP_SELECTION;
// record PREVIOUS selection state...
editor->RecordSelection(archive);
+85 -6
View File
@@ -1294,7 +1294,15 @@ void EditorComponent::Load()
exitButton.SetTooltip("Exit");
exitButton.SetColor(wi::Color(160, 50, 50, 180), wi::gui::WIDGETSTATE::IDLE);
exitButton.SetColor(wi::Color(200, 50, 50, 255), wi::gui::WIDGETSTATE::FOCUS);
exitButton.OnClick([](wi::gui::EventArgs args) {
exitButton.OnClick([this](wi::gui::EventArgs args) {
// Check all scenes for unsaved changes
for (int i = 0; i < (int)scenes.size(); ++i)
{
if (!CheckUnsavedChanges(i))
{
return;
}
}
wi::platform::Exit();
});
topmenuWnd.AddWidget(&exitButton);
@@ -2350,7 +2358,7 @@ void EditorComponent::Update(float dt)
// Select...
if (leftmouse_select || selectAll || clear_selected)
{
wi::Archive& archive = AdvanceHistory();
wi::Archive& archive = AdvanceHistory(true);
archive << HISTORYOP_SELECTION;
// record PREVIOUS selection state...
RecordSelection(archive);
@@ -4735,8 +4743,9 @@ void EditorComponent::ResetHistory()
EditorScene& editorscene = GetCurrentEditorScene();
editorscene.historyPos = -1;
editorscene.history.clear();
editorscene.has_unsaved_changes = false;
}
wi::Archive& EditorComponent::AdvanceHistory()
wi::Archive& EditorComponent::AdvanceHistory(const bool scene_unchanged)
{
EditorScene& editorscene = GetCurrentEditorScene();
editorscene.historyPos++;
@@ -4749,6 +4758,12 @@ wi::Archive& EditorComponent::AdvanceHistory()
editorscene.history.emplace_back();
editorscene.history.back().SetReadModeAndResetPos(false);
if (!scene_unchanged)
{
editorscene.has_unsaved_changes = true;
RefreshSceneList();
}
return editorscene.history.back();
}
void EditorComponent::ConsumeHistoryOperation(bool undo)
@@ -5218,6 +5233,10 @@ void EditorComponent::Open(std::string filename)
componentsWnd.weatherWnd.UpdateData();
componentsWnd.RefreshEntityTree();
GetCurrentEditorScene().has_unsaved_changes = false;
RefreshSceneList();
wi::backlog::post("[Editor] finished loading model: " + filename);
});
});
@@ -5282,6 +5301,7 @@ void EditorComponent::Save(const std::string& filename)
}
GetCurrentEditorScene().path = filename;
GetCurrentEditorScene().has_unsaved_changes = false;
RefreshSceneList();
RegisterRecentlyUsed(filename);
@@ -5994,28 +6014,42 @@ void EditorComponent::RefreshSceneList()
for (int i = 0; i < int(scenes.size()); ++i)
{
auto& editorscene = scenes[i];
std::string tabText;
if (editorscene->path.empty())
{
if (current_localization.Get((size_t)EditorLocalization::UntitledScene))
{
editorscene->tabSelectButton.SetText(current_localization.Get((size_t)EditorLocalization::UntitledScene));
tabText = current_localization.Get((size_t)EditorLocalization::UntitledScene);
}
else
{
editorscene->tabSelectButton.SetText("Untitled scene");
tabText = "Untitled scene";
}
editorscene->tabSelectButton.SetTooltip("");
}
else
{
editorscene->tabSelectButton.SetText(wi::helper::GetFileNameFromPath(editorscene->path));
tabText = wi::helper::GetFileNameFromPath(editorscene->path);
editorscene->tabSelectButton.SetTooltip(editorscene->path);
}
if (editorscene->has_unsaved_changes)
{
tabText = tabText + " *";
}
editorscene->tabSelectButton.SetText(tabText);
editorscene->tabSelectButton.OnClick([this, i](wi::gui::EventArgs args) {
SetCurrentScene(i);
});
editorscene->tabCloseButton.OnClick([this, i](wi::gui::EventArgs args) {
// Check for unsaved changes before closing
if (!CheckUnsavedChanges(i))
{
return;
}
wi::lua::KillProcesses();
translator.selected.clear();
@@ -6090,6 +6124,51 @@ void EditorComponent::NewScene()
cameraWnd.ResetCam();
}
bool EditorComponent::CheckUnsavedChanges(int scene_index)
{
if (scene_index < 0)
scene_index = current_scene;
if (scene_index < 0 || scene_index >= (int)scenes.size())
return true;
EditorScene& editorscene = *scenes[scene_index];
if (!editorscene.has_unsaved_changes)
return true;
std::string sceneName = editorscene.path.empty() ? "" : wi::helper::GetFileNameFromPath(editorscene.path);
std::string message = sceneName.empty() ? "Do you want to save the untitled scene?" : "Do you want to save changes to \"" + sceneName + "\"?";
wi::helper::MessageBoxResult result = wi::helper::messageBoxCustom(message, "Unsaved changes", "YesNoCancel");
if (result == wi::helper::MessageBoxResult::Yes)
{
// User wants to save
if (editorscene.path.empty())
{
// Need to prompt for save location
SaveAs();
// After SaveAs, check if the scene was actually saved
return !editorscene.has_unsaved_changes;
}
else
{
// Save to existing path
Save(editorscene.path);
return true;
}
}
else if (result == wi::helper::MessageBoxResult::No)
{
// User doesn't want to save, proceed
return true;
}
else // Cancel or closed dialog
{
// User cancelled, don't proceed
return false;
}
}
void EditorComponent::FocusCameraOnSelected()
{
Scene& scene = GetCurrentScene();
+4 -1
View File
@@ -174,7 +174,7 @@ public:
void RecordEntity(wi::Archive& archive, const wi::vector<wi::ecs::Entity>& entities);
void ResetHistory();
wi::Archive& AdvanceHistory();
wi::Archive& AdvanceHistory(bool scene_unchanged = false);
void ConsumeHistoryOperation(bool undo);
wi::vector<std::string> recentFilenames;
@@ -209,6 +209,7 @@ public:
wi::scene::TransformComponent camera_target;
wi::vector<wi::Archive> history;
int historyPos = -1;
bool has_unsaved_changes = false;
wi::gui::Button tabSelectButton;
wi::gui::Button tabCloseButton;
};
@@ -222,6 +223,8 @@ public:
void RefreshSceneList();
void NewScene();
bool CheckUnsavedChanges(int scene_index = -1);
void FocusCameraOnSelected();
wi::Localization default_localization;
+1 -1
View File
@@ -169,7 +169,7 @@ void HumanoidWindow::Create(EditorComponent* _editor)
if (args.iValue < 0)
return;
wi::Archive& archive = editor->AdvanceHistory();
wi::Archive& archive = editor->AdvanceHistory(true);
archive << EditorComponent::HISTORYOP_SELECTION;
// record PREVIOUS selection state...
editor->RecordSelection(archive);
+1 -1
View File
@@ -40,7 +40,7 @@ void MaterialPickerWindow::RecreateButtons()
button.OnClick([entity, this](wi::gui::EventArgs args) {
wi::Archive& archive = editor->AdvanceHistory();
wi::Archive& archive = editor->AdvanceHistory(true);
archive << EditorComponent::HISTORYOP_SELECTION;
// record PREVIOUS selection state...
editor->RecordSelection(archive);
+82 -5
View File
@@ -81,6 +81,83 @@ namespace wi::helper
#endif // SDL2
}
MessageBoxResult messageBoxCustom(const std::string& msg, const std::string& caption, const std::string& buttons)
{
#ifdef PLATFORM_WINDOWS_DESKTOP
std::wstring wmsg;
std::wstring wcaption;
StringConvert(msg, wmsg);
StringConvert(caption, wcaption);
UINT type = MB_ICONQUESTION;
if (buttons == "YesNoCancel")
{
type |= MB_YESNOCANCEL;
}
else if (buttons == "YesNo")
{
type |= MB_YESNO;
}
else if (buttons == "OKCancel")
{
type |= MB_OKCANCEL;
}
else if (buttons == "AbortRetryIgnore")
{
type |= MB_ABORTRETRYIGNORE;
}
else // default to OK
{
type |= MB_OK;
}
const int result = MessageBox(GetActiveWindow(), wmsg.c_str(), wcaption.c_str(), type);
switch (result)
{
case IDOK: return MessageBoxResult::OK;
case IDCANCEL: return MessageBoxResult::Cancel;
case IDYES: return MessageBoxResult::Yes;
case IDNO: return MessageBoxResult::No;
case IDABORT: return MessageBoxResult::Abort;
case IDRETRY: return MessageBoxResult::Retry;
case IDIGNORE: return MessageBoxResult::Ignore;
default: return MessageBoxResult::Cancel;
}
#endif // PLATFORM_WINDOWS_DESKTOP
#ifdef SDL2
const SDL_MessageBoxButtonData buttons_data[] = {
{ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Yes" },
{ 0, 1, "No" },
{ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "Cancel" },
};
const SDL_MessageBoxData messageboxdata = {
SDL_MESSAGEBOX_INFORMATION,
NULL,
caption.c_str(),
msg.c_str(),
SDL_arraysize(buttons_data),
buttons_data,
NULL
};
int buttonid;
if (SDL_ShowMessageBox(&messageboxdata, &buttonid) < 0)
{
return MessageBoxResult::Cancel;
}
switch (buttonid)
{
case 0: return MessageBoxResult::Yes;
case 1: return MessageBoxResult::No;
case 2: return MessageBoxResult::Cancel;
default: return MessageBoxResult::Cancel;
}
#endif // SDL2
return MessageBoxResult::Cancel;
}
std::string screenshot(const wi::graphics::SwapChain& swapchain, const std::string& name)
{
return screenshot(wi::graphics::GetDevice()->GetBackBuffer(&swapchain));
@@ -1294,7 +1371,7 @@ namespace wi::helper
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = nullptr;
ofn.lpstrFile = szFile;
// Set lpstrFile[0] to '\0' so that GetOpenFileName does not
// Set lpstrFile[0] to '\0' so that GetOpenFileName does not
// use the contents of szFile to initialize itself.
ofn.lpstrFile[0] = '\0';
ofn.nMaxFile = sizeof(szFile);
@@ -1582,7 +1659,7 @@ namespace wi::helper
}
}
}
void StringConvert(const std::wstring& from, std::string& to)
{
to.clear();
@@ -1639,7 +1716,7 @@ namespace wi::helper
}
}
}
int StringConvert(const char* from, wchar_t* to, int dest_size_in_characters)
{
if (!from || !to || dest_size_in_characters <= 0)
@@ -1709,7 +1786,7 @@ namespace wi::helper
to[written] = 0;
return written;
}
int StringConvert(const wchar_t* from, char* to, int dest_size_in_characters)
{
if (!from || !to || dest_size_in_characters <= 0)
@@ -1817,7 +1894,7 @@ namespace wi::helper
}
#endif // _WIN32
}
void Sleep(float milliseconds)
{
std::this_thread::sleep_for(std::chrono::milliseconds((int)milliseconds));
+15
View File
@@ -45,6 +45,21 @@ namespace wi::helper
void messageBox(const std::string& msg, const std::string& caption = "Warning!");
enum class MessageBoxResult
{
OK,
Cancel,
Yes,
No,
Abort,
Retry,
Ignore
};
// Shows a message box with custom buttons and returns the user's choice
// buttons can be combinations like: "OK", "OKCancel", "YesNo", "YesNoCancel", etc.
MessageBoxResult messageBoxCustom(const std::string& msg, const std::string& caption = "Warning!", const std::string& buttons = "OK");
// Returns file path if successful, empty string otherwise
std::string screenshot(const wi::graphics::SwapChain& swapchain, const std::string& name = "");