Browse Source

Add manufacturer item to ModuleBrowser, other ModuleBrowser

functionality
tags/v0.6.0
Andrew Belt 6 years ago
parent
commit
e8d0783aba
8 changed files with 435 additions and 226 deletions
  1. +6
    -0
      include/ui.hpp
  2. +14
    -7
      include/util/math.hpp
  3. +389
    -0
      src/app/ModuleBrowser.cpp
  4. +0
    -212
      src/app/moduleBrowser.cpp
  5. +2
    -2
      src/engine.cpp
  6. +11
    -1
      src/ui/Label.cpp
  7. +1
    -1
      src/ui/List.cpp
  8. +12
    -3
      src/ui/ScrollWidget.cpp

+ 6
- 0
include/ui.hpp View File

@@ -8,6 +8,12 @@ namespace rack {

struct Label : Widget {
std::string text;
enum Align {
LEFT_ALIGN,
CENTER_ALIGN,
RIGHT_ALIGN
};
Align align = LEFT_ALIGN;
Label() {
box.size.y = BND_WIDGET_HEIGHT;
}


+ 14
- 7
include/util/math.hpp View File

@@ -28,8 +28,8 @@ inline int max(int a, int b) {
/** Limits a value between a minimum and maximum
Assumes min <= max
*/
inline int clamp(int x, int minimum, int maximum) {
return min(max(x, minimum), maximum);
inline int clamp(int x, int min, int max) {
return rack::min(rack::max(x, min), max);
}

/** Euclidean modulus, always returns 0 <= mod < base for positive base.
@@ -74,11 +74,11 @@ inline bool isNear(float a, float b, float epsilon = 1.0e-6f) {
/** Limits a value between a minimum and maximum
Assumes min <= max
*/
inline float clamp(float x, float minimum, float maximum) {
return fminf(fmaxf(x, minimum), maximum);
inline float clamp(float x, float min, float max) {
return fminf(fmaxf(x, min), max);
}

/** Limits a value between a minimum and maximum
/** Limits a value between a min and max
If min > max, switches the two values
*/
inline float clamp2(float x, float min, float max) {
@@ -180,6 +180,7 @@ struct Vec {
return isfinite(x) && isfinite(y);
}
Vec clamp(Rect bound);
Vec clamp2(Rect bound);
};


@@ -260,8 +261,14 @@ struct Rect {

inline Vec Vec::clamp(Rect bound) {
return Vec(
clamp2(x, bound.pos.x, bound.pos.x + bound.size.x),
clamp2(y, bound.pos.y, bound.pos.y + bound.size.y));
rack::clamp(x, bound.pos.x, bound.pos.x + bound.size.x),
rack::clamp(y, bound.pos.y, bound.pos.y + bound.size.y));
}

inline Vec Vec::clamp2(Rect bound) {
return Vec(
rack::clamp2(x, bound.pos.x, bound.pos.x + bound.size.x),
rack::clamp2(y, bound.pos.y, bound.pos.y + bound.size.y));
}




+ 389
- 0
src/app/ModuleBrowser.cpp View File

@@ -0,0 +1,389 @@
#include "app.hpp"
#include "plugin.hpp"
#include "window.hpp"
#include <set>


#define BND_LABEL_FONT_SIZE 13


namespace rack {


static std::set<Model*> sFavoriteModels;


bool isMatch(std::string s, std::string search) {
s = lowercase(s);
search = lowercase(search);
return (s.find(search) != std::string::npos);
}

static bool isModelMatch(Model *model, std::string search) {
if (search.empty())
return true;
std::string s;
s += model->plugin->slug;
s += " ";
s += model->manufacturer;
s += " ";
s += model->name;
s += " ";
s += model->slug;
for (ModelTag tag : model->tags) {
s += " ";
s += gTagNames[tag];
}
return isMatch(s, search);
}


struct FavoriteRadioButton : RadioButton {
Model *model = NULL;

void onAction(EventAction &e) override;
};


struct BrowserListItem : OpaqueWidget {
bool selected = false;

BrowserListItem() {
box.size.y = 3 * BND_WIDGET_HEIGHT;
}

void draw(NVGcontext *vg) override {
BNDwidgetState state = selected ? BND_HOVER : BND_DEFAULT;
bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, state, -1, "");
Widget::draw(vg);
}

void onDragDrop(EventDragDrop &e) override {
if (e.origin != this)
return;
doAction();
}

void doAction() {
EventAction eAction;
eAction.consumed = true;
onAction(eAction);
if (eAction.consumed) {
// deletes `this`
gScene->setOverlay(NULL);
}
}

void onMouseEnter(EventMouseEnter &e) override;
};


struct ModelItem : BrowserListItem {
Model *model;
FavoriteRadioButton *favoriteButton;
Label *nameLabel;
Label *manufacturerLabel;
Label *tagsLabel;

ModelItem() {
favoriteButton = new FavoriteRadioButton();
favoriteButton->box.pos = Vec(7, BND_WIDGET_HEIGHT);
favoriteButton->box.size.x = 20;
favoriteButton->label = "★";
addChild(favoriteButton);

nameLabel = Widget::create<Label>(Vec(0, 0));
addChild(nameLabel);
manufacturerLabel = Widget::create<Label>(Vec(0, 0));
manufacturerLabel->align = Label::RIGHT_ALIGN;
addChild(manufacturerLabel);
tagsLabel = Widget::create<Label>(Vec(26, BND_WIDGET_HEIGHT));
addChild(tagsLabel);
}

void setModel(Model *model) {
assert(model);
this->model = model;
auto it = sFavoriteModels.find(model);
if (it != sFavoriteModels.end())
favoriteButton->setValue(1);
favoriteButton->model = model;

nameLabel->text = model->name;
manufacturerLabel->text = model->manufacturer;
int i = 0;
for (ModelTag tag : model->tags) {
if (i++ > 0)
tagsLabel->text += ", ";
tagsLabel->text += gTagNames[tag];
}
}

void step() override {
BrowserListItem::step();
manufacturerLabel->box.size.x = box.size.x - BND_SCROLLBAR_WIDTH;
}

void onAction(EventAction &e) override {
ModuleWidget *moduleWidget = model->createModuleWidget();
gRackWidget->moduleContainer->addChild(moduleWidget);
// Move module nearest to the mouse position
moduleWidget->box.pos = gRackWidget->lastMousePos.minus(moduleWidget->box.size.div(2));
gRackWidget->requestModuleBoxNearest(moduleWidget, moduleWidget->box);
}
};


struct ManufacturerItem : BrowserListItem {
std::string manufacturer;
Label *manufacturerLabel;

ManufacturerItem() {
manufacturerLabel = Widget::create<Label>(Vec(0, 0));
manufacturerLabel->text = "Show all modules";
addChild(manufacturerLabel);
}

void setManufacturer(std::string manufacturer) {
this->manufacturer = manufacturer;
manufacturerLabel->text = manufacturer;
}

void step() override {
BrowserListItem::step();
}

void onAction(EventAction &e) override;
};


struct BrowserList : List {
int selected = 0;

void step() override {
// If we have zero children, this result doesn't matter anyway.
selected = clamp(selected, 0, children.size() - 1);
int i = 0;
for (Widget *w : children) {
BrowserListItem *item = dynamic_cast<BrowserListItem*>(w);
if (item) {
item->selected = (i == selected);
}
i++;
}
List::step();
}

void selectChild(Widget *child) {
int i = 0;
for (Widget *w : children) {
if (w == child) {
selected = i;
break;
}
i++;
}
}

Widget *getSelectedChild() {
int i = 0;
for (Widget *w : children) {
if (i == selected) {
return w;
}
i++;
}
return NULL;
}
};


struct ModuleBrowser;

struct SearchModuleField : TextField {
ModuleBrowser *moduleBrowser;
void onTextChange() override;
void onKey(EventKey &e) override;
};


struct ModuleBrowser : OpaqueWidget {
SearchModuleField *searchField;
ScrollWidget *moduleScroll;
BrowserList *moduleList;
std::string manufacturerFilter;

ModuleBrowser() {
box.size.x = 400;

// Search
searchField = new SearchModuleField();
searchField->box.size.x = box.size.x;
searchField->moduleBrowser = this;
addChild(searchField);

moduleList = new BrowserList();
moduleList->box.size = Vec(box.size.x, 0.0);

// Module Scroll
moduleScroll = new ScrollWidget();
moduleScroll->box.pos.y = searchField->box.size.y;
moduleScroll->box.size.x = box.size.x;
moduleScroll->container->addChild(moduleList);
addChild(moduleScroll);

// Trigger search update
searchField->setText("");
}

void refreshSearch() {
std::string search = searchField->text;
moduleList->clearChildren();
moduleList->selected = 0;

// Favorites
for (Model *model : sFavoriteModels) {
if ((manufacturerFilter.empty() || manufacturerFilter == model->manufacturer) && isModelMatch(model, search)) {
ModelItem *item = new ModelItem();
item->setModel(model);
moduleList->addChild(item);
}
}

// Manufacturers
if (manufacturerFilter.empty()) {
// Collect all manufacturers
std::set<std::string> manufacturers;
for (Plugin *plugin : gPlugins) {
for (Model *model : plugin->models) {
if (model->manufacturer.empty())
continue;
manufacturers.insert(model->manufacturer);
}
}
// Manufacturer items
for (std::string manufacturer : manufacturers) {
if (isMatch(manufacturer, search)) {
ManufacturerItem *item = new ManufacturerItem();
item->setManufacturer(manufacturer);
moduleList->addChild(item);
}
}
}
else {
// Dummy manufacturer for clearing manufacturer filter
ManufacturerItem *item = new ManufacturerItem();
moduleList->addChild(item);
}

// Models
for (Plugin *plugin : gPlugins) {
for (Model *model : plugin->models) {
if ((manufacturerFilter.empty() || manufacturerFilter == model->manufacturer) && isModelMatch(model, search)) {
ModelItem *item = new ModelItem();
item->setModel(model);
moduleList->addChild(item);
}
}
}
}

void step() override {
box.pos = parent->box.size.minus(box.size).div(2).round();
box.pos.y = 40;
box.size.y = parent->box.size.y - 2 * box.pos.y;

moduleScroll->box.size.y = box.size.y - moduleScroll->box.pos.y;
gFocusedWidget = searchField;
Widget::step();
}
};


// Implementations of inline methods above

void ManufacturerItem::onAction(EventAction &e) {
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>();
moduleBrowser->manufacturerFilter = manufacturer;
moduleBrowser->refreshSearch();
e.consumed = false;
}

void FavoriteRadioButton::onAction(EventAction &e) {
if (!model)
return;
if (value) {
sFavoriteModels.insert(model);
}
else {
auto it = sFavoriteModels.find(model);
if (it != sFavoriteModels.end())
sFavoriteModels.erase(it);
}

ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>();
if (moduleBrowser)
moduleBrowser->refreshSearch();
}

void BrowserListItem::onMouseEnter(EventMouseEnter &e) {
BrowserList *list = getAncestorOfType<BrowserList>();
list->selectChild(this);
}

void SearchModuleField::onTextChange() {
moduleBrowser->refreshSearch();
}

void SearchModuleField::onKey(EventKey &e) {
switch (e.key) {
case GLFW_KEY_ESCAPE: {
gScene->setOverlay(NULL);
e.consumed = true;
return;
} break;
case GLFW_KEY_UP: {
moduleBrowser->moduleList->selected--;
e.consumed = true;
} break;
case GLFW_KEY_DOWN: {
moduleBrowser->moduleList->selected++;
e.consumed = true;
} break;
case GLFW_KEY_ENTER: {
Widget *w = moduleBrowser->moduleList->getSelectedChild();
BrowserListItem *item = dynamic_cast<BrowserListItem*>(w);
if (item) {
item->doAction();
e.consumed = true;
return;
}
} break;
}

if (!e.consumed) {
TextField::onKey(e);
}
}

// Global functions

void appModuleBrowserCreate() {
MenuOverlay *overlay = new MenuOverlay();

ModuleBrowser *moduleBrowser = new ModuleBrowser();
overlay->addChild(moduleBrowser);
gScene->setOverlay(overlay);
}

json_t *appModuleBrowserToJson() {
// TODO
return json_object();
}

void appModuleBrowserFromJson(json_t *root) {
// TODO
}


} // namespace rack

+ 0
- 212
src/app/moduleBrowser.cpp View File

@@ -1,212 +0,0 @@
#include "app.hpp"
#include "plugin.hpp"
#include "window.hpp"
#include <set>


#define BND_LABEL_FONT_SIZE 13


namespace rack {


static std::string sSearch;
static std::set<Model*> sFavoriteModels;


struct FavoriteRadioButton : RadioButton {
Model *model = NULL;
void onChange(EventChange &e) override {
debug("HI");
if (!model)
return;
if (value) {
sFavoriteModels.insert(model);
}
else {
auto it = sFavoriteModels.find(model);
if (it != sFavoriteModels.end())
sFavoriteModels.erase(it);
}
}
};


struct ModuleListItem : OpaqueWidget {
bool selected = false;
FavoriteRadioButton *favoriteButton;

ModuleListItem() {
box.size.y = 3 * BND_WIDGET_HEIGHT;

favoriteButton = new FavoriteRadioButton();
favoriteButton->box.pos = Vec(7, BND_WIDGET_HEIGHT);
favoriteButton->box.size.x = 20;
favoriteButton->label = "★";
addChild(favoriteButton);
}

void draw(NVGcontext *vg) override {
BNDwidgetState state = selected ? BND_HOVER : BND_DEFAULT;
bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, state, -1, "");
Widget::draw(vg);
}

void onDragDrop(EventDragDrop &e) override {
if (e.origin != this)
return;

EventAction eAction;
eAction.consumed = true;
onAction(eAction);
if (eAction.consumed) {
// deletes `this`
gScene->setOverlay(NULL);
}
}
};

struct ModelItem : ModuleListItem {
Model *model;

void setModel(Model *model) {
this->model = model;
auto it = sFavoriteModels.find(model);
if (it != sFavoriteModels.end())
favoriteButton->setValue(1);
favoriteButton->model = model;
}

void draw(NVGcontext *vg) override {
ModuleListItem::draw(vg);

// bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, BND_DEFAULT, -1, model->name.c_str());

float x = box.size.x - bndLabelWidth(vg, -1, model->manufacturer.c_str());
NVGcolor rightColor = bndGetTheme()->menuTheme.textColor;
bndIconLabelValue(vg, x, 0.0, box.size.x, box.size.y, -1, rightColor, BND_LEFT, BND_LABEL_FONT_SIZE, model->manufacturer.c_str(), NULL);
}

void onAction(EventAction &e) override {
ModuleWidget *moduleWidget = model->createModuleWidget();
gRackWidget->moduleContainer->addChild(moduleWidget);
// Move module nearest to the mouse position
// Rect box;
// box.size = moduleWidget->box.size;
// AddModuleWindow *w = getAncestorOfType<AddModuleWindow>();
// box.pos = w->modulePos.minus(box.getCenter());
// gRackWidget->requestModuleBoxNearest(moduleWidget, box);
}
};


struct ModuleBrowser;

struct SearchModuleField : TextField {
ModuleBrowser *moduleBrowser;
void onTextChange() override;
void onKey(EventKey &e) override;
};


struct ModuleBrowser : OpaqueWidget {
SearchModuleField *searchField;
ScrollWidget *moduleScroll;
List *moduleList;

ModuleBrowser() {
box.size.x = 300;

// Search
searchField = new SearchModuleField();
searchField->box.size.x = box.size.x;
searchField->moduleBrowser = this;
addChild(searchField);

moduleList = new List();
moduleList->box.size = Vec(box.size.x, 0.0);

// Module Scroll
moduleScroll = new ScrollWidget();
moduleScroll->box.pos.y = searchField->box.size.y;
moduleScroll->box.size.x = box.size.x;
moduleScroll->container->addChild(moduleList);
addChild(moduleScroll);

// Focus search
searchField->setText(sSearch);
EventFocus eFocus;
searchField->onFocus(eFocus);
}

void setSearch(std::string search) {
moduleList->clearChildren();

// Favorites
for (Model *model : sFavoriteModels) {
ModelItem *item = new ModelItem();
item->setModel(model);
moduleList->addChild(item);
}

// Models
for (Plugin *plugin : gPlugins) {
for (Model *model : plugin->models) {
ModelItem *item = new ModelItem();
item->setModel(model);
moduleList->addChild(item);
}
}
}

void step() override {
box.pos = parent->box.size.minus(box.size).div(2).round();
box.pos.y = 40;
box.size.y = parent->box.size.y - 2 * box.pos.y;

moduleScroll->box.size.y = box.size.y - moduleScroll->box.pos.y;
gFocusedWidget = searchField;
Widget::step();
}
};


void SearchModuleField::onTextChange() {
sSearch = text;
moduleBrowser->setSearch(text);
}

void SearchModuleField::onKey(EventKey &e) {
switch (e.key) {
case GLFW_KEY_ESCAPE: {
gScene->setOverlay(NULL);
e.consumed = true;
return;
} break;
}

if (!e.consumed) {
TextField::onKey(e);
}
}


void appModuleBrowserCreate() {
MenuOverlay *overlay = new MenuOverlay();

ModuleBrowser *moduleBrowser = new ModuleBrowser();
overlay->addChild(moduleBrowser);
gScene->setOverlay(overlay);
}

json_t *appModuleBrowserToJson() {
// TODO
return json_object();
}

void appModuleBrowserFromJson(json_t *root) {
// TODO
}


} // namespace rack

+ 2
- 2
src/engine.cpp View File

@@ -92,14 +92,14 @@ static void engineStep() {
// Step ports
for (Input &input : module->inputs) {
if (input.active) {
float value = input.value / 10.0;
float value = input.value / 10.f;
input.plugLights[0].setBrightnessSmooth(value);
input.plugLights[1].setBrightnessSmooth(-value);
}
}
for (Output &output : module->outputs) {
if (output.active) {
float value = output.value / 10.0;
float value = output.value / 10.f;
output.plugLights[0].setBrightnessSmooth(value);
output.plugLights[1].setBrightnessSmooth(-value);
}


+ 11
- 1
src/ui/Label.cpp View File

@@ -3,8 +3,18 @@

namespace rack {



void Label::draw(NVGcontext *vg) {
bndLabel(vg, 0.0, 0.0, box.size.x, box.size.y, -1, text.c_str());
float x = 0.0;
if (align == RIGHT_ALIGN) {
x = box.size.x - bndLabelWidth(vg, -1, text.c_str());
}
else if (align == CENTER_ALIGN) {
x = (box.size.x - bndLabelWidth(vg, -1, text.c_str())) / 2.0;
}

bndLabel(vg, x, 0.0, box.size.x, box.size.y, -1, text.c_str());
}




+ 1
- 1
src/ui/List.cpp View File

@@ -16,7 +16,7 @@ void List::step() {
child->box.pos = Vec(0.0, box.size.y);
box.size.y += child->box.size.y;
// Resize width of child
child->box.size.x = box.size.x - BND_SCROLLBAR_WIDTH;
child->box.size.x = box.size.x;
}
}



+ 12
- 3
src/ui/ScrollWidget.cpp View File

@@ -5,6 +5,9 @@
namespace rack {


static const float SCROLL_SPEED = 1.2;


/** Parent must be a ScrollWidget */
struct ScrollBar : OpaqueWidget {
enum Orientation {
@@ -33,9 +36,9 @@ struct ScrollBar : OpaqueWidget {
ScrollWidget *scrollWidget = dynamic_cast<ScrollWidget*>(parent);
assert(scrollWidget);
if (orientation == HORIZONTAL)
scrollWidget->offset.x += e.mouseRel.x;
scrollWidget->offset.x += SCROLL_SPEED * e.mouseRel.x;
else
scrollWidget->offset.y += e.mouseRel.y;
scrollWidget->offset.y += SCROLL_SPEED * e.mouseRel.y;
}

void onDragEnd(EventDragEnd &e) override {
@@ -69,7 +72,13 @@ void ScrollWidget::draw(NVGcontext *vg) {
void ScrollWidget::step() {
// Clamp scroll offset
Vec containerCorner = container->getChildrenBoundingBox().getBottomRight();
offset = offset.clamp(Rect(Vec(0, 0), containerCorner.minus(box.size)));
Rect containerBox = Rect(Vec(0, 0), containerCorner.minus(box.size));
offset = offset.clamp(containerBox);
// Lock offset to top/left if no scrollbar will display
if (containerBox.size.x < 0.0)
offset.x = 0.0;
if (containerBox.size.y < 0.0)
offset.y = 0.0;

// Update the container's positions from the offset
container->box.pos = offset.neg().round();


Loading…
Cancel
Save