Browse Source

Implements "Batch Rename" editor tool.

Blazej Floch 7 years ago
parent
commit
e58b10c883

+ 2 - 2
editor/editor_data.cpp

@@ -867,7 +867,7 @@ Array EditorSelection::_get_transformable_selected_nodes() {
 	return ret;
 	return ret;
 }
 }
 
 
-Array EditorSelection::_get_selected_nodes() {
+Array EditorSelection::get_selected_nodes() {
 
 
 	Array ret;
 	Array ret;
 
 
@@ -885,7 +885,7 @@ void EditorSelection::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("clear"), &EditorSelection::clear);
 	ClassDB::bind_method(D_METHOD("clear"), &EditorSelection::clear);
 	ClassDB::bind_method(D_METHOD("add_node", "node"), &EditorSelection::add_node);
 	ClassDB::bind_method(D_METHOD("add_node", "node"), &EditorSelection::add_node);
 	ClassDB::bind_method(D_METHOD("remove_node", "node"), &EditorSelection::remove_node);
 	ClassDB::bind_method(D_METHOD("remove_node", "node"), &EditorSelection::remove_node);
-	ClassDB::bind_method(D_METHOD("get_selected_nodes"), &EditorSelection::_get_selected_nodes);
+	ClassDB::bind_method(D_METHOD("get_selected_nodes"), &EditorSelection::get_selected_nodes);
 	ClassDB::bind_method(D_METHOD("get_transformable_selected_nodes"), &EditorSelection::_get_transformable_selected_nodes);
 	ClassDB::bind_method(D_METHOD("get_transformable_selected_nodes"), &EditorSelection::_get_transformable_selected_nodes);
 	ClassDB::bind_method(D_METHOD("_emit_change"), &EditorSelection::_emit_change);
 	ClassDB::bind_method(D_METHOD("_emit_change"), &EditorSelection::_emit_change);
 	ADD_SIGNAL(MethodInfo("selection_changed"));
 	ADD_SIGNAL(MethodInfo("selection_changed"));

+ 1 - 1
editor/editor_data.h

@@ -224,7 +224,6 @@ private:
 	List<Node *> selected_node_list;
 	List<Node *> selected_node_list;
 
 
 	void _update_nl();
 	void _update_nl();
-	Array _get_selected_nodes();
 	Array _get_transformable_selected_nodes();
 	Array _get_transformable_selected_nodes();
 	void _emit_change();
 	void _emit_change();
 
 
@@ -232,6 +231,7 @@ protected:
 	static void _bind_methods();
 	static void _bind_methods();
 
 
 public:
 public:
+	Array get_selected_nodes();
 	void add_node(Node *p_node);
 	void add_node(Node *p_node);
 	void remove_node(Node *p_node);
 	void remove_node(Node *p_node);
 	bool is_selected(Node *) const;
 	bool is_selected(Node *) const;

+ 691 - 0
editor/rename_dialog.cpp

@@ -0,0 +1,691 @@
+/*************************************************************************/
+/*  rename_dialog.cpp                                                    */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md)    */
+/*                                                                       */
+/* 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 "rename_dialog.h"
+
+#include "editor_node.h"
+#include "editor_settings.h"
+#include "editor_themes.h"
+#include "modules/regex/regex.h"
+#include "plugins/script_editor_plugin.h"
+#include "print_string.h"
+#include "scene/gui/control.h"
+#include "scene/gui/label.h"
+#include "scene/gui/tab_container.h"
+
+RenameDialog::RenameDialog(SceneTreeEditor *p_scene_tree_editor, UndoRedo *p_undo_redo) {
+
+	scene_tree_editor = p_scene_tree_editor;
+	undo_redo = p_undo_redo;
+	preview_node = NULL;
+
+	set_title(TTR("Batch Rename"));
+
+	VBoxContainer *vbc = memnew(VBoxContainer);
+	add_child(vbc);
+
+	// -- Search/Replace Area
+
+	GridContainer *grd_main = memnew(GridContainer);
+	grd_main->set_columns(2);
+	grd_main->set_v_size_flags(SIZE_EXPAND_FILL);
+	vbc->add_child(grd_main);
+
+	// ---- 1st & 2nd row
+
+	Label *lbl_search = memnew(Label);
+	lbl_search->set_text(TTR("Search"));
+
+	lne_search = memnew(LineEdit);
+	lne_search->set_placeholder(TTR("Search"));
+	lne_search->set_name("lne_search");
+	lne_search->set_h_size_flags(SIZE_EXPAND_FILL);
+
+	Label *lbl_replace = memnew(Label);
+	lbl_replace->set_text(TTR("Replace"));
+
+	lne_replace = memnew(LineEdit);
+	lne_replace->set_placeholder(TTR("Replace"));
+	lne_replace->set_name("lne_replace");
+	lne_replace->set_h_size_flags(SIZE_EXPAND_FILL);
+
+	grd_main->add_child(lbl_search);
+	grd_main->add_child(lbl_replace);
+	grd_main->add_child(lne_search);
+	grd_main->add_child(lne_replace);
+
+	// ---- 3rd & 4th row
+
+	Label *lbl_prefix = memnew(Label);
+	lbl_prefix->set_text(TTR("Prefix"));
+
+	lne_prefix = memnew(LineEdit);
+	lne_prefix->set_placeholder(TTR("Prefix"));
+	lne_prefix->set_name("lne_prefix");
+	lne_prefix->set_h_size_flags(SIZE_EXPAND_FILL);
+
+	Label *lbl_suffix = memnew(Label);
+	lbl_suffix->set_text(TTR("Suffix"));
+
+	lne_suffix = memnew(LineEdit);
+	lne_suffix->set_placeholder(TTR("Suffix"));
+	lne_suffix->set_name("lne_suffix");
+	lne_suffix->set_h_size_flags(SIZE_EXPAND_FILL);
+
+	grd_main->add_child(lbl_prefix);
+	grd_main->add_child(lbl_suffix);
+	grd_main->add_child(lne_prefix);
+	grd_main->add_child(lne_suffix);
+
+	// -- Feature Tabs
+
+	const int feature_min_height = 160;
+
+	Ref<Theme> collapse_theme = create_editor_theme();
+	collapse_theme->set_icon("checked", "CheckBox", collapse_theme->get_icon("GuiTreeArrowDown", "EditorIcons"));
+	collapse_theme->set_icon("unchecked", "CheckBox", collapse_theme->get_icon("GuiTreeArrowRight", "EditorIcons"));
+
+	CheckBox *chk_collapse_features = memnew(CheckBox);
+	chk_collapse_features->set_text(TTR("Advanced options"));
+	chk_collapse_features->set_theme(collapse_theme);
+	chk_collapse_features->set_focus_mode(FOCUS_NONE);
+	vbc->add_child(chk_collapse_features);
+
+	tabc_features = memnew(TabContainer);
+	tabc_features->set_tab_align(TabContainer::ALIGN_LEFT);
+	vbc->add_child(tabc_features);
+
+	// ---- Tab Substitute
+
+	VBoxContainer *vbc_substitute = memnew(VBoxContainer);
+	vbc_substitute->set_h_size_flags(SIZE_EXPAND_FILL);
+	vbc_substitute->set_custom_minimum_size(Size2(0, feature_min_height));
+
+	vbc_substitute->set_name(TTR("Substitute"));
+	tabc_features->add_child(vbc_substitute);
+
+	cbut_substitute = memnew(CheckButton);
+	cbut_substitute->set_text(TTR("Substitute"));
+	vbc_substitute->add_child(cbut_substitute);
+
+	GridContainer *grd_substitute = memnew(GridContainer);
+	grd_substitute->set_columns(3);
+	vbc_substitute->add_child(grd_substitute);
+
+	// Name
+
+	but_insert_name = memnew(Button);
+	but_insert_name->set_text("NAME");
+	but_insert_name->set_tooltip(String("${NAME}\n") + TTR("Node name"));
+	but_insert_name->set_focus_mode(FOCUS_NONE);
+	but_insert_name->connect("pressed", this, "_insert_text", make_binds("${NAME}"));
+	but_insert_name->set_h_size_flags(SIZE_EXPAND_FILL);
+	grd_substitute->add_child(but_insert_name);
+
+	// Parent
+
+	but_insert_parent = memnew(Button);
+	but_insert_parent->set_text("PARENT");
+	but_insert_parent->set_tooltip(String("${PARENT}\n") + TTR("Node's parent name, if available"));
+	but_insert_parent->set_focus_mode(FOCUS_NONE);
+	but_insert_parent->connect("pressed", this, "_insert_text", make_binds("${PARENT}"));
+	but_insert_parent->set_h_size_flags(SIZE_EXPAND_FILL);
+	grd_substitute->add_child(but_insert_parent);
+
+	// Type
+
+	but_insert_type = memnew(Button);
+	but_insert_type->set_text("TYPE");
+	but_insert_type->set_tooltip(String("${TYPE}\n") + TTR("Node type"));
+	but_insert_type->set_focus_mode(FOCUS_NONE);
+	but_insert_type->connect("pressed", this, "_insert_text", make_binds("${TYPE}"));
+	but_insert_type->set_h_size_flags(SIZE_EXPAND_FILL);
+	grd_substitute->add_child(but_insert_type);
+
+	// Scene
+
+	but_insert_scene = memnew(Button);
+	but_insert_scene->set_text("SCENE");
+	but_insert_scene->set_tooltip(String("${SCENE}\n") + TTR("Current scene name"));
+	but_insert_scene->set_focus_mode(FOCUS_NONE);
+	but_insert_scene->connect("pressed", this, "_insert_text", make_binds("${SCENE}"));
+	but_insert_scene->set_h_size_flags(SIZE_EXPAND_FILL);
+	grd_substitute->add_child(but_insert_scene);
+
+	// Root
+
+	but_insert_root = memnew(Button);
+	but_insert_root->set_text("ROOT");
+	but_insert_root->set_tooltip(String("${ROOT}\n") + TTR("Root node name"));
+	but_insert_root->set_focus_mode(FOCUS_NONE);
+	but_insert_root->connect("pressed", this, "_insert_text", make_binds("${ROOT}"));
+	but_insert_root->set_h_size_flags(SIZE_EXPAND_FILL);
+	grd_substitute->add_child(but_insert_root);
+
+	// Count
+
+	but_insert_count = memnew(Button);
+	but_insert_count->set_text("COUNTER");
+	but_insert_count->set_tooltip(String("${COUNTER}\n") + TTR("Sequential integer counter.\nCompare counter options."));
+	but_insert_count->set_focus_mode(FOCUS_NONE);
+	but_insert_count->connect("pressed", this, "_insert_text", make_binds("${COUNTER}"));
+	but_insert_count->set_h_size_flags(SIZE_EXPAND_FILL);
+	grd_substitute->add_child(but_insert_count);
+
+	chk_per_level_counter = memnew(CheckBox);
+	chk_per_level_counter->set_text(TTR("Per Level counter"));
+	chk_per_level_counter->set_tooltip(TTR("If set the counter restarts for each group of child nodes"));
+	vbc_substitute->add_child(chk_per_level_counter);
+
+	HBoxContainer *hbc_count_options = memnew(HBoxContainer);
+	vbc_substitute->add_child(hbc_count_options);
+
+	Label *lbl_count_start = memnew(Label);
+	lbl_count_start->set_text(TTR("Start"));
+	lbl_count_start->set_tooltip(TTR("Initial value for the counter"));
+	hbc_count_options->add_child(lbl_count_start);
+
+	spn_count_start = memnew(SpinBox);
+	spn_count_start->set_tooltip(TTR("Initial value for the counter"));
+	spn_count_start->set_step(1);
+	spn_count_start->set_min(0);
+	hbc_count_options->add_child(spn_count_start);
+
+	Label *lbl_count_step = memnew(Label);
+	lbl_count_step->set_text(TTR("Step"));
+	lbl_count_step->set_tooltip(TTR("Ammount by which counter is incremented for each node"));
+	hbc_count_options->add_child(lbl_count_step);
+
+	spn_count_step = memnew(SpinBox);
+	spn_count_step->set_tooltip(TTR("Ammount by which counter is incremented for each node"));
+	spn_count_step->set_step(1);
+	hbc_count_options->add_child(spn_count_step);
+
+	Label *lbl_count_padding = memnew(Label);
+	lbl_count_padding->set_text(TTR("Padding"));
+	lbl_count_padding->set_tooltip(TTR("Minium number of digits for the counter.\nMissing digits are padded with leading zeros."));
+	hbc_count_options->add_child(lbl_count_padding);
+
+	spn_count_padding = memnew(SpinBox);
+	spn_count_padding->set_tooltip(TTR("Minium number of digits for the counter.\nMissing digits are padded with leading zeros."));
+	spn_count_padding->set_step(1);
+	hbc_count_options->add_child(spn_count_padding);
+
+	// ---- Tab RegEx
+
+	VBoxContainer *vbc_regex = memnew(VBoxContainer);
+	vbc_regex->set_h_size_flags(SIZE_EXPAND_FILL);
+	vbc_regex->set_name(TTR("Regular Expressions"));
+	vbc_regex->set_custom_minimum_size(Size2(0, feature_min_height));
+	tabc_features->add_child(vbc_regex);
+
+	cbut_regex = memnew(CheckButton);
+	cbut_regex->set_text(TTR("Regular Expressions"));
+	vbc_regex->add_child(cbut_regex);
+
+	// ---- Tab Process
+
+	VBoxContainer *vbc_process = memnew(VBoxContainer);
+	vbc_process->set_h_size_flags(SIZE_EXPAND_FILL);
+	vbc_process->set_name(TTR("Post-Process"));
+	vbc_process->set_custom_minimum_size(Size2(0, feature_min_height));
+	tabc_features->add_child(vbc_process);
+
+	cbut_process = memnew(CheckButton);
+	cbut_process->set_text(TTR("Post-Process"));
+	vbc_process->add_child(cbut_process);
+
+	// ------ Style
+
+	HBoxContainer *hbc_style = memnew(HBoxContainer);
+	vbc_process->add_child(hbc_style);
+
+	Label *lbl_style = memnew(Label);
+	lbl_style->set_text(TTR("Style"));
+	hbc_style->add_child(lbl_style);
+
+	opt_style = memnew(OptionButton);
+	opt_style->add_item(TTR("Keep"));
+	opt_style->add_item(TTR("CamelCase to under_scored"));
+	opt_style->add_item(TTR("under_scored to CamelCase"));
+	hbc_style->add_child(opt_style);
+
+	// ------ Case
+
+	HBoxContainer *hbc_case = memnew(HBoxContainer);
+	vbc_process->add_child(hbc_case);
+
+	Label *lbl_case = memnew(Label);
+	lbl_case->set_text(TTR("Case"));
+	hbc_case->add_child(lbl_case);
+
+	opt_case = memnew(OptionButton);
+	opt_case->add_item(TTR("Keep"));
+	opt_case->add_item(TTR("To Lowercase"));
+	opt_case->add_item(TTR("To Uppercase"));
+	hbc_case->add_child(opt_case);
+
+	// -- Preview
+
+	HSeparator *sep_preview = memnew(HSeparator);
+	sep_preview->set_custom_minimum_size(Size2(10, 20));
+	vbc->add_child(sep_preview);
+
+	lbl_preview_title = memnew(Label);
+	lbl_preview_title->set_text(TTR("Preview"));
+	vbc->add_child(lbl_preview_title);
+
+	lbl_preview = memnew(Label);
+	lbl_preview->set_text("");
+	lbl_preview->add_color_override("font_color", Color(1, 0.5f, 0, 1));
+	vbc->add_child(lbl_preview);
+
+	// ---- Dialog related
+
+	set_custom_minimum_size(Size2(383, 0));
+	set_as_toplevel(true);
+	get_ok()->set_text(TTR("Rename"));
+	Button *but_reset = add_button(TTR("Reset"));
+
+	eh.errfunc = _error_handler;
+	eh.userdata = this;
+
+	// ---- Connections
+
+	chk_collapse_features->connect("toggled", this, "_features_toggled");
+
+	// Substitite Buttons
+
+	lne_search->connect("focus_entered", this, "_update_substitute");
+	lne_search->connect("focus_exited", this, "_update_substitute");
+	lne_replace->connect("focus_entered", this, "_update_substitute");
+	lne_replace->connect("focus_exited", this, "_update_substitute");
+	lne_prefix->connect("focus_entered", this, "_update_substitute");
+	lne_prefix->connect("focus_exited", this, "_update_substitute");
+	lne_suffix->connect("focus_entered", this, "_update_substitute");
+	lne_suffix->connect("focus_exited", this, "_update_substitute");
+
+	// Preview
+
+	lne_prefix->connect("text_changed", this, "_update_preview");
+	lne_suffix->connect("text_changed", this, "_update_preview");
+	lne_search->connect("text_changed", this, "_update_preview");
+	lne_replace->connect("text_changed", this, "_update_preview");
+	spn_count_start->connect("value_changed", this, "_update_preview_int");
+	spn_count_step->connect("value_changed", this, "_update_preview_int");
+	spn_count_padding->connect("value_changed", this, "_update_preview_int");
+	opt_style->connect("item_selected", this, "_update_preview_int");
+	opt_case->connect("item_selected", this, "_update_preview_int");
+	cbut_substitute->connect("pressed", this, "_update_preview", varray(""));
+	cbut_regex->connect("pressed", this, "_update_preview", varray(""));
+	cbut_process->connect("pressed", this, "_update_preview", varray(""));
+
+	but_reset->connect("pressed", this, "reset");
+
+	reset();
+	_features_toggled(false);
+}
+
+void RenameDialog::_bind_methods() {
+
+	ClassDB::bind_method("_features_toggled", &RenameDialog::_features_toggled);
+	ClassDB::bind_method("_update_preview", &RenameDialog::_update_preview);
+	ClassDB::bind_method("_update_preview_int", &RenameDialog::_update_preview_int);
+	ClassDB::bind_method("_insert_text", &RenameDialog::_insert_text);
+	ClassDB::bind_method("_update_substitute", &RenameDialog::_update_substitute);
+	ClassDB::bind_method("reset", &RenameDialog::reset);
+	ClassDB::bind_method("rename", &RenameDialog::rename);
+}
+
+void RenameDialog::_update_substitute() {
+
+	LineEdit *focus_owner_line_edit = Object::cast_to<LineEdit>(get_focus_owner());
+	bool is_main_field = _is_main_field(focus_owner_line_edit);
+
+	but_insert_name->set_disabled(!is_main_field);
+	but_insert_parent->set_disabled(!is_main_field);
+	but_insert_type->set_disabled(!is_main_field);
+	but_insert_scene->set_disabled(!is_main_field);
+	but_insert_root->set_disabled(!is_main_field);
+	but_insert_count->set_disabled(!is_main_field);
+
+	// The focus mode seems to be reset when disabling/re-enabling
+	but_insert_name->set_focus_mode(FOCUS_NONE);
+	but_insert_parent->set_focus_mode(FOCUS_NONE);
+	but_insert_type->set_focus_mode(FOCUS_NONE);
+	but_insert_scene->set_focus_mode(FOCUS_NONE);
+	but_insert_root->set_focus_mode(FOCUS_NONE);
+	but_insert_count->set_focus_mode(FOCUS_NONE);
+}
+
+void RenameDialog::_post_popup() {
+
+	EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();
+	preview_node = NULL;
+
+	Array selected_node_list = editor_selection->get_selected_nodes();
+	ERR_FAIL_COND(selected_node_list.size() == 0);
+
+	preview_node = selected_node_list[0];
+
+	_update_preview();
+	_update_substitute();
+}
+
+void RenameDialog::_update_preview_int(int new_value) {
+	_update_preview();
+}
+
+void RenameDialog::_update_preview(String new_text) {
+
+	if (lock_preview_update || preview_node == NULL)
+		return;
+
+	has_errors = false;
+	add_error_handler(&eh);
+
+	String new_name = _apply_rename(preview_node, spn_count_start->get_value());
+
+	if (!has_errors) {
+
+		lbl_preview_title->set_text(TTR("Preview"));
+		lbl_preview->set_text(new_name);
+
+		if (new_name == preview_node->get_name()) {
+			lbl_preview->add_color_override("font_color", Color(0, 0.5f, 0.25f, 1));
+		} else {
+			lbl_preview->add_color_override("font_color", Color(0, 1, 0.5f, 1));
+		}
+	}
+
+	remove_error_handler(&eh);
+}
+
+String RenameDialog::_apply_rename(const Node *node, int count) {
+
+	String search = lne_search->get_text();
+	String replace = lne_replace->get_text();
+	String prefix = lne_prefix->get_text();
+	String suffix = lne_suffix->get_text();
+	String new_name = node->get_name();
+
+	if (cbut_substitute->is_pressed()) {
+		search = _substitute(search, node, count);
+		replace = _substitute(replace, node, count);
+		prefix = _substitute(prefix, node, count);
+		suffix = _substitute(suffix, node, count);
+	}
+
+	if (cbut_regex->is_pressed()) {
+
+		new_name = _regex(search, new_name, replace);
+	} else {
+		new_name = new_name.replace(search, replace);
+	}
+
+	new_name = prefix + new_name + suffix;
+
+	if (cbut_process->is_pressed()) {
+		new_name = _postprocess(new_name);
+	}
+
+	return new_name;
+}
+
+String RenameDialog::_substitute(const String &subject, const Node *node, int count) {
+
+	String result = subject.replace("${COUNTER}", vformat("%0" + itos(spn_count_padding->get_value()) + "d", count));
+
+	if (node) {
+		result = result.replace("${NAME}", node->get_name());
+		result = result.replace("${TYPE}", node->get_class());
+	}
+
+	int current = EditorNode::get_singleton()->get_editor_data().get_edited_scene();
+	result = result.replace("${SCENE}", EditorNode::get_singleton()->get_editor_data().get_scene_title(current));
+
+	Node *root_node = SceneTree::get_singleton()->get_edited_scene_root();
+	if (root_node) {
+		result = result.replace("${ROOT}", root_node->get_name());
+	}
+
+	Node *parent_node = node->get_parent();
+	if (parent_node) {
+		if (node == root_node) {
+			// Can not substitute parent of root.
+			result = result.replace("${PARENT}", "");
+		} else {
+			result = result.replace("${PARENT}", parent_node->get_name());
+		}
+	}
+
+	return result;
+}
+
+void RenameDialog::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, ErrorHandlerType p_type) {
+
+	RenameDialog *self = (RenameDialog *)p_self;
+	String source_file(p_file);
+
+	// Only show first error that is related to "regex"
+	if (self->has_errors || source_file.find("regex") < 0)
+		return;
+
+	String err_str;
+	if (p_errorexp && p_errorexp[0]) {
+		err_str = p_errorexp;
+	} else {
+		err_str = p_error;
+	}
+
+	self->has_errors = true;
+	self->lbl_preview_title->set_text(TTR("Error"));
+	self->lbl_preview->add_color_override("font_color", Color(1, 0.25f, 0, 1));
+	self->lbl_preview->set_text(err_str);
+}
+
+String RenameDialog::_regex(const String &pattern, const String &subject, const String &replacement) {
+
+	RegEx regex(pattern);
+
+	return regex.sub(subject, replacement, true);
+}
+
+String RenameDialog::_postprocess(const String &subject) {
+
+	int style_id = opt_style->get_selected();
+
+	String result = subject;
+
+	if (style_id == 1) {
+
+		// CamelCase to Under_Line
+		result = result.camelcase_to_underscore(true);
+		result = _regex("_+", result, "_");
+
+	} else if (style_id == 2) {
+
+		// Under_Line to CamelCase
+		RegEx pattern("_+(.?)");
+		Array matches = pattern.search_all(result);
+
+		// _ name would become empty. Ignore
+		if (matches.size() && result != "_") {
+			String buffer;
+			int start = 0;
+			int end = 0;
+			for (int i = 0; i < matches.size(); ++i) {
+				start = ((Ref<RegExMatch>)matches[i])->get_start(1);
+				buffer += result.substr(end, start - end - 1);
+				buffer += result.substr(start, 1).to_upper();
+				end = start + 1;
+			}
+			buffer += result.substr(end, result.size() - (end + 1));
+			result = buffer.replace("_", "").capitalize();
+		}
+	}
+
+	int case_id = opt_case->get_selected();
+
+	if (case_id == 1) {
+		// To Lowercase
+		result = result.to_lower();
+	} else if (case_id == 2) {
+		// To Upercase
+		result = result.to_upper();
+	}
+
+	return result;
+}
+
+void RenameDialog::_iterate_scene(const Node *node, const Array &selection, int *counter) {
+
+	if (!node)
+		return;
+
+	if (selection.has(node)) {
+
+		String new_name = _apply_rename(node, *counter);
+
+		if (node->get_name() != new_name) {
+			Pair<NodePath, String> rename_item;
+			rename_item.first = node->get_path();
+			rename_item.second = new_name;
+			to_rename.push_back(rename_item);
+		}
+
+		*counter += spn_count_step->get_value();
+	}
+
+	int *cur_counter = counter;
+	int level_counter = spn_count_start->get_value();
+
+	if (chk_per_level_counter->is_pressed()) {
+		cur_counter = &level_counter;
+	}
+
+	for (int i = 0; i < node->get_child_count(); ++i) {
+		_iterate_scene(node->get_child(i), selection, cur_counter);
+	}
+}
+
+void RenameDialog::rename() {
+
+	// Editor selection is not ordered via scene tree. Instead iterate
+	// over scene tree until all selected nodes are found in order.
+
+	EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();
+	Array selected_node_list = editor_selection->get_selected_nodes();
+	Node *root_node = SceneTree::get_singleton()->get_edited_scene_root();
+
+	global_count = spn_count_start->get_value();
+	to_rename.clear();
+
+	// Forward recursive as opposed to the actual renaming.
+	_iterate_scene(root_node, selected_node_list, &global_count);
+
+	if (undo_redo && !to_rename.empty()) {
+
+		undo_redo->create_action(TTR("Batch Rename"));
+
+		// Make sure to iterate reversed so that child nodes will find parents.
+		for (int i = to_rename.size() - 1; i >= 0; --i) {
+
+			Node *n = root_node->get_node(to_rename[i].first);
+			const String &new_name = to_rename[i].second;
+
+			if (!n) {
+				ERR_PRINTS("Skipping missing node: " + to_rename[i].first.get_concatenated_subnames());
+				continue;
+			}
+
+			scene_tree_editor->emit_signal("node_prerename", n, new_name);
+			undo_redo->add_do_method(scene_tree_editor, "_rename_node", n->get_instance_id(), new_name);
+			undo_redo->add_undo_method(scene_tree_editor, "_rename_node", n->get_instance_id(), n->get_name());
+		}
+
+		undo_redo->commit_action();
+	}
+}
+
+void RenameDialog::reset() {
+
+	lock_preview_update = true;
+
+	lne_prefix->clear();
+	lne_suffix->clear();
+	lne_search->clear();
+	lne_replace->clear();
+
+	cbut_substitute->set_pressed(false);
+	cbut_regex->set_pressed(false);
+	cbut_process->set_pressed(false);
+
+	chk_per_level_counter->set_pressed(true);
+
+	spn_count_start->set_value(1);
+	spn_count_step->set_value(1);
+	spn_count_padding->set_value(1);
+
+	opt_style->select(0);
+	opt_case->select(0);
+
+	lock_preview_update = false;
+	_update_preview();
+}
+
+bool RenameDialog::_is_main_field(LineEdit *line_edit) {
+	return line_edit &&
+		   (line_edit == lne_search || line_edit == lne_replace || line_edit == lne_prefix || line_edit == lne_suffix);
+}
+
+void RenameDialog::_insert_text(String text) {
+
+	LineEdit *focus_owner = Object::cast_to<LineEdit>(get_focus_owner());
+
+	if (_is_main_field(focus_owner)) {
+		focus_owner->selection_delete();
+		focus_owner->append_at_cursor(text);
+		_update_preview();
+	}
+}
+
+void RenameDialog::_features_toggled(bool pressed) {
+	if (pressed) {
+		tabc_features->show();
+	} else {
+		tabc_features->hide();
+	}
+
+	// Adjust to minimum size in y
+	Size2i size = get_size();
+	size.y = 0;
+	set_size(size);
+}

+ 119 - 0
editor/rename_dialog.h

@@ -0,0 +1,119 @@
+/*************************************************************************/
+/*  rename_dialog.h                                                      */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md)    */
+/*                                                                       */
+/* 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 RENAME_DIALOG_H
+#define RENAME_DIALOG_H
+
+#include "scene/gui/check_box.h"
+#include "scene/gui/check_button.h"
+#include "scene/gui/dialogs.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/spin_box.h"
+
+#include "editor/scene_tree_editor.h"
+#include "undo_redo.h"
+
+/**
+@author Blazej Floch
+*/
+
+class RenameDialog : public ConfirmationDialog {
+
+	GDCLASS(RenameDialog, ConfirmationDialog);
+
+	virtual void ok_pressed() { rename(); };
+	void _cancel_pressed(){};
+	void _features_toggled(bool pressed);
+	void _insert_text(String text);
+	void _update_substitute();
+	bool _is_main_field(LineEdit *line_edit);
+
+	void _iterate_scene(const Node *node, const Array &selection, int *count);
+	String _apply_rename(const Node *node, int count);
+	String _substitute(const String &subject, const Node *node, int count);
+	String _regex(const String &pattern, const String &subject, const String &replacement);
+	String _postprocess(const String &subject);
+	void _update_preview(String new_text = "");
+	void _update_preview_int(int new_value = 0);
+	static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, ErrorHandlerType p_type);
+
+	SceneTreeEditor *scene_tree_editor;
+	UndoRedo *undo_redo;
+	int global_count;
+
+	LineEdit *lne_search;
+	LineEdit *lne_replace;
+	LineEdit *lne_prefix;
+	LineEdit *lne_suffix;
+
+	TabContainer *tabc_features;
+
+	CheckButton *cbut_substitute;
+	CheckButton *cbut_regex;
+	CheckButton *cbut_process;
+	CheckBox *chk_per_level_counter;
+
+	Button *but_insert_name;
+	Button *but_insert_parent;
+	Button *but_insert_type;
+	Button *but_insert_scene;
+	Button *but_insert_root;
+	Button *but_insert_count;
+
+	SpinBox *spn_count_start;
+	SpinBox *spn_count_step;
+	SpinBox *spn_count_padding;
+
+	OptionButton *opt_style;
+	OptionButton *opt_case;
+
+	Label *lbl_preview_title;
+	Label *lbl_preview;
+
+	List<Pair<NodePath, String> > to_rename;
+	Node *preview_node;
+	bool lock_preview_update;
+	ErrorHandlerList eh;
+	bool has_errors;
+
+protected:
+	void _notification(int p_what){};
+	static void _bind_methods();
+	virtual void _post_popup();
+
+public:
+	void reset();
+	void rename();
+
+	RenameDialog(SceneTreeEditor *p_scene_tree_editor, UndoRedo *p_undo_redo = NULL);
+	~RenameDialog(){};
+};
+
+#endif

