The popups, the volume slider, the player name field and the main window get a class_name, so the main window and the quiz hold them by their real type instead of Control. The classes already use the names the files will get.
187 lines
5.8 KiB
GDScript
187 lines
5.8 KiB
GDScript
class_name SearchPopup
|
|
extends PopupWindow
|
|
|
|
@onready
|
|
var search_list: VBoxContainer = $ScrollContainer/VBoxContainer
|
|
|
|
@onready
|
|
var search_panel: Panel = %SearchPanel
|
|
|
|
@onready
|
|
var clear_button: Button = $ClearButton
|
|
|
|
@onready
|
|
var search_bar: TextEdit = $Searchbar
|
|
|
|
var soundtracks: Array = []
|
|
|
|
# Precomputed soundtrack data: array of dicts with original, lowercase, and cleaned fields
|
|
var precomputed_soundtracks: Array = []
|
|
|
|
# Regex cache: maps cleaned search terms to compiled RegEx objects
|
|
var regex_cache: Dictionary = {}
|
|
|
|
var label_pool: Array[Label] = []
|
|
|
|
# Called when the node enters the scene tree for the first time.
|
|
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 self.visible == true:
|
|
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 in range(precomputed_soundtracks.size()):
|
|
var st_data = 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 in range(precomputed_soundtracks.size()):
|
|
var st_data = 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 = compile_regex_cached(clean_search_text)
|
|
for i in range(precomputed_soundtracks.size()):
|
|
var st_data = 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 a regex pattern with caching to avoid recompiling the same pattern multiple times.
|
|
func compile_regex_cached(search_term: String) -> RegEx:
|
|
if not search_term in regex_cache:
|
|
var regex = RegEx.new()
|
|
var regText: String = ".*"
|
|
for letter: String in search_term:
|
|
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
|
|
if "\\^$.|?*+()[]{}".contains(letter):
|
|
regText += "\\"
|
|
regText += letter + ".*"
|
|
regex.compile(regText)
|
|
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)
|
|
|
|
# Optimized version that reuses label nodes from pool instead of creating new ones
|
|
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) -> 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()
|