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:
Sansan committed 2026-10-07 14:53:51 +02:00
1 parent 13fdb3f68c
commit 845d08c260
12 files changed
+2063 -808

No files matched your search

+57 -3
View File
@@ -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: