Browse Source

Standardize dialog input validation as a new class

kobewi 2 years ago
parent
commit
7f41403a6f

+ 12 - 28
editor/directory_create_dialog.cpp

@@ -33,10 +33,10 @@
 #include "core/io/dir_access.h"
 #include "editor/editor_node.h"
 #include "editor/editor_scale.h"
+#include "editor/gui/editor_validation_panel.h"
 #include "scene/gui/box_container.h"
 #include "scene/gui/label.h"
 #include "scene/gui/line_edit.h"
-#include "scene/gui/panel_container.h"
 
 static String sanitize_input(const String &p_path) {
 	String path = p_path.strip_edges();
@@ -73,24 +73,17 @@ String DirectoryCreateDialog::_validate_path(const String &p_path) const {
 	return String();
 }
 
-void DirectoryCreateDialog::_on_dir_path_changed(const String &p_text) {
-	const String path = sanitize_input(p_text);
+void DirectoryCreateDialog::_on_dir_path_changed() {
+	const String path = sanitize_input(dir_path->get_text());
 	const String error = _validate_path(path);
 
 	if (error.is_empty()) {
-		status_label->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
-
 		if (path.contains("/")) {
-			status_label->set_text(TTR("Using slashes in folder names will create subfolders recursively."));
-		} else {
-			status_label->set_text(TTR("Folder name is valid."));
+			validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Using slashes in folder names will create subfolders recursively."), EditorValidationPanel::MSG_OK);
 		}
 	} else {
-		status_label->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), SNAME("Editor")));
-		status_label->set_text(error);
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, error, EditorValidationPanel::MSG_ERROR);
 	}
-
-	get_ok_button()->set_disabled(!error.is_empty());
 }
 
 void DirectoryCreateDialog::ok_pressed() {
@@ -127,21 +120,13 @@ void DirectoryCreateDialog::config(const String &p_base_dir) {
 	label->set_text(vformat(TTR("Create new folder in %s:"), base_dir));
 	dir_path->set_text("new folder");
 	dir_path->select_all();
-	_on_dir_path_changed(dir_path->get_text());
+	validation_panel->update();
 }
 
 void DirectoryCreateDialog::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("dir_created"));
 }
 
-void DirectoryCreateDialog::_notification(int p_what) {
-	switch (p_what) {
-		case NOTIFICATION_THEME_CHANGED: {
-			status_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
-		} break;
-	}
-}
-
 DirectoryCreateDialog::DirectoryCreateDialog() {
 	set_title(TTR("Create Folder"));
 	set_min_size(Size2i(480, 0) * EDSCALE);
@@ -154,7 +139,6 @@ DirectoryCreateDialog::DirectoryCreateDialog() {
 	vb->add_child(label);
 
 	dir_path = memnew(LineEdit);
-	dir_path->connect("text_changed", callable_mp(this, &DirectoryCreateDialog::_on_dir_path_changed));
 	vb->add_child(dir_path);
 	register_text_enter(dir_path);
 
@@ -162,11 +146,11 @@ DirectoryCreateDialog::DirectoryCreateDialog() {
 	spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
 	vb->add_child(spacing);
 
-	status_panel = memnew(PanelContainer);
-	status_panel->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-	vb->add_child(status_panel);
+	validation_panel = memnew(EditorValidationPanel);
+	vb->add_child(validation_panel);
+	validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Folder name is valid."));
+	validation_panel->set_update_callback(callable_mp(this, &DirectoryCreateDialog::_on_dir_path_changed));
+	validation_panel->set_accept_button(get_ok_button());
 
-	status_label = memnew(Label);
-	status_label->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-	status_panel->add_child(status_label);
+	dir_path->connect("text_changed", callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
 }

+ 3 - 7
editor/directory_create_dialog.h

@@ -33,9 +33,9 @@
 
 #include "scene/gui/dialogs.h"
 
+class EditorValidationPanel;
 class Label;
 class LineEdit;
-class PanelContainer;
 
 class DirectoryCreateDialog : public ConfirmationDialog {
 	GDCLASS(DirectoryCreateDialog, ConfirmationDialog);
@@ -44,17 +44,13 @@ class DirectoryCreateDialog : public ConfirmationDialog {
 
 	Label *label = nullptr;
 	LineEdit *dir_path = nullptr;
-
-	PanelContainer *status_panel = nullptr;
-	Label *status_label = nullptr;
+	EditorValidationPanel *validation_panel = nullptr;
 
 	String _validate_path(const String &p_path) const;
-
-	void _on_dir_path_changed(const String &p_text);
+	void _on_dir_path_changed();
 
 protected:
 	static void _bind_methods();
-	void _notification(int p_what);
 
 	virtual void ok_pressed() override;
 	virtual void _post_popup() override;

+ 19 - 37
editor/editor_inspector.cpp

@@ -38,6 +38,7 @@
 #include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
 #include "editor/editor_undo_redo_manager.h"
+#include "editor/gui/editor_validation_panel.h"
 #include "editor/inspector_dock.h"
 #include "editor/plugins/script_editor_plugin.h"
 #include "multi_node_edit.h"
@@ -3927,12 +3928,6 @@ void EditorInspector::_notification(int p_what) {
 			}
 		} break;
 
-		case NOTIFICATION_THEME_CHANGED: {
-			if (add_meta_error_panel) {
-				add_meta_error_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
-			}
-		} break;
-
 		case NOTIFICATION_PREDELETE: {
 			edit(nullptr); //just in case
 		} break;
@@ -4083,27 +4078,17 @@ void EditorInspector::_add_meta_confirm() {
 	undo_redo->commit_action();
 }
 
-void EditorInspector::_check_meta_name(const String &p_name) {
-	String error;
-
-	if (p_name == "") {
-		error = TTR("Metadata name can't be empty.");
-	} else if (!p_name.is_valid_identifier()) {
-		error = TTR("Metadata name must be a valid identifier.");
-	} else if (object->has_meta(p_name)) {
-		error = vformat(TTR("Metadata with name \"%s\" already exists."), p_name);
-	} else if (p_name[0] == '_') {
-		error = TTR("Names starting with _ are reserved for editor-only metadata.");
-	}
+void EditorInspector::_check_meta_name() {
+	const String meta_name = add_meta_name->get_text();
 
-	if (error != "") {
-		add_meta_error->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), SNAME("Editor")));
-		add_meta_error->set_text(error);
-		add_meta_dialog->get_ok_button()->set_disabled(true);
-	} else {
-		add_meta_error->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
-		add_meta_error->set_text(TTR("Metadata name is valid."));
-		add_meta_dialog->get_ok_button()->set_disabled(false);
+	if (meta_name.is_empty()) {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR);
+	} else if (!meta_name.is_valid_identifier()) {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR);
+	} else if (object->has_meta(meta_name)) {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR);
+	} else if (meta_name[0] == '_') {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR);
 	}
 }
 
