Compare commits
2
Commits
13fdb3f68c
...
b2d547b650
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2d547b650 | ||
|
|
845d08c260 |
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
|
||||
|
||||
+20
-20
@@ -42,55 +42,55 @@ layout_mode = 1
|
||||
[node name="SearchPopup" parent="." unique_id=1661250375 instance=ExtResource("9_5ijvr")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 56.0
|
||||
offset_top = 96.0
|
||||
offset_right = 56.0
|
||||
offset_bottom = 96.0
|
||||
offset_left = 384.0
|
||||
offset_top = 100.0
|
||||
offset_right = 384.0
|
||||
offset_bottom = 100.0
|
||||
|
||||
[node name="SyncPopup" parent="." unique_id=182499699 instance=ExtResource("10_yxw0b")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 210.0
|
||||
offset_top = 153.0
|
||||
offset_top = 220.0
|
||||
offset_right = 210.0
|
||||
offset_bottom = 153.0
|
||||
offset_bottom = 220.0
|
||||
|
||||
[node name="QRPopup" parent="." unique_id=589733821 instance=ExtResource("15_qr_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 720.0
|
||||
offset_top = 153.0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 153.0
|
||||
offset_left = 735.0
|
||||
offset_top = 280.0
|
||||
offset_right = 735.0
|
||||
offset_bottom = 280.0
|
||||
|
||||
[node name="ShortcutsPopup" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 562.0
|
||||
offset_top = 160.0
|
||||
offset_right = 1262.0
|
||||
offset_bottom = 860.0
|
||||
offset_left = 610.0
|
||||
offset_top = 190.0
|
||||
offset_right = 1310.0
|
||||
offset_bottom = 890.0
|
||||
|
||||
[node name="AboutPopup" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 360.0
|
||||
offset_top = 153.0
|
||||
offset_top = 220.0
|
||||
offset_right = 360.0
|
||||
offset_bottom = 153.0
|
||||
offset_bottom = 220.0
|
||||
|
||||
[node name="LogPopup" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 562.0
|
||||
offset_top = 132.0
|
||||
offset_right = 1262.0
|
||||
offset_bottom = 832.0
|
||||
offset_left = 610.0
|
||||
offset_top = 190.0
|
||||
offset_right = 1310.0
|
||||
offset_bottom = 890.0
|
||||
|
||||
[node name="SoundtrackStatsPopup" parent="." unique_id=1213999001 instance=ExtResource("soundtrack_stats_popup_scene")]
|
||||
visible = false
|
||||
|
||||
+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()
|
||||
+10
-10
@@ -93,28 +93,28 @@ offset_bottom = 975.0
|
||||
[node name="SettingsPopup" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 720.0
|
||||
offset_top = 153.0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 153.0
|
||||
offset_left = 710.0
|
||||
offset_top = 220.0
|
||||
offset_right = 710.0
|
||||
offset_bottom = 220.0
|
||||
|
||||
[node name="CharacterSelectPopup" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 405.0
|
||||
offset_top = 165.0
|
||||
offset_right = 1557.0
|
||||
offset_bottom = 805.0
|
||||
offset_left = 315.0
|
||||
offset_top = 208.0
|
||||
offset_right = 1467.0
|
||||
offset_bottom = 848.0
|
||||
|
||||
[node name="WinnerPopup" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 160.0
|
||||
offset_top = 100.0
|
||||
offset_top = 80.0
|
||||
offset_right = 160.0
|
||||
offset_bottom = 100.0
|
||||
offset_bottom = 80.0
|
||||
|
||||
[node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
|
||||
visible = false
|
||||
|
||||
+4356
-3391
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,26 @@ func test_toggle_opens_and_closes() -> void:
|
||||
|
||||
shortcuts.toggle()
|
||||
assert_bool(shortcuts.visible).is_false()
|
||||
|
||||
|
||||
func test_every_popup_is_centred_in_the_window() -> void:
|
||||
var window_centre: Vector2 = Vector2(1920, 1080) / 2
|
||||
var popups: Array[BasePopup] = [_window.get_node("SoundtrackStatsPopup")]
|
||||
for node_name: String in MAIN_POPUPS + QUIZ_POPUPS:
|
||||
popups.append(_popup(node_name))
|
||||
for popup: BasePopup in popups:
|
||||
match popup.name:
|
||||
"WinnerPopup":
|
||||
popup.show_winner(_add_player("Crono"))
|
||||
"CharacterSelectPopup":
|
||||
popup.show_grid("Crono")
|
||||
"SoundtrackStatsPopup":
|
||||
popup.show_soundtrack(SoundtrackWithSongs.new())
|
||||
_:
|
||||
popup.open()
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
var centre: Vector2 = popup.panel.get_global_rect().get_center()
|
||||
assert_float(centre.distance_to(window_centre)).override_failure_message(
|
||||
"%s is centred at %s" % [popup.name, centre]).is_less_equal(1.0)
|
||||
popup.close()
|
||||
@@ -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