Files
MusicPlayer/popups/search_popup.gd
T
Sansan d77b72159d Sort the scripts and scenes into folders
main/ holds the main window and top bar, quiz/ the quiz and its parts,
player/ the music player, popups/ every popup and what they share, data/ the
songs, players, playlist and settings, server/ the MusicServer client,
common/ the list row and GameLog, and themes/ the themes and style boxes.
2026-10-06 11:57:29 +02:00

197 lines
6.0 KiB
GDScript

class_name SearchPopup
extends BasePopup
## Searches every soundtrack on the server as you type. Matches ignore case, spaces and
## special characters, and also find the letters in order with others in between.
var soundtracks: Array = []
## Each soundtrack as a Dictionary with its original, lowercase and cleaned name.
var precomputed_soundtracks: Array = []
## Compiled patterns for the letters-in-order search, by cleaned search term.
var regex_cache: Dictionary = {}
var label_pool: Array[Label] = []
@onready var search_list: VBoxContainer = %SearchResults
@onready var search_panel: Panel = %SearchPanel
@onready var clear_button: Button = $ClearButton
@onready var search_bar: TextEdit = $Searchbar
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()
func close() -> void:
clear()
super()
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 find_matches(search_text):
add_soundtrack_reuse(soundtrack)
## Returns the soundtracks matching [param search_text], best matches first: plain
## case-insensitive matches, then matches ignoring spaces and special characters,
## then matches with the letters in order but not next to each other.
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 matches: Array[String] = []
# Pass 1: Exact case-insensitive contains match on lowercase versions
for i: int in range(precomputed_soundtracks.size()):
var st_data: Dictionary = precomputed_soundtracks[i]
if st_data["lowercase"].contains(search_text_lower):
if not st_data["original"] in seen:
seen[st_data["original"]] = true
matches.append(st_data["original"])
# Pass 2: Contains match on cleaned terms
for i: int in range(precomputed_soundtracks.size()):
var st_data: Dictionary = precomputed_soundtracks[i]
if st_data["cleaned"].contains(clean_search_text):
if not st_data["original"] in seen:
seen[st_data["original"]] = true
matches.append(st_data["original"])
# Pass 3: Regex match on cleaned terms
var regex: RegEx = compile_regex_cached(clean_search_text)
for i: int in range(precomputed_soundtracks.size()):
var st_data: Dictionary = precomputed_soundtracks[i]
if regex.search(st_data["cleaned"]):
if not st_data["original"] in seen:
seen[st_data["original"]] = true
matches.append(st_data["original"])
return matches
## Cleans a term by removing spaces and replacing special characters.
## Matches MusicServer's clean_term() function.
func clean_term(term: String) -> String:
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and") \
.replace("'n", "and").to_lower()
## Compiles the letters-in-order pattern for [param search_term], once per term.
func compile_regex_cached(search_term: String) -> RegEx:
if not search_term in regex_cache:
var regex: RegEx = RegEx.new()
var regex_text: String = ".*"
for letter: String in search_term:
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
if "\\^$.|?*+()[]{}".contains(letter):
regex_text += "\\"
regex_text += letter + ".*"
regex.compile(regex_text)
regex_cache[search_term] = regex
return regex_cache[search_term]
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:
label = Label.new()
label.text = soundtrack
label.autowrap_mode = TextServer.AUTOWRAP_WORD
search_list.add_child(label)
label.visible = true
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
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 and precomputes their search forms.
func set_soundtracks(list: Array) -> void:
soundtracks = []
precomputed_soundtracks = []
soundtracks.append_array(list)
for soundtrack: String in soundtracks:
precomputed_soundtracks.append({
"original": soundtrack,
"lowercase": soundtrack.to_lower(),
"cleaned": clean_term(soundtrack)
})
func clear() -> void:
search_bar.text = ""
show_all_soundtracks()
search_bar.grab_focus()