Browse Source

Add numbers to authors, tags, and modules list in ModuleBrowser. Add favorite button and favorite filter to ModuleBrowser.

tags/v1.0.0
Andrew Belt 5 years ago
parent
commit
e1a55f4976
7 changed files with 229 additions and 144 deletions
  1. +0
    -2
      include/app/ModuleBrowser.hpp
  2. +1
    -1
      include/plugin/Plugin.hpp
  3. +2
    -0
      include/settings.hpp
  4. +1
    -0
      include/ui/RadioButton.hpp
  5. +175
    -121
      src/app/ModuleBrowser.cpp
  6. +36
    -8
      src/settings.cpp
  7. +14
    -12
      src/ui/RadioButton.cpp

+ 0
- 2
include/app/ModuleBrowser.hpp View File

@@ -8,8 +8,6 @@ namespace app {


widget::Widget *moduleBrowserCreate();
json_t *moduleBrowserToJson();
void moduleBrowserFromJson(json_t *rootJ);


} // namespace app


+ 1
- 1
include/plugin/Plugin.hpp View File

@@ -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);


+ 2
- 0
include/settings.hpp View File

@@ -1,6 +1,7 @@
#pragma once
#include "common.hpp"
#include "math.hpp"
#include "plugin/Model.hpp"
#include <jansson.h>


@@ -26,6 +27,7 @@ struct Settings {
bool frameRateSync = true;
bool skipLoadOnLaunch = false;
std::string patchPath;
std::set<plugin::Model*> favoriteModels;

json_t *toJson();
void fromJson(json_t *rootJ);


+ 1
- 0
include/ui/RadioButton.hpp View File

@@ -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;
};



+ 175
- 121
src/app/ModuleBrowser.cpp View File

@@ -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 <set>
#include <algorithm>
@@ -29,9 +31,6 @@ namespace rack {
namespace app {


static std::set<plugin::Model*> 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<ModelBox*>(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<ModelBox*>(w1);
@@ -437,9 +495,12 @@ struct ModuleBrowser : widget::OpaqueWidget {
for (Widget *w : modelContainer->children) {
ModelBox *m = dynamic_cast<ModelBox*>(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<AuthorItem*>(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<TagItem*>(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<ModuleBrowser>();
browser->search = string::trim(text);
browser->refreshModels();
browser->refresh();
}

inline void ShowFavoritesQuantity::setValue(float value) {
ModuleBrowser *browser = widget->getAncestorOfType<ModuleBrowser>();
browser->favorites = (bool) value;
browser->refresh();
}

inline float ShowFavoritesQuantity::getValue() {
ModuleBrowser *browser = widget->getAncestorOfType<ModuleBrowser>();
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

+ 36
- 8
src/settings.cpp View File

@@ -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);
}




+ 14
- 12
src/ui/RadioButton.cpp View File

@@ -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();
}



Loading…
Cancel
Save