@@ -4143,16 +4128,13 @@ void EditorInspector::_show_add_meta_dialog() {
 		add_meta_dialog->register_text_enter(add_meta_name);
 		add_meta_dialog->connect("confirmed", callable_mp(this, &EditorInspector::_add_meta_confirm));
 
-		add_meta_error_panel = memnew(PanelContainer);
-		vbc->add_child(add_meta_error_panel);
-		if (is_inside_tree()) {
-			add_meta_error_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
-		}
-
-		add_meta_error = memnew(Label);
-		add_meta_error_panel->add_child(add_meta_error);
+		validation_panel = memnew(EditorValidationPanel);
+		vbc->add_child(validation_panel);
+		validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid."));
+		validation_panel->set_update_callback(callable_mp(this, &EditorInspector::_check_meta_name));
+		validation_panel->set_accept_button(add_meta_dialog->get_ok_button());
 
-		add_meta_name->connect("text_changed", callable_mp(this, &EditorInspector::_check_meta_name));
+		add_meta_name->connect("text_changed", callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
 	}
 
 	Node *node = Object::cast_to<Node>(object);
@@ -4164,9 +4146,9 @@ void EditorInspector::_show_add_meta_dialog() {
 	}
 
 	add_meta_dialog->popup_centered();
-	add_meta_name->set_text("");
-	_check_meta_name("");
 	add_meta_name->grab_focus();
+	add_meta_name->set_text("");
+	validation_panel->update();
 }
 
 void EditorInspector::_bind_methods() {

+ 3 - 3
editor/editor_inspector.h

@@ -39,6 +39,7 @@ class AcceptDialog;
 class Button;
 class ConfirmationDialog;
 class EditorInspector;
+class EditorValidationPanel;
 class LineEdit;
 class OptionButton;
 class PanelContainer;
@@ -543,12 +544,11 @@ class EditorInspector : public ScrollContainer {
 	ConfirmationDialog *add_meta_dialog = nullptr;
 	LineEdit *add_meta_name = nullptr;
 	OptionButton *add_meta_type = nullptr;
-	PanelContainer *add_meta_error_panel = nullptr;
-	Label *add_meta_error = nullptr;
+	EditorValidationPanel *validation_panel = nullptr;
 
 	void _add_meta_confirm();
 	void _show_add_meta_dialog();
-	void _check_meta_name(const String &p_name);
+	void _check_meta_name();
 
 protected:
 	static void _bind_methods();

+ 1 - 0
editor/editor_themes.cpp

@@ -1280,6 +1280,7 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) {
 	}
 
 	theme->set_stylebox("panel", "Tree", style_tree_bg);
+	theme->set_stylebox("panel", "EditorValidationPanel", style_tree_bg);
 
 	// Tree
 	theme->set_icon("checked", "Tree", theme->get_icon(SNAME("GuiChecked"), SNAME("EditorIcons")));

+ 134 - 0
editor/gui/editor_validation_panel.cpp

@@ -0,0 +1,134 @@
+/**************************************************************************/
+/*  editor_validation_panel.cpp                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "editor_validation_panel.h"
+
+#include "editor/editor_scale.h"
+#include "scene/gui/box_container.h"
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+
+void EditorValidationPanel::_update() {
+	for (const KeyValue<int, String> &E : valid_messages) {
+		set_message(E.key, E.value, MSG_OK);
+	}
+
+	valid = true;
+	update_callback.callv(Array());
+
+	if (accept_button) {
+		accept_button->set_disabled(!valid);
+	}
+	pending_update = false;
+}
+
+void EditorValidationPanel::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_THEME_CHANGED: {
+			theme_cache.valid_color = get_theme_color(SNAME("success_color"), SNAME("Editor"));
+			theme_cache.warning_color = get_theme_color(SNAME("warning_color"), SNAME("Editor"));
+			theme_cache.error_color = get_theme_color(SNAME("error_color"), SNAME("Editor"));
+		} break;
+	}
+}
+
+void EditorValidationPanel::add_line(int p_id, const String &p_valid_message) {
+	ERR_FAIL_COND(valid_messages.has(p_id));
+
+	Label *label = memnew(Label);
+	message_container->add_child(label);
+	label->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
+	label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
+
+	valid_messages[p_id] = p_valid_message;
+	labels[p_id] = label;
+}
+
+void EditorValidationPanel::set_accept_button(Button *p_button) {
+	accept_button = p_button;
+}
+
+void EditorValidationPanel::set_update_callback(const Callable &p_callback) {
+	update_callback = p_callback;
+}
+
+void EditorValidationPanel::update() {
+	ERR_FAIL_COND(update_callback.is_null());
+
+	if (pending_update) {
+		return;
+	}
+	pending_update = true;
+	callable_mp(this, &EditorValidationPanel::_update).call_deferred();
+}
+
+void EditorValidationPanel::set_message(int p_id, const String &p_text, MessageType p_type, bool p_auto_prefix) {
+	ERR_FAIL_COND(!valid_messages.has(p_id));
+
+	Label *label = labels[p_id];
+	if (p_text.is_empty()) {
+		label->hide();
+		return;
+	}
+	label->show();
+
+	if (p_auto_prefix) {
+		label->set_text(String(U"•  ") + p_text);
+	} else {
+		label->set_text(p_text);
+	}
+
+	switch (p_type) {
+		case MSG_OK:
+			label->add_theme_color_override(SNAME("font_color"), theme_cache.valid_color);
+			break;
+		case MSG_WARNING:
+			label->add_theme_color_override(SNAME("font_color"), theme_cache.warning_color);
+			break;
+		case MSG_ERROR:
+			label->add_theme_color_override(SNAME("font_color"), theme_cache.error_color);
+			valid = false;
+			break;
+		case MSG_INFO:
+			label->remove_theme_color_override(SNAME("font_color"));
+			break;
+	}
+}
+
+bool EditorValidationPanel::is_valid() const {
+	return valid;
+}
+
+EditorValidationPanel::EditorValidationPanel() {
+	set_v_size_flags(SIZE_EXPAND_FILL);
+
+	message_container = memnew(VBoxContainer);
+	add_child(message_container);
+}

+ 88 - 0
editor/gui/editor_validation_panel.h

@@ -0,0 +1,88 @@
+/**************************************************************************/
+/*  editor_validation_panel.h                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef EDITOR_VALIDATION_PANEL_H
+#define EDITOR_VALIDATION_PANEL_H
+
+#include "scene/gui/panel_container.h"
+
+class Button;
+class Label;
+class VBoxContainer;
+
+class EditorValidationPanel : public PanelContainer {
+	GDCLASS(EditorValidationPanel, PanelContainer);
+
+public:
+	enum MessageType {
+		MSG_OK,
+		MSG_WARNING,
+		MSG_ERROR,
+		MSG_INFO,
+	};
+
+	static const int MSG_ID_DEFAULT = 0; // Avoids hard-coding ID in dialogs with single-line validation.
+
+private:
+	VBoxContainer *message_container = nullptr;
+
+	HashMap<int, String> valid_messages;
+	HashMap<int, Label *> labels;
+
+	bool valid = false;
+	bool pending_update = false;
+
+	struct ThemeCache {
+		Color valid_color;
+		Color warning_color;
+		Color error_color;
+	} theme_cache;
+
+	void _update();
+
+	Callable update_callback;
+	Button *accept_button = nullptr;
+
+protected:
+	void _notification(int p_what);
+
+public:
+	void add_line(int p_id, const String &p_valid_message = "");
+	void set_accept_button(Button *p_button);
+	void set_update_callback(const Callable &p_callback);
+
+	void update();
+	void set_message(int p_id, const String &p_text, MessageType p_type, bool p_auto_prefix = true);
+	bool is_valid() const;
+
+	EditorValidationPanel();
+};
+
+#endif // EDITOR_VALIDATION_PANEL_H

+ 26 - 64
editor/scene_create_dialog.cpp

@@ -34,6 +34,7 @@
 #include "editor/create_dialog.h"
 #include "editor/editor_node.h"
 #include "editor/editor_scale.h"
+#include "editor/gui/editor_validation_panel.h"
 #include "scene/2d/node_2d.h"
 #include "scene/3d/node_3d.h"
 #include "scene/gui/box_container.h"
@@ -41,7 +42,6 @@
 #include "scene/gui/grid_container.h"
 #include "scene/gui/line_edit.h"
 #include "scene/gui/option_button.h"
-#include "scene/gui/panel_container.h"
 #include "scene/resources/packed_scene.h"
 
 void SceneCreateDialog::_notification(int p_what) {
@@ -53,7 +53,6 @@ void SceneCreateDialog::_notification(int p_what) {
 			node_type_3d->set_icon(get_theme_icon(SNAME("Node3D"), SNAME("EditorIcons")));
 			node_type_gui->set_icon(get_theme_icon(SNAME("Control"), SNAME("EditorIcons")));
 			node_type_other->add_theme_icon_override(SNAME("icon"), get_theme_icon(SNAME("Node"), SNAME("EditorIcons")));
-			status_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
 		} break;
 	}
 }
@@ -63,7 +62,7 @@ void SceneCreateDialog::config(const String &p_dir) {
 	root_name_edit->set_text("");
 	scene_name_edit->set_text("");
 	scene_name_edit->call_deferred(SNAME("grab_focus"));
-	update_dialog();
+	validation_panel->update();
 }
 
 void SceneCreateDialog::accept_create() {
@@ -82,40 +81,35 @@ void SceneCreateDialog::browse_types() {
 void SceneCreateDialog::on_type_picked() {
 	other_type_display->set_text(select_node_dialog->get_selected_type().get_slice(" ", 0));
 	if (node_type_other->is_pressed()) {
-		update_dialog();
+		validation_panel->update();
 	} else {
-		node_type_other->set_pressed(true); // Calls update_dialog() via group.
+		node_type_other->set_pressed(true); // Calls validation_panel->update() via group.
 	}
 }
 
 void SceneCreateDialog::update_dialog() {
 	scene_name = scene_name_edit->get_text().strip_edges();
-	update_error(file_error_label, MSG_OK, TTR("Scene name is valid."));
 
-	bool is_valid = true;
 	if (scene_name.is_empty()) {
-		update_error(file_error_label, MSG_ERROR, TTR("Scene name is empty."));
-		is_valid = false;
+		validation_panel->set_message(MSG_ID_PATH, TTR("Scene name is empty."), EditorValidationPanel::MSG_ERROR);
 	}
 
-	if (is_valid) {
+	if (validation_panel->is_valid()) {
 		if (!scene_name.ends_with(".")) {
 			scene_name += ".";
 		}
 		scene_name += scene_extension_picker->get_selected_metadata().operator String();
 	}
 
-	if (is_valid && !scene_name.is_valid_filename()) {
-		update_error(file_error_label, MSG_ERROR, TTR("File name invalid."));
-		is_valid = false;
+	if (validation_panel->is_valid() && !scene_name.is_valid_filename()) {
+		validation_panel->set_message(MSG_ID_PATH, TTR("File name invalid."), EditorValidationPanel::MSG_ERROR);
 	}
 
-	if (is_valid) {
+	if (validation_panel->is_valid()) {
 		scene_name = directory.path_join(scene_name);
 		Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
 		if (da->file_exists(scene_name)) {
-			update_error(file_error_label, MSG_ERROR, TTR("File already exists."));
-			is_valid = false;
+			validation_panel->set_message(MSG_ID_PATH, TTR("File already exists."), EditorValidationPanel::MSG_ERROR);
 		}
 	}
 
@@ -126,8 +120,6 @@ void SceneCreateDialog::update_dialog() {
 		node_type_other->set_icon(nullptr);
 	}
 
-	update_error(node_error_label, MSG_OK, TTR("Root node valid."));
-
 	root_name = root_name_edit->get_text().strip_edges();
 	if (root_name.is_empty()) {
 		root_name = scene_name_edit->get_text().strip_edges();
@@ -135,39 +127,16 @@ void SceneCreateDialog::update_dialog() {
 		if (root_name.is_empty()) {
 			root_name_edit->set_placeholder(TTR("Leave empty to derive from scene name"));
 		} else {
-			// Respect the desired root node casing from ProjectSettings and ensure it's a valid node name.
-			String adjusted_root_name = Node::adjust_name_casing(root_name);
-			root_name = adjusted_root_name.validate_node_name();
-
-			bool has_invalid_characters = root_name != adjusted_root_name;
-			if (has_invalid_characters) {
-				update_error(node_error_label, MSG_WARNING, TTR("Invalid root node name characters have been replaced."));
-			}
-
-			root_name_edit->set_placeholder(root_name);
+			// Respect the desired root node casing from ProjectSettings.
+			root_name = Node::adjust_name_casing(root_name);
+			root_name_edit->set_placeholder(root_name.validate_node_name());
 		}
 	}
 
-	if (root_name.is_empty() || root_name.validate_node_name() != root_name) {
-		update_error(node_error_label, MSG_ERROR, TTR("Invalid root node name."));
-		is_valid = false;
-	}
-
-	get_ok_button()->set_disabled(!is_valid);
-}
-
-void SceneCreateDialog::update_error(Label *p_label, MsgType p_type, const String &p_msg) {
-	p_label->set_text(String::utf8("•  ") + p_msg);
-	switch (p_type) {
-		case MSG_OK:
-			p_label->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
-			break;
-		case MSG_ERROR:
-			p_label->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), SNAME("Editor")));
-			break;
-		case MSG_WARNING:
-			p_label->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), SNAME("Editor")));
-			break;
+	if (root_name.is_empty()) {
+		validation_panel->set_message(MSG_ID_ROOT, TTR("Invalid root node name."), EditorValidationPanel::MSG_ERROR);
+	} else if (root_name != root_name.validate_node_name()) {
+		validation_panel->set_message(MSG_ID_ROOT, TTR("Invalid root node name characters have been replaced."), EditorValidationPanel::MSG_WARNING);
 	}
 }
 
@@ -268,8 +237,6 @@ SceneCreateDialog::SceneCreateDialog() {
 		select_node_button = memnew(Button);
 		hb->add_child(select_node_button);
 		select_node_button->connect("pressed", callable_mp(this, &SceneCreateDialog::browse_types));
-
-		node_type_group->connect("pressed", callable_mp(this, &SceneCreateDialog::update_dialog).unbind(1));
 	}
 
 	{
@@ -282,7 +249,6 @@ SceneCreateDialog::SceneCreateDialog() {
 		scene_name_edit = memnew(LineEdit);
 		hb->add_child(scene_name_edit);
 		scene_name_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-		scene_name_edit->connect("text_changed", callable_mp(this, &SceneCreateDialog::update_dialog).unbind(1));
 		scene_name_edit->connect("text_submitted", callable_mp(this, &SceneCreateDialog::accept_create).unbind(1));
 
 		List<String> extensions;
@@ -305,7 +271,6 @@ SceneCreateDialog::SceneCreateDialog() {
 		gc->add_child(root_name_edit);
 		root_name_edit->set_tooltip_text(TTR("When empty, the root node name is derived from the scene name based on the \"editor/naming/node_name_casing\" project setting."));
 		root_name_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-		root_name_edit->connect("text_changed", callable_mp(this, &SceneCreateDialog::update_dialog).unbind(1));
 		root_name_edit->connect("text_submitted", callable_mp(this, &SceneCreateDialog::accept_create).unbind(1));
 	}
 
@@ -313,19 +278,16 @@ SceneCreateDialog::SceneCreateDialog() {
 	main_vb->add_child(spacing);
 	spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
 
-	status_panel = memnew(PanelContainer);
-	main_vb->add_child(status_panel);
-	status_panel->set_h_size_flags(Control::SIZE_FILL);
-	status_panel->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-
-	VBoxContainer *status_vb = memnew(VBoxContainer);
-	status_panel->add_child(status_vb);
-
-	file_error_label = memnew(Label);
-	status_vb->add_child(file_error_label);
+	validation_panel = memnew(EditorValidationPanel);
+	main_vb->add_child(validation_panel);
+	validation_panel->add_line(MSG_ID_PATH, TTR("Scene name is valid."));
+	validation_panel->add_line(MSG_ID_ROOT, TTR("Root node valid."));
+	validation_panel->set_update_callback(callable_mp(this, &SceneCreateDialog::update_dialog));
+	validation_panel->set_accept_button(get_ok_button());
 
-	node_error_label = memnew(Label);
-	status_vb->add_child(node_error_label);
+	node_type_group->connect("pressed", callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
+	scene_name_edit->connect("text_changed", callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
+	root_name_edit->connect("text_changed", callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
 
 	set_title(TTR("Create New Scene"));
 	set_min_size(Size2i(400 * EDSCALE, 0));

+ 5 - 9
editor/scene_create_dialog.h

@@ -37,18 +37,17 @@ class ButtonGroup;
 class CheckBox;
 class CreateDialog;
 class EditorFileDialog;
+class EditorValidationPanel;
 class Label;
 class LineEdit;
 class OptionButton;
-class PanelContainer;
 
 class SceneCreateDialog : public ConfirmationDialog {
 	GDCLASS(SceneCreateDialog, ConfirmationDialog);
 
-	enum MsgType {
-		MSG_OK,
-		MSG_ERROR,
-		MSG_WARNING,
+	enum {
+		MSG_ID_PATH,
+		MSG_ID_ROOT,
 	};
 
 	const StringName type_meta = StringName("type");
@@ -80,15 +79,12 @@ private:
 	OptionButton *scene_extension_picker = nullptr;
 	LineEdit *root_name_edit = nullptr;
 
-	PanelContainer *status_panel = nullptr;
-	Label *file_error_label = nullptr;
-	Label *node_error_label = nullptr;
+	EditorValidationPanel *validation_panel = nullptr;
 
 	void accept_create();
 	void browse_types();
 	void on_type_picked();
 	void update_dialog();
-	void update_error(Label *p_label, MsgType p_type, const String &p_msg);
 
 protected:
 	void _notification(int p_what);

+ 43 - 104
editor/script_create_dialog.cpp

@@ -41,6 +41,7 @@
 #include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
 #include "editor/gui/editor_file_dialog.h"
+#include "editor/gui/editor_validation_panel.h"
 
 static String _get_parent_class_of_script(String p_path) {
 	if (!ResourceLoader::exists(p_path, "Script")) {
@@ -136,7 +137,6 @@ void ScriptCreateDialog::_notification(int p_what) {
 			path_button->set_icon(get_theme_icon(SNAME("Folder"), SNAME("EditorIcons")));
 			parent_browse_button->set_icon(get_theme_icon(SNAME("Folder"), SNAME("EditorIcons")));
 			parent_search_button->set_icon(get_theme_icon(SNAME("ClassList"), SNAME("EditorIcons")));
-			status_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
 		} break;
 	}
 }
@@ -295,13 +295,7 @@ String ScriptCreateDialog::_validate_path(const String &p_path, bool p_file_must
 	}
 
 	// Let ScriptLanguage do custom validation.
-	String path_error = ScriptServer::get_language(language_menu->get_selected())->validate_path(p);
-	if (!path_error.is_empty()) {
-		return path_error;
-	}
-
-	// All checks passed.
-	return "";
+	return ScriptServer::get_language(language_menu->get_selected())->validate_path(p);
 }
 
 String ScriptCreateDialog::_get_class_name() const {
@@ -314,12 +308,12 @@ String ScriptCreateDialog::_get_class_name() const {
 
 void ScriptCreateDialog::_class_name_changed(const String &p_name) {
 	is_class_name_valid = _validate_class(class_name->get_text());
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_parent_name_changed(const String &p_parent) {
 	is_parent_name_valid = _validate_parent(parent_name->get_text());
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_template_changed(int p_template) {
@@ -347,6 +341,7 @@ void ScriptCreateDialog::_template_changed(int p_template) {
 			}
 		}
 	}
+
 	// Update template label information.
 	String template_info = U"•  ";
 	template_info += TTR("Template:");
@@ -354,8 +349,7 @@ void ScriptCreateDialog::_template_changed(int p_template) {
 	if (!sinfo.description.is_empty()) {
 		template_info += " - " + sinfo.description;
 	}
-	template_info_label->set_text(template_info);
-	template_info_label->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
+	validation_panel->set_message(MSG_ID_TEMPLATE, template_info, EditorValidationPanel::MSG_INFO, false);
 }
 
 void ScriptCreateDialog::ok_pressed() {
@@ -367,7 +361,7 @@ void ScriptCreateDialog::ok_pressed() {
 
 	EditorSettings::get_singleton()->save();
 	is_new_script_created = true;
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_create_new() {
@@ -471,7 +465,7 @@ void ScriptCreateDialog::_language_changed(int l) {
 	EditorSettings::get_singleton()->set_project_metadata("script_setup", "last_selected_language", language_menu->get_item_text(language_menu->get_selected()));
 
 	_parent_name_changed(parent_name->get_text());
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_built_in_pressed() {
@@ -482,13 +476,13 @@ void ScriptCreateDialog::_built_in_pressed() {
 		is_built_in = false;
 		_path_changed(file_path->get_text());
 	}
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_use_template_pressed() {
 	is_using_templates = use_templates->is_pressed();
 	EditorSettings::get_singleton()->set_meta("script_setup_use_script_templates", is_using_templates);
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_browse_path(bool browse_parent, bool p_save) {
@@ -555,10 +549,9 @@ void ScriptCreateDialog::_path_changed(const String &p_path) {
 	is_path_valid = false;
 	is_new_script_created = true;
 
-	String path_error = _validate_path(p_path, false);
+	path_error = _validate_path(p_path, false);
 	if (!path_error.is_empty()) {
-		_msg_path_valid(false, path_error);
-		_update_dialog();
+		validation_panel->update();
 		return;
 	}
 
@@ -567,32 +560,15 @@ void ScriptCreateDialog::_path_changed(const String &p_path) {
 	String p = ProjectSettings::get_singleton()->localize_path(p_path.strip_edges());
 	if (da->file_exists(p)) {
 		is_new_script_created = false;
-		_msg_path_valid(true, TTR("File exists, it will be reused."));
 	}
 
 	is_path_valid = true;
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ScriptCreateDialog::_path_submitted(const String &p_path) {
-	ok_pressed();
-}
-
-void ScriptCreateDialog::_msg_script_valid(bool valid, const String &p_msg) {
-	error_label->set_text(String::utf8("•  ") + p_msg);
-	if (valid) {
-		error_label->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
-	} else {
-		error_label->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), SNAME("Editor")));
-	}
-}
-
-void ScriptCreateDialog::_msg_path_valid(bool valid, const String &p_msg) {
-	path_error_label->set_text(String::utf8("•  ") + p_msg);
-	if (valid) {
-		path_error_label->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
-	} else {
-		path_error_label->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), SNAME("Editor")));
+	if (!get_ok_button()->is_disabled()) {
+		ok_pressed();
 	}
 }
 
@@ -688,25 +664,25 @@ void ScriptCreateDialog::_update_template_menu() {
 void ScriptCreateDialog::_update_dialog() {
 	// "Add Script Dialog" GUI logic and script checks.
 	_update_template_menu();
-	bool script_ok = true;
 
 	// Is script path/name valid (order from top to bottom)?
 
 	if (!is_built_in && !is_path_valid) {
-		_msg_script_valid(false, TTR("Invalid path."));
-		script_ok = false;
+		validation_panel->set_message(MSG_ID_SCRIPT, TTR("Invalid path."), EditorValidationPanel::MSG_ERROR);
 	}
 	if (has_named_classes && (is_new_script_created && !is_class_name_valid)) {
-		_msg_script_valid(false, TTR("Invalid class name."));
-		script_ok = false;
+		validation_panel->set_message(MSG_ID_SCRIPT, TTR("Invalid class name."), EditorValidationPanel::MSG_ERROR);
 	}
 	if (!is_parent_name_valid && is_new_script_created) {
-		_msg_script_valid(false, TTR("Invalid inherited parent name or path."));
-		script_ok = false;
+		validation_panel->set_message(MSG_ID_SCRIPT, TTR("Invalid inherited parent name or path."), EditorValidationPanel::MSG_ERROR);
 	}
 
-	if (script_ok) {
-		_msg_script_valid(true, TTR("Script path/name is valid."));
+	if (validation_panel->is_valid() && !is_new_script_created) {
+		validation_panel->set_message(MSG_ID_SCRIPT, TTR("File exists, it will be reused."), EditorValidationPanel::MSG_OK);
+	}
+
+	if (!path_error.is_empty()) {
+		validation_panel->set_message(MSG_ID_PATH, path_error, EditorValidationPanel::MSG_ERROR);
 	}
 
 	// Does script have named classes?
@@ -752,7 +728,11 @@ void ScriptCreateDialog::_update_dialog() {
 
 	// Is Script created or loaded from existing file?
 
-	builtin_warning_label->set_visible(is_built_in);
+	if (is_built_in) {
+		validation_panel->set_message(MSG_ID_BUILT_IN, TTR("Note: Built-in scripts have some limitations and can't be edited using an external editor."), EditorValidationPanel::MSG_INFO, false);
+	} else if (_get_class_name() == parent_name->get_text()) {
+		validation_panel->set_message(MSG_ID_BUILT_IN, TTR("Warning: Having the script name be the same as a built-in type is usually not desired."), EditorValidationPanel::MSG_WARNING, false);
+	}
 
 	path_controls[0]->set_visible(!is_built_in);
 	path_controls[1]->set_visible(!is_built_in);
@@ -761,7 +741,6 @@ void ScriptCreateDialog::_update_dialog() {
 
 	// Check if the script name is the same as the parent class.
 	// This warning isn't relevant if the script is built-in.
-	script_name_warning_label->set_visible(!is_built_in && _get_class_name() == parent_name->get_text());
 
 	bool is_new_file = is_built_in || is_new_script_created;
 
@@ -774,21 +753,16 @@ void ScriptCreateDialog::_update_dialog() {
 
 	if (is_new_file) {
 		if (is_built_in) {
-			_msg_path_valid(true, TTR("Built-in script (into scene file)."));
-		}
-		if (is_new_script_created && is_path_valid) {
-			_msg_path_valid(true, TTR("Will create a new script file."));
+			validation_panel->set_message(MSG_ID_PATH, TTR("Built-in script (into scene file)."), EditorValidationPanel::MSG_OK);
 		}
 	} else {
+		template_inactive_message = TTR("Using existing script file.");
 		if (load_enabled) {
-			template_inactive_message = TTR("Using existing script file.");
 			if (is_path_valid) {
-				_msg_path_valid(true, TTR("Will load an existing script file."));
+				validation_panel->set_message(MSG_ID_PATH, TTR("Will load an existing script file."), EditorValidationPanel::MSG_OK);
 			}
 		} else {
-			template_inactive_message = TTR("Using existing script file.");
-			_msg_path_valid(false, TTR("Script file already exists."));
-			script_ok = false;
+			validation_panel->set_message(MSG_ID_PATH, TTR("Script file already exists."), EditorValidationPanel::MSG_ERROR);
 		}
 	}
 
@@ -806,18 +780,7 @@ void ScriptCreateDialog::_update_dialog() {
 		template_menu->set_disabled(true);
 		template_menu->clear();
 		template_menu->add_item(template_inactive_message);
-	}
-	template_info_label->set_visible(!template_menu->is_disabled());
-
-	get_ok_button()->set_disabled(!script_ok);
-
-	Callable entered_call = callable_mp(this, &ScriptCreateDialog::_path_submitted);
-	if (script_ok) {
-		if (!file_path->is_connected("text_submitted", entered_call)) {
-			file_path->connect("text_submitted", entered_call);
-		}
-	} else if (file_path->is_connected("text_submitted", entered_call)) {
-		file_path->disconnect("text_submitted", entered_call);
+		validation_panel->set_message(MSG_ID_TEMPLATE, "", EditorValidationPanel::MSG_INFO);
 	}
 }
 
@@ -967,47 +930,23 @@ ScriptCreateDialog::ScriptCreateDialog() {
 
 	/* Information Messages Field */
 
-	VBoxContainer *vb = memnew(VBoxContainer);
-
-	error_label = memnew(Label);
-	vb->add_child(error_label);
-
-	path_error_label = memnew(Label);
-	vb->add_child(path_error_label);
-
-	builtin_warning_label = memnew(Label);
-	builtin_warning_label->set_text(
-			TTR("Note: Built-in scripts have some limitations and can't be edited using an external editor."));
-	vb->add_child(builtin_warning_label);
-	builtin_warning_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
-	builtin_warning_label->hide();
-
-	script_name_warning_label = memnew(Label);
-	script_name_warning_label->set_text(
-			TTR("Warning: Having the script name be the same as a built-in type is usually not desired."));
-	vb->add_child(script_name_warning_label);
-	script_name_warning_label->add_theme_color_override("font_color", Color(1, 0.85, 0.4));
-	script_name_warning_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
-	script_name_warning_label->hide();
-
-	template_info_label = memnew(Label);
-	vb->add_child(template_info_label);
-	template_info_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
-
-	status_panel = memnew(PanelContainer);
-	status_panel->set_h_size_flags(Control::SIZE_FILL);
-	status_panel->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-	status_panel->add_child(vb);
+	validation_panel = memnew(EditorValidationPanel);
+	validation_panel->add_line(MSG_ID_SCRIPT, TTR("Script path/name is valid."));
+	validation_panel->add_line(MSG_ID_PATH, TTR("Will create a new script file."));
+	validation_panel->add_line(MSG_ID_BUILT_IN);
+	validation_panel->add_line(MSG_ID_TEMPLATE);
+	validation_panel->set_update_callback(callable_mp(this, &ScriptCreateDialog::_update_dialog));
+	validation_panel->set_accept_button(get_ok_button());
 
 	/* Spacing */
 
 	Control *spacing = memnew(Control);
 	spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
 
-	vb = memnew(VBoxContainer);
+	VBoxContainer *vb = memnew(VBoxContainer);
 	vb->add_child(gc);
 	vb->add_child(spacing);
-	vb->add_child(status_panel);
+	vb->add_child(validation_panel);
 	add_child(vb);
 
 	/* Language */

+ 10 - 8
editor/script_create_dialog.h

@@ -41,17 +41,20 @@
 
 class CreateDialog;
 class EditorFileDialog;
+class EditorValidationPanel;
 
 class ScriptCreateDialog : public ConfirmationDialog {
 	GDCLASS(ScriptCreateDialog, ConfirmationDialog);
 
+	enum {
+		MSG_ID_SCRIPT,
+		MSG_ID_PATH,
+		MSG_ID_BUILT_IN,
+		MSG_ID_TEMPLATE,
+	};
+
 	LineEdit *class_name = nullptr;
-	Label *error_label = nullptr;
-	Label *path_error_label = nullptr;
-	Label *builtin_warning_label = nullptr;
-	Label *script_name_warning_label = nullptr;
-	Label *template_info_label = nullptr;
-	PanelContainer *status_panel = nullptr;
+	EditorValidationPanel *validation_panel = nullptr;
 	LineEdit *parent_name = nullptr;
 	Button *parent_browse_button = nullptr;
 	Button *parent_search_button = nullptr;
@@ -67,6 +70,7 @@ class ScriptCreateDialog : public ConfirmationDialog {
 	AcceptDialog *alert = nullptr;
 	CreateDialog *select_class = nullptr;
 	bool is_browsing_parent = false;
+	String path_error;
 	String template_inactive_message;
 	String initial_bp;
 	bool is_new_script_created = true;
@@ -113,8 +117,6 @@ class ScriptCreateDialog : public ConfirmationDialog {
 	virtual void ok_pressed() override;
 	void _create_new();
 	void _load_exist();
-	void _msg_script_valid(bool valid, const String &p_msg = String());
-	void _msg_path_valid(bool valid, const String &p_msg = String());
 	void _update_template_menu();
 	void _update_dialog();
 	ScriptLanguage::ScriptTemplate _get_current_template() const;

+ 29 - 75
editor/shader_create_dialog.cpp

@@ -33,6 +33,7 @@
 #include "core/config/project_settings.h"
 #include "editor/editor_scale.h"
 #include "editor/gui/editor_file_dialog.h"
+#include "editor/gui/editor_validation_panel.h"
 #include "scene/resources/shader_include.h"
 #include "scene/resources/visual_shader.h"
 #include "servers/rendering/shader_types.h"
@@ -89,7 +90,6 @@ void ShaderCreateDialog::_update_theme() {
 	}
 
 	path_button->set_icon(get_theme_icon(SNAME("Folder"), SNAME("EditorIcons")));
-	status_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
 }
 
 void ShaderCreateDialog::_update_language_info() {
@@ -147,7 +147,7 @@ void ShaderCreateDialog::ok_pressed() {
 	}
 
 	is_new_shader_created = true;
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ShaderCreateDialog::_create_new() {
@@ -327,7 +327,7 @@ void ShaderCreateDialog::_type_changed(int p_language) {
 	}
 
 	EditorSettings::get_singleton()->set_project_metadata("shader_setup", "last_selected_language", type_menu->get_item_text(type_menu->get_selected()));
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ShaderCreateDialog::_built_in_toggled(bool p_enabled) {
@@ -337,7 +337,7 @@ void ShaderCreateDialog::_built_in_toggled(bool p_enabled) {
 	} else {
 		_path_changed(file_path->get_text());
 	}
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ShaderCreateDialog::_browse_path() {
@@ -378,10 +378,9 @@ void ShaderCreateDialog::_path_changed(const String &p_path) {
 	is_path_valid = false;
 	is_new_shader_created = true;
 
-	String path_error = _validate_path(p_path);
+	path_error = _validate_path(p_path);
 	if (!path_error.is_empty()) {
-		_msg_path_valid(false, path_error);
-		_update_dialog();
+		validation_panel->update();
 		return;
 	}
 
@@ -389,15 +388,16 @@ void ShaderCreateDialog::_path_changed(const String &p_path) {
 	String p = ProjectSettings::get_singleton()->localize_path(p_path.strip_edges());
 	if (f->file_exists(p)) {
 		is_new_shader_created = false;
-		_msg_path_valid(true, TTR("File exists, it will be reused."));
 	}
 
 	is_path_valid = true;
-	_update_dialog();
+	validation_panel->update();
 }
 
 void ShaderCreateDialog::_path_submitted(const String &p_path) {
-	ok_pressed();
+	if (!get_ok_button()->is_disabled()) {
+		ok_pressed();
+	}
 }
 
 void ShaderCreateDialog::config(const String &p_base_path, bool p_built_in_enabled, bool p_load_enabled, int p_preferred_type, int p_preferred_mode) {
@@ -490,33 +490,14 @@ String ShaderCreateDialog::_validate_path(const String &p_path) {
 	return "";
 }
 
-void ShaderCreateDialog::_msg_script_valid(bool valid, const String &p_msg) {
-	error_label->set_text(String::utf8("•  ") + p_msg);
-	if (valid) {
-		error_label->add_theme_color_override("font_color", gc->get_theme_color(SNAME("success_color"), SNAME("Editor")));
-	} else {
-		error_label->add_theme_color_override("font_color", gc->get_theme_color(SNAME("error_color"), SNAME("Editor")));
-	}
-}
-
-void ShaderCreateDialog::_msg_path_valid(bool valid, const String &p_msg) {
-	path_error_label->set_text(String::utf8("•  ") + p_msg);
-	if (valid) {
-		path_error_label->add_theme_color_override("font_color", gc->get_theme_color(SNAME("success_color"), SNAME("Editor")));
-	} else {
-		path_error_label->add_theme_color_override("font_color", gc->get_theme_color(SNAME("error_color"), SNAME("Editor")));
-	}
-}
-
 void ShaderCreateDialog::_update_dialog() {
-	bool shader_ok = true;
-
 	if (!is_built_in && !is_path_valid) {
-		_msg_script_valid(false, TTR("Invalid path."));
-		shader_ok = false;
+		validation_panel->set_message(MSG_ID_SHADER, TTR("Invalid path."), EditorValidationPanel::MSG_ERROR);
 	}
-	if (shader_ok) {
-		_msg_script_valid(true, TTR("Shader path/name is valid."));
+	if (!path_error.is_empty()) {
+		validation_panel->set_message(MSG_ID_PATH, path_error, EditorValidationPanel::MSG_ERROR);
+	} else if (validation_panel->is_valid() && !is_new_shader_created) {
+		validation_panel->set_message(MSG_ID_SHADER, TTR("File exists, it will be reused."), EditorValidationPanel::MSG_OK);
 	}
 	if (!built_in_enabled) {
 		internal->set_pressed(false);
@@ -537,37 +518,23 @@ void ShaderCreateDialog::_update_dialog() {
 
 	internal->set_disabled(!built_in_enabled);
 
-	builtin_warning_label->set_visible(is_built_in);
+	if (is_built_in) {
+		validation_panel->set_message(MSG_ID_BUILT_IN, TTR("Note: Built-in shaders can't be edited using an external editor."), EditorValidationPanel::MSG_INFO, false);
+	}
 
 	if (is_built_in) {
 		set_ok_button_text(TTR("Create"));
-		_msg_path_valid(true, TTR("Built-in shader (into scene file)."));
+		validation_panel->set_message(MSG_ID_PATH, TTR("Built-in shader (into scene file)."), EditorValidationPanel::MSG_OK);
 	} else if (is_new_shader_created) {
 		set_ok_button_text(TTR("Create"));
-		if (is_path_valid) {
-			_msg_path_valid(true, TTR("Will create a new shader file."));
-		}
 	} else if (load_enabled) {
 		set_ok_button_text(TTR("Load"));
 		if (is_path_valid) {
-			_msg_path_valid(true, TTR("Will load an existing shader file."));
+			validation_panel->set_message(MSG_ID_PATH, TTR("Will load an existing shader file."), EditorValidationPanel::MSG_OK);
 		}
 	} else {
 		set_ok_button_text(TTR("Create"));
-		_msg_path_valid(false, TTR("Shader file already exists."));
-
-		shader_ok = false;
-	}
-
-	get_ok_button()->set_disabled(!shader_ok);
-
-	Callable entered_call = callable_mp(this, &ShaderCreateDialog::_path_submitted);
-	if (shader_ok) {
-		if (!file_path->is_connected("text_submitted", entered_call)) {
-			file_path->connect("text_submitted", entered_call);
-		}
-	} else if (file_path->is_connected("text_submitted", entered_call)) {
-		file_path->disconnect("text_submitted", entered_call);
+		validation_panel->set_message(MSG_ID_PATH, TTR("Shader file already exists."), EditorValidationPanel::MSG_ERROR);
 	}
 }
 
@@ -588,35 +555,22 @@ ShaderCreateDialog::ShaderCreateDialog() {
 
 	// Error Fields.
 
-	VBoxContainer *vb = memnew(VBoxContainer);
-
-	error_label = memnew(Label);
-	vb->add_child(error_label);
-
-	path_error_label = memnew(Label);
-	vb->add_child(path_error_label);
-
-	builtin_warning_label = memnew(Label);
-	builtin_warning_label->set_text(
-			TTR("Note: Built-in shaders can't be edited using an external editor."));
-	vb->add_child(builtin_warning_label);
-	builtin_warning_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
-	builtin_warning_label->hide();
-
-	status_panel = memnew(PanelContainer);
-	status_panel->set_h_size_flags(Control::SIZE_FILL);
-	status_panel->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-	status_panel->add_child(vb);
+	validation_panel = memnew(EditorValidationPanel);
+	validation_panel->add_line(MSG_ID_SHADER, TTR("Shader path/name is valid."));
+	validation_panel->add_line(MSG_ID_PATH, TTR("Will create a new shader file."));
+	validation_panel->add_line(MSG_ID_BUILT_IN);
+	validation_panel->set_update_callback(callable_mp(this, &ShaderCreateDialog::_update_dialog));
+	validation_panel->set_accept_button(get_ok_button());
 
 	// Spacing.
 
 	Control *spacing = memnew(Control);
 	spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
 
-	vb = memnew(VBoxContainer);
+	VBoxContainer *vb = memnew(VBoxContainer);
 	vb->add_child(gc);
 	vb->add_child(spacing);
-	vb->add_child(status_panel);
+	vb->add_child(validation_panel);
 	add_child(vb);
 
 	// Type.

+ 9 - 6
editor/shader_create_dialog.h

@@ -40,10 +40,17 @@
 #include "scene/gui/panel_container.h"
 
 class EditorFileDialog;
+class EditorValidationPanel;
 
 class ShaderCreateDialog : public ConfirmationDialog {
 	GDCLASS(ShaderCreateDialog, ConfirmationDialog);
 
+	enum {
+		MSG_ID_SHADER,
+		MSG_ID_PATH,
+		MSG_ID_BUILT_IN,
+	};
+
 	struct ShaderTypeData {
 		List<String> extensions;
 		String default_extension;
@@ -53,10 +60,7 @@ class ShaderCreateDialog : public ConfirmationDialog {
 	List<ShaderTypeData> type_data;
 
 	GridContainer *gc = nullptr;
-	Label *error_label = nullptr;
-	Label *path_error_label = nullptr;
-	Label *builtin_warning_label = nullptr;
-	PanelContainer *status_panel = nullptr;
+	EditorValidationPanel *validation_panel = nullptr;
 	OptionButton *type_menu = nullptr;
 	OptionButton *mode_menu = nullptr;
 	OptionButton *template_menu = nullptr;
@@ -67,6 +71,7 @@ class ShaderCreateDialog : public ConfirmationDialog {
 	AcceptDialog *alert = nullptr;
 
 	String initial_base_path;
+	String path_error;
 	bool is_new_shader_created = true;
 	bool is_path_valid = false;
 	bool is_built_in = false;
@@ -93,8 +98,6 @@ class ShaderCreateDialog : public ConfirmationDialog {
 	virtual void ok_pressed() override;
 	void _create_new();
 	void _load_exist();
-	void _msg_script_valid(bool valid, const String &p_msg = String());
-	void _msg_path_valid(bool valid, const String &p_msg = String());
 	void _update_dialog();
 
 protected: