Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2d547b650 | ||
|
|
845d08c260 |
No files matched your search
@@ -1,7 +1,8 @@
|
|||||||
class_name SoundtrackSearch
|
class_name SoundtrackSearch
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Finds soundtrack names matching a search text. Matches ignore case, spaces and
|
## 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.
|
## 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 seen: Dictionary = {}
|
||||||
var search_text_lower: String = search_text.to_lower()
|
var search_text_lower: String = search_text.to_lower()
|
||||||
var clean_search_text: String = clean_term(search_text)
|
var clean_search_text: String = clean_term(search_text)
|
||||||
var regex: RegEx = _compile_regex_cached(clean_search_text)
|
|
||||||
var passes: Array[Callable] = [
|
var passes: Array[Callable] = [
|
||||||
func(entry: Dictionary) -> bool: return entry.lowercase.contains(search_text_lower),
|
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 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] = []
|
var matches: Array[String] = []
|
||||||
for matches_pass: Callable in passes:
|
for matches_pass: Callable in passes:
|
||||||
@@ -46,6 +49,57 @@ func find_matches(search_text: String) -> Array[String]:
|
|||||||
return matches
|
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.
|
## Cleans a term by removing spaces and replacing special characters.
|
||||||
## Matches MusicServer's clean_term() function.
|
## Matches MusicServer's clean_term() function.
|
||||||
static func clean_term(term: String) -> String:
|
static func clean_term(term: String) -> String:
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ extends Node
|
|||||||
## Registered as the [code]Settings[/code] autoload.
|
## Registered as the [code]Settings[/code] autoload.
|
||||||
|
|
||||||
const MAX_NUMBER_OF_PLAYERS: int = 7
|
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"
|
var default_path: String = "https://music.sanplex.xyz"
|
||||||
## Password for getting an API token from the server. Any password works when the
|
## 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")]
|
[node name="SearchPopup" parent="." unique_id=1661250375 instance=ExtResource("9_5ijvr")]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 56.0
|
offset_left = 384.0
|
||||||
offset_top = 96.0
|
offset_top = 100.0
|
||||||
offset_right = 56.0
|
offset_right = 384.0
|
||||||
offset_bottom = 96.0
|
offset_bottom = 100.0
|
||||||
|
|
||||||
[node name="SyncPopup" parent="." unique_id=182499699 instance=ExtResource("10_yxw0b")]
|
[node name="SyncPopup" parent="." unique_id=182499699 instance=ExtResource("10_yxw0b")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 210.0
|
offset_left = 210.0
|
||||||
offset_top = 153.0
|
offset_top = 220.0
|
||||||
offset_right = 210.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")]
|
[node name="QRPopup" parent="." unique_id=589733821 instance=ExtResource("15_qr_window")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 720.0
|
offset_left = 735.0
|
||||||
offset_top = 153.0
|
offset_top = 280.0
|
||||||
offset_right = 720.0
|
offset_right = 735.0
|
||||||
offset_bottom = 153.0
|
offset_bottom = 280.0
|
||||||
|
|
||||||
[node name="ShortcutsPopup" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
[node name="ShortcutsPopup" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 562.0
|
offset_left = 610.0
|
||||||
offset_top = 160.0
|
offset_top = 190.0
|
||||||
offset_right = 1262.0
|
offset_right = 1310.0
|
||||||
offset_bottom = 860.0
|
offset_bottom = 890.0
|
||||||
|
|
||||||
[node name="AboutPopup" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
[node name="AboutPopup" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 360.0
|
offset_left = 360.0
|
||||||
offset_top = 153.0
|
offset_top = 220.0
|
||||||
offset_right = 360.0
|
offset_right = 360.0
|
||||||
offset_bottom = 153.0
|
offset_bottom = 220.0
|
||||||
|
|
||||||
[node name="LogPopup" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
|
[node name="LogPopup" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 562.0
|
offset_left = 610.0
|
||||||
offset_top = 132.0
|
offset_top = 190.0
|
||||||
offset_right = 1262.0
|
offset_right = 1310.0
|
||||||
offset_bottom = 832.0
|
offset_bottom = 890.0
|
||||||
|
|
||||||
[node name="SoundtrackStatsPopup" parent="." unique_id=1213999001 instance=ExtResource("soundtrack_stats_popup_scene")]
|
[node name="SoundtrackStatsPopup" parent="." unique_id=1213999001 instance=ExtResource("soundtrack_stats_popup_scene")]
|
||||||
visible = false
|
visible = false
|
||||||
|
|||||||
+53
-27
@@ -16,37 +16,63 @@ panel = NodePath("SearchPanel")
|
|||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_right = 1152.0
|
offset_right = 1152.0
|
||||||
offset_bottom = 640.0
|
offset_bottom = 880.0
|
||||||
theme_type_variation = &"PopupBackground"
|
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
|
layout_mode = 0
|
||||||
offset_left = 936.0
|
offset_left = 24.0
|
||||||
offset_top = 16.0
|
offset_top = 24.0
|
||||||
offset_right = 1027.0
|
offset_right = 1128.0
|
||||||
offset_bottom = 56.0
|
offset_bottom = 80.0
|
||||||
scale = Vector2(2, 2)
|
theme_override_constants/separation = 16
|
||||||
focus_mode = 0
|
|
||||||
action_mode = 0
|
|
||||||
text = "Clear"
|
|
||||||
|
|
||||||
[node name="Searchbar" type="TextEdit" parent="." unique_id=2112669372]
|
[node name="SearchField" type="LineEdit" parent="SearchPanel/TopRow" 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]
|
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
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
|
class_name SearchPopup
|
||||||
extends BasePopup
|
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 soundtracks: Array = []
|
||||||
var label_pool: Array[Label] = []
|
|
||||||
|
|
||||||
var _matcher: SoundtrackSearch = SoundtrackSearch.new()
|
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 search_panel: Panel = %SearchPanel
|
||||||
@onready var clear_button: Button = $ClearButton
|
@onready var search_field: LineEdit = %SearchField
|
||||||
@onready var search_bar: TextEdit = $Searchbar
|
@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:
|
func _ready() -> void:
|
||||||
clear_button.pressed.connect(clear)
|
clear_button.pressed.connect(clear)
|
||||||
search_bar.grab_focus()
|
search_field.text_changed.connect(func(_text: String) -> void: search())
|
||||||
search_bar.text_changed.connect(search)
|
visibility_changed.connect(_on_visibility_changed)
|
||||||
visibility_changed.connect(focus)
|
|
||||||
|
|
||||||
|
|
||||||
func focus() -> void:
|
|
||||||
if visible:
|
|
||||||
search_bar.grab_focus()
|
|
||||||
clear()
|
|
||||||
|
|
||||||
|
|
||||||
func close() -> void:
|
func close() -> void:
|
||||||
@@ -31,94 +30,124 @@ func close() -> void:
|
|||||||
super()
|
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:
|
func search() -> void:
|
||||||
GameLog.debug(search_bar.text)
|
var search_text: String = search_field.text.strip_edges()
|
||||||
var search_text: String = search_bar.text
|
var shown: Array[int] = []
|
||||||
|
if search_text.is_empty():
|
||||||
if search_text == "":
|
shown.assign(range(soundtracks.size()))
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
label = Label.new()
|
for soundtrack: String in _matcher.find_matches(search_text):
|
||||||
label.text = soundtrack
|
shown.append(_index_of[soundtrack])
|
||||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
|
||||||
search_list.add_child(label)
|
var is_shown: Array[bool] = []
|
||||||
label.visible = true
|
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:
|
## The names of the soundtracks shown, in the order they are shown.
|
||||||
var soundtrack_exists: bool = false
|
func shown_names() -> Array[String]:
|
||||||
for child: Label in search_list.get_children():
|
var names: Array[String] = []
|
||||||
if child.text == soundtrack and child.visible:
|
for row: PanelContainer in search_results.get_children():
|
||||||
soundtrack_exists = true
|
if row.visible:
|
||||||
return soundtrack_exists
|
names.append(row.get_meta(&"soundtrack"))
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
func get_list_of_soundtracks() -> void:
|
func get_list_of_soundtracks() -> void:
|
||||||
GameLog.debug("get_list_of_soundtracks")
|
GameLog.debug("get_list_of_soundtracks")
|
||||||
var handle_soundtracks: Callable = func handle_soundtracks(array: Variant) -> void:
|
var handle_soundtracks: Callable = func handle_soundtracks(array: Variant) -> void:
|
||||||
if typeof(array) == TYPE_ARRAY:
|
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)
|
set_soundtracks(array)
|
||||||
for soundtrack: String in soundtracks:
|
|
||||||
add_soundtrack_reuse(soundtrack)
|
|
||||||
else:
|
else:
|
||||||
GameLog.warning("Unexpected data")
|
GameLog.warning("Unexpected data")
|
||||||
MusicServerClient.fetch_all_soundtracks_ordered(handle_soundtracks)
|
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:
|
func set_soundtracks(list: Array) -> void:
|
||||||
soundtracks = []
|
soundtracks = []
|
||||||
soundtracks.append_array(list)
|
soundtracks.append_array(list)
|
||||||
_matcher.set_names(soundtracks)
|
_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:
|
## [param soundtrack_name] as BBCode, with the letters [param search_text] matched in
|
||||||
search_bar.text = ""
|
## [param highlight_color].
|
||||||
show_all_soundtracks()
|
static func highlighted_name(
|
||||||
search_bar.grab_focus()
|
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")]
|
[node name="SettingsPopup" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 720.0
|
offset_left = 710.0
|
||||||
offset_top = 153.0
|
offset_top = 220.0
|
||||||
offset_right = 720.0
|
offset_right = 710.0
|
||||||
offset_bottom = 153.0
|
offset_bottom = 220.0
|
||||||
|
|
||||||
[node name="CharacterSelectPopup" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
|
[node name="CharacterSelectPopup" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 405.0
|
offset_left = 315.0
|
||||||
offset_top = 165.0
|
offset_top = 208.0
|
||||||
offset_right = 1557.0
|
offset_right = 1467.0
|
||||||
offset_bottom = 805.0
|
offset_bottom = 848.0
|
||||||
|
|
||||||
[node name="WinnerPopup" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
|
[node name="WinnerPopup" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
offset_left = 160.0
|
offset_left = 160.0
|
||||||
offset_top = 100.0
|
offset_top = 80.0
|
||||||
offset_right = 160.0
|
offset_right = 160.0
|
||||||
offset_bottom = 100.0
|
offset_bottom = 80.0
|
||||||
|
|
||||||
[node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
|
[node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
|
||||||
visible = false
|
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)
|
_close_popup(popup)
|
||||||
await _settle()
|
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")
|
var sync_popup: BasePopup = _node("SyncPopup")
|
||||||
Settings.currently_syncing = false
|
Settings.currently_syncing = false
|
||||||
sync_popup.has_synced = 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:
|
func test_closing_search_clears_the_search() -> void:
|
||||||
var search: BasePopup = _popup("SearchPopup")
|
var search: BasePopup = _popup("SearchPopup")
|
||||||
search.open()
|
search.open()
|
||||||
var search_bar: TextEdit = search.get_node("Searchbar")
|
var search_field: LineEdit = search.search_field
|
||||||
search_bar.text = "mario"
|
search_field.text = "mario"
|
||||||
|
|
||||||
search.close()
|
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:
|
func test_closing_character_select_allows_song_clicks_again() -> void:
|
||||||
@@ -129,3 +129,26 @@ func test_toggle_opens_and_closes() -> void:
|
|||||||
|
|
||||||
shortcuts.toggle()
|
shortcuts.toggle()
|
||||||
assert_bool(shortcuts.visible).is_false()
|
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",
|
"StatisticsCardHover": "PanelContainer",
|
||||||
"StatisticsGrid": "GridContainer",
|
"StatisticsGrid": "GridContainer",
|
||||||
"TabButton": "Button",
|
"TabButton": "Button",
|
||||||
|
"LargeButton": "Button",
|
||||||
|
"SearchField": "LineEdit",
|
||||||
|
"SearchResult": "RichTextLabel",
|
||||||
}
|
}
|
||||||
|
|
||||||
## Theme types that only scripts read from, so they are not styles a node can pick.
|
## 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")
|
assert_str(SoundtrackSearch.clean_term("Ys I & II")).is_equal("ysiandii")
|
||||||
|
|
||||||
|
|
||||||
func test_search_popup_searches_with_soundtrack_search() -> void:
|
func test_short_search_only_finds_names_containing_it() -> void:
|
||||||
var popup: SearchPopup = auto_free(load("res://popups/SearchPopup.tscn").instantiate())
|
# "sm" is in order in "Super Mario 64" but not next to each other
|
||||||
add_child(popup)
|
assert_int(Settings.LETTERS_FOR_LOOSE_SEARCH).is_equal(3)
|
||||||
popup.set_soundtracks(["Chrono Trigger", "Super Mario 64"])
|
assert_array(_search.find_matches("sm")).is_empty()
|
||||||
popup.show_all_soundtracks()
|
assert_array(_search.find_matches("smr")).contains_exactly(["Super Mario 64"])
|
||||||
|
|
||||||
popup.perform_search("smr")
|
|
||||||
|
|
||||||
var shown: Array[String] = []
|
func test_matched_letters_of_plain_match() -> void:
|
||||||
for label: Label in popup.search_list.get_children():
|
assert_array(SoundtrackSearch.matched_letters("Chrono Trigger", "trig")) \
|
||||||
if label.visible:
|
.contains_exactly([7, 8, 9, 10])
|
||||||
shown.append(label.text)
|
|
||||||
assert_array(shown).contains_exactly(["Super Mario 64"])
|
|
||||||
|
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/font_sizes/font_size = 16
|
||||||
Label/styles/focus = SubResource("StyleBoxFlat_6h40v")
|
Label/styles/focus = SubResource("StyleBoxFlat_6h40v")
|
||||||
Label/styles/normal = SubResource("StyleBoxEmpty_s47sh")
|
Label/styles/normal = SubResource("StyleBoxEmpty_s47sh")
|
||||||
|
LargeButton/base_type = &"Button"
|
||||||
|
LargeButton/font_sizes/font_size = 24
|
||||||
ListRowEven/base_type = &"PanelContainer"
|
ListRowEven/base_type = &"PanelContainer"
|
||||||
ListRowEven/styles/panel = SubResource("StyleBoxFlat_u5sc5")
|
ListRowEven/styles/panel = SubResource("StyleBoxFlat_u5sc5")
|
||||||
ListRowOdd/base_type = &"PanelContainer"
|
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/colors/scroll_hint_vertical_color = Color(0, 0, 0, 1)
|
||||||
ScrollContainer/styles/focus = SubResource("StyleBoxFlat_35dwi")
|
ScrollContainer/styles/focus = SubResource("StyleBoxFlat_35dwi")
|
||||||
ScrollContainer/styles/panel = SubResource("StyleBoxEmpty_r3gy8")
|
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/base_type = &"PanelContainer"
|
||||||
ShortcutRowDark/styles/panel = SubResource("StyleBoxFlat_anjdx")
|
ShortcutRowDark/styles/panel = SubResource("StyleBoxFlat_anjdx")
|
||||||
SongRow/base_type = &"PanelContainer"
|
SongRow/base_type = &"PanelContainer"
|
||||||
|
|||||||
Reference in new issue
Block a user