godot/editor/script/find_in_files.cpp
2026-04-10 11:58:50 -04:00

1561 lines
51 KiB
C++

/**************************************************************************/
/* find_in_files.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 "find_in_files.h"
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
#include "core/object/callable_mp.h"
#include "core/os/os.h"
#include "editor/docks/editor_dock_manager.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/settings/editor_command_palette.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/check_box.h"
#include "scene/gui/check_button.h"
#include "scene/gui/grid_container.h"
#include "scene/gui/label.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/progress_bar.h"
#include "scene/gui/tab_container.h"
#include "scene/gui/tree.h"
#include "scene/main/scene_tree.h"
// TODO: Would be nice in Vector and Vectors.
template <typename T>
inline void pop_back(T &r_container) {
r_container.resize(r_container.size() - 1);
}
static bool find_next(const String &p_line, const String &p_pattern, int p_from, bool p_match_case, bool p_whole_words, int &r_out_begin, int &r_out_end) {
int end = p_from;
while (true) {
int begin = p_match_case ? p_line.find(p_pattern, end) : p_line.findn(p_pattern, end);
if (begin == -1) {
return false;
}
end = begin + p_pattern.length();
r_out_begin = begin;
r_out_end = end;
if (p_whole_words) {
if (begin > 0 && is_ascii_identifier_char(p_line[begin - 1])) {
continue;
}
if (end < p_line.size() && is_ascii_identifier_char(p_line[end])) {
continue;
}
}
return true;
}
}
//--------------------------------------------------------------------------------
void FindInFilesSearch::set_search_text(const String &p_pattern) {
pattern = p_pattern;
}
void FindInFilesSearch::set_whole_words(bool p_whole_word) {
whole_words = p_whole_word;
}
void FindInFilesSearch::set_match_case(bool p_match_case) {
match_case = p_match_case;
}
void FindInFilesSearch::set_folder(const String &p_folder) {
root_dir = p_folder;
}
void FindInFilesSearch::set_filter(const HashSet<String> &p_exts) {
extension_filter = p_exts;
}
void FindInFilesSearch::set_includes(const HashSet<String> &p_include_wildcards) {
include_wildcards = p_include_wildcards;
}
void FindInFilesSearch::set_excludes(const HashSet<String> &p_exclude_wildcards) {
exclude_wildcards = p_exclude_wildcards;
}
void FindInFilesSearch::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_PROCESS: {
_process();
} break;
}
}
void FindInFilesSearch::start() {
if (pattern.is_empty()) {
print_verbose("Nothing to search, pattern is empty");
emit_signal(SceneStringName(finished));
return;
}
if (extension_filter.is_empty()) {
print_verbose("Nothing to search, filter matches no files");
emit_signal(SceneStringName(finished));
return;
}
// Init search.
current_dir = "";
PackedStringArray init_folder;
init_folder.push_back(root_dir);
folders_stack.clear();
folders_stack.push_back(init_folder);
initial_files_count = 0;
searching = true;
set_process(true);
}
void FindInFilesSearch::stop() {
searching = false;
current_dir = "";
set_process(false);
}
void FindInFilesSearch::_process() {
// This part can be moved to a thread if needed.
OS &os = *OS::get_singleton();
uint64_t time_before = os.get_ticks_msec();
while (is_processing()) {
_iterate();
uint64_t elapsed = (os.get_ticks_msec() - time_before);
if (elapsed > 8) { // Process again after waiting 8 ticks.
break;
}
}
}
void FindInFilesSearch::_iterate() {
if (folders_stack.size() != 0) {
// Scan folders first so we can build a list of files and have progress info later.
PackedStringArray &folders_to_scan = folders_stack.write[folders_stack.size() - 1];
if (folders_to_scan.size() != 0) {
// Scan one folder below.
String folder_name = folders_to_scan[folders_to_scan.size() - 1];
pop_back(folders_to_scan);
current_dir = current_dir.path_join(folder_name);
PackedStringArray sub_dirs;
PackedStringArray new_files_to_scan;
_scan_dir("res://" + current_dir, sub_dirs, new_files_to_scan);
folders_stack.push_back(sub_dirs);
files_to_scan.append_array(new_files_to_scan);
} else {
// Go back one level.
pop_back(folders_stack);
current_dir = current_dir.get_base_dir();
if (folders_stack.is_empty()) {
// All folders scanned.
initial_files_count = files_to_scan.size();
}
}
} else if (files_to_scan.size() != 0) {
// Then scan files.
String fpath = files_to_scan[files_to_scan.size() - 1];
pop_back(files_to_scan);
_scan_file(fpath);
} else {
print_verbose("Search complete");
set_process(false);
current_dir = "";
searching = false;
emit_signal(SceneStringName(finished));
}
}
float FindInFilesSearch::get_progress() const {
if (initial_files_count != 0) {
return static_cast<float>(initial_files_count - files_to_scan.size()) / static_cast<float>(initial_files_count);
}
return 0;
}
void FindInFilesSearch::_scan_dir(const String &p_path, PackedStringArray &r_out_folders, PackedStringArray &r_out_files_to_scan) {
Ref<DirAccess> dir = DirAccess::open(p_path);
if (dir.is_null()) {
print_verbose("Cannot open directory! " + p_path);
return;
}
dir->list_dir_begin();
// Limit to 100,000 iterations to avoid an infinite loop just in case
// (this technically limits results to 100,000 files per folder).
for (int i = 0; i < 100'000; ++i) {
String file = dir->get_next();
if (file.is_empty()) {
break;
}
// If there is a .gdignore file in the directory, clear all the files/folders
// to be searched on this path and skip searching the directory.
if (file == ".gdignore") {
r_out_folders.clear();
r_out_files_to_scan.clear();
break;
}
// Ignore special directories (such as those beginning with . and the project data directory).
String project_data_dir_name = ProjectSettings::get_singleton()->get_project_data_dir_name();
if (file.begins_with(".") || file == project_data_dir_name) {
continue;
}
if (dir->current_is_hidden()) {
continue;
}
if (dir->current_is_dir()) {
r_out_folders.push_back(file);
} else {
String file_ext = file.get_extension();
if (extension_filter.has(file_ext)) {
String file_path = p_path.path_join(file);
bool case_sensitive = dir->is_case_sensitive(p_path);
if (!exclude_wildcards.is_empty() && _is_file_matched(exclude_wildcards, file_path, case_sensitive)) {
continue;
}
if (include_wildcards.is_empty() || _is_file_matched(include_wildcards, file_path, case_sensitive)) {
r_out_files_to_scan.push_back(file_path);
}
}
}
}
}
void FindInFilesSearch::_scan_file(const String &p_fpath) {
Ref<FileAccess> f = FileAccess::open(p_fpath, FileAccess::READ);
if (f.is_null()) {
print_verbose("Cannot open file " + p_fpath);
return;
}
int line_number = 0;
while (!f->eof_reached()) {
// Line number starts at 1.
++line_number;
int begin = 0;
int end = 0;
String line = f->get_line();
while (find_next(line, pattern, end, match_case, whole_words, begin, end)) {
emit_signal(SNAME("result_found"), p_fpath, line_number, begin, end, line);
}
}
}
bool FindInFilesSearch::_is_file_matched(const HashSet<String> &p_wildcards, const String &p_file_path, bool p_case_sensitive) const {
const String file_path = "/" + p_file_path.replace_char('\\', '/') + "/";
for (const String &wildcard : p_wildcards) {
if (p_case_sensitive && file_path.match(wildcard)) {
return true;
} else if (!p_case_sensitive && file_path.matchn(wildcard)) {
return true;
}
}
return false;
}
void FindInFilesSearch::_bind_methods() {
ADD_SIGNAL(MethodInfo("result_found",
PropertyInfo(Variant::STRING, "path"),
PropertyInfo(Variant::INT, "line_number"),
PropertyInfo(Variant::INT, "begin"),
PropertyInfo(Variant::INT, "end"),
PropertyInfo(Variant::STRING, "text")));
ADD_SIGNAL(MethodInfo("finished"));
}
//-----------------------------------------------------------------------------
void FindInFilesDialog::set_search_text(const String &p_text) {
if (!p_text.is_empty()) {
search_text_line_edit->set_text(p_text);
_on_search_text_modified(p_text);
}
if (replace_mode && !p_text.is_empty()) {
callable_mp((Control *)replace_text_line_edit, &Control::grab_focus).call_deferred(false);
replace_text_line_edit->select_all();
} else {
callable_mp((Control *)search_text_line_edit, &Control::grab_focus).call_deferred(false);
search_text_line_edit->select_all();
}
}
void FindInFilesDialog::set_replace_text(const String &p_text) {
replace_text_line_edit->set_text(p_text);
}
void FindInFilesDialog::set_replace_mode(bool p_replace) {
if (replace_mode == p_replace) {
return;
}
replace_mode = p_replace;
if (replace_mode) {
set_title(TTRC("Replace in Files"));
replace_label->show();
replace_text_line_edit->show();
} else {
set_title(TTRC("Find in Files"));
replace_label->hide();
replace_text_line_edit->hide();
}
// Recalculate the dialog size after hiding child controls.
set_size(Size2(get_size().x, 0));
}
String FindInFilesDialog::get_search_text() const {
return search_text_line_edit->get_text();
}
String FindInFilesDialog::get_replace_text() const {
return replace_text_line_edit->get_text();
}
bool FindInFilesDialog::is_match_case() const {
return match_case_checkbox->is_pressed();
}
bool FindInFilesDialog::is_whole_words() const {
return whole_words_checkbox->is_pressed();
}
String FindInFilesDialog::get_folder() const {
String p_text = folder_line_edit->get_text();
return p_text.strip_edges();
}
HashSet<String> FindInFilesDialog::get_filter() const {
// Could check the filters_preferences but it might not have been generated yet.
HashSet<String> filters;
for (int i = 0; i < filters_container->get_child_count(); ++i) {
CheckBox *cb = static_cast<CheckBox *>(filters_container->get_child(i));
if (cb->is_pressed()) {
filters.insert(cb->get_text());
}
}
return filters;
}
HashSet<String> FindInFilesDialog::get_includes() const {
HashSet<String> includes;
String p_text = includes_line_edit->get_text();
if (p_text.is_empty()) {
return includes;
}
PackedStringArray wildcards = p_text.split(",", false);
for (const String &wildcard : wildcards) {
includes.insert(_validate_filter_wildcard(wildcard));
}
return includes;
}
HashSet<String> FindInFilesDialog::get_excludes() const {
HashSet<String> excludes;
String p_text = excludes_line_edit->get_text();
if (p_text.is_empty()) {
return excludes;
}
PackedStringArray wildcards = p_text.split(",", false);
for (const String &wildcard : wildcards) {
excludes.insert(_validate_filter_wildcard(wildcard));
}
return excludes;
}
void FindInFilesDialog::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_VISIBILITY_CHANGED: {
if (is_visible()) {
// Extensions might have changed in the meantime, we clean them and instance them again.
for (int i = 0; i < filters_container->get_child_count(); i++) {
filters_container->get_child(i)->queue_free();
}
Array exts = GLOBAL_GET("editor/script/search_in_file_extensions");
for (int i = 0; i < exts.size(); ++i) {
CheckBox *cb = memnew(CheckBox);
cb->set_text(exts[i]);
if (!filters_preferences.has(exts[i])) {
filters_preferences[exts[i]] = true;
}
cb->set_pressed(filters_preferences[exts[i]]);
filters_container->add_child(cb);
}
}
} break;
}
}
void FindInFilesDialog::_on_folder_button_pressed() {
folder_dialog->popup_file_dialog();
}
void FindInFilesDialog::custom_action(const String &p_action) {
for (int i = 0; i < filters_container->get_child_count(); ++i) {
CheckBox *cb = static_cast<CheckBox *>(filters_container->get_child(i));
filters_preferences[cb->get_text()] = cb->is_pressed();
}
if (p_action == "find") {
emit_signal(SNAME("find_requested"));
} else if (p_action == "replace") {
emit_signal(SNAME("replace_requested"));
}
hide();
}
void FindInFilesDialog::_on_search_text_modified(const String &p_text) {
ERR_FAIL_NULL(find_button);
ERR_FAIL_NULL(replace_button);
find_button->set_disabled(get_search_text().is_empty());
replace_button->set_disabled(get_search_text().is_empty());
}
void FindInFilesDialog::_on_search_text_submitted(const String &p_text) {
// This allows to trigger a global search without leaving the keyboard.
if (!replace_mode && !find_button->is_disabled()) {
custom_action("find");
}
if (replace_mode && !replace_button->is_disabled()) {
custom_action("replace");
}
}
void FindInFilesDialog::_on_replace_text_submitted(const String &p_text) {
// This allows to trigger a global search without leaving the keyboard.
if (replace_mode && !replace_button->is_disabled()) {
custom_action("replace");
}
}
void FindInFilesDialog::_on_folder_selected(String p_path) {
int i = p_path.find("://");
if (i != -1) {
p_path = p_path.substr(i + 3);
}
folder_line_edit->set_text(p_path);
}
String FindInFilesDialog::_validate_filter_wildcard(const String &p_expression) const {
String ret = p_expression.replace_char('\\', '/');
if (ret.begins_with("./")) {
// Relative to the project root.
ret = "res://" + ret.trim_prefix("./");
}
if (ret.begins_with(".")) {
// To match extension.
ret = "*" + ret;
}
if (!ret.begins_with("*")) {
ret = "*/" + ret.trim_prefix("/");
}
if (!ret.ends_with("*")) {
ret = ret.trim_suffix("/") + "/*";
}
return ret;
}
void FindInFilesDialog::_bind_methods() {
ADD_SIGNAL(MethodInfo("find_requested"));
ADD_SIGNAL(MethodInfo("replace_requested"));
}
FindInFilesDialog::FindInFilesDialog() {
set_min_size(Size2(500 * EDSCALE, 0));
set_title(TTRC("Find in Files"));
VBoxContainer *vbc = memnew(VBoxContainer);
vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);
vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);
vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);
vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);
add_child(vbc);
GridContainer *gc = memnew(GridContainer);
gc->set_columns(2);
vbc->add_child(gc);
Label *find_label = memnew(Label);
find_label->set_text(TTRC("Find:"));
gc->add_child(find_label);
search_text_line_edit = memnew(LineEdit);
search_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
search_text_line_edit->set_accessibility_name(TTRC("Find:"));
search_text_line_edit->connect(SceneStringName(text_changed), callable_mp(this, &FindInFilesDialog::_on_search_text_modified));
search_text_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
gc->add_child(search_text_line_edit);
replace_label = memnew(Label);
replace_label->set_text(TTRC("Replace:"));
replace_label->hide();
gc->add_child(replace_label);
replace_text_line_edit = memnew(LineEdit);
replace_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
replace_text_line_edit->set_accessibility_name(TTRC("Replace:"));
replace_text_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_replace_text_submitted));
replace_text_line_edit->hide();
gc->add_child(replace_text_line_edit);
gc->add_child(memnew(Control)); // Space to maintain the grid alignment.
{
HBoxContainer *hbc = memnew(HBoxContainer);
whole_words_checkbox = memnew(CheckBox);
whole_words_checkbox->set_text(TTRC("Whole Words"));
hbc->add_child(whole_words_checkbox);
match_case_checkbox = memnew(CheckBox);
match_case_checkbox->set_text(TTRC("Match Case"));
hbc->add_child(match_case_checkbox);
gc->add_child(hbc);
}
Label *folder_label = memnew(Label);
folder_label->set_text(TTRC("Folder:"));
gc->add_child(folder_label);
{
HBoxContainer *hbc = memnew(HBoxContainer);
Label *prefix_label = memnew(Label);
prefix_label->set_text("res://");
prefix_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
hbc->add_child(prefix_label);
folder_line_edit = memnew(LineEdit);
folder_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
folder_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
folder_line_edit->set_accessibility_name(TTRC("Folder:"));
hbc->add_child(folder_line_edit);
Button *folder_button = memnew(Button);
folder_button->set_accessibility_name(TTRC("Select Folder"));
folder_button->set_text("...");
folder_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesDialog::_on_folder_button_pressed));
hbc->add_child(folder_button);
folder_dialog = memnew(EditorFileDialog);
folder_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR);
folder_dialog->connect("dir_selected", callable_mp(this, &FindInFilesDialog::_on_folder_selected));
add_child(folder_dialog);
gc->add_child(hbc);
}
Label *includes_label = memnew(Label);
includes_label->set_text(TTRC("Includes:"));
includes_label->set_tooltip_text(TTRC("Include the files with the following expressions. Use \",\" to separate."));
includes_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
gc->add_child(includes_label);
includes_line_edit = memnew(LineEdit);
includes_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
includes_line_edit->set_placeholder(TTRC("example: scripts,scenes/*/test.gd"));
includes_line_edit->set_accessibility_name(TTRC("Includes:"));
includes_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
gc->add_child(includes_line_edit);
Label *excludes_label = memnew(Label);
excludes_label->set_text(TTRC("Excludes:"));
excludes_label->set_tooltip_text(TTRC("Exclude the files with the following expressions. Use \",\" to separate."));
excludes_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
gc->add_child(excludes_label);
excludes_line_edit = memnew(LineEdit);
excludes_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
excludes_line_edit->set_placeholder(TTRC("example: res://addons,scenes/test/*.gd"));
excludes_line_edit->set_accessibility_name(TTRC("Excludes:"));
excludes_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
gc->add_child(excludes_line_edit);
Label *filter_label = memnew(Label);
filter_label->set_text(TTRC("Filters:"));
filter_label->set_tooltip_text(TTRC("Include the files with the following extensions. Add or remove them in ProjectSettings."));
filter_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
gc->add_child(filter_label);
filters_container = memnew(HBoxContainer);
gc->add_child(filters_container);
find_button = add_button(TTRC("Find..."), false, "find");
find_button->set_disabled(true);
replace_button = add_button(TTRC("Replace..."), false, "replace");
replace_button->set_disabled(true);
Button *cancel_button = get_ok_button();
cancel_button->set_text(TTRC("Cancel"));
}
//-----------------------------------------------------------------------------
void FindInFilesPanel::set_with_replace(bool p_with_replace) {
with_replace = p_with_replace;
replace_container->set_visible(p_with_replace);
if (with_replace) {
// Results show checkboxes on their left so they can be opted out.
results_display->set_columns(2);
results_display->set_column_expand(0, false);
results_display->set_column_custom_minimum_width(0, 48 * EDSCALE);
} else {
// Results are single-cell items.
results_display->set_column_expand(0, true);
results_display->set_columns(1);
}
}
void FindInFilesPanel::set_replace_text(const String &p_text) {
replace_line_edit->set_text(p_text);
}
bool FindInFilesPanel::is_keep_results() const {
return keep_results_button->is_pressed();
}
void FindInFilesPanel::set_search_labels_visibility(bool p_visible) {
find_label->set_visible(p_visible);
search_text_label->set_visible(p_visible);
close_button->set_visible(p_visible);
}
void FindInFilesPanel::_clear() {
file_items.clear();
file_items_results_count.clear();
result_items.clear();
results_display->clear();
results_display->create_item(); // Root
}
void FindInFilesPanel::start_search() {
_clear();
status_label->set_text(TTRC("Searching..."));
search_text_label->set_text(finder->get_search_text());
search_text_label->set_tooltip_text(finder->get_search_text());
int label_min_width = search_text_label->get_minimum_size().x + search_text_label->get_character_bounds(0).size.x;
search_text_label->set_custom_minimum_size(Size2(label_min_width, 0));
set_process(true);
progress_bar->set_visible(true);
finder->start();
_update_replace_buttons();
refresh_button->hide();
cancel_button->show();
}
void FindInFilesPanel::stop_search() {
finder->stop();
status_label->set_text("");
_update_replace_buttons();
progress_bar->set_visible(false);
refresh_button->show();
cancel_button->hide();
}
void FindInFilesPanel::update_layout(EditorDock::DockLayout p_layout) {
bool new_floating = (p_layout == EditorDock::DOCK_LAYOUT_FLOATING);
if (floating == new_floating) {
return;
}
floating = new_floating;
if (floating) {
results_mc->set_theme_type_variation("NoBorderHorizontalBottom");
results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_TOP);
} else {
results_mc->set_theme_type_variation("NoBorderHorizontal");
results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);
}
}
void FindInFilesPanel::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
_on_theme_changed();
} break;
case NOTIFICATION_TRANSLATION_CHANGED: {
_update_matches_text();
TreeItem *file_item = results_display->get_root()->get_first_child();
while (file_item) {
if (with_replace) {
file_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), TTR("Replace all matches in file"));
}
file_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result"));
TreeItem *result_item = file_item->get_first_child();
while (result_item) {
if (with_replace) {
result_item->set_button_tooltip_text(1, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), TTR("Replace"));
result_item->set_button_tooltip_text(1, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result"));
} else {
result_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result"));
}
result_item = result_item->get_next();
}
file_item = file_item->get_next();
}
} break;
case NOTIFICATION_PROCESS: {
progress_bar->set_as_ratio(finder->get_progress());
} break;
}
}
void FindInFilesPanel::_on_result_found(const String &p_fpath, int p_line_number, int p_begin, int p_end, const String &p_text) {
TreeItem *file_item;
Ref<Texture2D> remove_texture = get_editor_theme_icon(SNAME("Close"));
Ref<Texture2D> replace_texture = get_editor_theme_icon(SNAME("ReplaceText"));
HashMap<String, TreeItem *>::Iterator E = file_items.find(p_fpath);
if (!E) {
file_item = results_display->create_item();
file_item->set_text(0, p_fpath);
file_item->set_metadata(0, p_fpath);
if (with_replace) {
file_item->add_button(0, replace_texture, FIND_BUTTON_REPLACE, false, TTR("Replace all matches in file"));
}
file_item->add_button(0, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result"));
// The width of this column is restrained to checkboxes,
// but that doesn't make sense for the parent items,
// so we override their width so they can expand to full width.
file_item->set_expand_right(0, true);
file_items[p_fpath] = file_item;
file_items_results_count[file_item] = 1;
} else {
file_item = E->value;
file_items_results_count[file_item]++;
}
Color file_item_color = results_display->get_theme_color(SceneStringName(font_color)) * Color(1, 1, 1, 0.67);
file_item->set_custom_color(0, file_item_color);
file_item->set_selectable(0, false);
int text_index = with_replace ? 1 : 0;
TreeItem *item = results_display->create_item(file_item);
// Do this first because it resets properties of the cell...
item->set_cell_mode(text_index, TreeItem::CELL_MODE_CUSTOM);
// Trim result item line.
String trimmed_text = p_text.strip_edges(true, false);
int chars_removed = p_text.size() - trimmed_text.size();
String start = vformat("%3s: ", p_line_number);
item->set_text(text_index, start + trimmed_text);
item->set_custom_draw_callback(text_index, callable_mp(this, &FindInFilesPanel::_draw_result_text));
Result r;
r.line_number = p_line_number;
r.begin = p_begin;
r.end = p_end;
r.begin_trimmed = p_begin - chars_removed + start.size() - 1;
result_items[item] = r;
if (with_replace) {
item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
item->set_checked(0, true);
item->set_editable(0, true);
item->add_button(1, replace_texture, FIND_BUTTON_REPLACE, false, TTR("Replace"));
item->add_button(1, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result"));
} else {
item->add_button(0, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result"));
}
}
void FindInFilesPanel::_on_theme_changed() {
results_display->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("source"), EditorStringName(EditorFonts)));
results_display->add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size(SNAME("source_size"), EditorStringName(EditorFonts)));
Color file_item_color = results_display->get_theme_color(SceneStringName(font_color)) * Color(1, 1, 1, 0.67);
Ref<Texture2D> remove_texture = get_editor_theme_icon(SNAME("Close"));
Ref<Texture2D> replace_texture = get_editor_theme_icon(SNAME("ReplaceText"));
TreeItem *file_item = results_display->get_root()->get_first_child();
while (file_item) {
file_item->set_custom_color(0, file_item_color);
if (with_replace) {
file_item->set_button(0, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), replace_texture);
}
file_item->set_button(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), remove_texture);
TreeItem *result_item = file_item->get_first_child();
while (result_item) {
if (with_replace) {
result_item->set_button(1, result_item->get_button_by_id(1, FIND_BUTTON_REPLACE), replace_texture);
result_item->set_button(1, result_item->get_button_by_id(1, FIND_BUTTON_REMOVE), remove_texture);
} else {
result_item->set_button(0, result_item->get_button_by_id(0, FIND_BUTTON_REMOVE), remove_texture);
}
result_item = result_item->get_next();
}
file_item = file_item->get_next();
}
}
void FindInFilesPanel::_draw_result_text(Object *p_item_obj, const Rect2 &p_rect) {
TreeItem *item = Object::cast_to<TreeItem>(p_item_obj);
if (!item) {
return;
}
HashMap<TreeItem *, Result>::Iterator E = result_items.find(item);
if (!E) {
return;
}
Result r = E->value;
String item_text = item->get_text(with_replace ? 1 : 0);
Ref<Font> font = results_display->get_theme_font(SceneStringName(font));
int font_size = results_display->get_theme_font_size(SceneStringName(font_size));
Rect2 match_rect = p_rect;
match_rect.position.x += font->get_string_size(item_text.left(r.begin_trimmed), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 1;
match_rect.size.x = font->get_string_size(search_text_label->get_text(), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + 1;
match_rect.position.y += 1 * EDSCALE;
match_rect.size.y -= 2 * EDSCALE;
Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
results_display->draw_rect(match_rect, Color(accent_color, 0.33), false, 2.0);
results_display->draw_rect(match_rect, Color(accent_color, 0.17), true);
// Text is drawn by Tree already.
}
void FindInFilesPanel::_on_item_edited() {
TreeItem *item = results_display->get_selected();
// Change opacity to half if checkbox is checked, otherwise full.
Color use_color = results_display->get_theme_color(SceneStringName(font_color));
if (!item->is_checked(0)) {
use_color.a *= 0.5;
}
item->set_custom_color(1, use_color);
}
void FindInFilesPanel::_on_finished() {
_update_matches_text();
_update_replace_buttons();
progress_bar->set_visible(false);
refresh_button->show();
cancel_button->hide();
}
void FindInFilesPanel::_on_refresh_button_clicked() {
start_search();
}
void FindInFilesPanel::_on_cancel_button_clicked() {
stop_search();
}
void FindInFilesPanel::_on_close_button_clicked() {
emit_signal(SNAME("close_button_clicked"));
}
void FindInFilesPanel::_on_result_selected() {
TreeItem *item = results_display->get_selected();
HashMap<TreeItem *, Result>::Iterator E = result_items.find(item);
if (!E) {
return;
}
Result r = E->value;
TreeItem *file_item = item->get_parent();
String fpath = file_item->get_metadata(0);
emit_signal(SNAME("result_selected"), fpath, r.line_number, r.begin, r.end);
}
void FindInFilesPanel::_on_replace_text_changed(const String &p_text) {
_update_replace_buttons();
}
void FindInFilesPanel::_on_replace_all_clicked() {
String replace_text = _get_replace_text();
for (KeyValue<String, TreeItem *> &E : file_items) {
TreeItem *file_item = E.value;
String fpath = file_item->get_metadata(0);
Vector<Result> locations;
for (TreeItem *item = file_item->get_first_child(); item; item = item->get_next()) {
if (!item->is_checked(0)) {
continue;
}
HashMap<TreeItem *, Result>::Iterator F = result_items.find(item);
ERR_FAIL_COND(!F);
locations.push_back(F->value);
}
if (locations.size() != 0) {
// Results are sorted by file, so we can batch replaces.
_apply_replaces_in_file(fpath, locations, replace_text);
}
}
// Hide replace bar so we can't trigger the action twice without doing a new search.
replace_container->hide();
emit_signal(SNAME("files_modified"));
}
void FindInFilesPanel::_on_button_clicked(TreeItem *p_item, int p_column, int p_id, int p_mouse_button_index) {
const String file_path = p_item->get_metadata(0);
if (p_id == FIND_BUTTON_REPLACE) {
const String replace_text = _get_replace_text();
Vector<Result> locations;
if (file_items.has(file_path)) {
for (TreeItem *item = p_item->get_first_child(); item; item = item->get_next()) {
HashMap<TreeItem *, Result>::Iterator F = result_items.find(item);
ERR_FAIL_COND(!F);
locations.push_back(F->value);
}
_apply_replaces_in_file(file_path, locations, replace_text);
} else {
locations.push_back(result_items.find(p_item)->value);
const String path = p_item->get_parent()->get_metadata(0);
_apply_replaces_in_file(path, locations, replace_text);
}
emit_signal(SNAME("files_modified"));
}
result_items.erase(p_item);
if (file_items_results_count.has(p_item)) {
int match_count = p_item->get_child_count();
for (int i = 0; i < match_count; i++) {
TreeItem *child_item = p_item->get_child(i);
result_items.erase(child_item);
}
p_item->clear_children();
file_items.erase(file_path);
file_items_results_count.erase(p_item);
}
TreeItem *item_parent = p_item->get_parent();
if (item_parent) {
if (file_items_results_count.has(item_parent)) {
file_items_results_count[item_parent]--;
}
if (item_parent->get_child_count() < 2 && item_parent != results_display->get_root()) {
file_items.erase(item_parent->get_metadata(0));
get_tree()->queue_delete(item_parent);
}
}
get_tree()->queue_delete(p_item);
_update_matches_text();
}
// Same as get_line, but preserves line ending characters.
class ConservativeGetLine {
public:
String get_line(Ref<FileAccess> p_file) {
_line_buffer.clear();
char32_t c = p_file->get_8();
while (!p_file->eof_reached()) {
if (c == '\n') {
_line_buffer.push_back(c);
_line_buffer.push_back(0);
return String::utf8(_line_buffer.ptr());
} else if (c == '\0') {
_line_buffer.push_back(c);
return String::utf8(_line_buffer.ptr());
} else if (c != '\r') {
_line_buffer.push_back(c);
}
c = p_file->get_8();
}
_line_buffer.push_back(0);
return String::utf8(_line_buffer.ptr());
}
private:
Vector<char> _line_buffer;
};
void FindInFilesPanel::_apply_replaces_in_file(const String &p_fpath, const Vector<Result> &p_locations, const String &p_new_text) {
// If the file is already open, I assume the editor will reload it.
// If there are unsaved changes, the user will be asked on focus,
// however that means either losing changes or losing replaces.
Ref<FileAccess> f = FileAccess::open(p_fpath, FileAccess::READ);
ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file from path '" + p_fpath + "'.");
String buffer;
int current_line = 1;
ConservativeGetLine conservative;
String line = conservative.get_line(f);
String search_text = finder->get_search_text();
int offset = 0;
for (int i = 0; i < p_locations.size(); ++i) {
int repl_line_number = p_locations[i].line_number;
while (current_line < repl_line_number) {
buffer += line;
line = conservative.get_line(f);
++current_line;
offset = 0;
}
int repl_begin = p_locations[i].begin + offset;
int repl_end = p_locations[i].end + offset;
int _;
if (!find_next(line, search_text, repl_begin, finder->is_match_case(), finder->is_whole_words(), _, _)) {
// Make sure the replace is still valid in case the file was tampered with.
print_verbose(vformat(R"(Occurrence no longer matches, replace will be ignored in "%s": line %d, col %d.)", p_fpath, repl_line_number, repl_begin));
continue;
}
line = line.left(repl_begin) + p_new_text + line.substr(repl_end);
// Keep an offset in case there are successive replaces in the same line.
offset += p_new_text.length() - (repl_end - repl_begin);
}
buffer += line;
while (!f->eof_reached()) {
buffer += conservative.get_line(f);
}
// Now the modified contents are in the buffer, rewrite the file with our changes.
Error err = f->reopen(p_fpath, FileAccess::WRITE);
ERR_FAIL_COND_MSG(err != OK, "Cannot create file in path '" + p_fpath + "'.");
f->store_string(buffer);
}
String FindInFilesPanel::_get_replace_text() {
return replace_line_edit->get_text();
}
void FindInFilesPanel::_update_replace_buttons() {
bool disabled = finder->is_searching();
replace_all_button->set_disabled(disabled);
}
void FindInFilesPanel::_update_matches_text() {
String results_text;
int result_count = result_items.size();
int file_count = file_items.size();
if (result_count == 1 && file_count == 1) {
results_text = vformat(TTR("%d match in %d file"), result_count, file_count);
} else if (result_count != 1 && file_count == 1) {
results_text = vformat(TTR("%d matches in %d file"), result_count, file_count);
} else {
results_text = vformat(TTR("%d matches in %d files"), result_count, file_count);
}
status_label->set_text(results_text);
TreeItem *file_item = results_display->get_root()->get_first_child();
while (file_item) {
int file_matches_count = file_items_results_count[file_item];
file_item->set_text(0, (String)file_item->get_metadata(0) + " (" + vformat(TTRN("%d match", "%d matches", file_matches_count), file_matches_count) + ")");
file_item = file_item->get_next();
}
}
void FindInFilesPanel::_bind_methods() {
ADD_SIGNAL(MethodInfo("result_selected",
PropertyInfo(Variant::STRING, "path"),
PropertyInfo(Variant::INT, "line_number"),
PropertyInfo(Variant::INT, "begin"),
PropertyInfo(Variant::INT, "end")));
ADD_SIGNAL(MethodInfo("files_modified"));
ADD_SIGNAL(MethodInfo("close_button_clicked"));
}
FindInFilesPanel::FindInFilesPanel() {
finder = memnew(FindInFilesSearch);
finder->connect("result_found", callable_mp(this, &FindInFilesPanel::_on_result_found));
finder->connect(SceneStringName(finished), callable_mp(this, &FindInFilesPanel::_on_finished));
add_child(finder);
VBoxContainer *vbc = memnew(VBoxContainer);
vbc->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0);
vbc->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0);
vbc->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0);
vbc->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0);
add_child(vbc);
{
HBoxContainer *hbc = memnew(HBoxContainer);
hbc->set_alignment(BoxContainer::ALIGNMENT_END);
find_label = memnew(Label);
find_label->set_text(TTRC("Find:"));
hbc->add_child(find_label);
search_text_label = memnew(Label);
search_text_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
search_text_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
search_text_label->set_focus_mode(FOCUS_ACCESSIBILITY);
search_text_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
search_text_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
hbc->add_child(search_text_label);
progress_bar = memnew(ProgressBar);
progress_bar->set_h_size_flags(SIZE_EXPAND_FILL);
progress_bar->set_v_size_flags(SIZE_SHRINK_CENTER);
progress_bar->set_stretch_ratio(2.0);
progress_bar->set_visible(false);
hbc->add_child(progress_bar);
status_label = memnew(Label);
status_label->set_focus_mode(FOCUS_ACCESSIBILITY);
hbc->add_child(status_label);
keep_results_button = memnew(CheckButton);
keep_results_button->set_text(TTRC("Keep Results"));
keep_results_button->set_tooltip_text(TTRC("Keep these results and show subsequent results in a new window"));
keep_results_button->set_pressed(false);
hbc->add_child(keep_results_button);
refresh_button = memnew(Button);
refresh_button->set_text(TTRC("Refresh"));
refresh_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_refresh_button_clicked));
refresh_button->hide();
hbc->add_child(refresh_button);
cancel_button = memnew(Button);
cancel_button->set_text(TTRC("Cancel"));
cancel_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_cancel_button_clicked));
cancel_button->hide();
hbc->add_child(cancel_button);
close_button = memnew(Button);
close_button->set_text(TTRC("Close"));
close_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_close_button_clicked));
hbc->add_child(close_button);
vbc->add_child(hbc);
}
results_mc = memnew(MarginContainer);
results_mc->set_theme_type_variation("NoBorderHorizontal");
results_mc->set_v_size_flags(SIZE_EXPAND_FILL);
vbc->add_child(results_mc);
results_display = memnew(Tree);
results_display->set_accessibility_name(TTRC("Search Results"));
results_display->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);
results_display->connect(SceneStringName(item_selected), callable_mp(this, &FindInFilesPanel::_on_result_selected));
results_display->connect("item_edited", callable_mp(this, &FindInFilesPanel::_on_item_edited));
results_display->connect("button_clicked", callable_mp(this, &FindInFilesPanel::_on_button_clicked));
results_display->set_hide_root(true);
results_display->set_select_mode(Tree::SELECT_ROW);
results_display->set_allow_rmb_select(true);
results_display->set_allow_reselect(true);
results_display->add_theme_constant_override("inner_item_margin_left", 0);
results_display->add_theme_constant_override("inner_item_margin_right", 0);
results_display->create_item(); // Root
results_mc->add_child(results_display);
{
replace_container = memnew(HBoxContainer);
Label *replace_label = memnew(Label);
replace_label->set_text(TTRC("Replace:"));
replace_container->add_child(replace_label);
replace_line_edit = memnew(LineEdit);
replace_line_edit->set_accessibility_name(TTRC("Replace:"));
replace_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
replace_line_edit->connect(SceneStringName(text_changed), callable_mp(this, &FindInFilesPanel::_on_replace_text_changed));
replace_container->add_child(replace_line_edit);
replace_all_button = memnew(Button);
replace_all_button->set_text(TTRC("Replace all (no undo)"));
replace_all_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_replace_all_clicked));
replace_container->add_child(replace_all_button);
replace_container->hide();
vbc->add_child(replace_container);
}
}
//-----------------------------------------------------------------------------
FindInFilesPanel *FindInFilesContainer::_create_new_panel() {
int index = tabs->get_current_tab();
FindInFilesPanel *panel = memnew(FindInFilesPanel);
tabs->add_child(panel);
tabs->move_child(panel, index + 1); // New panel is added after the current activated panel.
tabs->set_current_tab(index + 1);
_update_bar_visibility();
panel->connect("result_selected", callable_mp(this, &FindInFilesContainer::_result_selected));
panel->connect("files_modified", callable_mp(this, &FindInFilesContainer::_files_modified));
panel->connect("close_button_clicked", callable_mp(this, &FindInFilesContainer::_close_panel).bind(panel));
return panel;
}
FindInFilesPanel *FindInFilesContainer::_get_current_panel() {
return Object::cast_to<FindInFilesPanel>(tabs->get_current_tab_control());
}
FindInFilesPanel *FindInFilesContainer::get_panel_for_results(const String &p_label) {
FindInFilesPanel *panel = nullptr;
// Prefer the current panel.
if (_get_current_panel() && !_get_current_panel()->is_keep_results()) {
panel = _get_current_panel();
} else {
// Find the first panel which does not keep results.
for (int i = 0; i < tabs->get_tab_count(); i++) {
FindInFilesPanel *p = Object::cast_to<FindInFilesPanel>(tabs->get_tab_control(i));
if (p && !p->is_keep_results()) {
panel = p;
tabs->set_current_tab(i);
break;
}
}
if (!panel) {
panel = _create_new_panel();
}
}
tabs->set_tab_title(tabs->get_current_tab(), p_label);
return panel;
}
void FindInFilesContainer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE: {
connect("closed", callable_mp(this, &FindInFilesContainer::_on_dock_closed));
} break;
}
}
void FindInFilesContainer::_on_theme_changed() {
const Ref<StyleBox> bottom_panel_style = EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanel"), EditorStringName(EditorStyles));
if (bottom_panel_style.is_valid()) {
begin_bulk_theme_override();
add_theme_constant_override("margin_top", -bottom_panel_style->get_margin(SIDE_TOP));
add_theme_constant_override("margin_left", -bottom_panel_style->get_margin(SIDE_LEFT));
add_theme_constant_override("margin_right", -bottom_panel_style->get_margin(SIDE_RIGHT));
add_theme_constant_override("margin_bottom", -bottom_panel_style->get_margin(SIDE_BOTTOM));
end_bulk_theme_override();
}
}
void FindInFilesContainer::_result_selected(const String &p_fpath, int p_line_number, int p_begin, int p_end) {
emit_signal(SNAME("result_selected"), p_fpath, p_line_number, p_begin, p_end);
}
void FindInFilesContainer::_files_modified() {
emit_signal(SNAME("files_modified"));
}
void FindInFilesContainer::_close_panel(FindInFilesPanel *p_panel) {
ERR_FAIL_COND_MSG(p_panel->get_parent() != tabs, "This panel is not a child!");
tabs->remove_child(p_panel);
p_panel->queue_free();
_update_bar_visibility();
if (tabs->get_tab_count() == 0) {
close();
}
}
void FindInFilesContainer::_on_dock_closed() {
while (tabs->get_tab_count() > 0) {
Control *tab = tabs->get_tab_control(0);
tabs->remove_child(tab);
tab->queue_free();
}
_update_bar_visibility();
}
void FindInFilesContainer::_on_tab_close_pressed(int p_tab) {
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(tabs->get_tab_control(p_tab));
if (panel) {
_close_panel(panel);
}
}
void FindInFilesContainer::_update_bar_visibility() {
if (!update_bar) {
return;
}
// If tab count <= 1, behaves like this is not a TabContainer and the bar is hidden.
bool bar_visible = tabs->get_tab_count() > 1;
tabs->set_tabs_visible(bar_visible);
// Hide or show the search labels based on the visibility of the bar, as the search terms are displayed in the title of each tab.
for (int i = 0; i < tabs->get_tab_count(); i++) {
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(tabs->get_tab_control(i));
if (panel) {
panel->set_search_labels_visibility(!bar_visible);
}
}
}
void FindInFilesContainer::_bar_menu_option(int p_option) {
int tab_index = tabs->get_current_tab();
switch (p_option) {
case PANEL_CLOSE: {
_on_tab_close_pressed(tab_index);
} break;
case PANEL_CLOSE_OTHERS: {
update_bar = false;
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(tabs->get_tab_control(tab_index));
for (int i = tabs->get_tab_count() - 1; i >= 0; i--) {
FindInFilesPanel *p = Object::cast_to<FindInFilesPanel>(tabs->get_tab_control(i));
if (p != panel) {
_close_panel(p);
}
}
update_bar = true;
_update_bar_visibility();
} break;
case PANEL_CLOSE_RIGHT: {
update_bar = false;
for (int i = tabs->get_tab_count() - 1; i > tab_index; i--) {
_on_tab_close_pressed(i);
}
update_bar = true;
_update_bar_visibility();
} break;
case PANEL_CLOSE_ALL: {
update_bar = false;
for (int i = tabs->get_tab_count() - 1; i >= 0; i--) {
_on_tab_close_pressed(i);
}
update_bar = true;
} break;
}
}
void FindInFilesContainer::_bar_input(const Ref<InputEvent> &p_input) {
int tab_id = tabs->get_tab_bar()->get_hovered_tab();
Ref<InputEventMouseButton> mb = p_input;
if (tab_id >= 0 && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
tabs_context_menu->set_item_disabled(tabs_context_menu->get_item_index(PANEL_CLOSE_RIGHT), tab_id == tabs->get_tab_count() - 1);
tabs_context_menu->set_position(tabs->get_tab_bar()->get_screen_position() + mb->get_position());
tabs_context_menu->reset_size();
tabs_context_menu->popup();
}
}
void FindInFilesContainer::update_layout(EditorDock::DockLayout p_layout) {
for (Node *node : tabs->iterate_children()) {
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(node);
if (panel) {
panel->update_layout(p_layout);
}
}
}
void FindInFilesContainer::_bind_methods() {
ADD_SIGNAL(MethodInfo("result_selected",
PropertyInfo(Variant::STRING, "path"),
PropertyInfo(Variant::INT, "line_number"),
PropertyInfo(Variant::INT, "begin"),
PropertyInfo(Variant::INT, "end")));
ADD_SIGNAL(MethodInfo("files_modified"));
}
FindInFilesContainer::FindInFilesContainer() {
set_name(TTRC("Search Results"));
set_icon_name("Search");
set_dock_shortcut(ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_search_results_bottom_panel", TTRC("Toggle Search Results Bottom Panel")));
set_default_slot(EditorDock::DOCK_SLOT_BOTTOM);
set_available_layouts(EditorDock::DOCK_LAYOUT_HORIZONTAL | EditorDock::DOCK_LAYOUT_FLOATING);
set_global(false);
set_transient(true);
set_closable(true);
set_custom_minimum_size(Size2(0, 200 * EDSCALE));
tabs = memnew(TabContainer);
tabs->set_tabs_visible(false);
add_child(tabs);
tabs->set_drag_to_rearrange_enabled(true);
tabs->get_tab_bar()->set_select_with_rmb(true);
tabs->get_tab_bar()->set_tab_close_display_policy(TabBar::CLOSE_BUTTON_SHOW_ACTIVE_ONLY);
tabs->get_tab_bar()->connect("tab_close_pressed", callable_mp(this, &FindInFilesContainer::_on_tab_close_pressed));
tabs->get_tab_bar()->connect(SceneStringName(gui_input), callable_mp(this, &FindInFilesContainer::_bar_input));
tabs_context_menu = memnew(PopupMenu);
add_child(tabs_context_menu);
tabs_context_menu->add_item(TTRC("Close Tab"), PANEL_CLOSE);
tabs_context_menu->add_item(TTRC("Close Other Tabs"), PANEL_CLOSE_OTHERS);
tabs_context_menu->add_item(TTRC("Close Tabs to the Right"), PANEL_CLOSE_RIGHT);
tabs_context_menu->add_item(TTRC("Close All Tabs"), PANEL_CLOSE_ALL);
tabs_context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FindInFilesContainer::_bar_menu_option));
EditorNode::get_singleton()->get_gui_base()->connect(SceneStringName(theme_changed), callable_mp(this, &FindInFilesContainer::_on_theme_changed));
}
void FindInFiles::_start_search(bool p_with_replace) {
FindInFilesPanel *panel = container->get_panel_for_results((p_with_replace ? TTR("Replace:") : TTR("Find:")) + " " + dialog->get_search_text());
FindInFilesSearch *search = panel->get_finder();
search->set_search_text(dialog->get_search_text());
search->set_match_case(dialog->is_match_case());
search->set_whole_words(dialog->is_whole_words());
search->set_folder(dialog->get_folder());
search->set_filter(dialog->get_filter());
search->set_includes(dialog->get_includes());
search->set_excludes(dialog->get_excludes());
panel->set_with_replace(p_with_replace);
panel->set_replace_text(dialog->get_replace_text());
panel->start_search();
container->make_visible();
}
void FindInFiles::open_dialog(const String &p_initial_text, bool p_replace) {
dialog->set_replace_mode(p_replace);
dialog->set_search_text(p_initial_text);
if (p_replace) {
dialog->set_replace_text(String());
}
dialog->popup_centered();
}
void FindInFiles::_result_selected(const String &p_fpath, int p_line_number, int p_begin, int p_end) {
emit_signal(SNAME("result_selected"), p_fpath, p_line_number, p_begin, p_end);
}
void FindInFiles::_files_modified() {
emit_signal(SNAME("files_modified"));
}
void FindInFiles::_bind_methods() {
ADD_SIGNAL(MethodInfo("result_selected",
PropertyInfo(Variant::STRING, "path"),
PropertyInfo(Variant::INT, "line_number"),
PropertyInfo(Variant::INT, "begin"),
PropertyInfo(Variant::INT, "end")));
ADD_SIGNAL(MethodInfo("files_modified"));
}
FindInFiles::FindInFiles() {
dialog = memnew(FindInFilesDialog);
dialog->connect("find_requested", callable_mp(this, &FindInFiles::_start_search).bind(false));
dialog->connect("replace_requested", callable_mp(this, &FindInFiles::_start_search).bind(true));
EditorNode::get_singleton()->get_gui_base()->add_child(dialog);
container = memnew(FindInFilesContainer);
EditorDockManager::get_singleton()->add_dock(container);
container->close();
container->connect("result_selected", callable_mp(this, &FindInFiles::_result_selected));
container->connect("files_modified", callable_mp(this, &FindInFiles::_files_modified));
}