diff --git a/include/app/ModuleBrowser.hpp b/include/app/ModuleBrowser.hpp index 1f033605..b62664ae 100644 --- a/include/app/ModuleBrowser.hpp +++ b/include/app/ModuleBrowser.hpp @@ -8,8 +8,6 @@ namespace app { widget::Widget *moduleBrowserCreate(); -json_t *moduleBrowserToJson(); -void moduleBrowserFromJson(json_t *rootJ); } // namespace app diff --git a/include/plugin/Plugin.hpp b/include/plugin/Plugin.hpp index 4fc12176..a681b481 100644 --- a/include/plugin/Plugin.hpp +++ b/include/plugin/Plugin.hpp @@ -55,7 +55,7 @@ struct Plugin { */ std::string donateUrl; - virtual ~Plugin(); + ~Plugin(); void addModel(Model *model); Model *getModel(std::string slug); void fromJson(json_t *rootJ); diff --git a/include/settings.hpp b/include/settings.hpp index ed66015e..3f4a8ba2 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -1,6 +1,7 @@ #pragma once #include "common.hpp" #include "math.hpp" +#include "plugin/Model.hpp" #include @@ -26,6 +27,7 @@ struct Settings { bool frameRateSync = true; bool skipLoadOnLaunch = false; std::string patchPath; + std::set favoriteModels; json_t *toJson(); void fromJson(json_t *rootJ); diff --git a/include/ui/RadioButton.hpp b/include/ui/RadioButton.hpp index 0c737886..44922260 100644 --- a/include/ui/RadioButton.hpp +++ b/include/ui/RadioButton.hpp @@ -17,6 +17,7 @@ struct RadioButton : widget::OpaqueWidget { void draw(const DrawArgs &args) override; void onEnter(const widget::EnterEvent &e) override; void onLeave(const widget::LeaveEvent &e) override; + void onDragStart(const widget::DragStartEvent &e) override; void onDragDrop(const widget::DragDropEvent &e) override; }; diff --git a/src/app/ModuleBrowser.cpp b/src/app/ModuleBrowser.cpp index 48ba9223..e52d0fc3 100644 --- a/src/app/ModuleBrowser.cpp +++ b/src/app/ModuleBrowser.cpp @@ -12,6 +12,7 @@ #include "ui/List.hpp" #include "ui/MenuItem.hpp" #include "ui/Button.hpp" +#include "ui/RadioButton.hpp" #include "ui/ChoiceButton.hpp" #include "app/ModuleWidget.hpp" #include "app/Scene.hpp" @@ -20,6 +21,7 @@ #include "plugin/Model.hpp" #include "string.hpp" #include "history.hpp" +#include "settings.hpp" #include #include @@ -29,9 +31,6 @@ namespace rack { namespace app { -static std::set sFavoriteModels; - - static float modelScore(plugin::Model *model, const std::string &search) { if (search.empty()) return true; @@ -74,93 +73,129 @@ struct BrowserOverlay : widget::OverlayWidget { }; -static const float MODEL_BOX_ZOOM = 0.5f; - - -struct ModelBox : widget::OpaqueWidget { - plugin::Model *model; - widget::Widget *infoWidget; - /** Lazily created */ - widget::Widget *previewWidget = NULL; - /** Number of frames since draw() has been called */ - int visibleFrames = 0; - bool selected = false; - - ModelBox() { - // Approximate size as 10HP before we know the actual size. - // We need a nonzero size, otherwise the parent widget will consider it not in the draw bounds, so its preview will not be lazily created. - box.size.x = 10 * RACK_GRID_WIDTH * MODEL_BOX_ZOOM; - box.size.y = RACK_GRID_HEIGHT * MODEL_BOX_ZOOM; - box.size = box.size.ceil(); - } - +struct InfoBox : widget::Widget { void setModel(plugin::Model *model) { - this->model = model; - - infoWidget = new widget::Widget; - infoWidget->hide(); - addChild(infoWidget); - math::Vec pos; // Name label ui::Label *nameLabel = new ui::Label; - // nameLabel->box.size.x = infoWidget->box.size.x; + // nameLabel->box.size.x = box.size.x; nameLabel->box.pos = pos; nameLabel->text = model->name; - infoWidget->addChild(nameLabel); + addChild(nameLabel); pos = nameLabel->box.getBottomLeft(); // Plugin label ui::Label *pluginLabel = new ui::Label; - // pluginLabel->box.size.x = infoWidget->box.size.x; + // pluginLabel->box.size.x = box.size.x; pluginLabel->box.pos = pos; pluginLabel->text = model->plugin->name; - infoWidget->addChild(pluginLabel); + addChild(pluginLabel); pos = pluginLabel->box.getBottomLeft(); ui::Label *descriptionLabel = new ui::Label; - descriptionLabel->box.size.x = infoWidget->box.size.x; + descriptionLabel->box.size.x = box.size.x; descriptionLabel->box.pos = pos; descriptionLabel->text = model->description; - infoWidget->addChild(descriptionLabel); + addChild(descriptionLabel); pos = descriptionLabel->box.getBottomLeft(); // for (const std::string &tag : model->tags) { // ui::Button *tagButton = new ui::Button; - // tagButton->box.size.x = infoWidget->box.size.x; + // tagButton->box.size.x = box.size.x; // tagButton->box.pos = pos; // tagButton->text = tag; - // infoWidget->addChild(tagButton); + // addChild(tagButton); // pos = tagButton->box.getTopLeft(); // } + } - // // Favorite button - // ui::Button *favoriteButton = new ui::Button; - // favoriteButton->box.size.x = box.size.x; - // favoriteButton->box.pos = pos; - // favoriteButton->box.pos.y -= favoriteButton->box.size.y; - // favoriteButton->text = "★"; - // addChild(favoriteButton); - // pos = favoriteButton->box.getTopLeft(); + void draw(const DrawArgs &args) override { + nvgBeginPath(args.vg); + nvgRect(args.vg, 0, 0, box.size.x, box.size.y); + nvgFillColor(args.vg, nvgRGBAf(1, 1, 1, 0.5)); + nvgFill(args.vg); + + Widget::draw(args); } +}; + + +struct ModelFavoriteQuantity : ui::Quantity { + plugin::Model *model; + std::string getLabel() override {return "★";} + void setValue(float value) override { + if (value) { + settings.favoriteModels.insert(model); + } + else { + auto it = settings.favoriteModels.find(model); + if (it != settings.favoriteModels.end()) + settings.favoriteModels.erase(it); + } + } + float getValue() override { + auto it = settings.favoriteModels.find(model); + return (it != settings.favoriteModels.end()); + } +}; + + +static const float MODEL_BOX_ZOOM = 0.5f; + + +struct ModelBox : widget::OpaqueWidget { + plugin::Model *model; + InfoBox *infoBox; + widget::Widget *previewWidget; + ui::RadioButton *favoriteButton; + /** Lazily created */ + widget::FramebufferWidget *previewFb = NULL; + /** Number of frames since draw() has been called */ + int visibleFrames = 0; + + ModelBox() { + // Approximate size as 10HP before we know the actual size. + // We need a nonzero size, otherwise the parent widget will consider it not in the draw bounds, so its preview will not be lazily created. + box.size.x = 10 * RACK_GRID_WIDTH * MODEL_BOX_ZOOM; + box.size.y = RACK_GRID_HEIGHT * MODEL_BOX_ZOOM; + box.size = box.size.ceil(); + } + + void setModel(plugin::Model *model) { + this->model = model; - void createPreview() { - assert(!previewWidget); previewWidget = new widget::TransparentWidget; previewWidget->box.size.y = std::ceil(RACK_GRID_HEIGHT * MODEL_BOX_ZOOM); addChild(previewWidget); - widget::FramebufferWidget *fbWidget = new widget::FramebufferWidget; + infoBox = new InfoBox; + infoBox->box.size = math::Vec(100, 100); + // infoBox->setModel(model); + infoBox->hide(); + addChild(infoBox); + + // Favorite button + favoriteButton = new ui::RadioButton; + ModelFavoriteQuantity *favoriteQuantity = new ModelFavoriteQuantity; + favoriteQuantity->model = model; + favoriteButton->quantity = favoriteQuantity; + favoriteButton->box.pos.y = box.size.y; + box.size.y += favoriteButton->box.size.y; + addChild(favoriteButton); + } + + void createPreview() { + previewFb = new widget::FramebufferWidget; if (math::isNear(APP->window->pixelRatio, 1.0)) { // Small details draw poorly at low DPI, so oversample when drawing to the framebuffer - fbWidget->oversample = 2.0; + previewFb->oversample = 2.0; } - previewWidget->addChild(fbWidget); + previewWidget->addChild(previewFb); widget::ZoomWidget *zoomWidget = new widget::ZoomWidget; zoomWidget->setZoom(MODEL_BOX_ZOOM); - fbWidget->addChild(zoomWidget); + previewFb->addChild(zoomWidget); ModuleWidget *moduleWidget = model->createModuleWidgetNull(); zoomWidget->addChild(moduleWidget); @@ -169,19 +204,20 @@ struct ModelBox : widget::OpaqueWidget { zoomWidget->box.size.y = RACK_GRID_HEIGHT * MODEL_BOX_ZOOM; previewWidget->box.size.x = std::ceil(zoomWidget->box.size.x); - infoWidget->box.size = previewWidget->box.size; + infoBox->box.size = previewWidget->box.size; + favoriteButton->box.size.x = previewWidget->box.size.x; box.size.x = previewWidget->box.size.x; } void deletePreview() { - assert(previewWidget); - removeChild(previewWidget); - delete previewWidget; - previewWidget = NULL; + assert(previewFb); + previewWidget->removeChild(previewFb); + delete previewFb; + previewFb = NULL; } void step() override { - if (previewWidget && ++visibleFrames >= 60) { + if (previewFb && ++visibleFrames >= 60) { deletePreview(); } OpaqueWidget::step(); @@ -191,7 +227,7 @@ struct ModelBox : widget::OpaqueWidget { visibleFrames = 0; // Lazily create preview when drawn - if (!previewWidget) { + if (!previewFb) { createPreview(); } @@ -205,29 +241,18 @@ struct ModelBox : widget::OpaqueWidget { nvgFillPaint(args.vg, nvgBoxGradient(args.vg, 0, 0, box.size.x, box.size.y, c, r, shadowColor, transparentColor)); nvgFill(args.vg); - nvgScissor(args.vg, RECT_ARGS(args.clipBox)); OpaqueWidget::draw(args); - - // Translucent overlay when selected - if (selected) { - nvgBeginPath(args.vg); - nvgRect(args.vg, 0.0, 0.0, box.size.x, box.size.y); - nvgFillColor(args.vg, nvgRGBAf(1, 1, 1, 0.25)); - nvgFill(args.vg); - } - - nvgResetScissor(args.vg); } void onButton(const widget::ButtonEvent &e) override; void onEnter(const widget::EnterEvent &e) override { e.consume(this); - selected = true; + infoBox->show(); } void onLeave(const widget::LeaveEvent &e) override { - selected = false; + infoBox->hide(); } }; @@ -276,8 +301,17 @@ struct BrowserSearchField : ui::TextField { }; +struct ShowFavoritesQuantity : ui::Quantity { + widget::Widget *widget; + std::string getLabel() override {return "Only show favorites";} + void setValue(float value) override; + float getValue() override; +}; + + struct BrowserSidebar : widget::Widget { BrowserSearchField *searchField; + ui::RadioButton *favoriteButton; ui::Label *authorLabel; ui::List *authorList; ui::ScrollWidget *authorScroll; @@ -289,6 +323,12 @@ struct BrowserSidebar : widget::Widget { searchField = new BrowserSearchField; addChild(searchField); + favoriteButton = new ui::RadioButton; + ShowFavoritesQuantity *favoriteQuantity = new ShowFavoritesQuantity; + favoriteQuantity->widget = favoriteButton; + favoriteButton->quantity = favoriteQuantity; + addChild(favoriteButton); + authorLabel = new ui::Label; authorLabel->color = nvgRGB(0x80, 0x80, 0x80); authorLabel->text = "Authors"; @@ -332,22 +372,25 @@ struct BrowserSidebar : widget::Widget { } void step() override { - float listHeight = (box.size.y - searchField->box.size.y) / 2 - authorLabel->box.size.y; - listHeight = std::floor(listHeight); searchField->box.size.x = box.size.x; + favoriteButton->box.pos = searchField->box.getBottomLeft(); + favoriteButton->box.size.x = box.size.x; - authorLabel->box.pos = searchField->box.getBottomLeft(); + float listHeight = (box.size.y - favoriteButton->box.getBottom()) / 2; + listHeight = std::floor(listHeight); + + authorLabel->box.pos = favoriteButton->box.getBottomLeft(); authorLabel->box.size.x = box.size.x; authorScroll->box.pos = authorLabel->box.getBottomLeft(); - authorScroll->box.size.y = listHeight; + authorScroll->box.size.y = listHeight - authorLabel->box.size.y; authorScroll->box.size.x = box.size.x; authorList->box.size.x = authorScroll->box.size.x; tagLabel->box.pos = authorScroll->box.getBottomLeft(); tagLabel->box.size.x = box.size.x; tagScroll->box.pos = tagLabel->box.getBottomLeft(); - tagScroll->box.size.y = listHeight; + tagScroll->box.size.y = listHeight - tagLabel->box.size.y; tagScroll->box.size.x = box.size.x; tagList->box.size.x = tagScroll->box.size.x; @@ -359,12 +402,14 @@ struct BrowserSidebar : widget::Widget { struct ModuleBrowser : widget::OpaqueWidget { BrowserSidebar *sidebar; ui::ScrollWidget *modelScroll; + ui::Label *modelLabel; ui::MarginLayout *modelMargin; ui::SequentialLayout *modelContainer; std::string search; std::string author; std::string tag; + bool favorites = false; ModuleBrowser() { sidebar = new BrowserSidebar; @@ -374,7 +419,12 @@ struct ModuleBrowser : widget::OpaqueWidget { modelScroll = new ui::ScrollWidget; addChild(modelScroll); + modelLabel = new ui::Label; + modelLabel->box.pos = math::Vec(10, 10); + modelScroll->container->addChild(modelLabel); + modelMargin = new ui::MarginLayout; + modelMargin->box.pos = modelLabel->box.getBottomLeft(); modelMargin->margin = math::Vec(10, 10); modelScroll->container->addChild(modelMargin); @@ -391,7 +441,7 @@ struct ModuleBrowser : widget::OpaqueWidget { } } - refreshModels(); + refresh(); } void step() override { @@ -413,15 +463,23 @@ struct ModuleBrowser : widget::OpaqueWidget { Widget::draw(args); } - void refreshModels() { + void refresh() { // Reset scroll position modelScroll->offset = math::Vec(); - if (search.empty()) { - // Make all ModelBoxes visible - for (Widget *w : modelContainer->children) { + // Show all or only favorites + for (Widget *w : modelContainer->children) { + if (favorites) { + ModelBox *m = dynamic_cast(w); + auto it = settings.favoriteModels.find(m->model); + w->visible = (it != settings.favoriteModels.end()); + } + else { w->visible = true; } + } + + if (search.empty()) { // Sort by plugin name and then module name modelContainer->children.sort([&](Widget *w1, Widget *w2) { ModelBox *m1 = dynamic_cast(w1); @@ -437,9 +495,12 @@ struct ModuleBrowser : widget::OpaqueWidget { for (Widget *w : modelContainer->children) { ModelBox *m = dynamic_cast(w); assert(m); - float score = modelScore(m->model, search); + float score = 0.f; + if (m->visible) { + modelScore(m->model, search); + m->visible = (score > 0); + } scores[m] = score; - m->visible = (score > 0); } // Sort by score modelContainer->children.sort([&](Widget *w1, Widget *w2) { @@ -493,19 +554,36 @@ struct ModuleBrowser : widget::OpaqueWidget { } } + // Count models + int modelsLen = 0; + for (Widget *w : modelContainer->children) { + if (w->visible) + modelsLen++; + } + modelLabel->text = string::f("Modules (%d)", modelsLen); + // Enable author and tag items that are available in visible ModelBoxes + int authorsLen = 0; for (Widget *w : sidebar->authorList->children) { AuthorItem *item = dynamic_cast(w); assert(item); auto it = enabledAuthors.find(item->text); item->disabled = (it == enabledAuthors.end()); + if (!item->disabled) + authorsLen++; } + sidebar->authorLabel->text = string::f("Authors (%d)", authorsLen); + + int tagsLen = 0; for (Widget *w : sidebar->tagList->children) { TagItem *item = dynamic_cast(w); assert(item); auto it = enabledTags.find(item->text); item->disabled = (it == enabledTags.end()); + if (!item->disabled) + tagsLen++; } + sidebar->tagLabel->text = string::f("Tags (%d)", tagsLen); } }; @@ -546,7 +624,7 @@ inline void AuthorItem::onAction(const widget::ActionEvent &e) { browser->author = ""; else browser->author = text; - browser->refreshModels(); + browser->refresh(); } @@ -556,14 +634,25 @@ inline void TagItem::onAction(const widget::ActionEvent &e) { browser->tag = ""; else browser->tag = text; - browser->refreshModels(); + browser->refresh(); } inline void BrowserSearchField::onChange(const widget::ChangeEvent &e) { ModuleBrowser *browser = getAncestorOfType(); browser->search = string::trim(text); - browser->refreshModels(); + browser->refresh(); +} + +inline void ShowFavoritesQuantity::setValue(float value) { + ModuleBrowser *browser = widget->getAncestorOfType(); + browser->favorites = (bool) value; + browser->refresh(); +} + +inline float ShowFavoritesQuantity::getValue() { + ModuleBrowser *browser = widget->getAncestorOfType(); + return browser->favorites; } @@ -579,41 +668,6 @@ widget::Widget *moduleBrowserCreate() { return overlay; } -json_t *moduleBrowserToJson() { - json_t *rootJ = json_object(); - - json_t *favoritesJ = json_array(); - for (plugin::Model *model : sFavoriteModels) { - json_t *modelJ = json_object(); - json_object_set_new(modelJ, "plugin", json_string(model->plugin->slug.c_str())); - json_object_set_new(modelJ, "model", json_string(model->slug.c_str())); - json_array_append_new(favoritesJ, modelJ); - } - json_object_set_new(rootJ, "favorites", favoritesJ); - - return rootJ; -} - -void moduleBrowserFromJson(json_t *rootJ) { - json_t *favoritesJ = json_object_get(rootJ, "favorites"); - if (favoritesJ) { - size_t i; - json_t *favoriteJ; - json_array_foreach(favoritesJ, i, favoriteJ) { - json_t *pluginJ = json_object_get(favoriteJ, "plugin"); - json_t *modelJ = json_object_get(favoriteJ, "model"); - if (!pluginJ || !modelJ) - continue; - std::string pluginSlug = json_string_value(pluginJ); - std::string modelSlug = json_string_value(modelJ); - plugin::Model *model = plugin::getModel(pluginSlug, modelSlug); - if (!model) - continue; - sFavoriteModels.insert(model); - } - } -} - } // namespace app } // namespace rack diff --git a/src/settings.cpp b/src/settings.cpp index cd735c37..f4e437ae 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -55,7 +55,14 @@ json_t *Settings::toJson() { json_object_set_new(rootJ, "patchPath", json_string(patchPath.c_str())); - json_object_set_new(rootJ, "moduleBrowser", app::moduleBrowserToJson()); + json_t *favoriteModelsJ = json_array(); + for (plugin::Model *model : favoriteModels) { + json_t *modelJ = json_object(); + json_object_set_new(modelJ, "plugin", json_string(model->plugin->slug.c_str())); + json_object_set_new(modelJ, "model", json_string(model->slug.c_str())); + json_array_append_new(favoriteModelsJ, modelJ); + } + json_object_set_new(rootJ, "favoriteModels", favoriteModelsJ); return rootJ; } @@ -139,28 +146,49 @@ void Settings::fromJson(json_t *rootJ) { if (patchPathJ) patchPath = json_string_value(patchPathJ); - json_t *moduleBrowserJ = json_object_get(rootJ, "moduleBrowser"); - if (moduleBrowserJ) - app::moduleBrowserFromJson(moduleBrowserJ); + + json_t *favoriteModelsJ = json_object_get(rootJ, "favoriteModels"); + // Legacy: "favorites" was defined under "moduleBrowser" until 1.0. + if (!favoriteModelsJ) { + json_t *moduleBrowserJ = json_object_get(rootJ, "moduleBrowser"); + if (moduleBrowserJ) + favoriteModelsJ = json_object_get(rootJ, "favorites"); + } + if (favoriteModelsJ) { + size_t i; + json_t *favoriteJ; + json_array_foreach(favoriteModelsJ, i, favoriteJ) { + json_t *pluginJ = json_object_get(favoriteJ, "plugin"); + json_t *modelJ = json_object_get(favoriteJ, "model"); + if (!pluginJ || !modelJ) + continue; + std::string pluginSlug = json_string_value(pluginJ); + std::string modelSlug = json_string_value(modelJ); + plugin::Model *model = plugin::getModel(pluginSlug, modelSlug); + if (!model) + continue; + favoriteModels.insert(model); + } + } } void Settings::save(std::string filename) { INFO("Saving settings %s", filename.c_str()); json_t *rootJ = toJson(); if (rootJ) { - FILE *file = fopen(filename.c_str(), "w"); + FILE *file = std::fopen(filename.c_str(), "w"); if (!file) return; json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); json_decref(rootJ); - fclose(file); + std::fclose(file); } } void Settings::load(std::string filename) { INFO("Loading settings %s", filename.c_str()); - FILE *file = fopen(filename.c_str(), "r"); + FILE *file = std::fopen(filename.c_str(), "r"); if (!file) return; @@ -174,7 +202,7 @@ void Settings::load(std::string filename) { WARN("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text); } - fclose(file); + std::fclose(file); } diff --git a/src/ui/RadioButton.cpp b/src/ui/RadioButton.cpp index 802058f5..446ef48b 100644 --- a/src/ui/RadioButton.cpp +++ b/src/ui/RadioButton.cpp @@ -15,33 +15,35 @@ RadioButton::~RadioButton() { } void RadioButton::draw(const DrawArgs &args) { + BNDwidgetState state = this->state; std::string label; - if (quantity) + if (quantity) { label = quantity->getLabel(); + if (quantity->isMax()) + state = BND_ACTIVE; + } bndRadioButton(args.vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, label.c_str()); } void RadioButton::onEnter(const widget::EnterEvent &e) { - if (state != BND_ACTIVE) - state = BND_HOVER; + state = BND_HOVER; e.consume(this); } void RadioButton::onLeave(const widget::LeaveEvent &e) { - if (state != BND_ACTIVE) - state = BND_DEFAULT; + state = BND_DEFAULT; +} + +void RadioButton::onDragStart(const widget::DragStartEvent &e) { + e.consume(this); } void RadioButton::onDragDrop(const widget::DragDropEvent &e) { if (e.origin == this) { - if (state == BND_ACTIVE) { - state = BND_HOVER; - if (quantity) + if (quantity) { + if (quantity->isMax()) quantity->setMin(); - } - else { - state = BND_ACTIVE; - if (quantity) + else quantity->setMax(); }