diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 957b6d8e88a..5b22d1e4ff9 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -280,6 +280,34 @@ [/codeblock] + + + + + 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(for_scene): + if not unsaved: + return "" + + if for_scene.is_empty(): + return "Save changes in MyCustomPlugin before closing?" + 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 bcf5da5e573..d61682c7363 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) { @@ -2793,6 +2798,28 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { 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(unsaved_status); + } else { + save_confirmation->set_ok_button_text(TTR("Save & Quit")); + 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; + } + _discard_changes(); break; } @@ -3031,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; @@ -5548,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_node.h b/editor/editor_node.h index 65f85a76c98..6384c27c722 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -382,6 +382,7 @@ private: 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 5170c2fdfbe..1fe18880f70 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 &p_for_scene) const { + String ret; + GDVIRTUAL_CALL(_get_unsaved_status, p_for_scene, 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, "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 69789a4d4f6..2bcdd387ff3 100644 --- a/editor/editor_plugin.h +++ b/editor/editor_plugin.h @@ -88,6 +88,7 @@ protected: GDVIRTUAL0RC(Dictionary, _get_state) GDVIRTUAL1(_set_state, Dictionary) GDVIRTUAL0(_clear) + GDVIRTUAL1RC(String, _get_unsaved_status, String) GDVIRTUAL0(_save_external_data) GDVIRTUAL0(_apply_changes) GDVIRTUAL0RC(Vector, _get_breakpoints) @@ -175,6 +176,7 @@ public: 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 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 791937dbf60..875ad1b96d0 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 && 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,49 @@ void ScriptEditorPlugin::selected_notify() { _focus_another_editor(); } +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] = TTR("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 e879920e410..198aaa6c4ea 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -512,6 +512,7 @@ public: void get_breakpoints(List *p_breakpoints); + PackedStringArray get_unsaved_scripts() const; void save_current_script(); void save_all_scripts(); @@ -572,6 +573,7 @@ public: virtual void make_visible(bool p_visible) override; virtual void selected_notify() 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 268828e8f53..247586fbfc7 100644 --- a/editor/plugins/shader_editor_plugin.cpp +++ b/editor/plugins/shader_editor_plugin.cpp @@ -287,6 +287,27 @@ void ShaderEditorPlugin::get_window_layout(Ref p_layout) { p_layout->set_value("ShaderEditor", "selected_shader", selected_shader); } +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(TTR("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 45b48a2f91c..fb7b2832668 100644 --- a/editor/plugins/shader_editor_plugin.h +++ b/editor/plugins/shader_editor_plugin.h @@ -115,6 +115,7 @@ public: virtual void set_window_layout(Ref p_layout) override; virtual void get_window_layout(Ref p_layout) override; + virtual String get_unsaved_status(const String &p_for_scene) const override; virtual void save_external_data() override; virtual void apply_changes() override;