1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00

Small fixes to search mapobject feature:

- Trim overly long names to prevent incorrect item display
- Use getLocaleName() for proper locale-aware to_lower conversion
- Implement scoring-based sorting for better search match ranking
- Remove Boost dependency: replace with std::string::find() and rfind()
This commit is contained in:
MichalZr6
2025-03-11 22:35:46 +01:00
parent 2973a769df
commit 5363424451
6 changed files with 134 additions and 75 deletions

View File

@@ -241,7 +241,7 @@ void CSpellWindow::processSpells()
mySpells.reserve(LIBRARY->spellh->objects.size());
for(auto const & spell : LIBRARY->spellh->objects)
{
bool searchTextFound = !searchBox || TextOperations::textSearchSimilar(searchBox->getText(), spell->getNameTranslated());
bool searchTextFound = !searchBox || TextOperations::textSearchSimilarityScore(searchBox->getText(), spell->getNameTranslated()) < 100;
if(onSpellSelect)
{

View File

@@ -1531,7 +1531,11 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
items.reserve(_items.size());
for(int id : _items)
items.push_back(std::make_pair(id, GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName()));
{
std::string objectName = GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName();
trimTextIfTooWide(objectName);
items.emplace_back(id, objectName);
}
itemsVisible = items;
init(titleWidget_, _title, _descr, searchBoxEnabled);
@@ -1551,7 +1555,11 @@ CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, st
items.reserve(_items.size());
for(size_t i = 0; i < _items.size(); i++)
items.push_back(std::make_pair(int(i), _items[i]));
{
std::string objectName = _items[i];
trimTextIfTooWide(objectName);
items.emplace_back(static_cast<int>(i), objectName);
}
itemsVisible = items;
init(titleWidget_, _title, _descr, searchBoxEnabled);
@@ -1590,13 +1598,69 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
searchBoxDescription = std::make_shared<CLabel>(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, LIBRARY->generaltexth->translate("vcmi.spellBook.search"));
searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, ETextAlignment::CENTER, true);
searchBox->setCallback([this](const std::string & text){
searchBox->setCallback(std::bind(&CObjectListWindow::itemsSearchCallback, this, std::placeholders::_1));
}
void CObjectListWindow::trimTextIfTooWide(std::string & text) const
{
int maxWidth = pos.w - 60;
// Create a temporary label to measure text width
auto label = std::make_shared<CLabel>(0, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, text);
int textWidth = label->getWidth();
std::regex pattern(R"(.*(\(\d+\))$)");
std::smatch match;
std::string quantity;
if(std::regex_match(text, match, pattern) && match.size() > 1)
quantity = match[1]; // Extract the quantity
std::string suffix = " ... " + quantity;
if(textWidth >= maxWidth)
{
logGlobal->warn("Mapobject name '%s' is too long and probably needs to be fixed! Trimming...",
text.substr(0, text.size() - quantity.size() + 1));
// Trim text until it fits
while(!text.empty())
{
std::string trimmedText = text + suffix;
label->setText(trimmedText);
if(label->getWidth() < maxWidth)
break;
text.resize(text.size() - 1);
}
text += suffix;
}
}
void CObjectListWindow::itemsSearchCallback(const std::string & text)
{
searchBoxDescription->setEnabled(text.empty());
itemsVisible.clear();
std::vector<std::pair<int, decltype(items)::value_type>> rankedItems; // Store (score, item)
for(auto & item : items)
if(TextOperations::textSearchSimilar(text, item.second))
itemsVisible.push_back(item);
{
int score = TextOperations::textSearchSimilarityScore(text, item.second);
if(score < 100) // Keep only relevant items
rankedItems.emplace_back(score, item);
}
// Sort: Lower score is better match
std::sort(rankedItems.begin(), rankedItems.end(), [](const auto & a, const auto & b)
{
return a.first < b.first;
});
for(auto & rankedItem : rankedItems)
itemsVisible.push_back(rankedItem.second);
selected = 0;
list->resize(itemsVisible.size());
@@ -1604,7 +1668,6 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
ok->block(!itemsVisible.size());
redraw();
});
}
std::shared_ptr<CIntObject> CObjectListWindow::genItem(size_t index)

View File

@@ -197,6 +197,8 @@ class CObjectListWindow : public CWindowObject
std::vector< std::pair<int, std::string> > itemsVisible; //visible items present in list
void init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled);
void trimTextIfTooWide(std::string & text) const; // trim items to fit within window's width
void itemsSearchCallback(const std::string & text);
void exitPressed();
public:
size_t selected;//index of currently selected item

