/*************************************************************************/ /* rename_dialog.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2022 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 "modules/modules_enabled.gen.h" // For regex. #ifdef MODULE_REGEX_ENABLED #include "core/string/print_string.h" #include "editor/editor_node.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_themes.h" #include "editor/editor_undo_redo_manager.h" #include "modules/regex/regex.h" #include "plugins/script_editor_plugin.h" #include "scene/gui/control.h" #include "scene/gui/label.h" #include "scene/gui/separator.h" #include "scene/gui/tab_container.h" RenameDialog::RenameDialog(SceneTreeEditor *p_scene_tree_editor, Ref<EditorUndoRedoManager> p_undo_redo) { scene_tree_editor = p_scene_tree_editor; undo_redo = p_undo_redo; preview_node = nullptr; 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(Control::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_name("lne_search"); lne_search->set_h_size_flags(Control::SIZE_EXPAND_FILL); Label *lbl_replace = memnew(Label); lbl_replace->set_text(TTR("Replace:")); lne_replace = memnew(LineEdit); lne_replace->set_name("lne_replace"); lne_replace->set_h_size_flags(Control::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_name("lne_prefix"); lne_prefix->set_h_size_flags(Control::SIZE_EXPAND_FILL); Label *lbl_suffix = memnew(Label); lbl_suffix->set_text(TTR("Suffix:")); lne_suffix = memnew(LineEdit); lne_suffix->set_name("lne_suffix"); lne_suffix->set_h_size_flags(Control::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 cbut_regex = memnew(CheckButton); cbut_regex->set_text(TTR("Use Regular Expressions")); vbc->add_child(cbut_regex); CheckButton *cbut_collapse_features = memnew(CheckButton); cbut_collapse_features->set_text(TTR("Advanced Options")); vbc->add_child(cbut_collapse_features); tabc_features = memnew(TabContainer); tabc_features->set_use_hidden_tabs_for_min_size(true); vbc->add_child(tabc_features); // ---- Tab Substitute VBoxContainer *vbc_substitute = memnew(VBoxContainer); vbc_substitute->set_h_size_flags(Control::SIZE_EXPAND_FILL); vbc_substitute->set_name(TTR("Substitute")); tabc_features->add_child(vbc_substitute); cbut_substitute = memnew(CheckBox); 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_text(String("${NAME}\n") + TTR("Node name.")); but_insert_name->set_focus_mode(Control::FOCUS_NONE); but_insert_name->connect("pressed", callable_mp(this, &RenameDialog::_insert_text).bind("${NAME}")); but_insert_name->set_h_size_flags(Control::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_text(String("${PARENT}\n") + TTR("Node's parent name, if available.")); but_insert_parent->set_focus_mode(Control::FOCUS_NONE); but_insert_parent->connect("pressed", callable_mp(this, &RenameDialog::_insert_text).bind("${PARENT}")); but_insert_parent->set_h_size_flags(Control::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_text(String("${TYPE}\n") + TTR("Node type.")); but_insert_type->set_focus_mode(Control::FOCUS_NONE); but_insert_type->connect("pressed", callable_mp(this, &RenameDialog::_insert_text).bind("${TYPE}")); but_insert_type->set_h_size_flags(Control::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_text(String("${SCENE}\n") + TTR("Current scene name.")); but_insert_scene->set_focus_mode(Control::FOCUS_NONE); but_insert_scene->connect("pressed", callable_mp(this, &RenameDialog::_insert_text).bind("${SCENE}")); but_insert_scene->set_h_size_flags(Control::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_text(String("${ROOT}\n") + TTR("Root node name.")); but_insert_root->set_focus_mode(Control::FOCUS_NONE); but_insert_root->connect("pressed", callable_mp(this, &RenameDialog::_insert_text).bind("${ROOT}")); but_insert_root->set_h_size_flags(Control::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_text(String("${COUNTER}\n") + TTR("Sequential integer counter.\nCompare counter options.")); but_insert_count->set_focus_mode(Control::FOCUS_NONE); but_insert_count->connect("pressed", callable_mp(this, &RenameDialog::_insert_text).bind("${COUNTER}")); but_insert_count->set_h_size_flags(Control::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_text(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_text(TTR("Initial value for the counter.")); hbc_count_options->add_child(lbl_count_start); spn_count_start = memnew(SpinBox); spn_count_start->set_tooltip_text(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_text(TTR("Amount 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_text(TTR("Amount 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_text(TTR("Minimum 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_text(TTR("Minimum 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 Process VBoxContainer *vbc_process = memnew(VBoxContainer); vbc_process->set_h_size_flags(Control::SIZE_EXPAND_FILL); vbc_process->set_name(TTR("Post-Process")); tabc_features->add_child(vbc_process); cbut_process = memnew(CheckBox); 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("PascalCase to snake_case")); opt_style->add_item(TTR("snake_case to PascalCase")); 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); vbc->add_child(lbl_preview_title); lbl_preview = memnew(Label); lbl_preview->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); vbc->add_child(lbl_preview); // ---- Dialog related set_min_size(Size2(383, 0)); set_ok_button_text(TTR("Rename")); Button *but_reset = add_button(TTR("Reset")); eh.errfunc = _error_handler; eh.userdata = this; // ---- Connections cbut_collapse_features->connect("toggled", callable_mp(this, &RenameDialog::_features_toggled)); // Substitute Buttons lne_search->connect("focus_entered", callable_mp(this, &RenameDialog::_update_substitute)); lne_search->connect("focus_exited", callable_mp(this, &RenameDialog::_update_substitute)); lne_replace->connect("focus_entered", callable_mp(this, &RenameDialog::_update_substitute)); lne_replace->connect("focus_exited", callable_mp(this, &RenameDialog::_update_substitute)); lne_prefix->connect("focus_entered", callable_mp(this, &RenameDialog::_update_substitute)); lne_prefix->connect("focus_exited", callable_mp(this, &RenameDialog::_update_substitute)); lne_suffix->connect("focus_entered", callable_mp(this, &RenameDialog::_update_substitute)); lne_suffix->connect("focus_exited", callable_mp(this, &RenameDialog::_update_substitute)); // Preview lne_prefix->connect("text_changed", callable_mp(this, &RenameDialog::_update_preview)); lne_suffix->connect("text_changed", callable_mp(this, &RenameDialog::_update_preview)); lne_search->connect("text_changed", callable_mp(this, &RenameDialog::_update_preview)); lne_replace->connect("text_changed", callable_mp(this, &RenameDialog::_update_preview)); spn_count_start->connect("value_changed", callable_mp(this, &RenameDialog::_update_preview_int)); spn_count_step->connect("value_changed", callable_mp(this, &RenameDialog::_update_preview_int)); spn_count_padding->connect("value_changed", callable_mp(this, &RenameDialog::_update_preview_int)); opt_style->connect("item_selected", callable_mp(this, &RenameDialog::_update_preview_int)); opt_case->connect("item_selected", callable_mp(this, &RenameDialog::_update_preview_int)); cbut_substitute->connect("pressed", callable_mp(this, &RenameDialog::_update_preview).bind("")); cbut_regex->connect("pressed", callable_mp(this, &RenameDialog::_update_preview).bind("")); cbut_process->connect("pressed", callable_mp(this, &RenameDialog::_update_preview).bind("")); but_reset->connect("pressed", callable_mp(this, &RenameDialog::reset)); reset(); _features_toggled(false); } void RenameDialog::_bind_methods() { ClassDB::bind_method("rename", &RenameDialog::rename); } void RenameDialog::_update_substitute() { LineEdit *focus_owner_line_edit = Object::cast_to<LineEdit>(scene_tree_editor->get_viewport()->gui_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(Control::FOCUS_NONE); but_insert_parent->set_focus_mode(Control::FOCUS_NONE); but_insert_type->set_focus_mode(Control::FOCUS_NONE); but_insert_scene->set_focus_mode(Control::FOCUS_NONE); but_insert_root->set_focus_mode(Control::FOCUS_NONE); but_insert_count->set_focus_mode(Control::FOCUS_NONE); } void RenameDialog::_post_popup() { ConfirmationDialog::_post_popup(); EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection(); preview_node = nullptr; Array selected_node_list = editor_selection->get_selected_nodes(); ERR_FAIL_COND(selected_node_list.size() == 0); preview_node = Object::cast_to<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 == nullptr) { 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()) { // New name is identical to the old one. Don't color it as much to avoid distracting the user. const Color accent_color = EditorNode::get_singleton()->get_gui_base()->get_theme_color(SNAME("accent_color"), SNAME("Editor")); const Color text_color = EditorNode::get_singleton()->get_gui_base()->get_theme_color(SNAME("default_color"), SNAME("RichTextLabel")); lbl_preview->add_theme_color_override("font_color", accent_color.lerp(text_color, 0.5)); } else { lbl_preview->add_theme_color_override("font_color", EditorNode::get_singleton()->get_gui_base()->get_theme_color(SNAME("success_color"), SNAME("Editor"))); } } 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(); // Always request the scene title with the extension stripped. // Otherwise, the result could vary depending on whether a scene with the same name // (but different extension) is currently open. result = result.replace("${SCENE}", EditorNode::get_singleton()->get_editor_data().get_scene_title(current, true)); Node *root_node = SceneTree::get_singleton()->get_edited_scene_root(); if (root_node) { result = result.replace("${ROOT}", root_node->get_name()); } if (node) { 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, bool p_editor_notify, ErrorHandlerType p_type) { RenameDialog *self = (RenameDialog *)p_self; String source_file = String::utf8(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 = String::utf8(p_errorexp); } else { err_str = String::utf8(p_error); } self->has_errors = true; self->lbl_preview_title->set_text(TTR("Regular Expression Error:")); self->lbl_preview->add_theme_color_override("font_color", EditorNode::get_singleton()->get_gui_base()->get_theme_color(SNAME("error_color"), SNAME("Editor"))); self->lbl_preview->set_text(vformat(TTR("At character %s"), 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) { // PascalCase to snake_case result = result.to_snake_case(); result = _regex("_+", result, "_"); } else if (style_id == 2) { // snake_case to PascalCase RegEx pattern("_+(.?)"); Array matches = pattern.search_all(result); // The name `_` would become empty; ignore it. 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.to_pascal_case(); } } int case_id = opt_case->get_selected(); if (case_id == 1) { // To Lowercase result = result.to_lower(); } else if (case_id == 2) { // To Uppercase 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.is_valid() && !to_rename.is_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_PRINT("Skipping missing node: " + to_rename[i].first.get_concatenated_subnames()); continue; } scene_tree_editor->emit_signal(SNAME("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>(scene_tree_editor->get_viewport()->gui_get_focus_owner()); if (_is_main_field(focus_owner)) { focus_owner->selection_delete(); focus_owner->insert_text_at_caret(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); } #endif // MODULE_REGEX_ENABLED