diff --git a/dep/Makefile b/dep/Makefile index b7dfdddc..7244b0d0 100755 --- a/dep/Makefile +++ b/dep/Makefile @@ -246,8 +246,9 @@ $(pffft): | pffft mkdir -p include cp pffft/*.h include/ -$(fuzzysearchdatabase): - cp fuzzysearchdatabase/src/FuzzySearchDatabase.hpp include/ +$(fuzzysearchdatabase): fuzzysearchdatabase/src/FuzzySearchDatabase.hpp + mkdir -p include + cp $^ include/ # Helpers diff --git a/dep/fuzzysearchdatabase b/dep/fuzzysearchdatabase index 0f7dd46b..797d13f6 160000 --- a/dep/fuzzysearchdatabase +++ b/dep/fuzzysearchdatabase @@ -1 +1 @@ -Subproject commit 0f7dd46b61fdec21cf99766611019dde731eb432 +Subproject commit 797d13f6ff3b55fcdfa34149ddb766b05119ca8a diff --git a/src/app/ModuleBrowser.cpp b/src/app/ModuleBrowser.cpp index c3b00a41..cc11e672 100644 --- a/src/app/ModuleBrowser.cpp +++ b/src/app/ModuleBrowser.cpp @@ -27,61 +27,52 @@ #include #include #include +#include namespace rack { namespace app { -// Static functions - - -static float modelScore(plugin::Model* model, const std::string& search) { - if (search.empty()) - return 1.f; - std::string s; - s += model->plugin->brand; - s += " "; - s += model->plugin->name; - s += " "; - s += model->name; - s += " "; - s += model->slug; - for (int tagId : model->tags) { - // Add all aliases of a tag - for (const std::string& alias : tag::tagAliases[tagId]) { - s += " "; - s += alias; - } - } - float score = string::fuzzyScore(string::lowercase(s), string::lowercase(search)); - return score; -} +static FuzzySearchDatabase modelDb; +static bool modelDbInitialized = false; -static bool isModelVisible(plugin::Model* model, const std::string& search, const std::string& brand, int tagId) { - // Filter search query - if (search != "") { - float score = modelScore(model, search); - if (score <= 0.f) - return false; - } - // Filter brand - if (brand != "") { - if (model->plugin->brand != brand) - return false; - } +static void modelDbInit() { + if (modelDbInitialized) + return; + modelDb.setWeights({1.f, 1.f, 0.25f, 1.f, 0.5f, 0.5f}); + modelDb.setThreshold(0.5f); - // Filter tag - if (tagId >= 0) { - auto it = std::find(model->tags.begin(), model->tags.end(), tagId); - if (it == model->tags.end()) - return false; + // Iterate plugins + for (plugin::Plugin* plugin : plugin::plugins) { + // Iterate model in plugin + for (plugin::Model* model : plugin->models) { + // Get search fields for model + std::string tagStr; + for (int tagId : model->tags) { + // Add all aliases of a tag + for (const std::string& tagAlias : tag::tagAliases[tagId]) { + tagStr += tagAlias; + tagStr += ", "; + } + } + std::vector fields { + model->plugin->brand, + model->plugin->name, + model->plugin->description, + model->name, + model->description, + tagStr, + }; + modelDb.addEntry(model, fields); + } } - return true; + modelDbInitialized = true; } + static ModuleWidget* chooseModel(plugin::Model* model) { // Create Module and ModuleWidget engine::Module* module = model->createModule(); @@ -428,12 +419,11 @@ struct ModuleBrowser : widget::OpaqueWidget { modelContainer->spacing = math::Vec(10, 10); modelMargin->addChild(modelContainer); - resetModelContainer(); - + resetModelBoxes(); clear(); } - void resetModelContainer() { + void resetModelBoxes() { modelContainer->clearChildren(); // Iterate plugins for (plugin::Plugin* plugin : plugin::plugins) { @@ -447,6 +437,7 @@ struct ModuleBrowser : widget::OpaqueWidget { if (moduleIt == pluginIt->second.end()) continue; } + // Create ModelBox ModelBox* modelBox = new ModelBox; modelBox->setModel(model); @@ -479,57 +470,82 @@ struct ModuleBrowser : widget::OpaqueWidget { // Reset scroll position modelScroll->offset = math::Vec(); - // Filter ModelBoxes + auto isModelVisible = [&](plugin::Model* model, const std::string& brand, int tagId) -> bool { + // Filter brand + if (brand != "") { + if (model->plugin->brand != brand) + return false; + } + + // Filter tag + if (tagId >= 0) { + auto it = std::find(model->tags.begin(), model->tags.end(), tagId); + if (it == model->tags.end()) + return false; + } + + return true; + }; + + // Filter ModelBoxes by brand and tag for (Widget* w : modelContainer->children) { ModelBox* m = dynamic_cast(w); assert(m); - m->visible = isModelVisible(m->model, search, brand, tagId); + m->visible = isModelVisible(m->model, brand, tagId); } - // Sort ModelBoxes - modelContainer->children.sort([&](Widget * w1, Widget * w2) { - ModelBox* m1 = dynamic_cast(w1); - ModelBox* m2 = dynamic_cast(w2); - // Sort by (modifiedTimestamp descending, plugin brand) - auto t1 = std::make_tuple(-m1->model->plugin->modifiedTimestamp, m1->model->plugin->brand); - auto t2 = std::make_tuple(-m2->model->plugin->modifiedTimestamp, m2->model->plugin->brand); - return t1 < t2; - }); + std::map prefilteredModelScores; + // Filter and sort by search results if (search.empty()) { - // We've already sorted above + // Add all models to prefilteredModelScores with scores of 1 + for (Widget* w : modelContainer->children) { + ModelBox* m = dynamic_cast(w); + assert(m); + prefilteredModelScores[m->model] = 1.f; + } + + // Sort ModelBoxes + modelContainer->children.sort([&](Widget * w1, Widget * w2) { + ModelBox* m1 = dynamic_cast(w1); + ModelBox* m2 = dynamic_cast(w2); + // Sort by (modifiedTimestamp descending, plugin brand) + auto t1 = std::make_tuple(-m1->model->plugin->modifiedTimestamp, m1->model->plugin->brand); + auto t2 = std::make_tuple(-m2->model->plugin->modifiedTimestamp, m2->model->plugin->brand); + return t1 < t2; + }); } else { - std::map scores; - // Compute scores + // Lazily initialize search database + modelDbInit(); + // Score results against search query + auto results = modelDb.search(search); + for (auto& result : results) { + prefilteredModelScores[result._key] = result._score; + } + // Sort by score + modelContainer->children.sort([&](Widget *w1, Widget *w2) { + ModelBox* m1 = dynamic_cast(w1); + ModelBox* m2 = dynamic_cast(w2); + // If score was not computed, the ModelBox will not visible so the order doesn't matter. + return get(prefilteredModelScores, m1->model, 0.f) > get(prefilteredModelScores, m2->model, 0.f); + }); + // Filter by whether the score is above the threshold for (Widget* w : modelContainer->children) { ModelBox* m = dynamic_cast(w); assert(m); - if (!m->visible) - continue; - scores[m] = modelScore(m->model, search); + if (m->visible) { + if (prefilteredModelScores.find(m->model) == prefilteredModelScores.end()) + m->visible = false; + } } - // // Sort by score - // modelContainer->children.sort([&](Widget *w1, Widget *w2) { - // // If score was not computed, scores[w] returns 0, but this doesn't matter because those widgets aren't visible. - // return get(scores, w1, 0.f) > get(scores, w2, 0.f); - // }); - } - - // Filter the brand and tag lists - - // Get modules that would be filtered by just the search query - std::vector filteredModels; - for (Widget* w : modelContainer->children) { - ModelBox* m = dynamic_cast(w); - assert(m); - if (isModelVisible(m->model, search, "", -1)) - filteredModels.push_back(m->model); } - auto hasModel = [&](const std::string & brand, int tagId) -> bool { - for (plugin::Model* model : filteredModels) { - if (isModelVisible(model, "", brand, tagId)) + // Determines if there is at least 1 visible Model with a given brand and tag + auto hasVisibleModel = [&](const std::string& brand, int tagId) -> bool { + for (auto& pair : prefilteredModelScores) { + plugin::Model* model = pair.first; + if (isModelVisible(model, brand, tagId)) return true; } return false; @@ -540,7 +556,7 @@ struct ModuleBrowser : widget::OpaqueWidget { for (Widget* w : sidebar->brandList->children) { BrandItem* item = dynamic_cast(w); assert(item); - item->disabled = !hasModel(item->text, tagId); + item->disabled = !hasVisibleModel(item->text, tagId); if (!item->disabled) brandsLen++; } @@ -550,7 +566,7 @@ struct ModuleBrowser : widget::OpaqueWidget { for (Widget* w : sidebar->tagList->children) { TagItem* item = dynamic_cast(w); assert(item); - item->disabled = !hasModel(brand, item->tagId); + item->disabled = !hasVisibleModel(brand, item->tagId); if (!item->disabled) tagsLen++; }