Merge pull request #98278 from a-johnston/fuzzy-search-rebase

Add fuzzy string matching to quick open search
This commit is contained in:
Thaddeus Crews 2024-11-10 12:12:56 -06:00
commit 63838c936c
No known key found for this signature in database
GPG Key ID: 62181B86FE9E5D84
14 changed files with 2095 additions and 499 deletions

View File

@ -0,0 +1,349 @@
/**************************************************************************/
/* fuzzy_search.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 "fuzzy_search.h"
constexpr float cull_factor = 0.1f;
constexpr float cull_cutoff = 30.0f;
const String boundary_chars = "/\\-_.";
static bool _is_valid_interval(const Vector2i &p_interval) {
// Empty intervals are represented as (-1, -1).
return p_interval.x >= 0 && p_interval.y >= p_interval.x;
}
static Vector2i _extend_interval(const Vector2i &p_a, const Vector2i &p_b) {
if (!_is_valid_interval(p_a)) {
return p_b;
}
if (!_is_valid_interval(p_b)) {
return p_a;
}
return Vector2i(MIN(p_a.x, p_b.x), MAX(p_a.y, p_b.y));
}
static bool _is_word_boundary(const String &p_str, int p_index) {
if (p_index == -1 || p_index == p_str.size()) {
return true;
}
return boundary_chars.find_char(p_str[p_index]) != -1;
}
bool FuzzySearchToken::try_exact_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset) const {
p_match.token_idx = idx;
p_match.token_length = string.length();
int match_idx = p_target.find(string, p_offset);
if (match_idx == -1) {
return false;
}
p_match.add_substring(match_idx, string.length());
return true;
}
bool FuzzySearchToken::try_fuzzy_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset, int p_miss_budget) const {
p_match.token_idx = idx;
p_match.token_length = string.length();
int run_start = -1;
int run_len = 0;
// Search for the subsequence p_token in p_target starting from p_offset, recording each substring for
// later scoring and display.
for (int i = 0; i < string.length(); i++) {
int new_offset = p_target.find_char(string[i], p_offset);
if (new_offset < 0) {
p_miss_budget--;
if (p_miss_budget < 0) {
return false;
}
} else {
if (run_start == -1 || p_offset != new_offset) {
if (run_start != -1) {
p_match.add_substring(run_start, run_len);
}
run_start = new_offset;
run_len = 1;
} else {
run_len += 1;
}
p_offset = new_offset + 1;
}
}
if (run_start != -1) {
p_match.add_substring(run_start, run_len);
}
return true;
}
void FuzzyTokenMatch::add_substring(int p_substring_start, int p_substring_length) {
substrings.append(Vector2i(p_substring_start, p_substring_length));
matched_length += p_substring_length;
Vector2i substring_interval = { p_substring_start, p_substring_start + p_substring_length - 1 };
interval = _extend_interval(interval, substring_interval);
}
bool FuzzyTokenMatch::intersects(const Vector2i &p_other_interval) const {
if (!_is_valid_interval(interval) || !_is_valid_interval(p_other_interval)) {
return false;
}
return interval.y >= p_other_interval.x && interval.x <= p_other_interval.y;
}
bool FuzzySearchResult::can_add_token_match(const FuzzyTokenMatch &p_match) const {
if (p_match.get_miss_count() > miss_budget) {
return false;
}
if (p_match.intersects(match_interval)) {
if (token_matches.size() == 1) {
return false;
}
for (const FuzzyTokenMatch &existing_match : token_matches) {
if (existing_match.intersects(p_match.interval)) {
return false;
}
}
}
return true;
}
bool FuzzyTokenMatch::is_case_insensitive(const String &p_original, const String &p_adjusted) const {
for (const Vector2i &substr : substrings) {
const int end = substr.x + substr.y;
for (int i = substr.x; i < end; i++) {
if (p_original[i] != p_adjusted[i]) {
return true;
}
}
}
return false;
}
void FuzzySearchResult::score_token_match(FuzzyTokenMatch &p_match, bool p_case_insensitive) const {
// This can always be tweaked more. The intuition is that exact matches should almost always
// be prioritized over broken up matches, and other criteria more or less act as tie breakers.
p_match.score = -20 * p_match.get_miss_count() - (p_case_insensitive ? 3 : 0);
for (const Vector2i &substring : p_match.substrings) {
// Score longer substrings higher than short substrings.
int substring_score = substring.y * substring.y;
// Score matches deeper in path higher than shallower matches
if (substring.x > dir_index) {
substring_score *= 2;
}
// Score matches on a word boundary higher than matches within a word
if (_is_word_boundary(target, substring.x - 1) || _is_word_boundary(target, substring.x + substring.y)) {
substring_score += 4;
}
// Score exact query matches higher than non-compact subsequence matches
if (substring.y == p_match.token_length) {
substring_score += 100;
}
p_match.score += substring_score;
}
}
void FuzzySearchResult::maybe_apply_score_bonus() {
// This adds a small bonus to results which match tokens in the same order they appear in the query.
int *token_range_starts = (int *)alloca(sizeof(int) * token_matches.size());
for (const FuzzyTokenMatch &match : token_matches) {
token_range_starts[match.token_idx] = match.interval.x;
}
int last = token_range_starts[0];
for (int i = 1; i < token_matches.size(); i++) {
if (last > token_range_starts[i]) {
return;
}
last = token_range_starts[i];
}
score += 1;
}
void FuzzySearchResult::add_token_match(const FuzzyTokenMatch &p_match) {
score += p_match.score;
match_interval = _extend_interval(match_interval, p_match.interval);
miss_budget -= p_match.get_miss_count();
token_matches.append(p_match);
}
void remove_low_scores(Vector<FuzzySearchResult> &p_results, float p_cull_score) {
// Removes all results with score < p_cull_score in-place.
int i = 0;
int j = p_results.size() - 1;
FuzzySearchResult *results = p_results.ptrw();
while (true) {
// Advances i to an element to remove and j to an element to keep.
while (j >= i && results[j].score < p_cull_score) {
j--;
}
while (i < j && results[i].score >= p_cull_score) {
i++;
}
if (i >= j) {
break;
}
results[i++] = results[j--];
}
p_results.resize(j + 1);
}
void FuzzySearch::sort_and_filter(Vector<FuzzySearchResult> &p_results) const {
if (p_results.is_empty()) {
return;
}
float avg_score = 0;
float max_score = 0;
for (const FuzzySearchResult &result : p_results) {
avg_score += result.score;
max_score = MAX(max_score, result.score);
}
// TODO: Tune scoring and culling here to display fewer subsequence soup matches when good matches
// are available.
avg_score /= p_results.size();
float cull_score = MIN(cull_cutoff, Math::lerp(avg_score, max_score, cull_factor));
remove_low_scores(p_results, cull_score);
struct FuzzySearchResultComparator {
bool operator()(const FuzzySearchResult &p_lhs, const FuzzySearchResult &p_rhs) const {
// Sort on (score, length, alphanumeric) to ensure consistent ordering.
if (p_lhs.score == p_rhs.score) {
if (p_lhs.target.length() == p_rhs.target.length()) {
return p_lhs.target < p_rhs.target;
}
return p_lhs.target.length() < p_rhs.target.length();
}
return p_lhs.score > p_rhs.score;
}
};
SortArray<FuzzySearchResult, FuzzySearchResultComparator> sorter;
if (p_results.size() > max_results) {
sorter.partial_sort(0, p_results.size(), max_results, p_results.ptrw());
p_results.resize(max_results);
} else {
sorter.sort(p_results.ptrw(), p_results.size());
}
}
void FuzzySearch::set_query(const String &p_query) {
tokens.clear();
for (const String &string : p_query.split(" ", false)) {
tokens.append({ static_cast<int>(tokens.size()), string });
}
case_sensitive = !p_query.is_lowercase();
struct TokenComparator {
bool operator()(const FuzzySearchToken &A, const FuzzySearchToken &B) const {
if (A.string.length() == B.string.length()) {
return A.idx < B.idx;
}
return A.string.length() > B.string.length();
}
};
// Prioritize matching longer tokens before shorter ones since match overlaps are not accepted.
tokens.sort_custom<TokenComparator>();
}
bool FuzzySearch::search(const String &p_target, FuzzySearchResult &p_result) const {
p_result.target = p_target;
p_result.dir_index = p_target.rfind_char('/');
p_result.miss_budget = max_misses;
String adjusted_target = case_sensitive ? p_target : p_target.to_lower();
// For each token, eagerly generate subsequences starting from index 0 and keep the best scoring one
// which does not conflict with prior token matches. This is not ensured to find the highest scoring
// combination of matches, or necessarily the highest scoring single subsequence, as it only considers
// eager subsequences for a given index, and likewise eagerly finds matches for each token in sequence.
for (const FuzzySearchToken &token : tokens) {
FuzzyTokenMatch best_match;
int offset = start_offset;
while (true) {
FuzzyTokenMatch match;
if (allow_subsequences) {
if (!token.try_fuzzy_match(match, adjusted_target, offset, p_result.miss_budget)) {
break;
}
} else {
if (!token.try_exact_match(match, adjusted_target, offset)) {
break;
}
}
if (p_result.can_add_token_match(match)) {
p_result.score_token_match(match, match.is_case_insensitive(p_target, adjusted_target));
if (best_match.token_idx == -1 || best_match.score < match.score) {
best_match = match;
}
}
if (_is_valid_interval(match.interval)) {
offset = match.interval.x + 1;
} else {
break;
}
}
if (best_match.token_idx == -1) {
return false;
}
p_result.add_token_match(best_match);
}
p_result.maybe_apply_score_bonus();
return true;
}
void FuzzySearch::search_all(const PackedStringArray &p_targets, Vector<FuzzySearchResult> &p_results) const {
p_results.clear();
for (const String &target : p_targets) {
FuzzySearchResult result;
if (search(target, result)) {
p_results.append(result);
}
}
sort_and_filter(p_results);
}

101
core/string/fuzzy_search.h Normal file
View File

@ -0,0 +1,101 @@
/**************************************************************************/
/* fuzzy_search.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef FUZZY_SEARCH_H
#define FUZZY_SEARCH_H
#include "core/variant/variant.h"
class FuzzyTokenMatch;
struct FuzzySearchToken {
int idx = -1;
String string;
bool try_exact_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset) const;
bool try_fuzzy_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset, int p_miss_budget) const;
};
class FuzzyTokenMatch {
friend struct FuzzySearchToken;
friend class FuzzySearchResult;
friend class FuzzySearch;
int matched_length = 0;
int token_length = 0;
int token_idx = -1;
Vector2i interval = Vector2i(-1, -1); // x and y are both inclusive indices.
void add_substring(int p_substring_start, int p_substring_length);
bool intersects(const Vector2i &p_other_interval) const;
bool is_case_insensitive(const String &p_original, const String &p_adjusted) const;
int get_miss_count() const { return token_length - matched_length; }
public:
int score = 0;
Vector<Vector2i> substrings; // x is start index, y is length.
};
class FuzzySearchResult {
friend class FuzzySearch;
int miss_budget = 0;
Vector2i match_interval = Vector2i(-1, -1);
bool can_add_token_match(const FuzzyTokenMatch &p_match) const;
void score_token_match(FuzzyTokenMatch &p_match, bool p_case_insensitive) const;
void add_token_match(const FuzzyTokenMatch &p_match);
void maybe_apply_score_bonus();
public:
String target;
int score = 0;
int dir_index = -1;
Vector<FuzzyTokenMatch> token_matches;
};
class FuzzySearch {
Vector<FuzzySearchToken> tokens;
void sort_and_filter(Vector<FuzzySearchResult> &p_results) const;
public:
int start_offset = 0;
bool case_sensitive = false;
int max_results = 100;
int max_misses = 2;
bool allow_subsequences = true;
void set_query(const String &p_query);
bool search(const String &p_target, FuzzySearchResult &p_result) const;
void search_all(const PackedStringArray &p_targets, Vector<FuzzySearchResult> &p_results) const;
};
#endif // FUZZY_SEARCH_H

View File

@ -3387,7 +3387,7 @@ int String::find(const char *p_str, int p_from) const {
return -1;
}
int String::find_char(const char32_t &p_char, int p_from) const {
int String::find_char(char32_t p_char, int p_from) const {
return _cowdata.find(p_char, p_from);
}
@ -3624,6 +3624,10 @@ int String::rfind(const char *p_str, int p_from) const {
return -1;
}
int String::rfind_char(char32_t p_char, int p_from) const {
return _cowdata.rfind(p_char, p_from);
}
int String::rfindn(const String &p_str, int p_from) const {
// establish a limit
int limit = length() - p_str.length();
@ -3837,6 +3841,15 @@ bool String::is_quoted() const {
return is_enclosed_in("\"") || is_enclosed_in("'");
}
bool String::is_lowercase() const {
for (const char32_t *str = &operator[](0); *str; str++) {
if (is_unicode_upper_case(*str)) {
return false;
}
}
return true;
}
int String::_count(const String &p_string, int p_from, int p_to, bool p_case_insensitive) const {
if (p_string.is_empty()) {
return 0;

View File

@ -287,11 +287,12 @@ public:
String substr(int p_from, int p_chars = -1) const;
int find(const String &p_str, int p_from = 0) const; ///< return <0 if failed
int find(const char *p_str, int p_from = 0) const; ///< return <0 if failed
int find_char(const char32_t &p_char, int p_from = 0) const; ///< return <0 if failed
int find_char(char32_t p_char, int p_from = 0) const; ///< return <0 if failed
int findn(const String &p_str, int p_from = 0) const; ///< return <0 if failed, case insensitive
int findn(const char *p_str, int p_from = 0) const; ///< return <0 if failed
int rfind(const String &p_str, int p_from = -1) const; ///< return <0 if failed
int rfind(const char *p_str, int p_from = -1) const; ///< return <0 if failed
int rfind_char(char32_t p_char, int p_from = -1) const; ///< return <0 if failed
int rfindn(const String &p_str, int p_from = -1) const; ///< return <0 if failed, case insensitive
int rfindn(const char *p_str, int p_from = -1) const; ///< return <0 if failed
int findmk(const Vector<String> &p_keys, int p_from = 0, int *r_key = nullptr) const; ///< return <0 if failed
@ -305,6 +306,7 @@ public:
bool is_subsequence_of(const String &p_string) const;
bool is_subsequence_ofn(const String &p_string) const;
bool is_quoted() const;
bool is_lowercase() const;
Vector<String> bigrams() const;
float similarity(const String &p_string) const;
String format(const Variant &values, const String &placeholder = "{_}") const;

View File

@ -717,9 +717,21 @@
<member name="filesystem/quick_open_dialog/default_display_mode" type="int" setter="" getter="">
If set to [code]Adaptive[/code], the dialog opens in list view or grid view depending on the requested type. If set to [code]Last Used[/code], the display mode will always open the way you last used it.
</member>
<member name="filesystem/quick_open_dialog/enable_fuzzy_matching" type="bool" setter="" getter="">
If [code]true[/code], fuzzy matching of search tokens is allowed.
</member>
<member name="filesystem/quick_open_dialog/include_addons" type="bool" setter="" getter="">
If [code]true[/code], results will include files located in the [code]addons[/code] folder.
</member>
<member name="filesystem/quick_open_dialog/max_fuzzy_misses" type="int" setter="" getter="">
The number of allowed missed query characters in a match, if fuzzy matching is enabled. For example, with the default value of 2, [code]foobar[/code] would match [code]foobur[/code] and [code]foob[/code] but not [code]foo[/code].
</member>
<member name="filesystem/quick_open_dialog/max_results" type="int" setter="" getter="">
Maximum number of matches to show in dialog.
</member>
<member name="filesystem/quick_open_dialog/show_search_highlight" type="bool" setter="" getter="">
If [code]true[/code], results will be highlighted with their search matches.
</member>
<member name="filesystem/tools/oidn/oidn_denoise_path" type="String" setter="" getter="">
The path to the directory containing the Open Image Denoise (OIDN) executable, used optionally for denoising lightmaps. It can be downloaded from [url=https://www.openimagedenoise.org/downloads.html]openimagedenoise.org[/url].
To enable this feature for your specific project, use [member ProjectSettings.rendering/lightmapping/denoising/denoiser].

View File

@ -602,6 +602,10 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/file_dialog/thumbnail_size", 64, "32,128,16")
// Quick Open dialog
EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/quick_open_dialog/max_results", 100, "0,10000,1", PROPERTY_USAGE_DEFAULT)
_initial_set("filesystem/quick_open_dialog/show_search_highlight", true);
_initial_set("filesystem/quick_open_dialog/enable_fuzzy_matching", true);
EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/quick_open_dialog/max_fuzzy_misses", 2, "0,10,1", PROPERTY_USAGE_DEFAULT)
_initial_set("filesystem/quick_open_dialog/include_addons", false);
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "filesystem/quick_open_dialog/default_display_mode", 0, "Adaptive,Last Used")

View File

@ -30,6 +30,7 @@
#include "editor_quick_open_dialog.h"
#include "core/string/fuzzy_search.h"
#include "editor/editor_file_system.h"
#include "editor/editor_node.h"
#include "editor/editor_resource_preview.h"
@ -45,6 +46,55 @@
#include "scene/gui/texture_rect.h"
#include "scene/gui/tree.h"
void HighlightedLabel::draw_substr_rects(const Vector2i &p_substr, Vector2 p_offset, int p_line_limit, int line_spacing) {
for (int i = get_lines_skipped(); i < p_line_limit; i++) {
RID line = get_line_rid(i);
Vector<Vector2> ranges = TS->shaped_text_get_selection(line, p_substr.x, p_substr.x + p_substr.y);
Rect2 line_rect = get_line_rect(i);
for (const Vector2 &range : ranges) {
Rect2 rect = Rect2(Point2(range.x, 0) + line_rect.position, Size2(range.y - range.x, line_rect.size.y));
rect.position = p_offset + line_rect.position;
rect.position.x += range.x;
rect.size = Size2(range.y - range.x, line_rect.size.y);
rect.size.x = MIN(rect.size.x, line_rect.size.x - range.x);
if (rect.size.x > 0) {
draw_rect(rect, Color(1, 1, 1, 0.07), true);
draw_rect(rect, Color(0.5, 0.7, 1.0, 0.4), false, 1);
}
}
p_offset.y += line_spacing + TS->shaped_text_get_ascent(line) + TS->shaped_text_get_descent(line);
}
}
void HighlightedLabel::add_highlight(const Vector2i &p_interval) {
if (p_interval.y > 0) {
highlights.append(p_interval);
queue_redraw();
}
}
void HighlightedLabel::reset_highlights() {
highlights.clear();
queue_redraw();
}
void HighlightedLabel::_notification(int p_notification) {
if (p_notification == NOTIFICATION_DRAW) {
if (highlights.is_empty()) {
return;
}
Vector2 offset;
int line_limit;
int line_spacing;
get_layout_data(offset, line_limit, line_spacing);
for (const Vector2i &substr : highlights) {
draw_substr_rects(substr, offset, line_limit, line_spacing);
}
}
}
EditorQuickOpenDialog::EditorQuickOpenDialog() {
VBoxContainer *vbc = memnew(VBoxContainer);
vbc->add_theme_constant_override("separation", 0);
@ -100,7 +150,7 @@ void EditorQuickOpenDialog::popup_dialog(const Vector<StringName> &p_base_types,
get_ok_button()->set_disabled(container->has_nothing_selected());
set_title(get_dialog_title(p_base_types));
popup_centered_clamped(Size2(710, 650) * EDSCALE, 0.8f);
popup_centered_clamped(Size2(780, 650) * EDSCALE, 0.8f);
search_box->grab_focus();
}
@ -119,13 +169,18 @@ void EditorQuickOpenDialog::cancel_pressed() {
}
void EditorQuickOpenDialog::_search_box_text_changed(const String &p_query) {
container->update_results(p_query.to_lower());
container->set_query_and_update(p_query);
get_ok_button()->set_disabled(container->has_nothing_selected());
}
//------------------------- Result Container
void style_button(Button *p_button) {
p_button->set_flat(true);
p_button->set_focus_mode(Control::FOCUS_NONE);
p_button->set_default_cursor_shape(Control::CURSOR_POINTING_HAND);
}
QuickOpenResultContainer::QuickOpenResultContainer() {
set_h_size_flags(Control::SIZE_EXPAND_FILL);
set_v_size_flags(Control::SIZE_EXPAND_FILL);
@ -175,91 +230,107 @@ QuickOpenResultContainer::QuickOpenResultContainer() {
}
{
// Bottom bar
HBoxContainer *bottom_bar = memnew(HBoxContainer);
add_child(bottom_bar);
// Selected filepath
file_details_path = memnew(Label);
file_details_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
file_details_path->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
file_details_path->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
bottom_bar->add_child(file_details_path);
{
HBoxContainer *hbc = memnew(HBoxContainer);
hbc->add_theme_constant_override("separation", 3);
bottom_bar->add_child(hbc);
include_addons_toggle = memnew(CheckButton);
include_addons_toggle->set_flat(true);
include_addons_toggle->set_focus_mode(Control::FOCUS_NONE);
include_addons_toggle->set_default_cursor_shape(CURSOR_POINTING_HAND);
include_addons_toggle->set_tooltip_text(TTR("Include files from addons"));
include_addons_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_include_addons));
hbc->add_child(include_addons_toggle);
VSeparator *vsep = memnew(VSeparator);
vsep->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
vsep->set_custom_minimum_size(Size2i(0, 14 * EDSCALE));
hbc->add_child(vsep);
display_mode_toggle = memnew(Button);
display_mode_toggle->set_flat(true);
display_mode_toggle->set_focus_mode(Control::FOCUS_NONE);
display_mode_toggle->set_default_cursor_shape(CURSOR_POINTING_HAND);
display_mode_toggle->connect(SceneStringName(pressed), callable_mp(this, &QuickOpenResultContainer::_toggle_display_mode));
hbc->add_child(display_mode_toggle);
}
add_child(file_details_path);
}
// Creating and deleting nodes while searching is slow, so we allocate
// a bunch of result nodes and fill in the content based on result ranking.
result_items.resize(TOTAL_ALLOCATED_RESULT_ITEMS);
for (int i = 0; i < TOTAL_ALLOCATED_RESULT_ITEMS; i++) {
QuickOpenResultItem *item = memnew(QuickOpenResultItem);
item->connect(SceneStringName(gui_input), callable_mp(this, &QuickOpenResultContainer::_item_input).bind(i));
result_items.write[i] = item;
{
// Bottom bar
HBoxContainer *bottom_bar = memnew(HBoxContainer);
bottom_bar->set_h_size_flags(Control::SIZE_EXPAND_FILL);
bottom_bar->set_alignment(ALIGNMENT_END);
bottom_bar->add_theme_constant_override("separation", 3);
add_child(bottom_bar);
fuzzy_search_toggle = memnew(CheckButton);
style_button(fuzzy_search_toggle);
fuzzy_search_toggle->set_text(TTR("Fuzzy Search"));
fuzzy_search_toggle->set_tooltip_text(TTR("Enable fuzzy matching"));
fuzzy_search_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_fuzzy_search));
bottom_bar->add_child(fuzzy_search_toggle);
include_addons_toggle = memnew(CheckButton);
style_button(include_addons_toggle);
include_addons_toggle->set_text(TTR("Addons"));
include_addons_toggle->set_tooltip_text(TTR("Include files from addons"));
include_addons_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_include_addons));
bottom_bar->add_child(include_addons_toggle);
VSeparator *vsep = memnew(VSeparator);
vsep->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
vsep->set_custom_minimum_size(Size2i(0, 14 * EDSCALE));
bottom_bar->add_child(vsep);
display_mode_toggle = memnew(Button);
style_button(display_mode_toggle);
display_mode_toggle->connect(SceneStringName(pressed), callable_mp(this, &QuickOpenResultContainer::_toggle_display_mode));
bottom_bar->add_child(display_mode_toggle);
}
}
QuickOpenResultContainer::~QuickOpenResultContainer() {
if (never_opened) {
for (QuickOpenResultItem *E : result_items) {
memdelete(E);
void QuickOpenResultContainer::_ensure_result_vector_capacity() {
int target_size = EDITOR_GET("filesystem/quick_open_dialog/max_results");
int initial_size = result_items.size();
for (int i = target_size; i < initial_size; i++) {
result_items[i]->queue_free();
}
result_items.resize(target_size);
for (int i = initial_size; i < target_size; i++) {
QuickOpenResultItem *item = memnew(QuickOpenResultItem);
item->connect(SceneStringName(gui_input), callable_mp(this, &QuickOpenResultContainer::_item_input).bind(i));
result_items.write[i] = item;
if (!never_opened) {
_layout_result_item(item);
}
}
}
void QuickOpenResultContainer::init(const Vector<StringName> &p_base_types) {
_ensure_result_vector_capacity();
base_types = p_base_types;
never_opened = false;
const int display_mode_behavior = EDITOR_GET("filesystem/quick_open_dialog/default_display_mode");
const bool adaptive_display_mode = (display_mode_behavior == 0);
if (adaptive_display_mode) {
_set_display_mode(get_adaptive_display_mode(p_base_types));
} else if (never_opened) {
int last = EditorSettings::get_singleton()->get_project_metadata("quick_open_dialog", "last_mode", (int)QuickOpenDisplayMode::LIST);
_set_display_mode((QuickOpenDisplayMode)last);
}
const bool fuzzy_matching = EDITOR_GET("filesystem/quick_open_dialog/enable_fuzzy_matching");
const bool include_addons = EDITOR_GET("filesystem/quick_open_dialog/include_addons");
fuzzy_search_toggle->set_pressed_no_signal(fuzzy_matching);
include_addons_toggle->set_pressed_no_signal(include_addons);
never_opened = false;
_create_initial_results(include_addons);
const bool enable_highlights = EDITOR_GET("filesystem/quick_open_dialog/show_search_highlight");
for (QuickOpenResultItem *E : result_items) {
E->enable_highlights = enable_highlights;
}
_create_initial_results();
}
void QuickOpenResultContainer::_create_initial_results(bool p_include_addons) {
file_type_icons.insert("__default_icon", get_editor_theme_icon(SNAME("Object")));
_find_candidates_in_folder(EditorFileSystem::get_singleton()->get_filesystem(), p_include_addons);
max_total_results = MIN(candidates.size(), TOTAL_ALLOCATED_RESULT_ITEMS);
void QuickOpenResultContainer::_create_initial_results() {
file_type_icons.clear();
update_results(query);
file_type_icons.insert("__default_icon", get_editor_theme_icon(SNAME("Object")));
filepaths.clear();
filetypes.clear();
_find_filepaths_in_folder(EditorFileSystem::get_singleton()->get_filesystem(), include_addons_toggle->is_pressed());
max_total_results = MIN(filepaths.size(), result_items.size());
update_results();
}
void QuickOpenResultContainer::_find_candidates_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons) {
void QuickOpenResultContainer::_find_filepaths_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons) {
for (int i = 0; i < p_directory->get_subdir_count(); i++) {
if (p_include_addons || p_directory->get_name() != "addons") {
_find_candidates_in_folder(p_directory->get_subdir(i), p_include_addons);
_find_filepaths_in_folder(p_directory->get_subdir(i), p_include_addons);
}
}
@ -276,146 +347,91 @@ void QuickOpenResultContainer::_find_candidates_in_folder(EditorFileSystemDirect
bool is_valid = ClassDB::is_parent_class(engine_type, parent_type) || (!is_engine_type && EditorNode::get_editor_data().script_class_is_parent(script_type, parent_type));
if (is_valid) {
Candidate c;
c.file_name = file_path.get_file();
c.file_directory = file_path.get_base_dir();
EditorResourcePreview::PreviewItem item = EditorResourcePreview::get_singleton()->get_resource_preview_if_available(file_path);
if (item.preview.is_valid()) {
c.thumbnail = item.preview;
} else if (file_type_icons.has(actual_type)) {
c.thumbnail = *file_type_icons.lookup_ptr(actual_type);
} else if (has_theme_icon(actual_type, EditorStringName(EditorIcons))) {
c.thumbnail = get_editor_theme_icon(actual_type);
file_type_icons.insert(actual_type, c.thumbnail);
} else {
c.thumbnail = *file_type_icons.lookup_ptr("__default_icon");
}
candidates.push_back(c);
filepaths.append(file_path);
filetypes.insert(file_path, actual_type);
break; // Stop testing base types as soon as we get a match.
}
}
}
}
void QuickOpenResultContainer::update_results(const String &p_query) {
void QuickOpenResultContainer::set_query_and_update(const String &p_query) {
query = p_query;
int relevant_candidates = _sort_candidates(p_query);
_update_result_items(MIN(relevant_candidates, max_total_results), 0);
update_results();
}
int QuickOpenResultContainer::_sort_candidates(const String &p_query) {
if (p_query.is_empty()) {
return 0;
void QuickOpenResultContainer::_setup_candidate(QuickOpenResultCandidate &candidate, const String &filepath) {
StringName actual_type = *filetypes.lookup_ptr(filepath);
candidate.file_path = filepath;
candidate.result = nullptr;
EditorResourcePreview::PreviewItem item = EditorResourcePreview::get_singleton()->get_resource_preview_if_available(filepath);
if (item.preview.is_valid()) {
candidate.thumbnail = item.preview;
} else if (file_type_icons.has(actual_type)) {
candidate.thumbnail = *file_type_icons.lookup_ptr(actual_type);
} else if (has_theme_icon(actual_type, EditorStringName(EditorIcons))) {
candidate.thumbnail = get_editor_theme_icon(actual_type);
file_type_icons.insert(actual_type, candidate.thumbnail);
} else {
candidate.thumbnail = *file_type_icons.lookup_ptr("__default_icon");
}
}
const PackedStringArray search_tokens = p_query.to_lower().replace("/", " ").split(" ", false);
void QuickOpenResultContainer::_setup_candidate(QuickOpenResultCandidate &p_candidate, const FuzzySearchResult &p_result) {
_setup_candidate(p_candidate, p_result.target);
p_candidate.result = &p_result;
}
if (search_tokens.is_empty()) {
return 0;
void QuickOpenResultContainer::update_results() {
showing_history = false;
candidates.clear();
if (query.is_empty()) {
_use_default_candidates();
} else {
_score_and_sort_candidates();
}
_update_result_items(MIN(candidates.size(), max_total_results), 0);
}
// First, we assign a score to each candidate.
int num_relevant_candidates = 0;
for (Candidate &c : candidates) {
c.score = 0;
int prev_token_match_pos = -1;
for (const String &token : search_tokens) {
const int file_pos = c.file_name.findn(token);
const int dir_pos = c.file_directory.findn(token);
const bool file_match = file_pos > -1;
const bool dir_match = dir_pos > -1;
if (!file_match && !dir_match) {
c.score = -1.0f;
break;
}
float token_score = file_match ? 0.6f : 0.1999f;
// Add bias for shorter filenames/paths: they resemble the query more.
const String &matched_string = file_match ? c.file_name : c.file_directory;
int matched_string_token_pos = file_match ? file_pos : dir_pos;
token_score += 0.1f * (1.0f - ((float)matched_string_token_pos / (float)matched_string.length()));
// Add bias if the match happened in the file name, not the extension.
if (file_match) {
int ext_pos = matched_string.rfind(".");
if (ext_pos == -1 || ext_pos > matched_string_token_pos) {
token_score += 0.1f;
}
}
// Add bias if token is in order.
{
int candidate_string_token_pos = file_match ? (c.file_directory.length() + file_pos) : dir_pos;
if (prev_token_match_pos != -1 && candidate_string_token_pos > prev_token_match_pos) {
token_score += 0.2f;
}
prev_token_match_pos = candidate_string_token_pos;
}
c.score += token_score;
void QuickOpenResultContainer::_use_default_candidates() {
if (filepaths.size() <= SHOW_ALL_FILES_THRESHOLD) {
candidates.resize(filepaths.size());
QuickOpenResultCandidate *candidates_write = candidates.ptrw();
for (const String &filepath : filepaths) {
_setup_candidate(*candidates_write++, filepath);
}
if (c.score > 0.0f) {
num_relevant_candidates++;
} else if (base_types.size() == 1) {
Vector<QuickOpenResultCandidate> *history = selected_history.lookup_ptr(base_types[0]);
if (history) {
showing_history = true;
candidates.append_array(*history);
}
}
}
// Now we will sort the candidates based on score, resolving ties by favoring:
// 1. Shorter file length.
// 2. Shorter directory length.
// 3. Lower alphabetic order.
struct CandidateComparator {
_FORCE_INLINE_ bool operator()(const Candidate &p_a, const Candidate &p_b) const {
if (!Math::is_equal_approx(p_a.score, p_b.score)) {
return p_a.score > p_b.score;
}
void QuickOpenResultContainer::_update_fuzzy_search_results() {
FuzzySearch fuzzy_search;
fuzzy_search.start_offset = 6; // Don't match against "res://" at the start of each filepath.
fuzzy_search.set_query(query);
fuzzy_search.max_results = max_total_results;
bool fuzzy_matching = EDITOR_GET("filesystem/quick_open_dialog/enable_fuzzy_matching");
int max_misses = EDITOR_GET("filesystem/quick_open_dialog/max_fuzzy_misses");
fuzzy_search.allow_subsequences = fuzzy_matching;
fuzzy_search.max_misses = fuzzy_matching ? max_misses : 0;
fuzzy_search.search_all(filepaths, search_results);
}
if (p_a.file_name.length() != p_b.file_name.length()) {
return p_a.file_name.length() < p_b.file_name.length();
}
if (p_a.file_directory.length() != p_b.file_directory.length()) {
return p_a.file_directory.length() < p_b.file_directory.length();
}
return p_a.file_name < p_b.file_name;
}
};
candidates.sort_custom<CandidateComparator>();
return num_relevant_candidates;
void QuickOpenResultContainer::_score_and_sort_candidates() {
_update_fuzzy_search_results();
candidates.resize(search_results.size());
QuickOpenResultCandidate *candidates_write = candidates.ptrw();
for (const FuzzySearchResult &result : search_results) {
_setup_candidate(*candidates_write++, result);
}
}
void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_count, int p_new_selection_index) {
List<Candidate> *type_history = nullptr;
showing_history = false;
if (query.is_empty()) {
if (candidates.size() <= SHOW_ALL_FILES_THRESHOLD) {
p_new_visible_results_count = candidates.size();
} else {
p_new_visible_results_count = 0;
if (base_types.size() == 1) {
type_history = selected_history.lookup_ptr(base_types[0]);
if (type_history) {
p_new_visible_results_count = type_history->size();
showing_history = true;
}
}
}
}
// Only need to update items that were not hidden in previous update.
int num_items_needing_updates = MAX(num_visible_results, p_new_visible_results_count);
num_visible_results = p_new_visible_results_count;
@ -424,13 +440,7 @@ void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_co
QuickOpenResultItem *item = result_items[i];
if (i < num_visible_results) {
if (type_history) {
const Candidate &c = type_history->get(i);
item->set_content(c.thumbnail, c.file_name, c.file_directory);
} else {
const Candidate &c = candidates[i];
item->set_content(c.thumbnail, c.file_name, c.file_directory);
}
item->set_content(candidates[i]);
} else {
item->reset();
}
@ -443,7 +453,7 @@ void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_co
no_results_container->set_visible(!any_results);
if (!any_results) {
if (candidates.is_empty()) {
if (filepaths.is_empty()) {
no_results_label->set_text(TTR("No files found for this type"));
} else if (query.is_empty()) {
no_results_label->set_text(TTR("Start searching to find files..."));
@ -471,10 +481,12 @@ void QuickOpenResultContainer::handle_search_box_input(const Ref<InputEvent> &p_
} break;
case Key::LEFT:
case Key::RIGHT: {
// Both grid and the search box use left/right keys. By default, grid will take it.
// It would be nice if we could check for ALT to give the event to the searchbox cursor.
// However, if you press ALT, the searchbox also denies the input.
move_selection = (content_display_mode == QuickOpenDisplayMode::GRID);
if (content_display_mode == QuickOpenDisplayMode::GRID) {
// Maybe strip off the shift modifier to allow non-selecting navigation by character?
if (key_event->get_modifiers_mask() == 0) {
move_selection = true;
}
}
} break;
default:
break; // Let the event through so it will reach the search box.
@ -566,11 +578,15 @@ void QuickOpenResultContainer::_item_input(const Ref<InputEvent> &p_ev, int p_in
}
}
void QuickOpenResultContainer::_toggle_fuzzy_search(bool p_pressed) {
EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/enable_fuzzy_matching", p_pressed);
update_results();
}
void QuickOpenResultContainer::_toggle_include_addons(bool p_pressed) {
EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/include_addons", p_pressed);
cleanup();
_create_initial_results(p_pressed);
_create_initial_results();
}
void QuickOpenResultContainer::_toggle_display_mode() {
@ -578,41 +594,41 @@ void QuickOpenResultContainer::_toggle_display_mode() {
_set_display_mode(new_display_mode);
}
void QuickOpenResultContainer::_set_display_mode(QuickOpenDisplayMode p_display_mode) {
content_display_mode = p_display_mode;
CanvasItem *QuickOpenResultContainer::_get_result_root() {
if (content_display_mode == QuickOpenDisplayMode::LIST) {
return list;
} else {
return grid;
}
}
const bool show_list = (content_display_mode == QuickOpenDisplayMode::LIST);
if ((show_list && list->is_visible()) || (!show_list && grid->is_visible())) {
void QuickOpenResultContainer::_layout_result_item(QuickOpenResultItem *item) {
item->set_display_mode(content_display_mode);
Node *parent = item->get_parent();
if (parent) {
parent->remove_child(item);
}
_get_result_root()->add_child(item);
}
void QuickOpenResultContainer::_set_display_mode(QuickOpenDisplayMode p_display_mode) {
CanvasItem *prev_root = _get_result_root();
if (prev_root->is_visible() && content_display_mode == p_display_mode) {
return;
}
hide();
content_display_mode = p_display_mode;
CanvasItem *next_root = _get_result_root();
// Move result item nodes from one container to the other.
CanvasItem *prev_root;
CanvasItem *next_root;
if (content_display_mode == QuickOpenDisplayMode::LIST) {
prev_root = Object::cast_to<CanvasItem>(grid);
next_root = Object::cast_to<CanvasItem>(list);
} else {
prev_root = Object::cast_to<CanvasItem>(list);
next_root = Object::cast_to<CanvasItem>(grid);
}
const bool first_time = !list->is_visible() && !grid->is_visible();
EditorSettings::get_singleton()->set_project_metadata("quick_open_dialog", "last_mode", (int)content_display_mode);
prev_root->hide();
for (QuickOpenResultItem *item : result_items) {
item->set_display_mode(content_display_mode);
if (!first_time) {
prev_root->remove_child(item);
}
next_root->add_child(item);
}
next_root->show();
show();
for (QuickOpenResultItem *item : result_items) {
_layout_result_item(item);
}
_update_result_items(num_visible_results, selection_index);
@ -631,16 +647,7 @@ bool QuickOpenResultContainer::has_nothing_selected() const {
String QuickOpenResultContainer::get_selected() const {
ERR_FAIL_COND_V_MSG(has_nothing_selected(), String(), "Tried to get selected file, but nothing was selected.");
if (showing_history) {
const List<Candidate> *type_history = selected_history.lookup_ptr(base_types[0]);
const Candidate &c = type_history->get(selection_index);
return c.file_directory.path_join(c.file_name);
} else {
const Candidate &c = candidates[selection_index];
return c.file_directory.path_join(c.file_name);
}
return candidates[selection_index].file_path;
}
QuickOpenDisplayMode QuickOpenResultContainer::get_adaptive_display_mode(const Vector<StringName> &p_base_types) {
@ -669,32 +676,27 @@ void QuickOpenResultContainer::save_selected_item() {
return;
}
if (showing_history) {
// Selecting from history, so already added.
return;
}
const StringName &base_type = base_types[0];
const QuickOpenResultCandidate &selected = candidates[selection_index];
Vector<QuickOpenResultCandidate> *type_history = selected_history.lookup_ptr(base_type);
List<Candidate> *type_history = selected_history.lookup_ptr(base_type);
if (!type_history) {
selected_history.insert(base_type, List<Candidate>());
selected_history.insert(base_type, Vector<QuickOpenResultCandidate>());
type_history = selected_history.lookup_ptr(base_type);
} else {
const Candidate &selected = candidates[selection_index];
for (const Candidate &candidate : *type_history) {
if (candidate.file_directory == selected.file_directory && candidate.file_name == selected.file_name) {
return;
for (int i = 0; i < type_history->size(); i++) {
if (selected.file_path == type_history->get(i).file_path) {
type_history->remove_at(i);
break;
}
}
if (type_history->size() > 8) {
type_history->pop_back();
}
}
type_history->push_front(candidates[selection_index]);
type_history->insert(0, selected);
type_history->ptrw()->result = nullptr;
if (type_history->size() > MAX_HISTORY_SIZE) {
type_history->resize(MAX_HISTORY_SIZE);
}
}
void QuickOpenResultContainer::cleanup() {
@ -748,36 +750,35 @@ QuickOpenResultItem::QuickOpenResultItem() {
void QuickOpenResultItem::set_display_mode(QuickOpenDisplayMode p_display_mode) {
if (p_display_mode == QuickOpenDisplayMode::LIST) {
grid_item->hide();
grid_item->reset();
list_item->show();
} else {
list_item->hide();
list_item->reset();
grid_item->show();
}
queue_redraw();
}
void QuickOpenResultItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file, const String &p_file_directory) {
void QuickOpenResultItem::set_content(const QuickOpenResultCandidate &p_candidate) {
_set_enabled(true);
if (list_item->is_visible()) {
list_item->set_content(p_thumbnail, p_file, p_file_directory);
list_item->set_content(p_candidate, enable_highlights);
} else {
grid_item->set_content(p_thumbnail, p_file);
grid_item->set_content(p_candidate, enable_highlights);
}
queue_redraw();
}
void QuickOpenResultItem::reset() {
_set_enabled(false);
is_hovering = false;
is_selected = false;
if (list_item->is_visible()) {
list_item->reset();
} else {
grid_item->reset();
}
list_item->reset();
grid_item->reset();
}
void QuickOpenResultItem::highlight_item(bool p_enabled) {
@ -830,6 +831,22 @@ void QuickOpenResultItem::_notification(int p_what) {
//----------------- List item
static Vector2i _get_path_interval(const Vector2i &p_interval, int p_dir_index) {
if (p_interval.x >= p_dir_index || p_interval.y < 1) {
return { -1, -1 };
}
return { p_interval.x, MIN(p_interval.x + p_interval.y, p_dir_index) - p_interval.x };
}
static Vector2i _get_name_interval(const Vector2i &p_interval, int p_dir_index) {
if (p_interval.x + p_interval.y <= p_dir_index || p_interval.y < 1) {
return { -1, -1 };
}
int first_name_idx = p_dir_index + 1;
int start = MAX(p_interval.x, first_name_idx);
return { start - first_name_idx, p_interval.y - start + p_interval.x };
}
QuickOpenResultListItem::QuickOpenResultListItem() {
set_h_size_flags(Control::SIZE_EXPAND_FILL);
add_theme_constant_override("separation", 4 * EDSCALE);
@ -857,13 +874,13 @@ QuickOpenResultListItem::QuickOpenResultListItem() {
text_container->set_v_size_flags(Control::SIZE_FILL);
add_child(text_container);
name = memnew(Label);
name = memnew(HighlightedLabel);
name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
name->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
name->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_LEFT);
text_container->add_child(name);
path = memnew(Label);
path = memnew(HighlightedLabel);
path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
path->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
path->add_theme_font_size_override(SceneStringName(font_size), 12 * EDSCALE);
@ -871,18 +888,29 @@ QuickOpenResultListItem::QuickOpenResultListItem() {
}
}
void QuickOpenResultListItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file, const String &p_file_directory) {
thumbnail->set_texture(p_thumbnail);
name->set_text(p_file);
path->set_text(p_file_directory);
void QuickOpenResultListItem::set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight) {
thumbnail->set_texture(p_candidate.thumbnail);
name->set_text(p_candidate.file_path.get_file());
path->set_text(p_candidate.file_path.get_base_dir());
name->reset_highlights();
path->reset_highlights();
if (p_highlight && p_candidate.result != nullptr) {
for (const FuzzyTokenMatch &match : p_candidate.result->token_matches) {
for (const Vector2i &interval : match.substrings) {
path->add_highlight(_get_path_interval(interval, p_candidate.result->dir_index));
name->add_highlight(_get_name_interval(interval, p_candidate.result->dir_index));
}
}
}
const int max_size = 32 * EDSCALE;
bool uses_icon = p_thumbnail->get_width() < max_size;
bool uses_icon = p_candidate.thumbnail->get_width() < max_size;
if (uses_icon) {
thumbnail->set_custom_minimum_size(p_thumbnail->get_size());
thumbnail->set_custom_minimum_size(p_candidate.thumbnail->get_size());
int margin_needed = (max_size - p_thumbnail->get_width()) / 2;
int margin_needed = (max_size - p_candidate.thumbnail->get_width()) / 2;
image_container->add_theme_constant_override("margin_left", CONTAINER_MARGIN + margin_needed);
image_container->add_theme_constant_override("margin_right", margin_needed);
} else {
@ -893,9 +921,11 @@ void QuickOpenResultListItem::set_content(const Ref<Texture2D> &p_thumbnail, con
}
void QuickOpenResultListItem::reset() {
name->set_text("");
thumbnail->set_texture(nullptr);
name->set_text("");
path->set_text("");
name->reset_highlights();
path->reset_highlights();
}
void QuickOpenResultListItem::highlight_item(const Color &p_color) {
@ -924,10 +954,10 @@ QuickOpenResultGridItem::QuickOpenResultGridItem() {
thumbnail = memnew(TextureRect);
thumbnail->set_h_size_flags(Control::SIZE_SHRINK_CENTER);
thumbnail->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
thumbnail->set_custom_minimum_size(Size2i(80 * EDSCALE, 64 * EDSCALE));
thumbnail->set_custom_minimum_size(Size2i(120 * EDSCALE, 64 * EDSCALE));
add_child(thumbnail);
name = memnew(Label);
name = memnew(HighlightedLabel);
name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
name->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
name->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
@ -935,16 +965,23 @@ QuickOpenResultGridItem::QuickOpenResultGridItem() {
add_child(name);
}
void QuickOpenResultGridItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file) {
thumbnail->set_texture(p_thumbnail);
void QuickOpenResultGridItem::set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight) {
thumbnail->set_texture(p_candidate.thumbnail);
name->set_text(p_candidate.file_path.get_file());
name->set_tooltip_text(p_candidate.file_path);
name->reset_highlights();
const String &file_name = p_file.get_basename();
name->set_text(file_name);
name->set_tooltip_text(file_name);
if (p_highlight && p_candidate.result != nullptr) {
for (const FuzzyTokenMatch &match : p_candidate.result->token_matches) {
for (const Vector2i &interval : match.substrings) {
name->add_highlight(_get_name_interval(interval, p_candidate.result->dir_index));
}
}
}
bool uses_icon = p_thumbnail->get_width() < (32 * EDSCALE);
bool uses_icon = p_candidate.thumbnail->get_width() < (32 * EDSCALE);
if (uses_icon || p_thumbnail->get_height() <= thumbnail->get_custom_minimum_size().y) {
if (uses_icon || p_candidate.thumbnail->get_height() <= thumbnail->get_custom_minimum_size().y) {
thumbnail->set_expand_mode(TextureRect::EXPAND_KEEP_SIZE);
thumbnail->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED);
} else {
@ -954,8 +991,9 @@ void QuickOpenResultGridItem::set_content(const Ref<Texture2D> &p_thumbnail, con
}
void QuickOpenResultGridItem::reset() {
name->set_text("");
thumbnail->set_texture(nullptr);
name->set_text("");
name->reset_highlights();
}
void QuickOpenResultGridItem::highlight_item(const Color &p_color) {

View File

@ -48,6 +48,8 @@ class Texture2D;
class TextureRect;
class VBoxContainer;
class FuzzySearchResult;
class QuickOpenResultItem;
enum class QuickOpenDisplayMode {
@ -55,13 +57,35 @@ enum class QuickOpenDisplayMode {
LIST,
};
struct QuickOpenResultCandidate {
String file_path;
Ref<Texture2D> thumbnail;
const FuzzySearchResult *result = nullptr;
};
class HighlightedLabel : public Label {
GDCLASS(HighlightedLabel, Label)
Vector<Vector2i> highlights;
void draw_substr_rects(const Vector2i &p_substr, Vector2 p_offset, int p_line_limit, int line_spacing);
public:
void add_highlight(const Vector2i &p_interval);
void reset_highlights();
protected:
void _notification(int p_notification);
};
class QuickOpenResultContainer : public VBoxContainer {
GDCLASS(QuickOpenResultContainer, VBoxContainer)
public:
void init(const Vector<StringName> &p_base_types);
void handle_search_box_input(const Ref<InputEvent> &p_ie);
void update_results(const String &p_query);
void set_query_and_update(const String &p_query);
void update_results();
bool has_nothing_selected() const;
String get_selected() const;
@ -70,27 +94,21 @@ public:
void cleanup();
QuickOpenResultContainer();
~QuickOpenResultContainer();
protected:
void _notification(int p_what);
private:
static const int TOTAL_ALLOCATED_RESULT_ITEMS = 100;
static const int SHOW_ALL_FILES_THRESHOLD = 30;
struct Candidate {
String file_name;
String file_directory;
Ref<Texture2D> thumbnail;
float score = 0;
};
static constexpr int SHOW_ALL_FILES_THRESHOLD = 30;
static constexpr int MAX_HISTORY_SIZE = 20;
Vector<FuzzySearchResult> search_results;
Vector<StringName> base_types;
Vector<Candidate> candidates;
Vector<String> filepaths;
OAHashMap<String, StringName> filetypes;
Vector<QuickOpenResultCandidate> candidates;
OAHashMap<StringName, List<Candidate>> selected_history;
OAHashMap<StringName, Vector<QuickOpenResultCandidate>> selected_history;
String query;
int selection_index = -1;
@ -114,15 +132,21 @@ private:
Label *file_details_path = nullptr;
Button *display_mode_toggle = nullptr;
CheckButton *include_addons_toggle = nullptr;
CheckButton *fuzzy_search_toggle = nullptr;
OAHashMap<StringName, Ref<Texture2D>> file_type_icons;
static QuickOpenDisplayMode get_adaptive_display_mode(const Vector<StringName> &p_base_types);
void _create_initial_results(bool p_include_addons);
void _find_candidates_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons);
void _ensure_result_vector_capacity();
void _create_initial_results();
void _find_filepaths_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons);
int _sort_candidates(const String &p_query);
void _setup_candidate(QuickOpenResultCandidate &p_candidate, const String &p_filepath);
void _setup_candidate(QuickOpenResultCandidate &p_candidate, const FuzzySearchResult &p_result);
void _update_fuzzy_search_results();
void _use_default_candidates();
void _score_and_sort_candidates();
void _update_result_items(int p_new_visible_results_count, int p_new_selection_index);
void _move_selection_index(Key p_key);
@ -130,9 +154,12 @@ private:
void _item_input(const Ref<InputEvent> &p_ev, int p_index);
CanvasItem *_get_result_root();
void _layout_result_item(QuickOpenResultItem *p_item);
void _set_display_mode(QuickOpenDisplayMode p_display_mode);
void _toggle_display_mode();
void _toggle_include_addons(bool p_pressed);
void _toggle_fuzzy_search(bool p_pressed);
static void _bind_methods();
};
@ -143,14 +170,14 @@ class QuickOpenResultGridItem : public VBoxContainer {
public:
QuickOpenResultGridItem();
void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name);
void reset();
void set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight);
void highlight_item(const Color &p_color);
void remove_highlight();
private:
TextureRect *thumbnail = nullptr;
Label *name = nullptr;
HighlightedLabel *name = nullptr;
};
class QuickOpenResultListItem : public HBoxContainer {
@ -159,8 +186,8 @@ class QuickOpenResultListItem : public HBoxContainer {
public:
QuickOpenResultListItem();
void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name, const String &p_file_directory);
void reset();
void set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight);
void highlight_item(const Color &p_color);
void remove_highlight();
@ -174,8 +201,8 @@ private:
VBoxContainer *text_container = nullptr;
TextureRect *thumbnail = nullptr;
Label *name = nullptr;
Label *path = nullptr;
HighlightedLabel *name = nullptr;
HighlightedLabel *path = nullptr;
};
class QuickOpenResultItem : public HBoxContainer {
@ -184,10 +211,11 @@ class QuickOpenResultItem : public HBoxContainer {
public:
QuickOpenResultItem();
void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name, const String &p_file_directory);
void set_display_mode(QuickOpenDisplayMode p_display_mode);
void reset();
bool enable_highlights = true;
void reset();
void set_content(const QuickOpenResultCandidate &p_candidate);
void set_display_mode(QuickOpenDisplayMode p_display_mode);
void highlight_item(bool p_enabled);
protected:

View File

@ -335,6 +335,121 @@ inline void draw_glyph_outline(const Glyph &p_gl, const RID &p_canvas, const Col
}
}
void Label::_ensure_shaped() const {
if (dirty || font_dirty || lines_dirty) {
const_cast<Label *>(this)->_shape();
}
}
RID Label::get_line_rid(int p_line) const {
return lines_rid[p_line];
}
Rect2 Label::get_line_rect(int p_line) const {
// Returns a rect providing the line's horizontal offset and total size. To determine the vertical
// offset, use r_offset and r_line_spacing from get_layout_data.
bool rtl = TS->shaped_text_get_inferred_direction(text_rid) == TextServer::DIRECTION_RTL;
bool rtl_layout = is_layout_rtl();
Ref<StyleBox> style = theme_cache.normal_style;
Size2 size = get_size();
Size2 line_size = TS->shaped_text_get_size(lines_rid[p_line]);
Vector2 offset;
switch (horizontal_alignment) {
case HORIZONTAL_ALIGNMENT_FILL:
if (rtl && autowrap_mode != TextServer::AUTOWRAP_OFF) {
offset.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
} else {
offset.x = style->get_offset().x;
}
break;
case HORIZONTAL_ALIGNMENT_LEFT: {
if (rtl_layout) {
offset.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
} else {
offset.x = style->get_offset().x;
}
} break;
case HORIZONTAL_ALIGNMENT_CENTER: {
offset.x = int(size.width - line_size.width) / 2;
} break;
case HORIZONTAL_ALIGNMENT_RIGHT: {
if (rtl_layout) {
offset.x = style->get_offset().x;
} else {
offset.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
}
} break;
}
return Rect2(offset, line_size);
}
void Label::get_layout_data(Vector2 &r_offset, int &r_line_limit, int &r_line_spacing) const {
// Computes several common parameters involved in laying out and rendering text set to this label.
// Only vertical margin is considered in r_offset: use get_line_rect to get the horizontal offset
// for a given line of text.
Size2 size = get_size();
Ref<StyleBox> style = theme_cache.normal_style;
int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing;
float total_h = 0.0;
int lines_visible = 0;
// Get number of lines to fit to the height.
for (int64_t i = lines_skipped; i < lines_rid.size(); i++) {
total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) {
break;
}
lines_visible++;
}
if (max_lines_visible >= 0 && lines_visible > max_lines_visible) {
lines_visible = max_lines_visible;
}
r_line_limit = MIN(lines_rid.size(), lines_visible + lines_skipped);
// Get real total height.
total_h = 0;
for (int64_t i = lines_skipped; i < r_line_limit; i++) {
total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
}
total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM);
int vbegin = 0, vsep = 0;
if (lines_visible > 0) {
switch (vertical_alignment) {
case VERTICAL_ALIGNMENT_TOP: {
// Nothing.
} break;
case VERTICAL_ALIGNMENT_CENTER: {
vbegin = (size.y - (total_h - line_spacing)) / 2;
vsep = 0;
} break;
case VERTICAL_ALIGNMENT_BOTTOM: {
vbegin = size.y - (total_h - line_spacing);
vsep = 0;
} break;
case VERTICAL_ALIGNMENT_FILL: {
vbegin = 0;
if (lines_visible > 1) {
vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1);
} else {
vsep = 0;
}
} break;
}
}
r_offset = { 0, style->get_offset().y + vbegin };
r_line_spacing = line_spacing + vsep;
}
PackedStringArray Label::get_configuration_warnings() const {
PackedStringArray warnings = Control::get_configuration_warnings();
@ -361,10 +476,7 @@ PackedStringArray Label::get_configuration_warnings() const {
}
if (font.is_valid()) {
if (dirty || font_dirty || lines_dirty) {
const_cast<Label *>(this)->_shape();
}
_ensure_shaped();
const Glyph *glyph = TS->shaped_text_get_glyphs(text_rid);
int64_t glyph_count = TS->shaped_text_get_glyph_count(text_rid);
for (int64_t i = 0; i < glyph_count; i++) {
@ -416,22 +528,17 @@ void Label::_notification(int p_what) {
}
}
if (dirty || font_dirty || lines_dirty) {
_shape();
}
_ensure_shaped();
RID ci = get_canvas_item();
bool has_settings = settings.is_valid();
Size2 string_size;
Size2 size = get_size();
Ref<StyleBox> style = theme_cache.normal_style;
Ref<Font> font = (has_settings && settings->get_font().is_valid()) ? settings->get_font() : theme_cache.font;
Color font_color = has_settings ? settings->get_font_color() : theme_cache.font_color;
Color font_shadow_color = has_settings ? settings->get_shadow_color() : theme_cache.font_shadow_color;
Point2 shadow_ofs = has_settings ? settings->get_shadow_offset() : theme_cache.font_shadow_offset;
int line_spacing = has_settings ? settings->get_line_spacing() : theme_cache.line_spacing;
Color font_outline_color = has_settings ? settings->get_outline_color() : theme_cache.font_outline_color;
int outline_size = has_settings ? settings->get_outline_size() : theme_cache.font_outline_size;
int shadow_outline_size = has_settings ? settings->get_shadow_size() : theme_cache.font_shadow_outline_size;
@ -440,98 +547,28 @@ void Label::_notification(int p_what) {
style->draw(ci, Rect2(Point2(0, 0), get_size()));
float total_h = 0.0;
int lines_visible = 0;
// Get number of lines to fit to the height.
for (int64_t i = lines_skipped; i < lines_rid.size(); i++) {
total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) {
break;
}
lines_visible++;
}
if (max_lines_visible >= 0 && lines_visible > max_lines_visible) {
lines_visible = max_lines_visible;
}
int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped);
bool trim_chars = (visible_chars >= 0) && (visible_chars_behavior == TextServer::VC_CHARS_AFTER_SHAPING);
bool trim_glyphs_ltr = (visible_chars >= 0) && ((visible_chars_behavior == TextServer::VC_GLYPHS_LTR) || ((visible_chars_behavior == TextServer::VC_GLYPHS_AUTO) && !rtl_layout));
bool trim_glyphs_rtl = (visible_chars >= 0) && ((visible_chars_behavior == TextServer::VC_GLYPHS_RTL) || ((visible_chars_behavior == TextServer::VC_GLYPHS_AUTO) && rtl_layout));
// Get real total height.
Vector2 ofs;
int line_limit;
int line_spacing;
get_layout_data(ofs, line_limit, line_spacing);
int processed_glyphs = 0;
int total_glyphs = 0;
total_h = 0;
for (int64_t i = lines_skipped; i < last_line; i++) {
total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
for (int64_t i = lines_skipped; i < line_limit; i++) {
total_glyphs += TS->shaped_text_get_glyph_count(lines_rid[i]) + TS->shaped_text_get_ellipsis_glyph_count(lines_rid[i]);
}
int visible_glyphs = total_glyphs * visible_ratio;
int processed_glyphs = 0;
total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM);
int vbegin = 0, vsep = 0;
if (lines_visible > 0) {
switch (vertical_alignment) {
case VERTICAL_ALIGNMENT_TOP: {
// Nothing.
} break;
case VERTICAL_ALIGNMENT_CENTER: {
vbegin = (size.y - (total_h - line_spacing)) / 2;
vsep = 0;
} break;
case VERTICAL_ALIGNMENT_BOTTOM: {
vbegin = size.y - (total_h - line_spacing);
vsep = 0;
} break;
case VERTICAL_ALIGNMENT_FILL: {
vbegin = 0;
if (lines_visible > 1) {
vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1);
} else {
vsep = 0;
}
} break;
}
}
Vector2 ofs;
ofs.y = style->get_offset().y + vbegin;
for (int i = lines_skipped; i < last_line; i++) {
Size2 line_size = TS->shaped_text_get_size(lines_rid[i]);
ofs.x = 0;
for (int i = lines_skipped; i < line_limit; i++) {
Vector2 line_offset = get_line_rect(i).position;
ofs.x = line_offset.x;
ofs.y += TS->shaped_text_get_ascent(lines_rid[i]);
switch (horizontal_alignment) {
case HORIZONTAL_ALIGNMENT_FILL:
if (rtl && autowrap_mode != TextServer::AUTOWRAP_OFF) {
ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
} else {
ofs.x = style->get_offset().x;
}
break;
case HORIZONTAL_ALIGNMENT_LEFT: {
if (rtl_layout) {
ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
} else {
ofs.x = style->get_offset().x;
}
} break;
case HORIZONTAL_ALIGNMENT_CENTER: {
ofs.x = int(size.width - line_size.width) / 2;
} break;
case HORIZONTAL_ALIGNMENT_RIGHT: {
if (rtl_layout) {
ofs.x = style->get_offset().x;
} else {
ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
}
} break;
}
const Glyph *glyphs = TS->shaped_text_get_glyphs(lines_rid[i]);
int gl_size = TS->shaped_text_get_glyph_count(lines_rid[i]);
@ -621,7 +658,7 @@ void Label::_notification(int p_what) {
}
}
}
ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing;
ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + line_spacing;
}
} break;
@ -637,102 +674,16 @@ void Label::_notification(int p_what) {
}
Rect2 Label::get_character_bounds(int p_pos) const {
if (dirty || font_dirty || lines_dirty) {
const_cast<Label *>(this)->_shape();
}
bool has_settings = settings.is_valid();
Size2 size = get_size();
Ref<StyleBox> style = theme_cache.normal_style;
int line_spacing = has_settings ? settings->get_line_spacing() : theme_cache.line_spacing;
bool rtl = (TS->shaped_text_get_inferred_direction(text_rid) == TextServer::DIRECTION_RTL);
bool rtl_layout = is_layout_rtl();
float total_h = 0.0;
int lines_visible = 0;
// Get number of lines to fit to the height.
for (int64_t i = lines_skipped; i < lines_rid.size(); i++) {
total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) {
break;
}
lines_visible++;
}
if (max_lines_visible >= 0 && lines_visible > max_lines_visible) {
lines_visible = max_lines_visible;
}
int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped);
// Get real total height.
total_h = 0;
for (int64_t i = lines_skipped; i < last_line; i++) {
total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
}
total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM);
int vbegin = 0, vsep = 0;
if (lines_visible > 0) {
switch (vertical_alignment) {
case VERTICAL_ALIGNMENT_TOP: {
// Nothing.
} break;
case VERTICAL_ALIGNMENT_CENTER: {
vbegin = (size.y - (total_h - line_spacing)) / 2;
vsep = 0;
} break;
case VERTICAL_ALIGNMENT_BOTTOM: {
vbegin = size.y - (total_h - line_spacing);
vsep = 0;
} break;
case VERTICAL_ALIGNMENT_FILL: {
vbegin = 0;
if (lines_visible > 1) {
vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1);
} else {
vsep = 0;
}
} break;
}
}
_ensure_shaped();
Vector2 ofs;
ofs.y = style->get_offset().y + vbegin;
for (int i = lines_skipped; i < last_line; i++) {
Size2 line_size = TS->shaped_text_get_size(lines_rid[i]);
ofs.x = 0;
switch (horizontal_alignment) {
case HORIZONTAL_ALIGNMENT_FILL:
if (rtl && autowrap_mode != TextServer::AUTOWRAP_OFF) {
ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
} else {
ofs.x = style->get_offset().x;
}
break;
case HORIZONTAL_ALIGNMENT_LEFT: {
if (rtl_layout) {
ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
} else {
ofs.x = style->get_offset().x;
}
} break;
case HORIZONTAL_ALIGNMENT_CENTER: {
ofs.x = int(size.width - line_size.width) / 2;
} break;
case HORIZONTAL_ALIGNMENT_RIGHT: {
if (rtl_layout) {
ofs.x = style->get_offset().x;
} else {
ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width);
}
} break;
}
int line_limit;
int line_spacing;
get_layout_data(ofs, line_limit, line_spacing);
for (int i = lines_skipped; i < line_limit; i++) {
Rect2 line_rect = get_line_rect(i);
ofs.x = line_rect.position.x;
int v_size = TS->shaped_text_get_glyph_count(lines_rid[i]);
const Glyph *glyphs = TS->shaped_text_get_glyphs(lines_rid[i]);
@ -746,22 +697,19 @@ Rect2 Label::get_character_bounds(int p_pos) const {
}
Rect2 rect;
rect.position = ofs + Vector2(gl_off, 0);
rect.size = Vector2(advance, TS->shaped_text_get_size(lines_rid[i]).y);
rect.size = Vector2(advance, line_rect.size.y);
return rect;
}
}
gl_off += glyphs[j].advance * glyphs[j].repeat;
}
ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing;
ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + TS->shaped_text_get_descent(lines_rid[i]) + line_spacing;
}
return Rect2();
}
Size2 Label::get_minimum_size() const {
// don't want to mutable everything
if (dirty || font_dirty || lines_dirty) {
const_cast<Label *>(this)->_shape();
}
_ensure_shaped();
Size2 min_size = minsize;
@ -798,10 +746,7 @@ int Label::get_line_count() const {
if (!is_inside_tree()) {
return 1;
}
if (dirty || font_dirty || lines_dirty) {
const_cast<Label *>(this)->_shape();
}
_ensure_shaped();
return lines_rid.size();
}
@ -1104,10 +1049,7 @@ int Label::get_max_lines_visible() const {
}
int Label::get_total_character_count() const {
if (dirty || font_dirty || lines_dirty) {
const_cast<Label *>(this)->_shape();
}
_ensure_shaped();
return xl_text.length();
}

View File

@ -91,11 +91,16 @@ private:
int font_shadow_outline_size;
} theme_cache;
void _ensure_shaped() const;
void _update_visible();
void _shape();
void _invalidate();
protected:
RID get_line_rid(int p_line) const;
Rect2 get_line_rect(int p_line) const;
void get_layout_data(Vector2 &r_offset, int &r_line_limit, int &r_line_spacing) const;
void _notification(int p_what);
static void _bind_methods();
#ifndef DISABLE_DEPRECATED

View File

@ -0,0 +1,83 @@
/**************************************************************************/
/* test_fuzzy_search.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef TEST_FUZZY_SEARCH_H
#define TEST_FUZZY_SEARCH_H
#include "core/string/fuzzy_search.h"
#include "tests/test_macros.h"
namespace TestFuzzySearch {
struct FuzzySearchTestCase {
String query;
String expected;
};
// Ideally each of these test queries should represent a different aspect, and potentially bottleneck, of the search process.
const FuzzySearchTestCase test_cases[] = {
// Short query, many matches, few adjacent characters
{ "///gd", "./menu/hud/hud.gd" },
// Filename match with typo
{ "sm.png", "./entity/blood_sword/sam.png" },
// Multipart filename word matches
{ "ham ", "./entity/game_trap/ha_missed_me.wav" },
// Single word token matches
{ "push background", "./entity/background_zone1/background/push.png" },
// Long token matches
{ "background_freighter background png", "./entity/background_freighter/background/background.png" },
// Many matches, many short tokens
{ "menu menu characters wav", "./menu/menu/characters/smoker/0.wav" },
// Maximize total matches
{ "entity gd", "./entity/entity_man.gd" }
};
Vector<String> load_test_data() {
Ref<FileAccess> fp = FileAccess::open(TestUtils::get_data_path("fuzzy_search/project_dir_tree.txt"), FileAccess::READ);
REQUIRE(fp.is_valid());
return fp->get_as_utf8_string().split("\n");
}
TEST_CASE("[FuzzySearch] Test fuzzy search results") {
FuzzySearch search;
Vector<FuzzySearchResult> results;
Vector<String> targets = load_test_data();
for (FuzzySearchTestCase test_case : test_cases) {
search.set_query(test_case.query);
search.search_all(targets, results);
CHECK_GT(results.size(), 0);
CHECK_EQ(results[0].target, test_case.expected);
}
}
} //namespace TestFuzzySearch
#endif // TEST_FUZZY_SEARCH_H

View File

@ -389,6 +389,19 @@ TEST_CASE("[String] Find") {
MULTICHECK_STRING_INT_EQ(s, rfind, "", 15, -1);
}
TEST_CASE("[String] Find character") {
String s = "racecar";
CHECK_EQ(s.find_char('r'), 0);
CHECK_EQ(s.find_char('r', 1), 6);
CHECK_EQ(s.find_char('e'), 3);
CHECK_EQ(s.find_char('e', 4), -1);
CHECK_EQ(s.rfind_char('r'), 6);
CHECK_EQ(s.rfind_char('r', 5), 0);
CHECK_EQ(s.rfind_char('e'), 3);
CHECK_EQ(s.rfind_char('e', 2), -1);
}
TEST_CASE("[String] Find case insensitive") {
String s = "Pretty Whale Whale";
MULTICHECK_STRING_EQ(s, findn, "WHA", 7);
@ -1254,6 +1267,12 @@ TEST_CASE("[String] is_subsequence_of") {
CHECK(String("Sub").is_subsequence_ofn(a));
}
TEST_CASE("[String] is_lowercase") {
CHECK(String("abcd1234 !@#$%^&*()_-=+,.<>/\\|[]{};':\"`~").is_lowercase());
CHECK(String("").is_lowercase());
CHECK(!String("abc_ABC").is_lowercase());
}
TEST_CASE("[String] match") {
CHECK(String("img1.png").match("*.png"));
CHECK(!String("img1.jpeg").match("*.png"));

View File

@ -0,0 +1,999 @@
./menu/home/home_menu.tscn
./menu/tooltips/tooltip_server.tscn
./menu/tooltips/tooltip_server.gd
./menu/tooltips/tooltip.gd
./menu/menu/characters/smoker/4.wav
./menu/menu/characters/smoker/6.wav
./menu/menu/characters/smoker/10.wav
./menu/menu/characters/smoker/smoker.tscn
./menu/menu/characters/smoker/8.wav
./menu/menu/characters/smoker/type.gd
./menu/menu/characters/smoker/9.wav
./menu/menu/characters/smoker/5.wav
./menu/menu/characters/smoker/0.wav
./menu/menu/characters/smoker/back_light.png
./menu/menu/characters/smoker/glasses.png
./menu/menu/characters/smoker/smoker.gd
./menu/menu/characters/smoker/cig.gd
./menu/menu/characters/smoker/eyes.png
./menu/menu/characters/smoker/3.wav
./menu/menu/characters/smoker/to_pixelate.gd
./menu/menu/characters/smoker/7.wav
./menu/menu/characters/smoker/cig.png
./menu/menu/characters/smoker/2.wav
./menu/menu/characters/smoker/1.wav
./menu/menu/characters/smoke.png
./menu/menu/characters/space_bandit.tres
./menu/menu/characters/dead_guy/blood_texture.png
./menu/menu/characters/dead_guy/head_gibbed.png
./menu/menu/characters/dead_guy/back_light.png
./menu/menu/characters/dead_guy/smoker.gd
./menu/menu/characters/dead_guy/eyes.png
./menu/menu/characters/dead_guy/to_pixelate.gd
./menu/menu/characters/dead_guy/dead_guy.gd
./menu/menu/characters/dead_guy/eyes.gd
./menu/menu/characters/dead_guy/x.png
./menu/menu/characters/dead_guy/dead_guy.tscn
./menu/menu/characters/dead_guy/mouth.png
./menu/menu/characters/dead_guy/dead_guy.tres
./menu/menu/characters/Label.gd
./menu/menu/characters/guns2.png
./menu/menu/characters/c.gd
./menu/menu/characters/smoke.gd
./menu/menu/characters/character.gd
./menu/menu/characters/space_bandit/eyes.tres
./menu/menu/characters/space_bandit/space_bandit_face_happy.png
./menu/menu/characters/space_bandit/space_bandit.gd
./menu/menu/characters/space_bandit/space_bandit.tscn
./menu/menu/characters/boss/smoker.tscn
./menu/menu/characters/boss/back_light.png
./menu/menu/characters/boss/glasses.png
./menu/menu/characters/boss/smoker.gd
./menu/menu/characters/boss/cig.gd
./menu/menu/characters/boss/eyes.png
./menu/menu/characters/boss/to_pixelate.gd
./menu/menu/characters/boss/x.png
./menu/menu/characters/boss/cig.png
./menu/menu/characters/eye.gd
./menu/menu/characters/space_bandit_face_happy.png
./menu/menu/characters/face.gd
./menu/menu/characters/color.tres
./menu/menu/characters/space_bandit.tscn
./menu/menu/characters/space_bandit_face_bloody.png
./menu/menu/characters/guns.png
./menu/menu/characters/eyes2.tres
./menu/options/controls/use.tres
./menu/options/controls/input_map_button.gd
./menu/options/controls/swap.tres
./menu/options/controls/teleport.tres
./menu/options/controls/joy_controls.tscn
./menu/options/controls/mouse_and_keyboard_controls.tscn
./menu/options/controls/input_map_button.tscn
./menu/options/controls/special.tres
./menu/options/controls/throw.tres
./menu/options/controls/center.tres
./menu/options/controls/input_action.gd
./menu/options/controls/move.tres
./menu/options/controls/melee.tres
./menu/options/controls/controls.gd
./menu/options/options.gd
./menu/options/options.tscn
./menu/options/graphics/graphics.tscn
./menu/options/graphics/graphics.gd
./menu/options/audio/audio.gd
./menu/options/audio/audio.tscn
./menu/options/game/game.gd
./menu/options/game/game.tscn
./menu/circle.tres
./menu/fonts/keys.png
./menu/fonts/rainbow_font.tres
./menu/fonts/fallback_font.tres
./menu/fonts/taxi_Driver.png
./menu/fonts/NotoSansJP-Regular.ttf
./menu/fonts/taxi_Driver_noise.png
./menu/fonts/rainbow_font_shader.tres
./menu/fonts/m5x7.ttf
./menu/colors.gd
./menu/toast_enter.wav
./menu/ui_colors.tres
./menu/pause/pause.gd
./menu/pause/rainbow.tres
./menu/pause/Label.gd
./menu/pause/label.tscn
./menu/pause/pause.tscn
./menu/hoola.wav
./menu/in_game_fallback.tres
./menu/widgets/next_unlock.gd
./menu/widgets/slider.gd
./menu/widgets/fade.tscn
./menu/widgets/background_hint.gd
./menu/widgets/panel_container_smoke.gd
./menu/widgets/wishlist_sticker.gd
./menu/widgets/smoke.tres
./menu/widgets/color_grade.gd
./menu/widgets/rich_text_button.gd
./menu/widgets/panel_container_smok2.tscn
./menu/widgets/slider.tscn
./menu/widgets/rich_text_heading.gd
./menu/widgets/background_hint.tscn
./menu/widgets/tip.tscn
./menu/widgets/rich_text_button.tscn
./menu/widgets/toggle.tscn
./menu/widgets/heading.tscn
./menu/widgets/hover.tscn
./menu/widgets/toggle.gd
./menu/widgets/smoke_panel_material.tres
./menu/widgets/confirm.gd
./menu/widgets/tip.gd
./menu/widgets/panel.gd
./menu/widgets/modal.gd
./menu/widgets/NinePatchRect.gd
./menu/widgets/smoke.shader
./menu/widgets/9patch.png
./menu/widgets/big_hint.gd
./menu/widgets/TDVB1i.png
./menu/widgets/color_grade.tscn
./menu/widgets/text.gd
./menu/widgets/panel_container_smoke.tscn
./menu/widgets/1x1.png
./menu/widgets/confirm.tscn
./menu/widgets/RichTextPanel.tscn
./menu/hud/cursor.png
./menu/hud/inventory/draggable.gd
./menu/hud/inventory/menu/characters/color.tres
./menu/hud/inventory/drop_zone.tscn
./menu/hud/inventory/RichTextLabel.gd
./menu/hud/inventory/hud_icon_mutation.tscn
./menu/hud/inventory/use_count.gd
./menu/hud/inventory/draggable.tscn
./menu/hud/inventory/black_shadow_font.tres
./menu/hud/inventory/x.png
./menu/hud/inventory/hud_icon_mutation.gd
./menu/hud/inventory/flash_parent.gd
./menu/hud/inventory/TextureRect4.gd
./menu/hud/cursor.tscn
./menu/hud/hud.tscn
./menu/hud/cursor.gd
./menu/hud/hud.gd
./menu/metal_text.tres
./menu/rich_text_effects/RichTextType.gd
./menu/rich_text_effects/RichTextPanel.gd
./menu/rich_text_effects/RichTextFlash.gd
./menu/rich_text_effects/RichTextTranslate.gd
./menu/in_game.tres
./menu/lcd_screen_font.tres
./menu/toast_exit.wav
./menu/stack/ahses_material.tres
./menu/stack/home.kra
./menu/stack/fade.gd
./menu/stack/stack.tscn
./menu/stack/stack.gd
./menu/stack/version.gd
./menu/stack/art.kra
./entity/unlock_skin_classic/icon.png
./entity/use.gd
./entity/chair/entity.tscn
./entity/chair/icon.png
./entity/chair/data.gd
./entity/man_desert/entity.tscn
./entity/man_desert/icon.png
./entity/man_desert/teleprompts/need_medbay.wav
./entity/man_desert/teleprompts/me_too.wav
./entity/man_desert/teleprompts/get_up_alt.wav
./entity/man_desert/teleprompts/getting_a_medpack.wav
./entity/man_desert/teleprompts/firstaid-incoming.wav
./entity/man_desert/teleprompts/batch_name.py
./entity/man_desert/teleprompts/what.wav
./entity/man_desert/teleprompts/oo.wav
./entity/man_desert/teleprompts/yell.wav
./entity/man_desert/teleprompts/rushing.wav
./entity/man_desert/teleprompts/ooo.wav
./entity/man_desert/teleprompts/coming_to_heal_ya.wav
./entity/man_desert/teleprompts/where_is_the_medpack.wav
./entity/man_desert/teleprompts/ah.wav
./entity/man_desert/teleprompts/no.wav
./entity/man_desert/teleprompts/going_to_camp_medbay.wav
./entity/man_desert/teleprompts/aa.wav
./entity/man_desert/teleprompts/pirate_alt.wav
./entity/man_desert/teleprompts/take_morphine.wav
./entity/man_desert/teleprompts/ee.wav
./entity/man_desert/teleprompts/get_up.wav
./entity/man_desert/teleprompts/aw.wav
./entity/man_desert/teleprompts/easy.wav
./entity/man_desert/teleprompts/intruder.wav
./entity/man_desert/teleprompts/amateur.wav
./entity/man_desert/teleprompts/hes_not_moving.wav
./entity/man_desert/teleprompts/pirate.wav
./entity/man_desert/teleprompts/i_dont_know.wav
./entity/man_desert/teleprompts/index.txt
./entity/man_desert/teleprompts/move.wav
./entity/man_desert/teleprompts/hes_stuck.wav
./entity/man_desert/teleprompts/how.wav
./entity/man_desert/teleprompts/uu.wav
./entity/man_desert/teleprompts/where_is_the_gun.wav
./entity/man_desert/teleprompts/getting_a_gun.wav
./entity/man_desert/data.gd
./entity/man_desert/hand.png
./entity/barrel_side_smoke/entity.tscn
./entity/barrel_side_smoke/icon.png
./entity/barrel_side_smoke/data.gd
./entity/barrel_smoke/entity.tscn
./entity/barrel_smoke/icon.png
./entity/barrel_smoke/data.gd
./entity/project_box/entity.tscn
./entity/project_box/icon.png
./entity/project_box/data.gd
./entity/mutation_saw/entity.tscn
./entity/mutation_saw/icon.png
./entity/mutation_saw/special.gd
./entity/mutation_saw/data.gd
./entity/lift_entrance/entity.tscn
./entity/lift_entrance/icon.png
./entity/lift_entrance/special.gd
./entity/lift_entrance/data.gd
./entity/mutation_accuracy_boost_DELETE/entity.tscn
./entity/mutation_accuracy_boost_DELETE/icon.png
./entity/mutation_accuracy_boost_DELETE/special.gd
./entity/mutation_accuracy_boost_DELETE/data.gd
./entity/skin_ruffle/entity.tscn
./entity/skin_ruffle/icon.png
./entity/skin_ruffle/carried.png
./entity/skin_ruffle/data.gd
./entity/editor_only_icon.gd
./entity/console_dark/entity.tscn
./entity/console_dark/icon.png
./entity/console_dark/data.gd
./entity/console_dark/animation.png
./entity/smg2/entity.tscn
./entity/smg2/used.wav
./entity/smg2/icon.png
./entity/smg2/data.gd
./entity/smg2/debug.gd
./entity/grenade_launcher/entity.tscn
./entity/grenade_launcher/used.wav
./entity/grenade_launcher/icon.png
./entity/grenade_launcher/special.gd
./entity/grenade_launcher/data.gd
./entity/floor_tile_full_square/entity.tscn
./entity/floor_tile_full_square/icon.png
./entity/floor_tile_full_square/data.gd
./entity/grate_1/entity.tscn
./entity/grate_1/icon.png
./entity/grate_1/data.gd
./entity/bed_bunk_corner/entity.tscn
./entity/bed_bunk_corner/icon.png
./entity/bed_bunk_corner/data.gd
./entity/kill_streak_rail_gun_level_3/entity.tscn
./entity/kill_streak_rail_gun_level_3/data.gd
./entity/teleporter_random_weak/entity.tscn
./entity/teleporter_random_weak/teleporter_model.gd
./entity/teleporter_random_weak/used.wav
./entity/teleporter_random_weak/icon.png
./entity/teleporter_random_weak/special.gd
./entity/teleporter_random_weak/ray.gd
./entity/teleporter_random_weak/data.gd
./entity/teleporter_random_weak/flap.png
./entity/entities.kra
./entity/jerry_can/entity.tscn
./entity/jerry_can/icon.png
./entity/jerry_can/data.gd
./entity/kill_streak_helmet_full/entity.tscn
./entity/kill_streak_helmet_full/data.gd
./entity/background_derelict/background2.gd
./entity/background_derelict/entity.tscn
./entity/background_derelict/icon.png
./entity/background_derelict/background/space.png
./entity/background_derelict/background/line.png
./entity/background_derelict/background/overlay.png
./entity/background_derelict/background/background2.png
./entity/background_derelict/background/background.png
./entity/background_derelict/background/engine_glow.tscn
./entity/background_derelict/background/lines3.png
./entity/background_derelict/background/background.tscn
./entity/background_derelict/background/lines.tres
./entity/background_derelict/background/xx.gd
./entity/background_derelict/background/background.gd
./entity/background_derelict/background/bayer16tile2.png
./entity/background_derelict/background/push.png
./entity/background_derelict/background/palette_mono.png
./entity/background_derelict/background/stars.gd
./entity/background_derelict/background/lines2.png
./entity/background_derelict/background/lines.shader
./entity/background_derelict/background/ambience.gd
./entity/background_derelict/background/space_ship_ambience.ogg
./entity/background_derelict/background/stars.png
./entity/background_derelict/data.gd
./entity/smoker/entity.tscn
./entity/smoker/right_hand.png
./entity/smoker/eyes.png
./entity/smoker/data.gd
./entity/smoker/animate.gd
./entity/smoker/left_hand.png
./entity/EntityStatic.gd
./entity/level_model.gd
./entity/class_teleporter_drop_chance/entity.tscn
./entity/class_teleporter_drop_chance/icon.png
./entity/class_teleporter_drop_chance/special.gd
./entity/class_teleporter_drop_chance/data.gd
./entity/smg4/entity.tscn
./entity/smg4/used.wav
./entity/smg4/icon.png
./entity/smg4/data.gd
./entity/medpack/entity.tscn
./entity/medpack/icon.png
./entity/medpack/dead.png
./entity/medpack/data.gd
./entity/model.gd
./entity/doom_transition/entity.tscn
./entity/doom_transition/icon.png
./entity/doom_transition/special.gd
./entity/doom_transition/Screenshot from 2021-12-08 18-25-03.png
./entity/doom_transition/data.gd
./entity/glass_block_exploding/entity.tscn
./entity/glass_block_exploding/icon.png
./entity/glass_block_exploding/special.gd
./entity/glass_block_exploding/dead.png
./entity/glass_block_exploding/data.gd
./entity/floor_ting/entity.tscn
./entity/floor_ting/icon.png
./entity/floor_ting/data.gd
./entity/background_crashed_ship/entity.tscn
./entity/background_crashed_ship/icon.png
./entity/background_crashed_ship/background/background2.kra
./entity/background_crashed_ship/background/dust_storm_negative.png
./entity/background_crashed_ship/background/background2.png
./entity/background_crashed_ship/background/background2 (copy 1).png
./entity/background_crashed_ship/background/dust_bowl.ogg
./entity/background_crashed_ship/background/background.tscn
./entity/background_crashed_ship/background/background.kra
./entity/background_crashed_ship/data.gd
./entity/game_aim_hack_boss/entity.tscn
./entity/game_aim_hack_boss/icon.png
./entity/game_aim_hack_boss/special.gd
./entity/game_aim_hack_boss/give_my_arm_back.wav
./entity/game_aim_hack_boss/my_arm_came_off.wav
./entity/game_aim_hack_boss/data.gd
./entity/sink/entity.tscn
./entity/sink/icon.png
./entity/sink/data.gd
./entity/grate_2/entity.tscn
./entity/grate_2/icon.png
./entity/grate_2/data.gd
./entity/barrel_side/entity.tscn
./entity/barrel_side/icon.png
./entity/barrel_side/data.gd
./entity/oxygen/entity.tscn
./entity/oxygen/icon.png
./entity/oxygen/shadow.png
./entity/oxygen/data.gd
./entity/oxygen/normal.png
./entity/unlock_skin_robo/entity.tscn
./entity/unlock_skin_robo/icon.png
./entity/unlock_skin_robo/special.gd
./entity/unlock_skin_robo/data.gd
./entity/entity_agency_model.gd
./entity/floor_tile_wood/entity.tscn
./entity/floor_tile_wood/icon.png
./entity/floor_tile_wood/data.gd
./entity/qr_code/entity.tscn
./entity/qr_code/icon.png
./entity/qr_code/data.gd
./entity/background_sun/overlay.png
./entity/background_sun/entity.tscn
./entity/background_sun/c.gd
./entity/background_sun/kill.tscn
./entity/background_sun/icon.png
./entity/background_sun/special.gd
./entity/background_sun/wtf.tres
./entity/background_sun/background/background2.png
./entity/background_sun/background/background.tscn
./entity/background_sun/background/color2s.tres
./entity/background_sun/background/background_glow.png
./entity/background_sun/data.gd
./entity/background_sun/kill.gd
./entity/background_sun/stars.png
./entity/background_zone_intro/overlay.png
./entity/background_zone_intro/entity.tscn
./entity/background_zone_intro/icon.png
./entity/background_zone_intro/special.gd
./entity/background_zone_intro/background/space.png
./entity/background_zone_intro/background/line.png
./entity/background_zone_intro/background/background2.png
./entity/background_zone_intro/background/background.png
./entity/background_zone_intro/background/engine_glow.tscn
./entity/background_zone_intro/background/lines3.png
./entity/background_zone_intro/background/background.tscn
./entity/background_zone_intro/background/lines.tres
./entity/background_zone_intro/background/background.gd
./entity/background_zone_intro/background/bayer16tile2.png
./entity/background_zone_intro/background/push.png
./entity/background_zone_intro/background/palette_mono.png
./entity/background_zone_intro/background/stars.gd
./entity/background_zone_intro/background/lines2.png
./entity/background_zone_intro/background/lines.shader
./entity/background_zone_intro/background/ambience.gd
./entity/background_zone_intro/background/space_ship_ambience.ogg
./entity/background_zone_intro/background/stars.png
./entity/background_zone_intro/background_end.png
./entity/background_zone_intro/data.gd
./entity/background_zone_intro/tinge.png
./entity/closet_alt/entity.tscn
./entity/closet_alt/icon.png
./entity/closet_alt/data.gd
./entity/meta_random_sound/entity.tscn
./entity/meta_random_sound/giberish.wav
./entity/meta_random_sound/icon.png
./entity/meta_random_sound/special.gd
./entity/meta_random_sound/who.wav
./entity/meta_random_sound/data.gd
./entity/meta_random_sound/hoola_boola.wav
./entity/meta_random_sound/space_bandit.wav
./entity/lines/entity.tscn
./entity/lines/icon.png
./entity/lines/data.gd
./entity/teleporter_random_avoid_ray/entity.tscn
./entity/teleporter_random_avoid_ray/used.wav
./entity/teleporter_random_avoid_ray/icon.png
./entity/teleporter_random_avoid_ray/ray.gd
./entity/teleporter_random_avoid_ray/data.gd
./entity/teleporter_random_avoid_ray/flap.png
./entity/teleporter_random_avoid_ray/RayCast2D.gd
./entity/teleporter_random_avoid_ray/area.gd
./entity/teleporter_random_avoid_ray/flap.gd
./entity/saw/blades.gd
./entity/saw/entity.tscn
./entity/saw/used.wav
./entity/saw/icon.png
./entity/saw/special.gd
./entity/saw/carried.png
./entity/saw/data.gd
./entity/saw/used (copy 1).wav
./entity/saw/saw.wav
./entity/saw/carried_blades.png
./entity/floor_tile_checkerdboard/damage.png
./entity/floor_tile_checkerdboard/entity.tscn
./entity/floor_tile_checkerdboard/icon.png
./entity/floor_tile_checkerdboard/entity.tres
./entity/floor_tile_checkerdboard/data.gd
./entity/mutation_smoke_grenade_upgrade/entity.tscn
./entity/mutation_smoke_grenade_upgrade/icon.png
./entity/mutation_smoke_grenade_upgrade/special.gd
./entity/mutation_smoke_grenade_upgrade/data.gd
./entity/mutation_smoke_grenade_upgrade/mutation_model.gd
./entity/helmet_full/entity.tscn
./entity/helmet_full/pick_up.wav
./entity/helmet_full/icon.png
./entity/helmet_full/data.gd
./entity/helmet_full/helmet-ping.wav
./entity/barrel_explosive/entity.tscn
./entity/barrel_explosive/icon.png
./entity/barrel_explosive/data.gd
./entity/bank/entity.tscn
./entity/bank/icon.png
./entity/bank/special.gd
./entity/bank/data.gd
./entity/kick/entity.tscn
./entity/kick/swipe.png
./entity/kick/used.wav
./entity/kick/icon.png
./entity/kick/AnimatedSprite.gd
./entity/kick/data.gd
./entity/battery/entity.tscn
./entity/battery/icon.png
./entity/battery/data.gd
./entity/lift/entity.tscn
./entity/lift/opening.wav
./entity/lift/doors_open.png
./entity/lift/RichTextLabel.gd
./entity/lift/icon.png
./entity/lift/open.wav
./entity/lift/elevator_end.wav
./entity/lift/lift_model.gd
./entity/lift/label.tscn
./entity/lift/rumble.gd
./entity/lift/level_portal_model.gd
./entity/lift/data.gd
./entity/lift/doors.png
./entity/lift/area.gd
./entity/snes/entity.tscn
./entity/snes/icon.png
./entity/snes/data.gd
./entity/passive_disarm/entity.tscn
./entity/passive_disarm/icon.png
./entity/passive_disarm/special.gd
./entity/passive_disarm/data.gd
./entity/mutation_lots_of_shot/entity.tscn
./entity/mutation_lots_of_shot/icon.png
./entity/mutation_lots_of_shot/special.gd
./entity/mutation_lots_of_shot/data.gd
./entity/pallet2/entity.tscn
./entity/pallet2/icon.png
./entity/pallet2/data.gd
./entity/kill_streak_sword/entity.tscn
./entity/kill_streak_sword/data.gd
./entity/rain/entity.tscn
./entity/rain/icon.png
./entity/rain/special.gd
./entity/rain/rain.png
./entity/rain/rain.tscn
./entity/rain/data.gd
./entity/rain/rain.gd
./entity/white_line/entity.tscn
./entity/white_line/icon.png
./entity/white_line/data.gd
./entity/game_break_sword/entity.tscn
./entity/game_break_sword/icon.png
./entity/game_break_sword/special.gd
./entity/game_break_sword/data.gd
./entity/background_zone1/overlay.png
./entity/background_zone1/entity.tscn
./entity/background_zone1/icon.png
./entity/background_zone1/special.gd
./entity/background_zone1/background/space.png
./entity/background_zone1/background/line.png
./entity/background_zone1/background/background2.png
./entity/background_zone1/background/background.png
./entity/background_zone1/background/engine_glow.tscn
./entity/background_zone1/background/lines3.png
./entity/background_zone1/background/background.tscn
./entity/background_zone1/background/lines.tres
./entity/background_zone1/background/background.gd
./entity/background_zone1/background/bayer16tile2.png
./entity/background_zone1/background/push.png
./entity/background_zone1/background/palette_mono.png
./entity/background_zone1/background/stars.gd
./entity/background_zone1/background/lines2.png
./entity/background_zone1/background/lines.shader
./entity/background_zone1/background/ambience.gd
./entity/background_zone1/background/space_ship_ambience.ogg
./entity/background_zone1/background/stars.png
./entity/background_zone1/data.gd
./entity/background_zone1/tinge.png
./entity/mutation_throw_trap_DELETE/entity.tscn
./entity/mutation_throw_trap_DELETE/icon.png
./entity/mutation_throw_trap_DELETE/special.gd
./entity/mutation_throw_trap_DELETE/data.gd
./entity/agency.gd
./entity/skin_cheese/entity.tscn
./entity/skin_cheese/icon.png
./entity/skin_cheese/carried.png
./entity/skin_cheese/data.gd
./entity/toilet/entity.tscn
./entity/toilet/icon.png
./entity/toilet/special.gd
./entity/toilet/water.png
./entity/toilet/drink.wav
./entity/toilet/data.gd
./entity/smg3/entity.tscn
./entity/smg3/used.wav
./entity/smg3/icon.png
./entity/smg3/dead.png
./entity/smg3/data.gd
./entity/smg3/debug.gd
./entity/teleporter_super/entity.tscn
./entity/teleporter_super/icon.png
./entity/teleporter_super/data.gd
./entity/background_zone_end/overlay.png
./entity/background_zone_end/entity.tscn
./entity/background_zone_end/icon.png
./entity/background_zone_end/special.gd
./entity/background_zone_end/stars2.png
./entity/background_zone_end/background_end.png
./entity/background_zone_end/data.gd
./entity/background_zone_end/tinge.png
./entity/kill_streak_barricade/entity.tscn
./entity/kill_streak_barricade/data.gd
./entity/game_zone_4_boss_1/entity.tscn
./entity/game_zone_4_boss_1/icon.png
./entity/game_zone_4_boss_1/special.gd
./entity/game_zone_4_boss_1/data.gd
./entity/game_zone_4_boss_1/kill_me_and_explode_ship.wav
./entity/mutation_remove_melee/entity.tscn
./entity/mutation_remove_melee/icon.png
./entity/mutation_remove_melee/special.gd
./entity/mutation_remove_melee/data.gd
./entity/he_grenade_level_2/entity.tscn
./entity/he_grenade_level_2/icon.png
./entity/he_grenade_level_2/data.gd
./entity/background_zone_2/entity.tscn
./entity/background_zone_2/icon.png
./entity/background_zone_2/background/background2.kra
./entity/background_zone_2/background/grad.png
./entity/background_zone_2/background/background2.png
./entity/background_zone_2/background/background.png
./entity/background_zone_2/background/background2 (copy 1).png
./entity/background_zone_2/background/backgrounds.gd
./entity/background_zone_2/background/wall_overlay.png
./entity/background_zone_2/background/background.tscn
./entity/background_zone_2/background/Screenshot from 2022-07-07 10-58-48.png
./entity/background_zone_2/background/background.gd
./entity/background_zone_2/background/shadow.png
./entity/background_zone_2/background/engine smoke.png
./entity/background_zone_2/background/background.kra
./entity/background_zone_2/background/sea.ogg
./entity/background_zone_2/background/background2blur.png
./entity/background_zone_2/background/test.gd
./entity/background_zone_2/background/grad3.png
./entity/background_zone_2/background/lines2.png
./entity/background_zone_2/background/smoke.tscn
./entity/background_zone_2/background/left_water.tscn
./entity/background_zone_2/background/grad2.png
./entity/background_zone_2/background/para.png
./entity/background_zone_2/data.gd
./entity/pipe_corner/entity.tscn
./entity/pipe_corner/icon.png
./entity/pipe_corner/data.gd
./entity/floor_tile_metal_cow_trap/entity.tscn
./entity/floor_tile_metal_cow_trap/icon.png
./entity/floor_tile_metal_cow_trap/data.gd
./entity/skin_naked/entity.tscn
./entity/skin_naked/icon.png
./entity/skin_naked/carried.png
./entity/skin_naked/data.gd
./entity/valve/entity.tscn
./entity/valve/icon.png
./entity/valve/.icon.png-autosave.kra
./entity/valve/data.gd
./entity/bed/entity.tscn
./entity/bed/icon.png
./entity/bed/data.gd
./entity/game_invisible_guy/entity.tscn
./entity/game_invisible_guy/icon.png
./entity/game_invisible_guy/special.gd
./entity/game_invisible_guy/data.gd
./entity/smg/entity.tscn
./entity/smg/used.wav
./entity/smg/icon.png
./entity/smg/data.gd
./entity/skin_robo/entity.tscn
./entity/skin_robo/icon.png
./entity/skin_robo/carried.png
./entity/skin_robo/data.gd
./entity/bandana/entity.tscn
./entity/bandana/bob.gd
./entity/bandana/icon.png
./entity/bandana/special.gd
./entity/bandana/carried.png
./entity/bandana/data.gd
./entity/bandana/pixel.png
./entity/floor_plug/entity.tscn
./entity/floor_plug/icon.png
./entity/floor_plug/data.gd
./entity/bench/entity.tscn
./entity/bench/icon.png
./entity/bench/data.gd
./entity/meta_strip_items/entity.tscn
./entity/meta_strip_items/special.gd
./entity/meta_strip_items/meta_strip_items_model.gd
./entity/meta_strip_items/data.gd
./entity/crate_teleporter/entity.tscn
./entity/crate_teleporter/icon.png
./entity/crate_teleporter/data.gd
./entity/crate_teleporter/satellite.kra
./entity/crate_garbage/entity.tscn
./entity/crate_garbage/icon.png
./entity/crate_garbage/data.gd
./entity/crate_garbage/gibbed.png
./entity/meta_stats/entity.tscn
./entity/meta_stats/letters.tres
./entity/meta_stats/icon.png
./entity/meta_stats/special.gd
./entity/meta_stats/data.gd
./entity/meta_stats/meta_stats_model.gd
./entity/rail_gun/entity.tscn
./entity/rail_gun/used.wav
./entity/rail_gun/icon.png
./entity/rail_gun/special.gd
./entity/rail_gun/carried.png
./entity/rail_gun/data.gd
./entity/drop_ship_door/entity.tscn
./entity/drop_ship_door/icon.png
./entity/drop_ship_door/data.gd
./entity/floor_lines/entity.tscn
./entity/floor_lines/icon.png
./entity/floor_lines/data.gd
./entity/game_trap/entity.tscn
./entity/game_trap/you_blew_up_my_force_field.wav
./entity/game_trap/droped_my_grenade_2.wav
./entity/game_trap/icon.png
./entity/game_trap/special.gd
./entity/game_trap/droped_my_grenade_0.wav
./entity/game_trap/shock.wav
./entity/game_trap/uh_my_helmet.wav
./entity/game_trap/ha_missed_me.wav
./entity/game_trap/data.gd
./entity/game_trap/try_beat_this_force_field.wav
./entity/game_trap/droped_my_grenade_1.wav
./entity/blood_sword/entity.tscn
./entity/blood_sword/pick_up.wav
./entity/blood_sword/used.wav
./entity/blood_sword/sam2.png
./entity/blood_sword/icon.png
./entity/blood_sword/special.gd
./entity/blood_sword/hit_bar.gd
./entity/blood_sword/data.gd
./entity/blood_sword/sam.png
./entity/blood_sword/dead.wav
./entity/blood_sword/animation.png
./entity/auto_cables_thick/entity.tscn
./entity/auto_cables_thick/data.gd
./entity/auto_cables_thick/wires2.png
./entity/shield/entity.tscn
./entity/shield/pick_up.wav
./entity/shield/icon.png
./entity/shield/carried.png
./entity/shield/data.gd
./entity/shield/helmet-ping.wav
./entity/game_teleport_in/entity.tscn
./entity/game_teleport_in/icon.png
./entity/game_teleport_in/special.gd
./entity/game_teleport_in/data.gd
./entity/shotgun_super/entity.tscn
./entity/shotgun_super/icon.png
./entity/shotgun_super/data.gd
./entity/bottle/entity.tscn
./entity/bottle/icon.png
./entity/bottle/data.gd
./entity/bottle/normal.png
./entity/bottle/icon_shadow.png
./entity/kill_streak_p90/entity.tscn
./entity/kill_streak_p90/data.gd
./entity/drain/entity.tscn
./entity/drain/icon.png
./entity/drain/data.gd
./entity/auto_wires_three/entity.tscn
./entity/auto_wires_three/data.gd
./entity/light/entity.tscn
./entity/light/icon.png
./entity/light/special.gd
./entity/light/light.wav
./entity/light/data.gd
./entity/debris/entity.tscn
./entity/debris/icon.png
./entity/debris/data.gd
./entity/debris/gibbed.png
./entity/mutation_rail_gun_upgrade/entity.tscn
./entity/mutation_rail_gun_upgrade/icon.png
./entity/mutation_rail_gun_upgrade/special.gd
./entity/mutation_rail_gun_upgrade/data.gd
./entity/mutation_rail_gun_upgrade/mutation_model.gd
./entity/auto_cables/entity.tscn
./entity/auto_cables/data.gd
./entity/auto_cables/wires2.png
./entity/stealth_camo/entity.tscn
./entity/stealth_camo/special.gd
./entity/stealth_camo/data.gd
./entity/colt_45/entity.tscn
./entity/colt_45/used.wav
./entity/colt_45/icon.png
./entity/colt_45/dead.png
./entity/colt_45/data.gd
./entity/quantum_suicide_drive/entity.tscn
./entity/quantum_suicide_drive/heart.ogg
./entity/quantum_suicide_drive/icon.png
./entity/quantum_suicide_drive/special.gd
./entity/quantum_suicide_drive/qsd_model.gd
./entity/quantum_suicide_drive/multi.gd
./entity/quantum_suicide_drive/multi.tscn
./entity/quantum_suicide_drive/CenterContainer.gd
./entity/quantum_suicide_drive/carried.png
./entity/quantum_suicide_drive/data.gd
./entity/helmet/entity.tscn
./entity/helmet/pick_up.wav
./entity/helmet/icon.png
./entity/helmet/special.gd
./entity/helmet/die.wav
./entity/helmet/carried.png
./entity/helmet/data.gd
./entity/helmet/helmet-ping.wav
./entity/ammo_box/entity.tscn
./entity/ammo_box/icon.png
./entity/ammo_box/data.gd
./entity/rail_gun_level_2/entity.tscn
./entity/rail_gun_level_2/icon.png
./entity/rail_gun_level_2/data.gd
./entity/glass_block_backup/entity.tscn
./entity/glass_block_backup/icon.png
./entity/glass_block_backup/data.gd
./entity/closet/entity.tscn
./entity/closet/icon.png
./entity/closet/data.gd
./entity/little_boxes/entity.tscn
./entity/little_boxes/icon.png
./entity/little_boxes/data.gd
./entity/meta_health_bar/entity.tscn
./entity/meta_health_bar/health_bar_model.gd
./entity/meta_health_bar/icon.png
./entity/meta_health_bar/special.gd
./entity/meta_health_bar/invunerable.png
./entity/meta_health_bar/data.gd
./entity/night_stand/entity.tscn
./entity/night_stand/icon_normal.png
./entity/night_stand/icon.png
./entity/night_stand/shadow.png
./entity/night_stand/data.gd
./entity/fan/entity.tscn
./entity/fan/flap2.png
./entity/fan/flaps.gd
./entity/fan/icon.png
./entity/fan/data.gd
./entity/fan/flap.png
./entity/fan/icon_shadow.png
./entity/fan/animation.png
./entity/fan/gibbed.png
./entity/game_tutorial_end/entity.tscn
./entity/game_tutorial_end/icon.png
./entity/game_tutorial_end/special.gd
./entity/game_tutorial_end/data.gd
./entity/mutation_disarmament/entity.tscn
./entity/mutation_disarmament/icon.png
./entity/mutation_disarmament/special.gd
./entity/mutation_disarmament/data.gd
./entity/air_lock/icon_open.png
./entity/air_lock/entity.tscn
./entity/air_lock/door_close.wav
./entity/air_lock/icon.png
./entity/air_lock/special.gd
./entity/air_lock/air_lock_model.gd
./entity/air_lock/data.gd
./entity/scorpion/entity.tscn
./entity/scorpion/used.wav
./entity/scorpion/laser.gd
./entity/scorpion/icon.png
./entity/scorpion/data.gd
./entity/kill_streak_aim_hack/entity.tscn
./entity/kill_streak_aim_hack/data.gd
./entity/dungeon_proc_debug/entity.tscn
./entity/dungeon_proc_debug/icon.png
./entity/dungeon_proc_debug/data.gd
./entity/dungeon_proc_debug/debug.gd
./entity/dungeon_proc_debug/debug.tscn
./entity/tarp/entity.tscn
./entity/tarp/icon.png
./entity/tarp/data.gd
./entity/hit_indicator/entity.tscn
./entity/hit_indicator/data.gd
./entity/console_corner/entity.tscn
./entity/console_corner/animation2.tscn
./entity/console_corner/icon.png
./entity/console_corner/data.gd
./entity/console_corner/animation.tscn
./entity/icon.png
./entity/couch_corner/entity.tscn
./entity/couch_corner/icon.png
./entity/couch_corner/data.gd
./entity/m4/entity.tscn
./entity/m4/used.wav
./entity/m4/icon.png
./entity/m4/data.gd
./entity/game_hud/entity.tscn
./entity/game_hud/icon.png
./entity/game_hud/data.gd
./entity/game_hud/inventory_game.tscn
./entity/prototypes.gd
./entity/agent_chicken/emotes.png
./entity/agent_chicken/entity.tscn
./entity/agent_chicken/sound_board.gd
./entity/agent_chicken/bones.tscn
./entity/agent_chicken/bones.gd
./entity/agent_chicken/barks.gd
./entity/agent_chicken/emote.gd
./entity/agent_chicken/icon.png
./entity/agent_chicken/special.gd
./entity/agent_chicken/bark.gd
./entity/agent_chicken/deaad.png
./entity/agent_chicken/icon.gd
./entity/agent_chicken/data.gd
./entity/agent_chicken/animation.tscn
./entity/agent_chicken/emote.tscn
./entity/agent_chicken/hand.png
./entity/velocity/entity.tscn
./entity/velocity/icon.png
./entity/velocity/special.gd
./entity/velocity/data.gd
./entity/aircon/entity.tscn
./entity/aircon/grate.png
./entity/aircon/icon.png
./entity/aircon/data.gd
./entity/aircon/animation.png
./entity/floor_tile_bricks/entity.tscn
./entity/floor_tile_bricks/icon.png
./entity/floor_tile_bricks/data.gd
./entity/pallet/entity.tscn
./entity/pallet/icon.png
./entity/pallet/data.gd
./entity/barricade_deployed/debug.png
./entity/barricade_deployed/field.tscn
./entity/barricade_deployed/entity.tscn
./entity/barricade_deployed/ambience.ogg
./entity/barricade_deployed/icon.png
./entity/barricade_deployed/field.gd
./entity/barricade_deployed/field_material.tres
./entity/barricade_deployed/debug2.png
./entity/barricade_deployed/data.gd
./entity/barricade_deployed/field_material_invert.tres
./entity/barricade_deployed/field_material.gd
./entity/barricade_deployed/gibbed.png
./entity/helmet_nv/entity.tscn
./entity/helmet_nv/pick_up.wav
./entity/helmet_nv/icon.png
./entity/helmet_nv/special.gd
./entity/helmet_nv/carried.png
./entity/helmet_nv/eyes.png
./entity/helmet_nv/data.gd
./entity/helmet_nv/helmet-ping.wav
./entity/helmet_nv/eyes.gd
./entity/mutation_sword/entity.tscn
./entity/mutation_sword/icon.png
./entity/mutation_sword/special.gd
./entity/mutation_sword/data.gd
./entity/field_full_super/entity.tscn
./entity/field_full_super/icon.png
./entity/field_full_super/special.gd
./entity/field_full_super/carried.png
./entity/field_full_super/data.gd
./entity/entity_man.gd
./entity/couch/entity.tscn
./entity/couch/icon.png
./entity/couch/data.gd
./entity/teleporter_lil_hunter/entity.tscn
./entity/teleporter_lil_hunter/icon.png
./entity/teleporter_lil_hunter/tubes.png
./entity/teleporter_lil_hunter/osc_shader.tres
./entity/teleporter_lil_hunter/eyes.png
./entity/teleporter_lil_hunter/data.gd
./entity/teleporter_lil_hunter/osc.tres
./entity/game_tutorial_melee_zone/entity.tscn
./entity/game_tutorial_melee_zone/icon.png
./entity/game_tutorial_melee_zone/special.gd
./entity/game_tutorial_melee_zone/data.gd
./entity/kill_streak_glock/entity.tscn
./entity/kill_streak_glock/data.gd
./entity/skin_mime/entity.tscn
./entity/skin_mime/icon.png
./entity/skin_mime/special.gd
./entity/skin_mime/carried.png
./entity/skin_mime/data.gd
./entity/medpack_hard/entity.tscn
./entity/medpack_hard/icon.png
./entity/medpack_hard/data.gd
./entity/teleporter_overload/entity.tscn
./entity/teleporter_overload/icon.png
./entity/teleporter_overload/special.gd
./entity/teleporter_overload/carried.png
./entity/teleporter_overload/data.gd
./entity/background_freighter/overlay.png
./entity/background_freighter/entity.tscn
./entity/background_freighter/icon.png
./entity/background_freighter/Master.ogg
./entity/background_freighter/background/space.png
./entity/background_freighter/background/line.png
./entity/background_freighter/background/background2.gd
./entity/background_freighter/background/good create.png
./entity/background_freighter/background/backgip.png
./entity/background_freighter/background/background2.png
./entity/background_freighter/background/background.png
./entity/background_freighter/background/engine_glow.tscn
./entity/background_freighter/background/gra2d.png
./entity/background_freighter/background/lines3.png
./entity/background_freighter/background/background.tscn
./entity/background_freighter/background/lines.tres
./entity/background_freighter/background/background.gd
./entity/background_freighter/background/bayer16tile2.png
./entity/background_freighter/background/goodcrate.png
./entity/background_freighter/background/push.png
./entity/background_freighter/background/background_floor.png
./entity/background_freighter/background/palette_mono.png
./entity/background_freighter/background/stars.gd
./entity/background_freighter/background/lines2.png
./entity/background_freighter/background/lines.shader
./entity/background_freighter/background/ambience.gd
./entity/background_freighter/background/bacsdas.png
./entity/background_freighter/background/space_ship_ambience.ogg
./entity/background_freighter/background/stars.png
./entity/background_freighter/data.gd
./entity/auto_wires/entity.tscn
./entity/auto_wires/data.gd
./entity/kill_streak/entity.tscn
./entity/kill_streak/kill_streak_toast.tscn
./entity/kill_streak/icon.png

View File

@ -82,6 +82,7 @@
#include "tests/core/object/test_object.h"
#include "tests/core/object/test_undo_redo.h"
#include "tests/core/os/test_os.h"
#include "tests/core/string/test_fuzzy_search.h"
#include "tests/core/string/test_node_path.h"
#include "tests/core/string/test_string.h"
#include "tests/core/string/test_translation.h"