Rework the Search popup and highlight what matched
- The search field is a single-line field, so Enter no longer adds a
line break that made every search come up empty.
- The field, button and list use real font sizes from the theme instead
of being drawn at half size and scaled up, so the text is sharp, and
everything fits inside the panel.
- Results have alternating row backgrounds like the other lists, a count
("3 of 1045 soundtracks") and a message when nothing matches.
- The letters that matched are highlighted.
- Searches shorter than Settings.LETTERS_FOR_LOOSE_SEARCH (3) only find
names that contain the text, instead of names with the letters far
apart, which matched most of the library. The Statistics tab uses the
same search.
This commit is contained in:
1 parent
13fdb3f68c
commit
845d08c260
12 files changed
+2063
-808
No files matched your search
@@ -1,7 +1,8 @@
|
||||
class_name SoundtrackSearch
|
||||
extends RefCounted
|
||||
## Finds soundtrack names matching a search text. Matches ignore case, spaces and
|
||||
## special characters, and also find the letters in order with others in between.
|
||||
## special characters. Searches of at least [code]Settings.LETTERS_FOR_LOOSE_SEARCH[/code]
|
||||
## letters also find the letters in order with others in between.
|
||||
##
|
||||
## Used by the Search popup and the statistics view, so both find the same soundtracks.
|
||||
|
||||
@@ -30,12 +31,14 @@ func find_matches(search_text: String) -> Array[String]:
|
||||
var seen: Dictionary = {}
|
||||
var search_text_lower: String = search_text.to_lower()
|
||||
var clean_search_text: String = clean_term(search_text)
|
||||
var regex: RegEx = _compile_regex_cached(clean_search_text)
|
||||
var passes: Array[Callable] = [
|
||||
func(entry: Dictionary) -> bool: return entry.lowercase.contains(search_text_lower),
|
||||
func(entry: Dictionary) -> bool: return entry.cleaned.contains(clean_search_text),
|
||||
func(entry: Dictionary) -> bool: return regex.search(entry.cleaned) != null,
|
||||
]
|
||||
if clean_search_text.length() >= Settings.LETTERS_FOR_LOOSE_SEARCH:
|
||||
var regex: RegEx = _compile_regex_cached(clean_search_text)
|
||||
passes.append(
|
||||
func(entry: Dictionary) -> bool: return regex.search(entry.cleaned) != null)
|
||||
|
||||
var matches: Array[String] = []
|
||||
for matches_pass: Callable in passes:
|
||||
@@ -46,6 +49,57 @@ func find_matches(search_text: String) -> Array[String]:
|
||||
return matches
|
||||
|
||||
|
||||
## The positions of the letters in [param soundtrack_name] that [param search_text]
|
||||
## matched, for highlighting them: the text itself if the name contains it, otherwise the
|
||||
## letters found in order the way [method find_matches] finds them. Empty if it does not
|
||||
## match.
|
||||
static func matched_letters(soundtrack_name: String, search_text: String) -> Array[int]:
|
||||
var positions: Array[int] = []
|
||||
if search_text.is_empty():
|
||||
return positions
|
||||
var start: int = soundtrack_name.to_lower().find(search_text.to_lower())
|
||||
if start >= 0:
|
||||
for i: int in search_text.length():
|
||||
positions.append(start + i)
|
||||
return positions
|
||||
|
||||
# Clean the name piece by piece, remembering which letter each cleaned letter came from
|
||||
var cleaned: String = ""
|
||||
var source: Array[int] = []
|
||||
var i: int = 0
|
||||
while i < soundtrack_name.length():
|
||||
var piece: String
|
||||
var length: int = 1
|
||||
if soundtrack_name.substr(i, 2).to_lower() == "'n":
|
||||
piece = "and"
|
||||
length = 2
|
||||
else:
|
||||
piece = clean_term(soundtrack_name[i])
|
||||
for letter: String in piece:
|
||||
cleaned += letter
|
||||
source.append(i)
|
||||
i += length
|
||||
|
||||
var clean_search_text: String = clean_term(search_text)
|
||||
var found: int = cleaned.find(clean_search_text)
|
||||
var cleaned_positions: Array[int] = []
|
||||
if found >= 0:
|
||||
for j: int in clean_search_text.length():
|
||||
cleaned_positions.append(found + j)
|
||||
elif clean_search_text.length() >= Settings.LETTERS_FOR_LOOSE_SEARCH:
|
||||
var at: int = 0
|
||||
for letter: String in clean_search_text:
|
||||
at = cleaned.find(letter, at)
|
||||
if at < 0:
|
||||
return positions
|
||||
cleaned_positions.append(at)
|
||||
at += 1
|
||||
for cleaned_position: int in cleaned_positions:
|
||||
if !positions.has(source[cleaned_position]):
|
||||
positions.append(source[cleaned_position])
|
||||
return positions
|
||||
|
||||
|
||||
## Cleans a term by removing spaces and replacing special characters.
|
||||
## Matches MusicServer's clean_term() function.
|
||||
static func clean_term(term: String) -> String:
|
||||
|
||||
@@ -4,6 +4,10 @@ extends Node
|
||||
## Registered as the [code]Settings[/code] autoload.
|
||||
|
||||
const MAX_NUMBER_OF_PLAYERS: int = 7
|
||||
## How many letters a search needs before it also finds names with the letters in order
|
||||
## but apart, e.g. "lttp" for "A Link to the Past". Shorter searches only find names
|
||||
## that contain the text, since a letter or two in order matches most of the library.
|
||||
const LETTERS_FOR_LOOSE_SEARCH: int = 3
|
||||
|
||||
var default_path: String = "https://music.sanplex.xyz"
|
||||
## Password for getting an API token from the server. Any password works when the
|
||||
|
||||
+53
-27
@@ -16,37 +16,63 @@ panel = NodePath("SearchPanel")
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 640.0
|
||||
offset_bottom = 880.0
|
||||
theme_type_variation = &"PopupBackground"
|
||||
|
||||
[node name="ClearButton" type="Button" parent="." unique_id=1549439398]
|
||||
[node name="TopRow" type="HBoxContainer" parent="SearchPanel" unique_id=1262945995]
|
||||
layout_mode = 0
|
||||
offset_left = 936.0
|
||||
offset_top = 16.0
|
||||
offset_right = 1027.0
|
||||
offset_bottom = 56.0
|
||||
scale = Vector2(2, 2)
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Clear"
|
||||
offset_left = 24.0
|
||||
offset_top = 24.0
|
||||
offset_right = 1128.0
|
||||
offset_bottom = 80.0
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="Searchbar" type="TextEdit" parent="." unique_id=2112669372]
|
||||
layout_mode = 0
|
||||
offset_left = 32.0
|
||||
offset_top = 16.0
|
||||
offset_right = 468.0
|
||||
offset_bottom = 56.0
|
||||
scale = Vector2(2, 2)
|
||||
placeholder_text = "Search"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=213733486]
|
||||
layout_mode = 0
|
||||
offset_top = 128.0
|
||||
offset_right = 576.0
|
||||
offset_bottom = 384.0
|
||||
scale = Vector2(2, 2)
|
||||
|
||||
[node name="SearchResults" type="VBoxContainer" parent="ScrollContainer" unique_id=1567403144]
|
||||
[node name="SearchField" type="LineEdit" parent="SearchPanel/TopRow" unique_id=2112669372]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"SearchField"
|
||||
placeholder_text = "Search"
|
||||
caret_blink = true
|
||||
|
||||
[node name="ClearButton" type="Button" parent="SearchPanel/TopRow" unique_id=1549439398]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"LargeButton"
|
||||
action_mode = 0
|
||||
text = "Clear"
|
||||
|
||||
[node name="CountLabel" type="Label" parent="SearchPanel" unique_id=1262945996]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 24.0
|
||||
offset_top = 90.0
|
||||
offset_right = 1128.0
|
||||
offset_bottom = 116.0
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="SearchPanel" unique_id=213733486]
|
||||
layout_mode = 0
|
||||
offset_left = 24.0
|
||||
offset_top = 124.0
|
||||
offset_right = 1128.0
|
||||
offset_bottom = 856.0
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="SearchResults" type="VBoxContainer" parent="SearchPanel/ScrollContainer" unique_id=1567403144]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="EmptyLabel" type="Label" parent="SearchPanel" unique_id=1262945997]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 24.0
|
||||
offset_top = 160.0
|
||||
offset_right = 1128.0
|
||||
offset_bottom = 200.0
|
||||
theme_type_variation = &"Title"
|
||||
text = "No soundtracks found"
|
||||
horizontal_alignment = 1
|
||||
+112
-83
@@ -1,29 +1,28 @@
|
||||
class_name SearchPopup
|
||||
extends BasePopup
|
||||
## Searches every soundtrack on the server as you type, with [SoundtrackSearch].
|
||||
## Searches every soundtrack on the server as you type, with [SoundtrackSearch], and
|
||||
## highlights the letters that matched.
|
||||
|
||||
## Every soundtrack on the server, sorted by name.
|
||||
var soundtracks: Array = []
|
||||
var label_pool: Array[Label] = []
|
||||
|
||||
var _matcher: SoundtrackSearch = SoundtrackSearch.new()
|
||||
# One row per soundtrack, in the order of soundtracks. Rows are reused between searches.
|
||||
var _rows: Array[PanelContainer] = []
|
||||
var _index_of: Dictionary = {}
|
||||
|
||||
@onready var search_list: VBoxContainer = %SearchResults
|
||||
@onready var search_panel: Panel = %SearchPanel
|
||||
@onready var clear_button: Button = $ClearButton
|
||||
@onready var search_bar: TextEdit = $Searchbar
|
||||
@onready var search_field: LineEdit = %SearchField
|
||||
@onready var clear_button: Button = %ClearButton
|
||||
@onready var count_label: Label = %CountLabel
|
||||
@onready var empty_label: Label = %EmptyLabel
|
||||
@onready var search_results: VBoxContainer = %SearchResults
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
clear_button.pressed.connect(clear)
|
||||
search_bar.grab_focus()
|
||||
search_bar.text_changed.connect(search)
|
||||
visibility_changed.connect(focus)
|
||||
|
||||
|
||||
func focus() -> void:
|
||||
if visible:
|
||||
search_bar.grab_focus()
|
||||
clear()
|
||||
search_field.text_changed.connect(func(_text: String) -> void: search())
|
||||
visibility_changed.connect(_on_visibility_changed)
|
||||
|
||||
|
||||
func close() -> void:
|
||||
@@ -31,94 +30,124 @@ func close() -> void:
|
||||
super()
|
||||
|
||||
|
||||
## Empties the search field and shows every soundtrack.
|
||||
func clear() -> void:
|
||||
search_field.text = ""
|
||||
search()
|
||||
search_field.grab_focus()
|
||||
|
||||
|
||||
## Shows the soundtracks matching the search field, best matches first, or all of them
|
||||
## when it is empty.
|
||||
func search() -> void:
|
||||
GameLog.debug(search_bar.text)
|
||||
var search_text: String = search_bar.text
|
||||
|
||||
if search_text == "":
|
||||
show_all_soundtracks()
|
||||
return
|
||||
|
||||
perform_search(search_text)
|
||||
|
||||
|
||||
func show_all_soundtracks() -> void:
|
||||
# Hide all visible labels and add to pool
|
||||
for child: Label in search_list.get_children():
|
||||
if child.visible:
|
||||
child.visible = false
|
||||
label_pool.append(child)
|
||||
|
||||
# Show all soundtracks in order
|
||||
for soundtrack: String in soundtracks:
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
|
||||
|
||||
func perform_search(search_text: String) -> void:
|
||||
# Hide all visible labels and add to pool
|
||||
for child: Label in search_list.get_children():
|
||||
if child.visible:
|
||||
child.visible = false
|
||||
label_pool.append(child)
|
||||
|
||||
for soundtrack: String in _matcher.find_matches(search_text):
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
|
||||
|
||||
func add_soundtrack(soundtrack: String) -> void:
|
||||
# Backward compatibility - delegates to reuse version
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
|
||||
|
||||
## Shows [param soundtrack], reusing a hidden label from the pool when there is one.
|
||||
func add_soundtrack_reuse(soundtrack: String) -> void:
|
||||
var label: Label
|
||||
if label_pool.size() > 0:
|
||||
label = label_pool.pop_front()
|
||||
label.text = soundtrack
|
||||
label.visible = true
|
||||
# Move to end to maintain display order
|
||||
search_list.move_child(label, search_list.get_child_count() - 1)
|
||||
var search_text: String = search_field.text.strip_edges()
|
||||
var shown: Array[int] = []
|
||||
if search_text.is_empty():
|
||||
shown.assign(range(soundtracks.size()))
|
||||
else:
|
||||
label = Label.new()
|
||||
label.text = soundtrack
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
search_list.add_child(label)
|
||||
label.visible = true
|
||||
for soundtrack: String in _matcher.find_matches(search_text):
|
||||
shown.append(_index_of[soundtrack])
|
||||
|
||||
var is_shown: Array[bool] = []
|
||||
is_shown.resize(_rows.size())
|
||||
for position: int in shown.size():
|
||||
var row: PanelContainer = _rows[shown[position]]
|
||||
if row.get_index() != position:
|
||||
search_results.move_child(row, position)
|
||||
row.visible = true
|
||||
row.theme_type_variation = &"ListRowOdd" if position % 2 != 0 else &"ListRowEven"
|
||||
_show_name(row, soundtracks[shown[position]], search_text)
|
||||
is_shown[shown[position]] = true
|
||||
for index: int in _rows.size():
|
||||
if !is_shown[index]:
|
||||
_rows[index].visible = false
|
||||
|
||||
empty_label.visible = shown.is_empty() && !soundtracks.is_empty()
|
||||
if search_text.is_empty():
|
||||
count_label.text = "%d soundtracks" % soundtracks.size()
|
||||
else:
|
||||
count_label.text = "%d of %d soundtracks" % [shown.size(), soundtracks.size()]
|
||||
|
||||
|
||||
func check_if_soundtrack_exists(soundtrack: String) -> bool:
|
||||
var soundtrack_exists: bool = false
|
||||
for child: Label in search_list.get_children():
|
||||
if child.text == soundtrack and child.visible:
|
||||
soundtrack_exists = true
|
||||
return soundtrack_exists
|
||||
## The names of the soundtracks shown, in the order they are shown.
|
||||
func shown_names() -> Array[String]:
|
||||
var names: Array[String] = []
|
||||
for row: PanelContainer in search_results.get_children():
|
||||
if row.visible:
|
||||
names.append(row.get_meta(&"soundtrack"))
|
||||
return names
|
||||
|
||||
|
||||
func get_list_of_soundtracks() -> void:
|
||||
GameLog.debug("get_list_of_soundtracks")
|
||||
var handle_soundtracks: Callable = func handle_soundtracks(array: Variant) -> void:
|
||||
if typeof(array) == TYPE_ARRAY:
|
||||
# Reuse existing labels: hide all and add to pool
|
||||
for child: Label in search_list.get_children():
|
||||
child.visible = false
|
||||
label_pool.append(child)
|
||||
set_soundtracks(array)
|
||||
for soundtrack: String in soundtracks:
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
else:
|
||||
GameLog.warning("Unexpected data")
|
||||
MusicServerClient.fetch_all_soundtracks_ordered(handle_soundtracks)
|
||||
|
||||
|
||||
## Replaces the searchable soundtracks.
|
||||
## Replaces the searchable soundtracks and shows them, keeping the current search.
|
||||
func set_soundtracks(list: Array) -> void:
|
||||
soundtracks = []
|
||||
soundtracks.append_array(list)
|
||||
_matcher.set_names(soundtracks)
|
||||
_index_of.clear()
|
||||
for index: int in range(soundtracks.size() - 1, -1, -1):
|
||||
_index_of[soundtracks[index]] = index
|
||||
while _rows.size() < soundtracks.size():
|
||||
var row: PanelContainer = _make_row()
|
||||
search_results.add_child(row)
|
||||
_rows.append(row)
|
||||
while _rows.size() > soundtracks.size():
|
||||
_rows.pop_back().free()
|
||||
for index: int in soundtracks.size():
|
||||
_rows[index].set_meta(&"soundtrack", soundtracks[index])
|
||||
search()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
search_bar.text = ""
|
||||
show_all_soundtracks()
|
||||
search_bar.grab_focus()
|
||||
## [param soundtrack_name] as BBCode, with the letters [param search_text] matched in
|
||||
## [param highlight_color].
|
||||
static func highlighted_name(
|
||||
soundtrack_name: String, search_text: String, highlight_color: Color) -> String:
|
||||
var matched: Array[int] = SoundtrackSearch.matched_letters(soundtrack_name, search_text)
|
||||
var text: String = ""
|
||||
var in_highlight: bool = false
|
||||
for i: int in soundtrack_name.length():
|
||||
var highlight: bool = matched.has(i)
|
||||
if highlight != in_highlight:
|
||||
text += "[color=#%s]" % highlight_color.to_html(false) if highlight else "[/color]"
|
||||
in_highlight = highlight
|
||||
var letter: String = soundtrack_name[i]
|
||||
text += "[lb]" if letter == "[" else letter
|
||||
if in_highlight:
|
||||
text += "[/color]"
|
||||
return text
|
||||
|
||||
|
||||
func _make_row() -> PanelContainer:
|
||||
var row: PanelContainer = PanelContainer.new()
|
||||
var label: RichTextLabel = RichTextLabel.new()
|
||||
label.theme_type_variation = &"SearchResult"
|
||||
label.bbcode_enabled = true
|
||||
label.fit_content = true
|
||||
label.scroll_active = false
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
row.add_child(label)
|
||||
return row
|
||||
|
||||
|
||||
func _show_name(row: PanelContainer, soundtrack_name: String, search_text: String) -> void:
|
||||
var text: String = highlighted_name(soundtrack_name, search_text,
|
||||
get_theme_color(&"font_color", &"HighlightLarge"))
|
||||
var label: RichTextLabel = row.get_child(0)
|
||||
# Setting the same text again would make the label lay it out again for nothing
|
||||
if label.text != text:
|
||||
label.text = text
|
||||
|
||||
|
||||
func _on_visibility_changed() -> void:
|
||||
if visible:
|
||||
clear()
|
||||
+1646
-681
File diff suppressed because it is too large.
Load diff
@@ -215,6 +215,15 @@ func run(on_state: Callable) -> void:
|
||||
_close_popup(popup)
|
||||
await _settle()
|
||||
|
||||
var search_popup: SearchPopup = _node("SearchPopup")
|
||||
search_popup.open()
|
||||
search_popup.search_field.text = "lttp"
|
||||
search_popup.search()
|
||||
await _settle()
|
||||
await on_state.call("popup_Search_typed", search_popup)
|
||||
search_popup.close()
|
||||
await _settle()
|
||||
|
||||
var sync_popup: BasePopup = _node("SyncPopup")
|
||||
Settings.currently_syncing = false
|
||||
sync_popup.has_synced = false
|
||||
|
||||
@@ -103,12 +103,12 @@ func test_button_works_again_after_popup_closed() -> void:
|
||||
func test_closing_search_clears_the_search() -> void:
|
||||
var search: BasePopup = _popup("SearchPopup")
|
||||
search.open()
|
||||
var search_bar: TextEdit = search.get_node("Searchbar")
|
||||
search_bar.text = "mario"
|
||||
var search_field: LineEdit = search.search_field
|
||||
search_field.text = "mario"
|
||||
|
||||
search.close()
|
||||
|
||||
assert_str(search_bar.text).is_empty()
|
||||
assert_str(search_field.text).is_empty()
|
||||
|
||||
|
||||
func test_closing_character_select_allows_song_clicks_again() -> void:
|
||||
@@ -129,3 +129,4 @@ func test_toggle_opens_and_closes() -> void:
|
||||
|
||||
shortcuts.toggle()
|
||||
assert_bool(shortcuts.visible).is_false()
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
extends MainWindowTestSuite
|
||||
## The Search popup: typing, the result list, the count, the highlighted letters and
|
||||
## the layout.
|
||||
|
||||
const SOUNDTRACKS: Array[String] = [
|
||||
"Chrono Trigger", "Final Fantasy VI", "Mario & Luigi: Superstar Saga", "Super Mario 64",
|
||||
"The Legend of Zelda: A Link to the Past",
|
||||
]
|
||||
|
||||
var _popup: SearchPopup
|
||||
|
||||
|
||||
func _before_window_starts() -> void:
|
||||
_server.respond_json("/music/all/order", SOUNDTRACKS)
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
super()
|
||||
_popup = _window.get_node("SearchPopup")
|
||||
|
||||
|
||||
func _open() -> void:
|
||||
await _click_at(_window.get_node("TopBar/SearchButton").get_global_rect().get_center())
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
|
||||
func _type(text: String) -> void:
|
||||
for letter: String in text:
|
||||
_popup.search_field.insert_text_at_caret(letter)
|
||||
_popup.search_field.text_changed.emit(_popup.search_field.text)
|
||||
|
||||
|
||||
func _row_texts() -> Array[String]:
|
||||
var texts: Array[String] = []
|
||||
for row: PanelContainer in _popup.search_results.get_children():
|
||||
if row.visible:
|
||||
texts.append((row.get_child(0) as RichTextLabel).text)
|
||||
return texts
|
||||
|
||||
|
||||
func test_shows_every_soundtrack_with_a_count() -> void:
|
||||
await _open()
|
||||
|
||||
assert_array(_popup.shown_names()).contains_exactly(SOUNDTRACKS)
|
||||
assert_str(_popup.count_label.text).is_equal("5 soundtracks")
|
||||
assert_bool(_popup.empty_label.visible).is_false()
|
||||
assert_bool(_popup.search_field.has_focus()).is_true()
|
||||
|
||||
|
||||
func test_typing_filters_and_counts() -> void:
|
||||
await _open()
|
||||
|
||||
_type("mario")
|
||||
|
||||
assert_array(_popup.shown_names()) \
|
||||
.contains_exactly(["Mario & Luigi: Superstar Saga", "Super Mario 64"])
|
||||
assert_str(_popup.count_label.text).is_equal("2 of 5 soundtracks")
|
||||
|
||||
|
||||
func test_enter_does_not_break_the_search() -> void:
|
||||
await _open()
|
||||
_type("chrono")
|
||||
|
||||
_runner.simulate_key_pressed(KEY_ENTER)
|
||||
await _runner.await_input_processed()
|
||||
|
||||
assert_str(_popup.search_field.text).is_equal("chrono")
|
||||
assert_array(_popup.shown_names()).contains_exactly(["Chrono Trigger"])
|
||||
|
||||
|
||||
func test_no_match_says_so() -> void:
|
||||
await _open()
|
||||
|
||||
_type("metroid")
|
||||
|
||||
assert_array(_popup.shown_names()).is_empty()
|
||||
assert_bool(_popup.empty_label.visible).is_true()
|
||||
assert_str(_popup.count_label.text).is_equal("0 of 5 soundtracks")
|
||||
|
||||
|
||||
func test_short_search_does_not_match_letters_far_apart() -> void:
|
||||
await _open()
|
||||
|
||||
_type("sm")
|
||||
|
||||
assert_array(_popup.shown_names()).is_empty()
|
||||
_type("r")
|
||||
assert_array(_popup.shown_names()).contains_exactly(["Super Mario 64"])
|
||||
|
||||
|
||||
func test_matched_letters_are_highlighted() -> void:
|
||||
await _open()
|
||||
var color: String = _popup.get_theme_color(&"font_color", &"HighlightLarge").to_html(false)
|
||||
|
||||
_type("trig")
|
||||
|
||||
assert_array(_row_texts()).contains_exactly(["Chrono [color=#%s]Trig[/color]ger" % color])
|
||||
|
||||
|
||||
func test_letters_in_order_are_highlighted_one_by_one() -> void:
|
||||
var color: Color = Color.ORANGE
|
||||
var text: String = SearchPopup.highlighted_name("Super Mario 64", "smr", color)
|
||||
var c: String = color.to_html(false)
|
||||
|
||||
assert_str(text).is_equal(
|
||||
"[color=#%s]S[/color]uper [color=#%s]M[/color]a[color=#%s]r[/color]io 64" % [c, c, c])
|
||||
|
||||
|
||||
func test_square_brackets_in_names_are_shown_as_text() -> void:
|
||||
assert_str(SearchPopup.highlighted_name("Disc [1]", "", Color.ORANGE)) \
|
||||
.is_equal("Disc [lb]1]")
|
||||
|
||||
|
||||
func test_clear_shows_everything_again_without_highlights() -> void:
|
||||
await _open()
|
||||
_type("mario")
|
||||
|
||||
await _click_at(_popup.clear_button.get_global_rect().get_center())
|
||||
|
||||
assert_str(_popup.search_field.text).is_empty()
|
||||
assert_array(_row_texts()).contains_exactly(SOUNDTRACKS)
|
||||
|
||||
|
||||
func test_rows_alternate_like_the_other_lists() -> void:
|
||||
await _open()
|
||||
_type("mario")
|
||||
|
||||
var rows: Array[Node] = _popup.search_results.get_children().filter(
|
||||
func(row: Control) -> bool: return row.visible)
|
||||
assert_str(String(rows[0].theme_type_variation)).is_equal("ListRowEven")
|
||||
assert_str(String(rows[1].theme_type_variation)).is_equal("ListRowOdd")
|
||||
|
||||
|
||||
func test_everything_fits_inside_the_panel_at_its_real_size() -> void:
|
||||
await _open()
|
||||
|
||||
var panel: Rect2 = _popup.search_panel.get_global_rect()
|
||||
for node: Control in [_popup.search_field, _popup.clear_button, _popup.count_label,
|
||||
_popup.search_results.get_parent()]:
|
||||
assert_bool(panel.encloses(node.get_global_rect())) \
|
||||
.override_failure_message(node.name + " is outside the panel").is_true()
|
||||
assert_vector(node.scale).is_equal(Vector2.ONE)
|
||||
assert_bool(Rect2(Vector2.ZERO, Vector2(1920, 1080)).encloses(panel)).is_true()
|
||||
@@ -0,0 +1 @@
|
||||
uid://s5ps2kikouh7
|
||||
@@ -34,6 +34,9 @@ const STYLES: Dictionary = {
|
||||
"StatisticsCardHover": "PanelContainer",
|
||||
"StatisticsGrid": "GridContainer",
|
||||
"TabButton": "Button",
|
||||
"LargeButton": "Button",
|
||||
"SearchField": "LineEdit",
|
||||
"SearchResult": "RichTextLabel",
|
||||
}
|
||||
|
||||
## Theme types that only scripts read from, so they are not styles a node can pick.
|
||||
|
||||
@@ -63,16 +63,30 @@ func test_clean_term() -> void:
|
||||
assert_str(SoundtrackSearch.clean_term("Ys I & II")).is_equal("ysiandii")
|
||||
|
||||
|
||||
func test_search_popup_searches_with_soundtrack_search() -> void:
|
||||
var popup: SearchPopup = auto_free(load("res://popups/SearchPopup.tscn").instantiate())
|
||||
add_child(popup)
|
||||
popup.set_soundtracks(["Chrono Trigger", "Super Mario 64"])
|
||||
popup.show_all_soundtracks()
|
||||
func test_short_search_only_finds_names_containing_it() -> void:
|
||||
# "sm" is in order in "Super Mario 64" but not next to each other
|
||||
assert_int(Settings.LETTERS_FOR_LOOSE_SEARCH).is_equal(3)
|
||||
assert_array(_search.find_matches("sm")).is_empty()
|
||||
assert_array(_search.find_matches("smr")).contains_exactly(["Super Mario 64"])
|
||||
|
||||
popup.perform_search("smr")
|
||||
|
||||
var shown: Array[String] = []
|
||||
for label: Label in popup.search_list.get_children():
|
||||
if label.visible:
|
||||
shown.append(label.text)
|
||||
assert_array(shown).contains_exactly(["Super Mario 64"])
|
||||
func test_matched_letters_of_plain_match() -> void:
|
||||
assert_array(SoundtrackSearch.matched_letters("Chrono Trigger", "trig")) \
|
||||
.contains_exactly([7, 8, 9, 10])
|
||||
|
||||
|
||||
func test_matched_letters_ignoring_spaces_and_special_characters() -> void:
|
||||
# "mario and luigi" matches "Mario & Luigi" with & read as "and"
|
||||
var letters: Array[int] = SoundtrackSearch.matched_letters(
|
||||
"Mario & Luigi: Superstar Saga", "mario and luigi")
|
||||
assert_array(letters).contains_exactly([0, 1, 2, 3, 4, 6, 8, 9, 10, 11, 12])
|
||||
|
||||
|
||||
func test_matched_letters_in_order() -> void:
|
||||
assert_array(SoundtrackSearch.matched_letters("Super Mario 64", "smr")) \
|
||||
.contains_exactly([0, 6, 8])
|
||||
|
||||
|
||||
func test_matched_letters_of_no_match() -> void:
|
||||
assert_array(SoundtrackSearch.matched_letters("Chrono Trigger", "metroid")).is_empty()
|
||||
assert_array(SoundtrackSearch.matched_letters("Super Mario 64", "sm")).is_empty()
|
||||
@@ -830,6 +830,8 @@ Label/constants/shadow_outline_size = 1
|
||||
Label/font_sizes/font_size = 16
|
||||
Label/styles/focus = SubResource("StyleBoxFlat_6h40v")
|
||||
Label/styles/normal = SubResource("StyleBoxEmpty_s47sh")
|
||||
LargeButton/base_type = &"Button"
|
||||
LargeButton/font_sizes/font_size = 24
|
||||
ListRowEven/base_type = &"PanelContainer"
|
||||
ListRowEven/styles/panel = SubResource("StyleBoxFlat_u5sc5")
|
||||
ListRowOdd/base_type = &"PanelContainer"
|
||||
@@ -914,6 +916,10 @@ ScrollContainer/colors/scroll_hint_horizontal_color = Color(0, 0, 0, 1)
|
||||
ScrollContainer/colors/scroll_hint_vertical_color = Color(0, 0, 0, 1)
|
||||
ScrollContainer/styles/focus = SubResource("StyleBoxFlat_35dwi")
|
||||
ScrollContainer/styles/panel = SubResource("StyleBoxEmpty_r3gy8")
|
||||
SearchField/base_type = &"LineEdit"
|
||||
SearchField/font_sizes/font_size = 28
|
||||
SearchResult/base_type = &"RichTextLabel"
|
||||
SearchResult/font_sizes/normal_font_size = 22
|
||||
ShortcutRowDark/base_type = &"PanelContainer"
|
||||
ShortcutRowDark/styles/panel = SubResource("StyleBoxFlat_anjdx")
|
||||
SongRow/base_type = &"PanelContainer"
|
||||
|
||||
Reference in new issue
Block a user