diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 02baaa17e..d454a95fe 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -1098,7 +1099,7 @@ void Sidebar::search() void Sidebar::jump_to_option(size_t selected) { const Search::Option& opt = p->searcher.get_option(selected); - wxGetApp().get_tab(opt.type)->activate_option(opt.opt_key, opt.category); + wxGetApp().get_tab(opt.type)->activate_option(boost::nowide::narrow(opt.opt_key), boost::nowide::narrow(opt.category)); // Switch to the Settings NotePad, if plater is shown if (p->plater->IsShown()) diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 90566c370..c6894bc5b 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include "libslic3r/PrintConfig.hpp" #include "GUI_App.hpp" @@ -31,64 +33,35 @@ static std::map NameByType = { { Preset::TYPE_PRINTER, L("Printer") } }; -FMFlag Option::fuzzy_match_simple(char const * search_pattern) const -{ - return fts::fuzzy_match_simple(search_pattern, label_local.utf8_str()) ? fmLabelLocal : - fts::fuzzy_match_simple(search_pattern, group_local.utf8_str()) ? fmGroupLocal : - fts::fuzzy_match_simple(search_pattern, category_local.utf8_str()) ? fmCategoryLocal : - fts::fuzzy_match_simple(search_pattern, opt_key.c_str()) ? fmOptKey : - fts::fuzzy_match_simple(search_pattern, label.utf8_str()) ? fmLabel : - fts::fuzzy_match_simple(search_pattern, group.utf8_str()) ? fmGroup : - fts::fuzzy_match_simple(search_pattern, category.utf8_str()) ? fmCategory : fmUndef ; -} - -FMFlag Option::fuzzy_match_simple(const wxString& search) const -{ - char const* search_pattern = search.utf8_str(); - return fuzzy_match_simple(search_pattern); -} - -FMFlag Option::fuzzy_match_simple(const std::string& search) const -{ - char const* search_pattern = search.c_str(); - return fuzzy_match_simple(search_pattern); -} - -FMFlag Option::fuzzy_match(char const* search_pattern, int& outScore) const +FMFlag Option::fuzzy_match(wchar_t const* search_pattern, int& outScore, std::vector &out_matches) const { FMFlag flag = fmUndef; int score; - if (fts::fuzzy_match(search_pattern, label_local.utf8_str(), score) && outScore < score) { - outScore = score; flag = fmLabelLocal ; } - if (fts::fuzzy_match(search_pattern, group_local.utf8_str(), score) && outScore < score) { - outScore = score; flag = fmGroupLocal ; } - if (fts::fuzzy_match(search_pattern, category_local.utf8_str(), score) && outScore < score) { - outScore = score; flag = fmCategoryLocal; } - if (fts::fuzzy_match(search_pattern, opt_key.c_str(), score) && outScore < score) { - outScore = score; flag = fmOptKey ; } - if (fts::fuzzy_match(search_pattern, label.utf8_str(), score) && outScore < score) { - outScore = score; flag = fmLabel ; } - if (fts::fuzzy_match(search_pattern, group.utf8_str(), score) && outScore < score) { - outScore = score; flag = fmGroup ; } - if (fts::fuzzy_match(search_pattern, category.utf8_str(), score) && outScore < score) { - outScore = score; flag = fmCategory ; } + uint16_t matches[fts::max_matches + 1]; // +1 for the stopper + auto save_matches = [&matches, &out_matches]() { + size_t cnt = 0; + for (; matches[cnt] != fts::stopper; ++cnt); + out_matches.assign(matches, matches + cnt); + }; + if (fts::fuzzy_match(search_pattern, label_local.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmLabelLocal ; save_matches(); } + if (fts::fuzzy_match(search_pattern, group_local.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmGroupLocal ; save_matches(); } + if (fts::fuzzy_match(search_pattern, category_local.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmCategoryLocal; save_matches(); } + if (fts::fuzzy_match(search_pattern, opt_key.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmOptKey ; save_matches(); } + if (fts::fuzzy_match(search_pattern, label.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmLabel ; save_matches(); } + if (fts::fuzzy_match(search_pattern, group.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmGroup ; save_matches(); } + if (fts::fuzzy_match(search_pattern, category.c_str(), score, matches) && outScore < score) { + outScore = score; flag = fmCategory ; save_matches(); } return flag; } -FMFlag Option::fuzzy_match(const wxString& search, int& outScore) const -{ - char const* search_pattern = search.utf8_str(); - return fuzzy_match(search_pattern, outScore); -} - -FMFlag Option::fuzzy_match(const std::string& search, int& outScore) const -{ - char const* search_pattern = search.c_str(); - return fuzzy_match(search_pattern, outScore); -} - void FoundOption::get_marked_label_and_tooltip(const char** label_, const char** tooltip_) const { *label_ = marked_label.c_str(); @@ -120,10 +93,10 @@ void OptionsSearcher::append_options(DynamicPrintConfig* config, Preset::Type ty suffix = opt_key.back()=='1' ? L("Stealth") : L("Normal"); if (!label.IsEmpty()) - options.emplace_back(Option{ opt_key, type, - label+ " " + suffix, _(label)+ " " + _(suffix), - gc.group, _(gc.group), - gc.category, _(gc.category) }); + options.emplace_back(Option{ boost::nowide::widen(opt_key), type, + (label+ " " + suffix).ToStdWstring(), (_(label)+ " " + _(suffix)).ToStdWstring(), + gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), + gc.category.ToStdWstring(), _(gc.category).ToStdWstring() }); }; for (std::string opt_key : config->keys()) @@ -173,41 +146,32 @@ static wxString wrap_string(const wxString& str) } // Mark a string using ColorMarkerStart and ColorMarkerEnd symbols -static void mark_string(wxString& str, const wxString& search_str) +static std::wstring mark_string(const std::wstring &str, const std::vector &matches) { - // Try to find whole search string - if (str.Replace(search_str, wrap_string(search_str), false) != 0) - return; - - // Try to find whole capitalized search string - wxString search_str_capitalized = search_str.Capitalize(); - if (str.Replace(search_str_capitalized, wrap_string(search_str_capitalized), false) != 0) - return; - - // if search string is just a one letter now, there is no reason to continue - if (search_str.Len()==1) - return; - - // Split a search string for two strings (string without last letter and last letter) - // and repeat a function with new search strings - mark_string(str, search_str.SubString(0, search_str.Len() - 2)); - mark_string(str, search_str.Last()); -} - -// clear marked string from a redundant use of ColorMarkers -static void clear_marked_string(wxString& str) -{ - // Check if the string has a several ColorMarkerStart in a row and replace them to only one, if any - wxString delete_string = wxString::Format("%c%c", ImGui::ColorMarkerStart, ImGui::ColorMarkerStart); - str.Replace(delete_string, ImGui::ColorMarkerStart, true); - // If there were several ColorMarkerStart in a row, it means there should be a several ColorMarkerStop in a row, - // replace them to only one - delete_string = wxString::Format("%c%c", ImGui::ColorMarkerEnd, ImGui::ColorMarkerEnd); - str.Replace(delete_string, ImGui::ColorMarkerEnd, true); - - // And we should to remove redundant ColorMarkers, if they are in "End, Start" sequence in a row - delete_string = wxString::Format("%c%c", ImGui::ColorMarkerEnd, ImGui::ColorMarkerStart); - str.Replace(delete_string, wxEmptyString, true); + std::wstring out; + if (matches.empty()) + out = str; + else { + out.reserve(str.size() * 2); + if (matches.front() > 0) + out += str.substr(0, matches.front()); + for (size_t i = 0;;) { + // Find the longest string of successive indices. + size_t j = i + 1; + while (j < matches.size() && matches[j] == matches[j - 1] + 1) + ++ j; + out += ImGui::ColorMarkerStart; + out += str.substr(matches[i], matches[j - 1] - matches[i] + 1); + out += ImGui::ColorMarkerEnd; + if (j == matches.size()) { + out += str.substr(matches[j - 1] + 1); + break; + } + out += str.substr(matches[j - 1] + 1, matches[j] - matches[j - 1] - 1); + i = j; + } + } + return out; } bool OptionsSearcher::search() @@ -245,32 +209,50 @@ bool OptionsSearcher::search(const std::string& search, bool force/* = false*/) opt.group_local + sep + opt.label_local; }; + std::vector matches; for (size_t i=0; i < options.size(); i++) { const Option &opt = options[i]; if (full_list) { std::string label = into_u8(get_label(opt)); - found.emplace_back(FoundOption{ label, label, into_u8(get_tooltip(opt)), i, 0 }); + found.emplace_back(FoundOption{ label, label, into_u8(get_tooltip(opt)), i, fmUndef, 0 }); continue; } int score = 0; - - FMFlag fuzzy_match_flag = opt.fuzzy_match(search, score); + FMFlag fuzzy_match_flag = opt.fuzzy_match(boost::nowide::widen(search).c_str(), score, matches); if (fuzzy_match_flag != fmUndef) { - wxString label = get_label(opt); + wxString label; - if ( fuzzy_match_flag == fmLabel ) label += "(" + opt.label + ")"; - else if (fuzzy_match_flag == fmGroup ) label += "(" + opt.group + ")"; - else if (fuzzy_match_flag == fmCategory) label += "(" + opt.category + ")"; - else if (fuzzy_match_flag == fmOptKey ) label += "(" + opt.opt_key + ")"; + if (view_params.type) + label += _(NameByType[opt.type]) + sep; + if (fuzzy_match_flag == fmCategoryLocal) + label += mark_string(opt.category_local, matches) + sep; + else if (view_params.category) + label += opt.category_local + sep; + if (fuzzy_match_flag == fmGroupLocal) + label += mark_string(opt.group_local, matches) + sep; + else if (view_params.group) + label += opt.group_local + sep; + label += ((fuzzy_match_flag == fmLabelLocal) ? mark_string(opt.label_local, matches) : opt.label_local) + sep; - wxString marked_label = label; - mark_string(marked_label, from_u8(search)); - clear_marked_string(marked_label); + switch (fuzzy_match_flag) { + case fmLabelLocal: + case fmGroupLocal: + case fmCategoryLocal: + break; + case fmLabel: label = get_label(opt) + "(" + mark_string(opt.label, matches) + ")"; break; + case fmGroup: label = get_label(opt) + "(" + mark_string(opt.group, matches) + ")"; break; + case fmCategory: label = get_label(opt) + "(" + mark_string(opt.category, matches) + ")"; break; + case fmOptKey: label = get_label(opt) + "(" + mark_string(opt.opt_key, matches) + ")"; break; + case fmUndef: assert(false); break; + } - found.emplace_back(FoundOption{ into_u8(label), into_u8(marked_label), into_u8(get_tooltip(opt)), i, score }); + std::string label_plain = into_u8(label); + boost::erase_all(label_plain, std::wstring(1, wchar_t(ImGui::ColorMarkerStart))); + boost::erase_all(label_plain, std::wstring(1, wchar_t(ImGui::ColorMarkerEnd))); + found.emplace_back(FoundOption{ label_plain, into_u8(label), into_u8(get_tooltip(opt)), i, fuzzy_match_flag, score }); } } diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 37767b0c6..1c24248ac 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -37,6 +37,7 @@ struct GroupAndCategory { }; // fuzzy_match flag +// Sorted by the order of importance. The outputs will be sorted by the importance if the match value given by fuzzy_match is equal. enum FMFlag { fmUndef = 0, // didn't find @@ -53,28 +54,27 @@ struct Option { bool operator<(const Option& other) const { return other.label > this->label; } bool operator>(const Option& other) const { return other.label < this->label; } - std::string opt_key; + // Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters, + // though for some languages (Chinese?) it may not work correctly. + std::wstring opt_key; Preset::Type type {Preset::TYPE_INVALID}; - wxString label; - wxString label_local; - wxString group; - wxString group_local; - wxString category; - wxString category_local; + std::wstring label; + std::wstring label_local; + std::wstring group; + std::wstring group_local; + std::wstring category; + std::wstring category_local; - FMFlag fuzzy_match_simple(char const *search_pattern) const; - FMFlag fuzzy_match_simple(const wxString& search) const; - FMFlag fuzzy_match_simple(const std::string &search) const; - FMFlag fuzzy_match(char const *search_pattern, int &outScore) const; - FMFlag fuzzy_match(const wxString &search, int &outScore) const ; - FMFlag fuzzy_match(const std::string &search, int &outScore) const ; + FMFlag fuzzy_match(wchar_t const *search_pattern, int &outScore, std::vector &out_matches) const; }; struct FoundOption { + // UTF8 encoding, to be consumed by ImGUI by reference. std::string label; std::string marked_label; std::string tooltip; size_t option_idx {0}; + FMFlag category {fmUndef}; int outScore {0}; // Returning pointers to contents of std::string members, to be used by ImGUI for rendering. @@ -106,7 +106,7 @@ class OptionsSearcher } void sort_found() { std::sort(found.begin(), found.end(), [](const FoundOption& f1, const FoundOption& f2) { - return f1.outScore > f2.outScore; }); + return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && int(f1.category) < int(f2.category)); }); }; size_t options_size() const { return options.size(); } diff --git a/src/slic3r/GUI/fts_fuzzy_match.h b/src/slic3r/GUI/fts_fuzzy_match.h index da4b5d2a0..4776a3647 100644 --- a/src/slic3r/GUI/fts_fuzzy_match.h +++ b/src/slic3r/GUI/fts_fuzzy_match.h @@ -1,4 +1,4 @@ - // LICENSE +// LICENSE // // This software is dual-licensed to the public domain and under the following // license: you are granted a perpetual, irrevocable license to copy, modify, @@ -23,7 +23,7 @@ // Performs exhaustive search via recursion to find all possible matches and match with highest score. // Scores values have no intrinsic meaning. Possible score range is not normalized and varies with pattern. // Recursion is limited internally (default=10) to prevent degenerate cases (pattern="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") -// Uses uint8_t for match indices. Therefore patterns are limited to 256 characters. +// Uses uint8_t for match indices. Therefore patterns are limited to max_matches characters. // Score system should be tuned for YOUR use case. Words, sentences, file names, or method names all prefer different tuning. @@ -39,54 +39,61 @@ // Public interface namespace fts { - static bool fuzzy_match_simple(char const * pattern, char const * str); - static bool fuzzy_match(char const * pattern, char const * str, int & outScore); - static bool fuzzy_match(char const * pattern, char const * str, int & outScore, uint8_t * matches, int maxMatches); -} + using char_type = wchar_t; + using pos_type = uint16_t; + static constexpr pos_type stopper = pos_type(-1); + static constexpr int max_matches = 255; + static bool fuzzy_match(char_type const * pattern, char_type const * str, int & outScore); + static bool fuzzy_match(char_type const * pattern, char_type const * str, int & outScore, pos_type * matches); +} #ifdef FTS_FUZZY_MATCH_IMPLEMENTATION namespace fts { // Forward declarations for "private" implementation namespace fuzzy_internal { - static bool fuzzy_match_recursive(const char * pattern, const char * str, int & outScore, const char * strBegin, - uint8_t const * srcMatches, uint8_t * newMatches, int maxMatches, int nextMatch, + static bool fuzzy_match_recursive(const char_type * pattern, const char_type * str, int & outScore, const char_type * strBegin, + pos_type const * srcMatches, pos_type * newMatches, int nextMatch, int & recursionCount, int recursionLimit); + static void copy_matches(pos_type * dst, pos_type const* src); } // Public interface - static bool fuzzy_match_simple(char const * pattern, char const * str) { - while (*pattern != '\0' && *str != '\0') { - if (tolower(*pattern) == tolower(*str)) - ++pattern; - ++str; - } - - return *pattern == '\0' ? true : false; - } - - static bool fuzzy_match(char const * pattern, char const * str, int & outScore) { + static bool fuzzy_match(char_type const * pattern, char_type const * str, int & outScore) { - uint8_t matches[256]; - return fuzzy_match(pattern, str, outScore, matches, sizeof(matches)); + pos_type matches[max_matches + 1]; // with the room for the stopper + matches[0] = stopper; + return fuzzy_match(pattern, str, outScore, matches); } - static bool fuzzy_match(char const * pattern, char const * str, int & outScore, uint8_t * matches, int maxMatches) { + static bool fuzzy_match(char_type const * pattern, char_type const * str, int & outScore, pos_type * matches) { int recursionCount = 0; int recursionLimit = 10; - - return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, maxMatches, 0, recursionCount, recursionLimit); + return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, 0, recursionCount, recursionLimit); } // Private implementation - static bool fuzzy_internal::fuzzy_match_recursive(const char * pattern, const char * str, int & outScore, - const char * strBegin, uint8_t const * srcMatches, uint8_t * matches, int maxMatches, - int nextMatch, int & recursionCount, int recursionLimit) + static bool fuzzy_internal::fuzzy_match_recursive( + // Pattern to match over str. + const char_type * pattern, + // Text to match the pattern over. + const char_type * str, + // Score of the pattern matching str. Output variable. + int & outScore, + const char_type * strBegin, + // Matches when entering this function. + pos_type const * srcMatches, + // Output matches. + pos_type * matches, + // Number of matched characters stored in srcMatches when entering this function, also tracking the successive matches. + int nextMatch, + // Recursion count is input / output to track the maximum depth reached. + int & recursionCount, + int recursionLimit) { // Count recursions - ++recursionCount; - if (recursionCount >= recursionLimit) + if (++ recursionCount >= recursionLimit) return false; // Detect end of strings @@ -95,7 +102,7 @@ namespace fts { // Recursion params bool recursiveMatch = false; - uint8_t bestRecursiveMatches[256]; + pos_type bestRecursiveMatches[max_matches + 1]; // with the room for the stopper int bestRecursiveScore = 0; // Loop through pattern and str looking for a match @@ -106,30 +113,32 @@ namespace fts { if (tolower(*pattern) == tolower(*str)) { // Supplied matches buffer was too short - if (nextMatch >= maxMatches) + if (nextMatch >= max_matches) return false; // "Copy-on-Write" srcMatches into matches if (first_match && srcMatches) { - memcpy(matches, srcMatches, nextMatch); + memcpy(matches, srcMatches, sizeof(pos_type) * (nextMatch + 1)); // including the stopper first_match = false; } // Recursive call that "skips" this match - uint8_t recursiveMatches[256]; + pos_type recursiveMatches[max_matches]; int recursiveScore; - if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) { + if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, nextMatch, recursionCount, recursionLimit)) { // Pick best recursive score if (!recursiveMatch || recursiveScore > bestRecursiveScore) { - memcpy(bestRecursiveMatches, recursiveMatches, 256); - bestRecursiveScore = recursiveScore; + copy_matches(bestRecursiveMatches, recursiveMatches); + bestRecursiveScore = recursiveScore; } recursiveMatch = true; } // Advance - matches[nextMatch++] = (uint8_t)(str - strBegin); + matches[nextMatch++] = (pos_type)(str - strBegin); + // Write a stopper sign. + matches[nextMatch] = stopper; ++pattern; } ++str; @@ -168,10 +177,10 @@ namespace fts { // Apply ordering bonuses for (int i = 0; i < nextMatch; ++i) { - uint8_t currIdx = matches[i]; + pos_type currIdx = matches[i]; if (i > 0) { - uint8_t prevIdx = matches[i - 1]; + pos_type prevIdx = matches[i - 1]; // Sequential if (currIdx == (prevIdx + 1)) @@ -182,13 +191,13 @@ namespace fts { if (currIdx > 0) { // Camel case // ::islower() expects an unsigned char in range of 0 to 255. - unsigned char uneighbor = ((unsigned char *)strBegin)[currIdx - 1]; - unsigned char ucurr = ((unsigned char*)strBegin)[currIdx]; - if (::islower(uneighbor) && ::isupper(ucurr)) + char_type uneighbor = strBegin[currIdx - 1]; + char_type ucurr = strBegin[currIdx]; + if (std::islower(uneighbor) && std::isupper(ucurr)) outScore += camel_bonus; // Separator - char neighbor = strBegin[currIdx - 1]; + char_type neighbor = strBegin[currIdx - 1]; bool neighborSeparator = neighbor == '_' || neighbor == ' '; if (neighborSeparator) outScore += separator_bonus; @@ -203,7 +212,7 @@ namespace fts { // Return best result if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) { // Recursive score is better than "this" - memcpy(matches, bestRecursiveMatches, maxMatches); + copy_matches(matches, bestRecursiveMatches); outScore = bestRecursiveScore; return true; } @@ -216,6 +225,15 @@ namespace fts { return false; } } + + // Copy matches up to a stopper. + static void fuzzy_internal::copy_matches(pos_type * dst, pos_type const* src) + { + while (*src != stopper) + *dst++ = *src++; + *dst = stopper; + } + } // namespace fts #endif // FTS_FUZZY_MATCH_IMPLEMENTATION