diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index 98b04a63d..0607c1b01 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -114,7 +114,9 @@ void CopyrightsDialog::fill_entries() { "Icons for STL and GCODE files." , "Akira Yasuda" , "http://3dp0.com/icons-for-stl-and-gcode/" }, { "AppImage packaging for Linux using AppImageKit" - , "2004-2019 Simon Peter and contributors" , "https://appimage.org/" } + , "2004-2019 Simon Peter and contributors" , "https://appimage.org/" }, + { "lib_fts" + , "Forrest Smith" , "https://www.forrestthewoods.com/" } }; } diff --git a/src/slic3r/GUI/SearchComboBox.cpp b/src/slic3r/GUI/SearchComboBox.cpp index 11742f25c..a2efd5b02 100644 --- a/src/slic3r/GUI/SearchComboBox.cpp +++ b/src/slic3r/GUI/SearchComboBox.cpp @@ -20,6 +20,9 @@ #include "Tab.hpp" #include "PresetBundle.hpp" +#define FTS_FUZZY_MATCH_IMPLEMENTATION +#include "fts_fuzzy_match.h" + using boost::optional; namespace Slic3r { @@ -27,23 +30,22 @@ namespace GUI { bool SearchOptions::Option::containes(const wxString& search_) const { - wxString search = search_.Lower(); - wxString label_ = label.Lower(); - wxString category_ = category.Lower(); + char const* search_pattern = search_.utf8_str(); + char const* opt_key_str = opt_key.c_str(); + char const* label_str = label.utf8_str(); - return (opt_key.find(into_u8(search)) != std::string::npos || - label_.Find(search) != wxNOT_FOUND || - category_.Find(search) != wxNOT_FOUND); + return fts::fuzzy_match_simple(search_pattern, label_str ) || + fts::fuzzy_match_simple(search_pattern, opt_key_str ) ; +} - auto search_str = into_u8(search); - auto pos = opt_key.find(into_u8(search)); - bool in_opt_key = pos != std::string::npos; - bool in_label = label_.Find(search) != wxNOT_FOUND; - bool in_category = category_.Find(search) != wxNOT_FOUND; +bool SearchOptions::Option::is_matched_option(const wxString& search, int& outScore) +{ + char const* search_pattern = search.utf8_str(); + char const* opt_key_str = opt_key.c_str(); + char const* label_str = label.utf8_str(); - if (in_opt_key || in_label || in_category) - return true; - return false; + return (fts::fuzzy_match(search_pattern, label_str , outScore) || + fts::fuzzy_match(search_pattern, opt_key_str , outScore) ); } @@ -80,10 +82,20 @@ void SearchOptions::append_options(DynamicPrintConfig* config, Preset::Type type label += _(opt.category) + " : "; label += _(opt.full_label.empty() ? opt.label : opt.full_label); - options.emplace(Option{ opt_key, label, opt.category, type }); + options.emplace_back(Option{ label, opt_key, opt.category, type }); } } +void SearchOptions::apply_filters(const wxString& search) +{ + clear_filters(); + for (auto option : options) { + int score; + if (option.is_matched_option(search, score)) + filters.emplace_back(Filter{ option.label, score }); + } + sort_filters(); +} SearchComboBox::SearchComboBox(wxWindow *parent) : wxBitmapComboBox(parent, wxID_ANY, _(L("Type here to search")) + dots, wxDefaultPosition, wxSize(25 * wxGetApp().em_unit(), -1)), @@ -148,18 +160,19 @@ void SearchComboBox::msw_rescale() void SearchComboBox::init(DynamicPrintConfig* config, Preset::Type type, ConfigOptionMode mode) { - search_list.clear(); + search_list.clear_options(); search_list.append_options(config, type, mode); + search_list.sort_options(); update_combobox(); } void SearchComboBox::init(std::vector<SearchInput> input_values) { - search_list.clear(); - + search_list.clear_options(); for (auto i : input_values) search_list.append_options(i.config, i.type, i.mode); + search_list.sort_options(); update_combobox(); } @@ -188,15 +201,19 @@ void SearchComboBox::append_all_items() void SearchComboBox::append_items(const wxString& search) { this->Clear(); - - auto cmp = [](SearchOptions::Option* o1, SearchOptions::Option* o2) { return o1->label > o2->label; }; - std::set<SearchOptions::Option*, decltype(cmp)> ret(cmp); +/* + search_list.apply_filters(search); + for (auto filter : search_list.filters) { + auto it = std::lower_bound(search_list.options.begin(), search_list.options.end(), SearchOptions::Option{filter.label}); + if (it != search_list.options.end()) + append(it->label, (void*)(&(*it))); + } +*/ for (const SearchOptions::Option& option : search_list.options) if (option.containes(search)) append(option.label, (void*)&option); -// this->Popup(); SuppressUpdate su(this); this->SetValue(search); this->SetInsertionPointEnd(); diff --git a/src/slic3r/GUI/SearchComboBox.hpp b/src/slic3r/GUI/SearchComboBox.hpp index 0f8e83137..482bb18eb 100644 --- a/src/slic3r/GUI/SearchComboBox.hpp +++ b/src/slic3r/GUI/SearchComboBox.hpp @@ -29,19 +29,36 @@ public: 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; wxString label; + std::string opt_key; wxString category; Preset::Type type {Preset::TYPE_INVALID}; // wxString grope; bool containes(const wxString& search) const; + bool is_matched_option(const wxString &search, int &outScore); }; + std::vector<Option> options {}; - std::set<Option> options {}; + struct Filter { + wxString label; + int outScore {0}; + }; + std::vector<Filter> filters {}; - void clear() { options. clear(); } + void clear_options() { options.clear(); } + void clear_filters() { filters.clear(); } void append_options(DynamicPrintConfig* config, Preset::Type type, ConfigOptionMode mode); + void apply_filters(const wxString& search); + + void sort_options() { + std::sort(options.begin(), options.end(), [](const Option& o1, const Option& o2) { + return o1.label < o2.label; }); + } + void sort_filters() { + std::sort(filters.begin(), filters.end(), [](const Filter& f1, const Filter& f2) { + return f1.outScore > f2.outScore; }); + }; }; class SearchComboBox : public wxBitmapComboBox diff --git a/src/slic3r/GUI/fts_fuzzy_match.h b/src/slic3r/GUI/fts_fuzzy_match.h new file mode 100644 index 000000000..046027e16 --- /dev/null +++ b/src/slic3r/GUI/fts_fuzzy_match.h @@ -0,0 +1,221 @@ + // 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, +// publish, and distribute this file as you see fit. +// +// VERSION +// 0.2.0 (2017-02-18) Scored matches perform exhaustive search for best score +// 0.1.0 (2016-03-28) Initial release +// +// AUTHOR +// Forrest Smith +// +// NOTES +// Compiling +// You MUST add '#define FTS_FUZZY_MATCH_IMPLEMENTATION' before including this header in ONE source file to create implementation. +// +// fuzzy_match_simple(...) +// Returns true if each character in pattern is found sequentially within str +// +// fuzzy_match(...) +// Returns true if pattern is found AND calculates a score. +// 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. +// Score system should be tuned for YOUR use case. Words, sentences, file names, or method names all prefer different tuning. + + +#ifndef FTS_FUZZY_MATCH_H +#define FTS_FUZZY_MATCH_H + + +#include <cstdint> // uint8_t +#include <ctype.h> // ::tolower, ::toupper +#include <cstring> // memcpy + +#include <cstdio> + +// 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); +} + + +#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, + int & recursionCount, int recursionLimit); + } + + // 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) { + + uint8_t matches[256]; + return fuzzy_match(pattern, str, outScore, matches, sizeof(matches)); + } + + static bool fuzzy_match(char const * pattern, char const * str, int & outScore, uint8_t * matches, int maxMatches) { + int recursionCount = 0; + int recursionLimit = 10; + + return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, maxMatches, 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) + { + // Count recursions + ++recursionCount; + if (recursionCount >= recursionLimit) + return false; + + // Detect end of strings + if (*pattern == '\0' || *str == '\0') + return false; + + // Recursion params + bool recursiveMatch = false; + uint8_t bestRecursiveMatches[256]; + int bestRecursiveScore = 0; + + // Loop through pattern and str looking for a match + bool first_match = true; + while (*pattern != '\0' && *str != '\0') { + + // Found match + if (tolower(*pattern) == tolower(*str)) { + + // Supplied matches buffer was too short + if (nextMatch >= maxMatches) + return false; + + // "Copy-on-Write" srcMatches into matches + if (first_match && srcMatches) { + memcpy(matches, srcMatches, nextMatch); + first_match = false; + } + + // Recursive call that "skips" this match + uint8_t recursiveMatches[256]; + int recursiveScore; + if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) { + + // Pick best recursive score + if (!recursiveMatch || recursiveScore > bestRecursiveScore) { + memcpy(bestRecursiveMatches, recursiveMatches, 256); + bestRecursiveScore = recursiveScore; + } + recursiveMatch = true; + } + + // Advance + matches[nextMatch++] = (uint8_t)(str - strBegin); + ++pattern; + } + ++str; + } + + // Determine if full pattern was matched + bool matched = *pattern == '\0' ? true : false; + + // Calculate score + if (matched) { + const int sequential_bonus = 15; // bonus for adjacent matches + const int separator_bonus = 30; // bonus if match occurs after a separator + const int camel_bonus = 30; // bonus if match is uppercase and prev is lower + const int first_letter_bonus = 15; // bonus if the first letter is matched + + const int leading_letter_penalty = -5; // penalty applied for every letter in str before the first match + const int max_leading_letter_penalty = -15; // maximum penalty for leading letters + const int unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter + + // Iterate str to end + while (*str != '\0') + ++str; + + // Initialize score + outScore = 100; + + // Apply leading letter penalty + int penalty = leading_letter_penalty * matches[0]; + if (penalty < max_leading_letter_penalty) + penalty = max_leading_letter_penalty; + outScore += penalty; + + // Apply unmatched penalty + int unmatched = (int)(str - strBegin) - nextMatch; + outScore += unmatched_letter_penalty * unmatched; + + // Apply ordering bonuses + for (int i = 0; i < nextMatch; ++i) { + uint8_t currIdx = matches[i]; + + if (i > 0) { + uint8_t prevIdx = matches[i - 1]; + + // Sequential + if (currIdx == (prevIdx + 1)) + outScore += sequential_bonus; + } + + // Check for bonuses based on neighbor character value + if (currIdx > 0) { + // Camel case + char neighbor = strBegin[currIdx - 1]; + char curr = strBegin[currIdx]; + if (::islower(neighbor) && ::isupper(curr)) + outScore += camel_bonus; + + // Separator + bool neighborSeparator = neighbor == '_' || neighbor == ' '; + if (neighborSeparator) + outScore += separator_bonus; + } + else { + // First letter + outScore += first_letter_bonus; + } + } + } + + // Return best result + if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) { + // Recursive score is better than "this" + memcpy(matches, bestRecursiveMatches, maxMatches); + outScore = bestRecursiveScore; + return true; + } + else if (matched) { + // "this" score is better than recursive + return true; + } + else { + // no match + return false; + } + } +} // namespace fts + +#endif // FTS_FUZZY_MATCH_IMPLEMENTATION + +#endif // FTS_FUZZY_MATCH_H