View File

@@ -86,46 +86,26 @@ inline const auto & getLanguageList()
{
static const std::array<Options, 20> languages
{ {
<<<<<<< HEAD
{ "czech", "Czech", "Čeština", "CP1250", "cs", "cze", "%d.%m.%Y %H:%M", EPluralForms::CZ_3, true },
{ "chinese", "Chinese", "简体中文", "GBK", "zh", "chi", "%Y-%m-%d %H:%M", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
{ "english", "English", "English", "CP1252", "en", "eng", "%Y-%m-%d %H:%M", EPluralForms::EN_2, true }, // English uses international date/time format here
{ "finnish", "Finnish", "Suomi", "CP1252", "fi", "fin", "%d.%m.%Y %H:%M", EPluralForms::EN_2, true },
{ "french", "French", "Français", "CP1252", "fr", "fre", "%d/%m/%Y %H:%M", EPluralForms::FR_2, true },
{ "german", "German", "Deutsch", "CP1252", "de", "ger", "%d.%m.%Y %H:%M", EPluralForms::EN_2, true },
{ "greek", "Greek", "ελληνικά", "CP1253", "el", "ell", "%d/%m/%Y %H:%M", EPluralForms::EN_2, false },
{ "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2, true },
{ "italian", "Italian", "Italiano", "CP1250", "it", "ita", "%d/%m/%Y %H:%M", EPluralForms::EN_2, true },
{ "japanese", "Japanese", "日本語", "JIS", "ja", "jpn", "%Y%m%d %H:%M", EPluralForms::NONE, false },
{ "korean", "Korean", "한국어", "CP949", "ko", "kor", "%Y-%m-%d %H:%M", EPluralForms::VI_1, true },
{ "polish", "Polish", "Polski", "CP1250", "pl", "pol", "%d.%m.%Y %H:%M", EPluralForms::PL_3, true },
{ "portuguese", "Portuguese", "Português", "CP1252", "pt", "por", "%d/%m/%Y %H:%M", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
{ "russian", "Russian", "Русский", "CP1251", "ru", "rus", "%d.%m.%Y %H:%M", EPluralForms::UK_3, true },
{ "spanish", "Spanish", "Español", "CP1252", "es", "spa", "%d/%m/%Y %H:%M", EPluralForms::EN_2, true },
{ "swedish", "Swedish", "Svenska", "CP1252", "sv", "swe", "%Y-%m-%d %H:%M", EPluralForms::EN_2, true },
{ "norwegian", "Norwegian", "Norsk", "CP1252", "no", "nor", "%d/%m/%Y %H:%M", EPluralForms::EN_2, false },
{ "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tur", "%d.%m.%Y %H:%M", EPluralForms::EN_2, true },
{ "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %H:%M", EPluralForms::UK_3, true },
{ "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vie", "%d/%m/%Y %H:%M", EPluralForms::VI_1, true }, // Fan translation uses special encoding
=======
{ "czech", "Czech", "Čeština", "CP1250", "cs_CZ.UTF-8", "cs", "cze", "%d.%m.%Y %H:%M", EPluralForms::CZ_3 },
{ "chinese", "Chinese", "简体中文", "GBK", "zh_CN.UTF-8", "zh", "chi", "%Y-%m-%d %H:%M", EPluralForms::VI_1 }, // Note: actually Simplified Chinese
{ "english", "English", "English", "CP1252", "en_US.UTF-8", "en", "eng", "%Y-%m-%d %H:%M", EPluralForms::EN_2 }, // English uses international date/time format here
{ "finnish", "Finnish", "Suomi", "CP1252", "fi_FI.UTF-8", "fi", "fin", "%d.%m.%Y %H:%M", EPluralForms::EN_2, },
{ "french", "French", "Français", "CP1252", "fr_FR.UTF-8", "fr", "fre", "%d/%m/%Y %H:%M", EPluralForms::FR_2, },
{ "german", "German", "Deutsch", "CP1252", "de_DE.UTF-8", "de", "ger", "%d.%m.%Y %H:%M", EPluralForms::EN_2, },
{ "hungarian", "Hungarian", "Magyar", "CP1250", "hu_HU.UTF-8", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2 },
{ "italian", "Italian", "Italiano", "CP1250", "it_IT.UTF-8", "it", "ita", "%d/%m/%Y %H:%M", EPluralForms::EN_2 },
{ "korean", "Korean", "한국어", "CP949", "ko_KR.UTF-8", "ko", "kor", "%Y-%m-%d %H:%M", EPluralForms::VI_1 },
{ "polish", "Polish", "Polski", "CP1250", "pl_PL.UTF-8", "pl", "pol", "%d.%m.%Y %H:%M", EPluralForms::PL_3 },
{ "portuguese", "Portuguese", "Português", "CP1252", "pt_BR.UTF-8", "pt", "por", "%d/%m/%Y %H:%M", EPluralForms::EN_2 }, // Note: actually Brazilian Portuguese
{ "russian", "Russian", "Русский", "CP1251", "ru_RU.UTF-8", "ru", "rus", "%d.%m.%Y %H:%M", EPluralForms::UK_3 },
{ "spanish", "Spanish", "Español", "CP1252", "es_ES.UTF-8", "es", "spa", "%d/%m/%Y %H:%M", EPluralForms::EN_2 },
{ "swedish", "Swedish", "Svenska", "CP1252", "sv_SE.UTF-8", "sv", "swe", "%Y-%m-%d %H:%M", EPluralForms::EN_2 },
{ "turkish", "Turkish", "Türkçe", "CP1254", "tr_TR.UTF-8", "tr", "tur", "%d.%m.%Y %H:%M", EPluralForms::EN_2 },
{ "ukrainian", "Ukrainian", "Українська", "CP1251", "uk_UA.UTF-8", "uk", "ukr", "%d.%m.%Y %H:%M", EPluralForms::UK_3 },
{ "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi_VN.UTF-8", "vi", "vie", "%d/%m/%Y %H:%M", EPluralForms::VI_1 }, // Fan translation uses special encoding
>>>>>>> 364b3cef9 (Use locale based on language set in config)
{ "czech", "Czech", "Čeština", "CP1250", "cs_CZ.UTF-8", "cs", "cze", "%d.%m.%Y %H:%M", EPluralForms::CZ_3, true },
{ "chinese", "Chinese", "简体中文", "GBK", "zh_CN.UTF-8", "zh", "chi", "%Y-%m-%d %H:%M", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
{ "english", "English", "English", "CP1252", "en_US.UTF-8", "en", "eng", "%Y-%m-%d %H:%M", EPluralForms::EN_2, true }, // English uses international date/time format here
{ "finnish", "Finnish", "Suomi", "CP1252", "fi_FI.UTF-8", "fi", "fin", "%d.%m.%Y %H:%M", EPluralForms::EN_2, true },
{ "french", "French", "Français", "CP1252", "fr_FR.UTF-8", "fr", "fre", "%d/%m/%Y %H:%M", EPluralForms::FR_2, true },
{ "german", "German", "Deutsch", "CP1252", "de_DE.UTF-8", "de", "ger", "%d.%m.%Y %H:%M", EPluralForms::EN_2, true },
{ "greek", "Greek", "ελληνικά", "CP1253", "el_GR.UTF-8", "el", "ell", "%d/%m/%Y %H:%M", EPluralForms::EN_2, false },
{ "hungarian", "Hungarian", "Magyar", "CP1250", "hu_HU.UTF-8", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2, true },
{ "italian", "Italian", "Italiano", "CP1250", "it_IT.UTF-8", "it", "ita", "%d/%m/%Y %H:%M", EPluralForms::EN_2, true },
{ "japanese", "Japanese", "日本語", "JIS", "ja_JP.UTF-8", "ja", "jpn", "%Y年%m月%d日 %H:%M", EPluralForms::NONE, false },
{ "korean", "Korean", "한국어", "CP949", "ko_KR.UTF-8", "ko", "kor", "%Y-%m-%d %H:%M", EPluralForms::VI_1, true },
{ "polish", "Polish", "Polski", "CP1250", "pl_PL.UTF-8", "pl", "pol", "%d.%m.%Y %H:%M", EPluralForms::PL_3, true },
{ "portuguese", "Portuguese", "Português", "CP1252", "pt_BR.UTF-8", "pt", "por", "%d/%m/%Y %H:%M", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
{ "russian", "Russian", "Русский", "CP1251", "ru_RU.UTF-8", "ru", "rus", "%d.%m.%Y %H:%M", EPluralForms::UK_3, true },
{ "spanish", "Spanish", "Español", "CP1252", "es_ES.UTF-8", "es", "spa", "%d/%m/%Y %H:%M", EPluralForms::EN_2, true },
{ "swedish", "Swedish", "Svenska", "CP1252", "sv_SE.UTF-8", "sv", "swe", "%Y-%m-%d %H:%M", EPluralForms::EN_2, true },
{ "norwegian", "Norwegian", "Norsk Bokmål", "UTF-8", "nb_NO.UTF-8", "nb", "nor", "%d/%m/%Y %H:%M", EPluralForms::EN_2, false },
{ "turkish", "Turkish", "Türkçe", "CP1254", "tr_TR.UTF-8", "tr", "tur", "%d.%m.%Y %H:%M", EPluralForms::EN_2, true },
{ "ukrainian", "Ukrainian", "Українська", "CP1251", "uk_UA.UTF-8", "uk", "ukr", "%d.%m.%Y %H:%M", EPluralForms::UK_3, true },
{ "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi_VN.UTF-8", "vi", "vie", "%d/%m/%Y %H:%M", EPluralForms::VI_1, true }, // Fan translation uses special encoding
} };
static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");

View File

@@ -252,7 +252,7 @@ std::string TextOperations::getCurrentFormattedDateTimeLocal(std::chrono::second
return TextOperations::getFormattedDateTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
}
int TextOperations::getLevenshteinDistance(const std::string & s, const std::string & t)
int TextOperations::getLevenshteinDistance(std::string_view s, std::string_view t)
{
int n = t.size();
int m = s.size();
@@ -319,7 +319,7 @@ DLL_LINKAGE std::string TextOperations::getLocaleName()
}
}
bool TextOperations::textSearchSimilar(const std::string & s, const std::string & t)
int TextOperations::textSearchSimilarityScore(const std::string & s, const std::string & t)
{
boost::locale::generator gen;
std::locale loc = gen(getLocaleName());
@@ -327,22 +327,35 @@ bool TextOperations::textSearchSimilar(const std::string & s, const std::string
auto haystack = boost::locale::to_lower(t, loc);
auto needle = boost::locale::to_lower(s, loc);
if(boost::algorithm::contains(haystack, needle))
return true;
// 0 - Best possible match: text starts with the search string
if(haystack.rfind(needle, 0) == 0)
return 0;
// 1 - Strong match: text contains the search string
if(haystack.find(needle) != std::string::npos)
return 1;
// If the search string is longer than the text, return a high penalty
if(needle.size() > haystack.size())
return false;
return 100;
for(int i = 0; i < haystack.size() - needle.size() + 1; i++)
// Compute Levenshtein distance for fuzzy similarity
int minDist = 100;
for(size_t i = 0; i <= haystack.size() - needle.size(); i++)
{
auto dist = getLevenshteinDistance(haystack.substr(i, needle.size()), needle);
if(needle.size() > 2 && dist <= 1)
return true;
else if(needle.size() > 4 && dist <= 2)
return true;
std::string_view subHaystack = std::string_view(haystack).substr(i, needle.size());
int dist = getLevenshteinDistance(subHaystack, needle);
if(dist < minDist)
minDist = dist;
}
return false;
// Apply scaling: Short words tolerate smaller distances
if(needle.size() > 2 && minDist <= 1)
return minDist + 1; // +1 to ensure it's worse than an exact match
else if(needle.size() > 4 && minDist <= 2)
return minDist + 1;
return 100; // Worst similarity
}
VCMI_LIB_NAMESPACE_END

View File

@@ -77,7 +77,7 @@ namespace TextOperations
/// Algorithm for detection of typos in words
/// Determines how 'different' two strings are - how many changes must be done to turn one string into another one
/// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
DLL_LINKAGE int getLevenshteinDistance(const std::string & s, const std::string & t);
DLL_LINKAGE int getLevenshteinDistance(std::string_view s, std::string_view t);
/// Retrieves the locale name based on the selected (in config) game language, with a safe fallback.
DLL_LINKAGE std::string getLocaleName();
@@ -93,7 +93,8 @@ namespace TextOperations
}
/// Check if texts have similarity when typing into search boxes
DLL_LINKAGE bool textSearchSimilar(const std::string & s, const std::string & t);
/// 0 -> Exact match or substring match, 1 - 2 -> Close match based on Levenshtein distance, >100 -> Unrelated word(bad match).
DLL_LINKAGE int textSearchSimilarityScore(const std::string & s, const std::string & t);
};