+ 14 - 1
editor/scene_tree_dock.cpp

@@ -70,7 +70,9 @@ void SceneTreeDock::_unhandled_key_input(Ref<InputEvent> p_event) {
 	if (!p_event->is_pressed() || p_event->is_echo())
 	if (!p_event->is_pressed() || p_event->is_echo())
 		return;
 		return;
 
 
-	if (ED_IS_SHORTCUT("scene_tree/add_child_node", p_event)) {
+	if (ED_IS_SHORTCUT("scene_tree/batch_rename", p_event)) {
+		_tool_selected(TOOL_BATCH_RENAME);
+	} else if (ED_IS_SHORTCUT("scene_tree/add_child_node", p_event)) {
 		_tool_selected(TOOL_NEW);
 		_tool_selected(TOOL_NEW);
 	} else if (ED_IS_SHORTCUT("scene_tree/instance_scene", p_event)) {
 	} else if (ED_IS_SHORTCUT("scene_tree/instance_scene", p_event)) {
 		_tool_selected(TOOL_INSTANCE);
 		_tool_selected(TOOL_INSTANCE);
@@ -277,6 +279,12 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) {
 
 
 	switch (p_tool) {
 	switch (p_tool) {
 
 
+		case TOOL_BATCH_RENAME: {
+			Tree *tree = scene_tree->get_scene_tree();
+			if (tree->is_anything_selected()) {
+				rename_dialog->popup_centered();
+			}
+		} break;
 		case TOOL_NEW: {
 		case TOOL_NEW: {
 
 
 			String preferred = "";
 			String preferred = "";
@@ -1745,6 +1753,7 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) {
 
 
 	menu->clear();
 	menu->clear();
 
 
+	menu->add_icon_shortcut(get_icon("Rename", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/batch_rename"), TOOL_BATCH_RENAME);
 	if (selection.size() == 1) {
 	if (selection.size() == 1) {
 
 
 		subresources.clear();
 		subresources.clear();
@@ -1934,6 +1943,7 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel
 	filter_hbc->add_constant_override("separate", 0);
 	filter_hbc->add_constant_override("separate", 0);
 	ToolButton *tb;
 	ToolButton *tb;
 
 
+	ED_SHORTCUT("scene_tree/batch_rename", TTR("Batch Rename"), KEY_MASK_CMD | KEY_F2);
 	ED_SHORTCUT("scene_tree/add_child_node", TTR("Add Child Node"), KEY_MASK_CMD | KEY_A);
 	ED_SHORTCUT("scene_tree/add_child_node", TTR("Add Child Node"), KEY_MASK_CMD | KEY_A);
 	ED_SHORTCUT("scene_tree/instance_scene", TTR("Instance Child Scene"));
 	ED_SHORTCUT("scene_tree/instance_scene", TTR("Instance Child Scene"));
 	ED_SHORTCUT("scene_tree/change_node_type", TTR("Change Type"));
 	ED_SHORTCUT("scene_tree/change_node_type", TTR("Change Type"));
@@ -2032,6 +2042,9 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel
 	add_child(create_dialog);
 	add_child(create_dialog);
 	create_dialog->connect("create", this, "_create");
 	create_dialog->connect("create", this, "_create");
 
 
+	rename_dialog = memnew(RenameDialog(scene_tree, &editor_data->get_undo_redo()));
+	add_child(rename_dialog);
+
 	script_create_dialog = memnew(ScriptCreateDialog);
 	script_create_dialog = memnew(ScriptCreateDialog);
 	add_child(script_create_dialog);
 	add_child(script_create_dialog);
 	script_create_dialog->connect("script_created", this, "_script_created");
 	script_create_dialog->connect("script_created", this, "_script_created");

+ 3 - 0
editor/scene_tree_dock.h

@@ -36,6 +36,7 @@
 #include "editor/editor_data.h"
 #include "editor/editor_data.h"
 #include "editor/editor_sub_scene.h"
 #include "editor/editor_sub_scene.h"
 #include "editor/groups_editor.h"
 #include "editor/groups_editor.h"
+#include "editor/rename_dialog.h"
 #include "editor/reparent_dialog.h"
 #include "editor/reparent_dialog.h"
 #include "editor/script_create_dialog.h"
 #include "editor/script_create_dialog.h"
 #include "scene/animation/animation_player.h"
 #include "scene/animation/animation_player.h"
@@ -58,6 +59,7 @@ class SceneTreeDock : public VBoxContainer {
 
 
 		TOOL_NEW,
 		TOOL_NEW,
 		TOOL_INSTANCE,
 		TOOL_INSTANCE,
+		TOOL_BATCH_RENAME,
 		TOOL_REPLACE,
 		TOOL_REPLACE,
 		TOOL_ATTACH_SCRIPT,
 		TOOL_ATTACH_SCRIPT,
 		TOOL_CLEAR_SCRIPT,
 		TOOL_CLEAR_SCRIPT,
@@ -90,6 +92,7 @@ class SceneTreeDock : public VBoxContainer {
 
 
 	int current_option;
 	int current_option;
 	CreateDialog *create_dialog;
 	CreateDialog *create_dialog;
+	RenameDialog *rename_dialog;
 
 
 	ToolButton *button_add;
 	ToolButton *button_add;
 	ToolButton *button_instance;
 	ToolButton *button_instance;

+ 1 - 1
scene/gui/line_edit.h

@@ -121,7 +121,6 @@ private:
 	void shift_selection_check_post(bool);
 	void shift_selection_check_post(bool);
 
 
 	void selection_fill_at_cursor();
 	void selection_fill_at_cursor();
-	void selection_delete();
 	void set_window_pos(int p_pos);
 	void set_window_pos(int p_pos);
 
 
 	void set_cursor_at_pixel_pos(int p_x);
 	void set_cursor_at_pixel_pos(int p_x);
@@ -157,6 +156,7 @@ public:
 
 
 	void select(int p_from = 0, int p_to = -1);
 	void select(int p_from = 0, int p_to = -1);
 	void select_all();
 	void select_all();
+	void selection_delete();
 	void deselect();
 	void deselect();
 
 
 	void delete_char();
 	void delete_char();