From 6dc5dc3479f7758d6f52038659ebefbd9ae4d3a4 Mon Sep 17 00:00:00 2001 From: kobewi Date: Sun, 16 Oct 2022 18:09:17 +0200 Subject: [PATCH 1/3] Add `_get_unsaved_status()` method to EditorPlugin --- doc/classes/EditorPlugin.xml | 16 ++++++++++++++++ editor/editor_node.cpp | 26 ++++++++++++++++++++++++++ editor/editor_node.h | 1 + editor/editor_plugin.cpp | 8 +++++++- editor/editor_plugin.h | 2 ++ 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 957b6d8e88a1..8d0a13279d17 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -280,6 +280,22 @@ [/codeblock] + + + + Override this method to provide a custom message that lists unsaved changes. The editor will call this method on exit and display it in a confirmation dialog. Return empty string if the plugin has no unsaved changes. + If the user confirms saving, [method _save_external_data] will be called, before closing the editor. + [codeblock] + func _get_unsaved_status(): + if unsaved: + return "Save changes in MyCustomPlugin before closing?" + return "" + + func _save_external_data(): + unsaved = false + [/codeblock] + + diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index bcf5da5e573c..f304850b5070 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -2777,6 +2777,11 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { case FILE_QUIT: case RUN_PROJECT_MANAGER: case RELOAD_CURRENT_PROJECT: { + if (p_confirmed && plugin_to_save) { + plugin_to_save->save_external_data(); + p_confirmed = false; + } + if (!p_confirmed) { bool save_each = EDITOR_GET("interface/editor/save_each_scene_on_quit"); if (_next_unsaved_scene(!save_each) == -1) { @@ -2792,6 +2797,27 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { save_confirmation->popup_centered(); break; } + + plugin_to_save = nullptr; + for (int i = 0; i < editor_data.get_editor_plugin_count(); i++) { + const String unsaved_status = editor_data.get_editor_plugin(i)->get_unsaved_status(); + if (!unsaved_status.is_empty()) { + if (p_option == RELOAD_CURRENT_PROJECT) { + save_confirmation->set_ok_button_text(TTR("Save & Reload")); + save_confirmation->set_text(RTR(unsaved_status)); + } else { + save_confirmation->set_ok_button_text(TTR("Save & Quit")); + save_confirmation->set_text(RTR(unsaved_status)); + } + save_confirmation->popup_centered(); + plugin_to_save = editor_data.get_editor_plugin(i); + break; + } + } + + if (plugin_to_save) { + break; + } _discard_changes(); break; diff --git a/editor/editor_node.h b/editor/editor_node.h index 65f85a76c98a..6384c27c7223 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -382,6 +382,7 @@ class EditorNode : public Node { AcceptDialog *save_accept = nullptr; EditorAbout *about = nullptr; AcceptDialog *warning = nullptr; + EditorPlugin *plugin_to_save = nullptr; int overridden_default_layout = -1; Ref default_layout; diff --git a/editor/editor_plugin.cpp b/editor/editor_plugin.cpp index 5170c2fdfbec..f2a8324332c8 100644 --- a/editor/editor_plugin.cpp +++ b/editor/editor_plugin.cpp @@ -341,7 +341,12 @@ void EditorPlugin::clear() { GDVIRTUAL_CALL(_clear); } -// if editor references external resources/scenes, save them +String EditorPlugin::get_unsaved_status() const { + String ret; + GDVIRTUAL_CALL(_get_unsaved_status, ret); + return ret; +} + void EditorPlugin::save_external_data() { GDVIRTUAL_CALL(_save_external_data); } @@ -594,6 +599,7 @@ void EditorPlugin::_bind_methods() { GDVIRTUAL_BIND(_get_state); GDVIRTUAL_BIND(_set_state, "state"); GDVIRTUAL_BIND(_clear); + GDVIRTUAL_BIND(_get_unsaved_status); GDVIRTUAL_BIND(_save_external_data); GDVIRTUAL_BIND(_apply_changes); GDVIRTUAL_BIND(_get_breakpoints); diff --git a/editor/editor_plugin.h b/editor/editor_plugin.h index 69789a4d4f62..d74064e89c7c 100644 --- a/editor/editor_plugin.h +++ b/editor/editor_plugin.h @@ -88,6 +88,7 @@ class EditorPlugin : public Node { GDVIRTUAL0RC(Dictionary, _get_state) GDVIRTUAL1(_set_state, Dictionary) GDVIRTUAL0(_clear) + GDVIRTUAL0RC(String, _get_unsaved_status) GDVIRTUAL0(_save_external_data) GDVIRTUAL0(_apply_changes) GDVIRTUAL0RC(Vector, _get_breakpoints) @@ -175,6 +176,7 @@ class EditorPlugin : public Node { virtual Dictionary get_state() const; //save editor state so it can't be reloaded when reloading scene virtual void set_state(const Dictionary &p_state); //restore editor state (likely was saved with the scene) virtual void clear(); // clear any temporary data in the editor, reset it (likely new scene or load another scene) + virtual String get_unsaved_status() const; virtual void save_external_data(); // if editor references external resources/scenes, save them virtual void apply_changes(); // if changes are pending in editor, apply them virtual void get_breakpoints(List *p_breakpoints); From 000471ee5691fa6f6a212944084d3aa434acc5b8 Mon Sep 17 00:00:00 2001 From: kobewi Date: Sun, 16 Oct 2022 19:30:38 +0200 Subject: [PATCH 2/3] Add unsaved status for script and shader editors --- editor/plugins/script_editor_plugin.cpp | 30 +++++++++++++++++++++++++ editor/plugins/script_editor_plugin.h | 2 ++ editor/plugins/shader_editor_plugin.cpp | 16 +++++++++++++ editor/plugins/shader_editor_plugin.h | 1 + 4 files changed, 49 insertions(+) diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 791937dbf607..5a65bc6c0456 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -2435,6 +2435,18 @@ bool ScriptEditor::edit(const Ref &p_resource, int p_line, int p_col, return true; } +PackedStringArray ScriptEditor::get_unsaved_scripts() const { + PackedStringArray unsaved_list; + + for (int i = 0; i < tab_container->get_tab_count(); i++) { + ScriptEditorBase *se = Object::cast_to(tab_container->get_tab_control(i)); + if (se->is_unsaved()) { + unsaved_list.append(se->get_name()); + } + } + return unsaved_list; +} + void ScriptEditor::save_current_script() { ScriptEditorBase *current = _get_current_editor(); if (!current || _test_script_times_on_disk()) { @@ -4207,6 +4219,24 @@ void ScriptEditorPlugin::selected_notify() { _focus_another_editor(); } +String ScriptEditorPlugin::get_unsaved_status() const { + const PackedStringArray unsaved_scripts = script_editor->get_unsaved_scripts(); + if (unsaved_scripts.is_empty()) { + return String(); + } + + PackedStringArray message; + message.resize(unsaved_scripts.size() + 1); + message.write[0] = "Save changes to the following script(s) before quitting?"; + + int i = 1; + for (const String &E : unsaved_scripts) { + message.write[i] = E.trim_suffix("(*)"); + i++; + } + return String("\n").join(message); +} + void ScriptEditorPlugin::save_external_data() { script_editor->save_all_scripts(); } diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h index e879920e4100..873d8c2e5b22 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -512,6 +512,7 @@ class ScriptEditor : public PanelContainer { void get_breakpoints(List *p_breakpoints); + PackedStringArray get_unsaved_scripts() const; void save_current_script(); void save_all_scripts(); @@ -572,6 +573,7 @@ class ScriptEditorPlugin : public EditorPlugin { virtual void make_visible(bool p_visible) override; virtual void selected_notify() override; + virtual String get_unsaved_status() const override; virtual void save_external_data() override; virtual void apply_changes() override; diff --git a/editor/plugins/shader_editor_plugin.cpp b/editor/plugins/shader_editor_plugin.cpp index 268828e8f532..89eb6e88a3eb 100644 --- a/editor/plugins/shader_editor_plugin.cpp +++ b/editor/plugins/shader_editor_plugin.cpp @@ -287,6 +287,22 @@ void ShaderEditorPlugin::get_window_layout(Ref p_layout) { p_layout->set_value("ShaderEditor", "selected_shader", selected_shader); } +String ShaderEditorPlugin::get_unsaved_status() const { + // TODO: This should also include visual shaders and shader includes, but save_external_data() doesn't seem to save them... + PackedStringArray unsaved_shaders; + for (uint32_t i = 0; i < edited_shaders.size(); i++) { + if (edited_shaders[i].shader_editor) { + if (edited_shaders[i].shader_editor->is_unsaved()) { + if (unsaved_shaders.is_empty()) { + unsaved_shaders.append("Save changes to the following shaders(s) before quitting?"); + } + unsaved_shaders.append(edited_shaders[i].shader_editor->get_name()); + } + } + } + return String("\n").join(unsaved_shaders); +} + void ShaderEditorPlugin::save_external_data() { for (EditedShader &edited_shader : edited_shaders) { if (edited_shader.shader_editor) { diff --git a/editor/plugins/shader_editor_plugin.h b/editor/plugins/shader_editor_plugin.h index 45b48a2f91cc..6f206d8504db 100644 --- a/editor/plugins/shader_editor_plugin.h +++ b/editor/plugins/shader_editor_plugin.h @@ -115,6 +115,7 @@ class ShaderEditorPlugin : public EditorPlugin { virtual void set_window_layout(Ref p_layout) override; virtual void get_window_layout(Ref p_layout) override; + virtual String get_unsaved_status() const override; virtual void save_external_data() override; virtual void apply_changes() override; From b883f3218895ba1992601b1721667823a99bca62 Mon Sep 17 00:00:00 2001 From: kobewi Date: Wed, 5 Apr 2023 15:45:41 +0200 Subject: [PATCH 3/3] Check for unsaved changes when closing a scene --- doc/classes/EditorPlugin.xml | 22 ++++++++--- editor/editor_node.cpp | 52 ++++++++++++++++++------- editor/editor_plugin.cpp | 6 +-- editor/editor_plugin.h | 4 +- editor/plugins/script_editor_plugin.cpp | 31 +++++++++++++-- editor/plugins/script_editor_plugin.h | 2 +- editor/plugins/shader_editor_plugin.cpp | 9 ++++- editor/plugins/shader_editor_plugin.h | 2 +- 8 files changed, 98 insertions(+), 30 deletions(-) diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 8d0a13279d17..5b22d1e4ff9f 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -282,18 +282,30 @@ + - Override this method to provide a custom message that lists unsaved changes. The editor will call this method on exit and display it in a confirmation dialog. Return empty string if the plugin has no unsaved changes. + Override this method to provide a custom message that lists unsaved changes. The editor will call this method when exiting or when closing a scene, and display the returned string in a confirmation dialog. Return empty string if the plugin has no unsaved changes. + When closing a scene, [param for_scene] is the path to the scene being closed. You can use it to handle built-in resources in that scene. If the user confirms saving, [method _save_external_data] will be called, before closing the editor. [codeblock] - func _get_unsaved_status(): - if unsaved: + func _get_unsaved_status(for_scene): + if not unsaved: + return "" + + if for_scene.is_empty(): return "Save changes in MyCustomPlugin before closing?" - return "" - + else: + return "Scene %s has changes from MyCustomPlugin. Save before closing?" % for_scene.get_file() + func _save_external_data(): unsaved = false [/codeblock] + If the plugin has no scene-specific changes, you can ignore the calls when closing scenes: + [codeblock] + func _get_unsaved_status(for_scene): + if not for_scene.is_empty(): + return "" + [/codeblock] diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index f304850b5070..d61682c7363d 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -2797,24 +2797,25 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { save_confirmation->popup_centered(); break; } - + plugin_to_save = nullptr; for (int i = 0; i < editor_data.get_editor_plugin_count(); i++) { const String unsaved_status = editor_data.get_editor_plugin(i)->get_unsaved_status(); if (!unsaved_status.is_empty()) { if (p_option == RELOAD_CURRENT_PROJECT) { save_confirmation->set_ok_button_text(TTR("Save & Reload")); - save_confirmation->set_text(RTR(unsaved_status)); + save_confirmation->set_text(unsaved_status); } else { save_confirmation->set_ok_button_text(TTR("Save & Quit")); - save_confirmation->set_text(RTR(unsaved_status)); + save_confirmation->set_text(unsaved_status); } + save_confirmation->reset_size(); save_confirmation->popup_centered(); plugin_to_save = editor_data.get_editor_plugin(i); break; } } - + if (plugin_to_save) { break; } @@ -3057,13 +3058,21 @@ int EditorNode::_next_unsaved_scene(bool p_valid_filename, int p_start) { if (!editor_data.get_edited_scene_root(i)) { continue; } + + String scene_filename = editor_data.get_edited_scene_root(i)->get_scene_file_path(); + if (p_valid_filename && scene_filename.is_empty()) { + continue; + } + bool unsaved = EditorUndoRedoManager::get_singleton()->is_history_unsaved(editor_data.get_scene_history_id(i)); if (unsaved) { - String scene_filename = editor_data.get_edited_scene_root(i)->get_scene_file_path(); - if (p_valid_filename && scene_filename.is_empty()) { - continue; - } return i; + } else { + for (int j = 0; j < editor_data.get_editor_plugin_count(); j++) { + if (!editor_data.get_editor_plugin(j)->get_unsaved_status(scene_filename).is_empty()) { + return i; + } + } } } return -1; @@ -5574,19 +5583,36 @@ void EditorNode::_scene_tab_closed(int p_tab, int p_option) { return; } - bool unsaved = EditorUndoRedoManager::get_singleton()->is_history_unsaved(editor_data.get_scene_history_id(p_tab)); - if (unsaved) { + String scene_filename = scene->get_scene_file_path(); + String unsaved_message; + + if (EditorUndoRedoManager::get_singleton()->is_history_unsaved(editor_data.get_scene_history_id(p_tab))) { + if (scene_filename.is_empty()) { + unsaved_message = TTR("This scene was never saved."); + } else { + unsaved_message = vformat(TTR("Scene \"%s\" has unsaved changes."), scene_filename); + } + } else { + // Check if any plugin has unsaved changes in that scene. + for (int i = 0; i < editor_data.get_editor_plugin_count(); i++) { + unsaved_message = editor_data.get_editor_plugin(i)->get_unsaved_status(scene_filename); + if (!unsaved_message.is_empty()) { + break; + } + } + } + + if (!unsaved_message.is_empty()) { if (get_current_tab() != p_tab) { set_current_scene(p_tab); } - String scene_filename = scene->get_scene_file_path(); if (current_menu_option == RELOAD_CURRENT_PROJECT) { save_confirmation->set_ok_button_text(TTR("Save & Reload")); - save_confirmation->set_text(vformat(TTR("Save changes to '%s' before reloading?"), !scene_filename.is_empty() ? scene_filename : "unsaved scene")); + save_confirmation->set_text(unsaved_message + "\n\n" + TTR("Save before reloading?")); } else { save_confirmation->set_ok_button_text(TTR("Save & Close")); - save_confirmation->set_text(vformat(TTR("Save changes to '%s' before closing?"), !scene_filename.is_empty() ? scene_filename : "unsaved scene")); + save_confirmation->set_text(unsaved_message + "\n\n" + TTR("Save before closing?")); } save_confirmation->reset_size(); save_confirmation->popup_centered(); diff --git a/editor/editor_plugin.cpp b/editor/editor_plugin.cpp index f2a8324332c8..1fe18880f70f 100644 --- a/editor/editor_plugin.cpp +++ b/editor/editor_plugin.cpp @@ -341,9 +341,9 @@ void EditorPlugin::clear() { GDVIRTUAL_CALL(_clear); } -String EditorPlugin::get_unsaved_status() const { +String EditorPlugin::get_unsaved_status(const String &p_for_scene) const { String ret; - GDVIRTUAL_CALL(_get_unsaved_status, ret); + GDVIRTUAL_CALL(_get_unsaved_status, p_for_scene, ret); return ret; } @@ -599,7 +599,7 @@ void EditorPlugin::_bind_methods() { GDVIRTUAL_BIND(_get_state); GDVIRTUAL_BIND(_set_state, "state"); GDVIRTUAL_BIND(_clear); - GDVIRTUAL_BIND(_get_unsaved_status); + GDVIRTUAL_BIND(_get_unsaved_status, "for_scene"); GDVIRTUAL_BIND(_save_external_data); GDVIRTUAL_BIND(_apply_changes); GDVIRTUAL_BIND(_get_breakpoints); diff --git a/editor/editor_plugin.h b/editor/editor_plugin.h index d74064e89c7c..2bcdd387ff31 100644 --- a/editor/editor_plugin.h +++ b/editor/editor_plugin.h @@ -88,7 +88,7 @@ class EditorPlugin : public Node { GDVIRTUAL0RC(Dictionary, _get_state) GDVIRTUAL1(_set_state, Dictionary) GDVIRTUAL0(_clear) - GDVIRTUAL0RC(String, _get_unsaved_status) + GDVIRTUAL1RC(String, _get_unsaved_status, String) GDVIRTUAL0(_save_external_data) GDVIRTUAL0(_apply_changes) GDVIRTUAL0RC(Vector, _get_breakpoints) @@ -176,7 +176,7 @@ class EditorPlugin : public Node { virtual Dictionary get_state() const; //save editor state so it can't be reloaded when reloading scene virtual void set_state(const Dictionary &p_state); //restore editor state (likely was saved with the scene) virtual void clear(); // clear any temporary data in the editor, reset it (likely new scene or load another scene) - virtual String get_unsaved_status() const; + virtual String get_unsaved_status(const String &p_for_scene = "") const; virtual void save_external_data(); // if editor references external resources/scenes, save them virtual void apply_changes(); // if changes are pending in editor, apply them virtual void get_breakpoints(List *p_breakpoints); diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 5a65bc6c0456..875ad1b96d02 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -2440,7 +2440,7 @@ PackedStringArray ScriptEditor::get_unsaved_scripts() const { for (int i = 0; i < tab_container->get_tab_count(); i++) { ScriptEditorBase *se = Object::cast_to(tab_container->get_tab_control(i)); - if (se->is_unsaved()) { + if (se && se->is_unsaved()) { unsaved_list.append(se->get_name()); } } @@ -4219,15 +4219,40 @@ void ScriptEditorPlugin::selected_notify() { _focus_another_editor(); } -String ScriptEditorPlugin::get_unsaved_status() const { +String ScriptEditorPlugin::get_unsaved_status(const String &p_for_scene) const { const PackedStringArray unsaved_scripts = script_editor->get_unsaved_scripts(); if (unsaved_scripts.is_empty()) { return String(); } PackedStringArray message; + if (!p_for_scene.is_empty()) { + PackedStringArray unsaved_built_in_scripts; + + const String scene_file = p_for_scene.get_file(); + for (const String &E : unsaved_scripts) { + if (!E.is_resource_file() && E.contains(scene_file)) { + unsaved_built_in_scripts.append(E); + } + } + + if (unsaved_built_in_scripts.is_empty()) { + return String(); + } else { + message.resize(unsaved_built_in_scripts.size() + 1); + message.write[0] = TTR("There are unsaved changes in the following built-in script(s):"); + + int i = 1; + for (const String &E : unsaved_built_in_scripts) { + message.write[i] = E.trim_suffix("(*)"); + i++; + } + return String("\n").join(message); + } + } + message.resize(unsaved_scripts.size() + 1); - message.write[0] = "Save changes to the following script(s) before quitting?"; + message.write[0] = TTR("Save changes to the following script(s) before quitting?"); int i = 1; for (const String &E : unsaved_scripts) { diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h index 873d8c2e5b22..198aaa6c4ea6 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -573,7 +573,7 @@ class ScriptEditorPlugin : public EditorPlugin { virtual void make_visible(bool p_visible) override; virtual void selected_notify() override; - virtual String get_unsaved_status() const override; + virtual String get_unsaved_status(const String &p_for_scene) const override; virtual void save_external_data() override; virtual void apply_changes() override; diff --git a/editor/plugins/shader_editor_plugin.cpp b/editor/plugins/shader_editor_plugin.cpp index 89eb6e88a3eb..247586fbfc72 100644 --- a/editor/plugins/shader_editor_plugin.cpp +++ b/editor/plugins/shader_editor_plugin.cpp @@ -287,14 +287,19 @@ void ShaderEditorPlugin::get_window_layout(Ref p_layout) { p_layout->set_value("ShaderEditor", "selected_shader", selected_shader); } -String ShaderEditorPlugin::get_unsaved_status() const { +String ShaderEditorPlugin::get_unsaved_status(const String &p_for_scene) const { + if (!p_for_scene.is_empty()) { + // TODO: handle built-in shaders. + return String(); + } + // TODO: This should also include visual shaders and shader includes, but save_external_data() doesn't seem to save them... PackedStringArray unsaved_shaders; for (uint32_t i = 0; i < edited_shaders.size(); i++) { if (edited_shaders[i].shader_editor) { if (edited_shaders[i].shader_editor->is_unsaved()) { if (unsaved_shaders.is_empty()) { - unsaved_shaders.append("Save changes to the following shaders(s) before quitting?"); + unsaved_shaders.append(TTR("Save changes to the following shaders(s) before quitting?")); } unsaved_shaders.append(edited_shaders[i].shader_editor->get_name()); } diff --git a/editor/plugins/shader_editor_plugin.h b/editor/plugins/shader_editor_plugin.h index 6f206d8504db..fb7b28326683 100644 --- a/editor/plugins/shader_editor_plugin.h +++ b/editor/plugins/shader_editor_plugin.h @@ -115,7 +115,7 @@ class ShaderEditorPlugin : public EditorPlugin { virtual void set_window_layout(Ref p_layout) override; virtual void get_window_layout(Ref p_layout) override; - virtual String get_unsaved_status() const override; + virtual String get_unsaved_status(const String &p_for_scene) const override; virtual void save_external_data() override; virtual void apply_changes() override;