@@ -296,18 +296,15 @@ struct Rect { | |||||
Rect zeroPos() const { | Rect zeroPos() const { | ||||
return Rect(Vec(), size); | return Rect(Vec(), size); | ||||
} | } | ||||
/** Expands each corner | |||||
Use a negative delta to shrink. | |||||
*/ | |||||
Rect grow(Vec delta) const { | Rect grow(Vec delta) const { | ||||
Rect r; | Rect r; | ||||
r.pos = pos.minus(delta); | r.pos = pos.minus(delta); | ||||
r.size = size.plus(delta.mult(2.f)); | r.size = size.plus(delta.mult(2.f)); | ||||
return r; | return r; | ||||
} | } | ||||
Rect shrink(Vec delta) const { | |||||
Rect r; | |||||
r.pos = pos.plus(delta); | |||||
r.size = size.minus(delta.mult(2.f)); | |||||
return r; | |||||
} | |||||
}; | }; | ||||
@@ -0,0 +1,35 @@ | |||||
#pragma once | |||||
#include "widgets/Widget.hpp" | |||||
namespace rack { | |||||
/** Widget that consumes recursing events without giving a chance for children to consume. | |||||
*/ | |||||
struct ObstructWidget : virtual Widget { | |||||
void onHover(const event::Hover &e) override { | |||||
e.consume(this); | |||||
} | |||||
void onButton(const event::Button &e) override { | |||||
e.consume(this); | |||||
} | |||||
void onHoverKey(const event::HoverKey &e) override { | |||||
e.consume(this); | |||||
} | |||||
void onHoverText(const event::HoverText &e) override { | |||||
e.consume(this); | |||||
} | |||||
void onHoverScroll(const event::HoverScroll &e) override { | |||||
e.consume(this); | |||||
} | |||||
void onDragHover(const event::DragHover &e) override { | |||||
e.consume(this); | |||||
} | |||||
void onPathDrop(const event::PathDrop &e) override { | |||||
e.consume(this); | |||||
} | |||||
}; | |||||
} // namespace rack |
@@ -22,11 +22,12 @@ struct ZoomWidget : virtual Widget { | |||||
} | } | ||||
void setZoom(float zoom) { | void setZoom(float zoom) { | ||||
if (zoom != this->zoom) { | |||||
event::Zoom eZoom; | |||||
Widget::onZoom(eZoom); | |||||
} | |||||
this->zoom = zoom; | this->zoom = zoom; | ||||
event::Context eZoomContext; | |||||
event::Zoom eZoom; | |||||
eZoom.context = &eZoomContext; | |||||
Widget::onZoom(eZoom); | |||||
} | } | ||||
void draw(NVGcontext *vg) override { | void draw(NVGcontext *vg) override { | ||||
@@ -276,7 +276,8 @@ struct AudioInterfaceWidget : ModuleWidget { | |||||
AudioWidget *audioWidget = createWidget<AudioWidget>(mm2px(Vec(3.2122073, 14.837339))); | AudioWidget *audioWidget = createWidget<AudioWidget>(mm2px(Vec(3.2122073, 14.837339))); | ||||
audioWidget->box.size = mm2px(Vec(44, 28)); | audioWidget->box.size = mm2px(Vec(44, 28)); | ||||
audioWidget->audioIO = &module->audioIO; | |||||
if (module) | |||||
audioWidget->audioIO = &module->audioIO; | |||||
addChild(audioWidget); | addChild(audioWidget); | ||||
} | } | ||||
}; | }; | ||||
@@ -119,6 +119,10 @@ struct MidiCcChoice : GridChoice { | |||||
} | } | ||||
void step() override { | void step() override { | ||||
if (!module) { | |||||
text = ""; | |||||
return; | |||||
} | |||||
if (module->learningId == id) { | if (module->learningId == id) { | ||||
if (0 <= focusCc) | if (0 <= focusCc) | ||||
text = string::f("%d", focusCc); | text = string::f("%d", focusCc); | ||||
@@ -136,11 +140,15 @@ struct MidiCcChoice : GridChoice { | |||||
void onSelect(const event::Select &e) override { | void onSelect(const event::Select &e) override { | ||||
e.consume(this); | e.consume(this); | ||||
if (!module) | |||||
return; | |||||
module->learningId = id; | module->learningId = id; | ||||
focusCc = -1; | focusCc = -1; | ||||
} | } | ||||
void onDeselect(const event::Deselect &e) override { | void onDeselect(const event::Deselect &e) override { | ||||
if (!module) | |||||
return; | |||||
if (0 <= focusCc && focusCc < 128) { | if (0 <= focusCc && focusCc < 128) { | ||||
module->learnedCcs[id] = focusCc; | module->learnedCcs[id] = focusCc; | ||||
} | } | ||||
@@ -159,7 +167,7 @@ struct MidiCcChoice : GridChoice { | |||||
void onSelectKey(const event::SelectKey &e) override { | void onSelectKey(const event::SelectKey &e) override { | ||||
if (context()->event->selectedWidget == this) { | if (context()->event->selectedWidget == this) { | ||||
if (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER) { | |||||
if (e.action == GLFW_PRESS && (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER)) { | |||||
event::Deselect eDeselect; | event::Deselect eDeselect; | ||||
onDeselect(eDeselect); | onDeselect(eDeselect); | ||||
context()->event->selectedWidget = NULL; | context()->event->selectedWidget = NULL; | ||||
@@ -209,7 +217,8 @@ struct MIDICCToCVInterfaceWidget : ModuleWidget { | |||||
MidiCcWidget *midiWidget = createWidget<MidiCcWidget>(mm2px(Vec(3.399621, 14.837339))); | MidiCcWidget *midiWidget = createWidget<MidiCcWidget>(mm2px(Vec(3.399621, 14.837339))); | ||||
midiWidget->module = module; | midiWidget->module = module; | ||||
midiWidget->box.size = mm2px(Vec(44, 54.667)); | midiWidget->box.size = mm2px(Vec(44, 54.667)); | ||||
midiWidget->midiIO = &module->midiInput; | |||||
if (module) | |||||
midiWidget->midiIO = &module->midiInput; | |||||
midiWidget->createGridChoices(); | midiWidget->createGridChoices(); | ||||
addChild(midiWidget); | addChild(midiWidget); | ||||
} | } | ||||
@@ -283,7 +283,8 @@ struct MIDIToCVInterfaceWidget : ModuleWidget { | |||||
MidiWidget *midiWidget = createWidget<MidiWidget>(mm2px(Vec(3.41891, 14.8373))); | MidiWidget *midiWidget = createWidget<MidiWidget>(mm2px(Vec(3.41891, 14.8373))); | ||||
midiWidget->box.size = mm2px(Vec(33.840, 28)); | midiWidget->box.size = mm2px(Vec(33.840, 28)); | ||||
midiWidget->midiIO = &module->midiInput; | |||||
if (module) | |||||
midiWidget->midiIO = &module->midiInput; | |||||
addChild(midiWidget); | addChild(midiWidget); | ||||
} | } | ||||
@@ -159,6 +159,8 @@ struct MidiTrigChoice : GridChoice { | |||||
} | } | ||||
void step() override { | void step() override { | ||||
if (!module) | |||||
return; | |||||
if (module->learningId == id) { | if (module->learningId == id) { | ||||
text = "LRN"; | text = "LRN"; | ||||
color.a = 0.5; | color.a = 0.5; | ||||
@@ -180,10 +182,14 @@ struct MidiTrigChoice : GridChoice { | |||||
void onSelect(const event::Select &e) override { | void onSelect(const event::Select &e) override { | ||||
e.consume(this); | e.consume(this); | ||||
if (!module) | |||||
return; | |||||
module->learningId = id; | module->learningId = id; | ||||
} | } | ||||
void onDeselect(const event::Deselect &e) override { | void onDeselect(const event::Deselect &e) override { | ||||
if (!module) | |||||
return; | |||||
module->learningId = -1; | module->learningId = -1; | ||||
} | } | ||||
}; | }; | ||||
@@ -228,7 +234,8 @@ struct MIDITriggerToCVInterfaceWidget : ModuleWidget { | |||||
MidiTrigWidget *midiWidget = createWidget<MidiTrigWidget>(mm2px(Vec(3.399621, 14.837339))); | MidiTrigWidget *midiWidget = createWidget<MidiTrigWidget>(mm2px(Vec(3.399621, 14.837339))); | ||||
midiWidget->module = module; | midiWidget->module = module; | ||||
midiWidget->box.size = mm2px(Vec(44, 54.667)); | midiWidget->box.size = mm2px(Vec(44, 54.667)); | ||||
midiWidget->midiIO = &module->midiInput; | |||||
if (module) | |||||
midiWidget->midiIO = &module->midiInput; | |||||
midiWidget->createGridChoices(); | midiWidget->createGridChoices(); | ||||
addChild(midiWidget); | addChild(midiWidget); | ||||
} | } | ||||
@@ -329,7 +329,8 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { | |||||
MidiWidget *midiWidget = createWidget<MidiWidget>(mm2px(Vec(3.4009969, 14.837336))); | MidiWidget *midiWidget = createWidget<MidiWidget>(mm2px(Vec(3.4009969, 14.837336))); | ||||
midiWidget->box.size = mm2px(Vec(44, 28)); | midiWidget->box.size = mm2px(Vec(44, 28)); | ||||
midiWidget->midiIO = &module->midiInput; | |||||
if (module) | |||||
midiWidget->midiIO = &module->midiInput; | |||||
addChild(midiWidget); | addChild(midiWidget); | ||||
} | } | ||||
@@ -17,6 +17,9 @@ struct AudioDriverItem : MenuItem { | |||||
struct AudioDriverChoice : LedDisplayChoice { | struct AudioDriverChoice : LedDisplayChoice { | ||||
AudioWidget *audioWidget; | AudioWidget *audioWidget; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!audioWidget->audioIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("Audio driver")); | menu->addChild(createMenuLabel("Audio driver")); | ||||
for (int driver : audioWidget->audioIO->getDrivers()) { | for (int driver : audioWidget->audioIO->getDrivers()) { | ||||
@@ -29,7 +32,10 @@ struct AudioDriverChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
text = audioWidget->audioIO->getDriverName(audioWidget->audioIO->driver); | |||||
if (audioWidget->audioIO) | |||||
text = audioWidget->audioIO->getDriverName(audioWidget->audioIO->driver); | |||||
else | |||||
text = ""; | |||||
} | } | ||||
}; | }; | ||||
@@ -49,6 +55,9 @@ struct AudioDeviceChoice : LedDisplayChoice { | |||||
int maxTotalChannels = 128; | int maxTotalChannels = 128; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!audioWidget->audioIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("Audio device")); | menu->addChild(createMenuLabel("Audio device")); | ||||
int deviceCount = audioWidget->audioIO->getDeviceCount(); | int deviceCount = audioWidget->audioIO->getDeviceCount(); | ||||
@@ -74,6 +83,10 @@ struct AudioDeviceChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
if (!audioWidget->audioIO) { | |||||
text = ""; | |||||
return; | |||||
} | |||||
text = audioWidget->audioIO->getDeviceDetail(audioWidget->audioIO->device, audioWidget->audioIO->offset); | text = audioWidget->audioIO->getDeviceDetail(audioWidget->audioIO->device, audioWidget->audioIO->offset); | ||||
if (text.empty()) { | if (text.empty()) { | ||||
text = "(No device)"; | text = "(No device)"; | ||||
@@ -97,6 +110,9 @@ struct AudioSampleRateItem : MenuItem { | |||||
struct AudioSampleRateChoice : LedDisplayChoice { | struct AudioSampleRateChoice : LedDisplayChoice { | ||||
AudioWidget *audioWidget; | AudioWidget *audioWidget; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!audioWidget->audioIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("Sample rate")); | menu->addChild(createMenuLabel("Sample rate")); | ||||
std::vector<int> sampleRates = audioWidget->audioIO->getSampleRates(); | std::vector<int> sampleRates = audioWidget->audioIO->getSampleRates(); | ||||
@@ -113,7 +129,10 @@ struct AudioSampleRateChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
text = string::f("%g kHz", audioWidget->audioIO->sampleRate / 1000.f); | |||||
if (audioWidget->audioIO) | |||||
text = string::f("%g kHz", audioWidget->audioIO->sampleRate / 1000.f); | |||||
else | |||||
text = ""; | |||||
} | } | ||||
}; | }; | ||||
@@ -129,6 +148,9 @@ struct AudioBlockSizeItem : MenuItem { | |||||
struct AudioBlockSizeChoice : LedDisplayChoice { | struct AudioBlockSizeChoice : LedDisplayChoice { | ||||
AudioWidget *audioWidget; | AudioWidget *audioWidget; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!audioWidget->audioIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("Block size")); | menu->addChild(createMenuLabel("Block size")); | ||||
std::vector<int> blockSizes = audioWidget->audioIO->getBlockSizes(); | std::vector<int> blockSizes = audioWidget->audioIO->getBlockSizes(); | ||||
@@ -146,7 +168,10 @@ struct AudioBlockSizeChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
text = string::f("%d", audioWidget->audioIO->blockSize); | |||||
if (audioWidget->audioIO) | |||||
text = string::f("%d", audioWidget->audioIO->blockSize); | |||||
else | |||||
text = ""; | |||||
} | } | ||||
}; | }; | ||||
@@ -17,6 +17,9 @@ struct MidiDriverItem : MenuItem { | |||||
struct MidiDriverChoice : LedDisplayChoice { | struct MidiDriverChoice : LedDisplayChoice { | ||||
MidiWidget *midiWidget; | MidiWidget *midiWidget; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!midiWidget->midiIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("MIDI driver")); | menu->addChild(createMenuLabel("MIDI driver")); | ||||
for (int driverId : midiWidget->midiIO->getDriverIds()) { | for (int driverId : midiWidget->midiIO->getDriverIds()) { | ||||
@@ -29,6 +32,10 @@ struct MidiDriverChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
if (!midiWidget->midiIO) { | |||||
text = ""; | |||||
return; | |||||
} | |||||
text = midiWidget->midiIO->getDriverName(midiWidget->midiIO->driverId); | text = midiWidget->midiIO->getDriverName(midiWidget->midiIO->driverId); | ||||
if (text.empty()) { | if (text.empty()) { | ||||
text = "(No driver)"; | text = "(No driver)"; | ||||
@@ -51,6 +58,9 @@ struct MidiDeviceItem : MenuItem { | |||||
struct MidiDeviceChoice : LedDisplayChoice { | struct MidiDeviceChoice : LedDisplayChoice { | ||||
MidiWidget *midiWidget; | MidiWidget *midiWidget; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!midiWidget->midiIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("MIDI device")); | menu->addChild(createMenuLabel("MIDI device")); | ||||
{ | { | ||||
@@ -71,6 +81,10 @@ struct MidiDeviceChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
if (!midiWidget->midiIO) { | |||||
text = ""; | |||||
return; | |||||
} | |||||
text = midiWidget->midiIO->getDeviceName(midiWidget->midiIO->deviceId); | text = midiWidget->midiIO->getDeviceName(midiWidget->midiIO->deviceId); | ||||
if (text.empty()) { | if (text.empty()) { | ||||
text = "(No device)"; | text = "(No device)"; | ||||
@@ -93,6 +107,9 @@ struct MidiChannelItem : MenuItem { | |||||
struct MidiChannelChoice : LedDisplayChoice { | struct MidiChannelChoice : LedDisplayChoice { | ||||
MidiWidget *midiWidget; | MidiWidget *midiWidget; | ||||
void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
if (!midiWidget->midiIO) | |||||
return; | |||||
Menu *menu = createMenu(); | Menu *menu = createMenu(); | ||||
menu->addChild(createMenuLabel("MIDI channel")); | menu->addChild(createMenuLabel("MIDI channel")); | ||||
for (int channel = -1; channel < 16; channel++) { | for (int channel = -1; channel < 16; channel++) { | ||||
@@ -105,7 +122,10 @@ struct MidiChannelChoice : LedDisplayChoice { | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
text = midiWidget->midiIO->getChannelName(midiWidget->midiIO->channel); | |||||
if (midiWidget->midiIO) | |||||
text = midiWidget->midiIO->getChannelName(midiWidget->midiIO->channel); | |||||
else | |||||
text = ""; | |||||
} | } | ||||
}; | }; | ||||
@@ -10,13 +10,12 @@ | |||||
#include "app/Scene.hpp" | #include "app/Scene.hpp" | ||||
#include "ui/List.hpp" | #include "ui/List.hpp" | ||||
#include "ui/TextField.hpp" | #include "ui/TextField.hpp" | ||||
#include "widgets/ObstructWidget.hpp" | |||||
#include "widgets/ZoomWidget.hpp" | |||||
#include "plugin.hpp" | #include "plugin.hpp" | ||||
#include "context.hpp" | #include "context.hpp" | ||||
static const float itemMargin = 2.0; | |||||
namespace rack { | namespace rack { | ||||
@@ -26,533 +25,46 @@ static std::string sTagFilter; | |||||
bool isMatch(std::string s, std::string search) { | |||||
s = string::lowercase(s); | |||||
search = string::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->plugin->author; | |||||
s += " "; | |||||
s += model->name; | |||||
s += " "; | |||||
s += model->slug; | |||||
for (std::string tag : model->tags) { | |||||
std::string allowedTag = plugin::getAllowedTag(tag); | |||||
if (!allowedTag.empty()) { | |||||
s += " "; | |||||
s += allowedTag; | |||||
} | |||||
} | |||||
return isMatch(s, search); | |||||
} | |||||
struct FavoriteQuantity : Quantity { | |||||
std::string getString() override { | |||||
return "★"; | |||||
} | |||||
}; | |||||
struct FavoriteRadioButton : RadioButton { | |||||
Model *model = NULL; | |||||
FavoriteRadioButton() { | |||||
quantity = new FavoriteQuantity; | |||||
} | |||||
void onAction(const event::Action &e) override; | |||||
}; | |||||
struct SeparatorItem : OpaqueWidget { | |||||
SeparatorItem() { | |||||
box.size.y = 2*BND_WIDGET_HEIGHT + 2*itemMargin; | |||||
} | |||||
void setText(std::string text) { | |||||
clearChildren(); | |||||
Label *label = createWidget<Label>(math::Vec(0, 12 + itemMargin)); | |||||
label->text = text; | |||||
label->fontSize = 20; | |||||
label->color.a *= 0.5; | |||||
addChild(label); | |||||
} | |||||
}; | |||||
struct BrowserListItem : OpaqueWidget { | |||||
bool selected = false; | |||||
BrowserListItem() { | |||||
box.size.y = BND_WIDGET_HEIGHT + 2*itemMargin; | |||||
} | |||||
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 onDragStart(const event::DragStart &e) override; | |||||
void onDragDrop(const event::DragDrop &e) override { | |||||
if (e.origin != this) | |||||
return; | |||||
doAction(); | |||||
} | |||||
void doAction() { | |||||
event::Context eActionContext; | |||||
event::Action eAction; | |||||
eAction.context = &eActionContext; | |||||
eAction.consume(this); | |||||
onAction(eAction); | |||||
if (eActionContext.consumed) { | |||||
MenuOverlay *overlay = getAncestorOfType<MenuOverlay>(); | |||||
overlay->requestedDelete = true; | |||||
} | |||||
} | |||||
}; | |||||
struct ModelItem : BrowserListItem { | |||||
Model *model; | |||||
Label *pluginLabel = NULL; | |||||
void setModel(Model *model) { | |||||
clearChildren(); | |||||
assert(model); | |||||
this->model = model; | |||||
FavoriteRadioButton *favoriteButton = createWidget<FavoriteRadioButton>(math::Vec(8, itemMargin)); | |||||
favoriteButton->box.size.x = 20; | |||||
addChild(favoriteButton); | |||||
// Set favorite button initial state | |||||
auto it = sFavoriteModels.find(model); | |||||
if (it != sFavoriteModels.end()) | |||||
favoriteButton->quantity->setValue(1); | |||||
favoriteButton->model = model; | |||||
Label *nameLabel = createWidget<Label>(favoriteButton->box.getTopRight()); | |||||
nameLabel->text = model->name; | |||||
addChild(nameLabel); | |||||
pluginLabel = createWidget<Label>(math::Vec(0, itemMargin)); | |||||
pluginLabel->alignment = Label::RIGHT_ALIGNMENT; | |||||
pluginLabel->text = model->plugin->slug + " " + model->plugin->version; | |||||
pluginLabel->color.a = 0.5; | |||||
addChild(pluginLabel); | |||||
} | |||||
void step() override { | |||||
BrowserListItem::step(); | |||||
if (pluginLabel) | |||||
pluginLabel->box.size.x = box.size.x - BND_SCROLLBAR_WIDTH; | |||||
} | |||||
void onAction(const event::Action &e) override { | |||||
ModuleWidget *moduleWidget = model->createModuleWidget(); | |||||
if (!moduleWidget) | |||||
return; | |||||
context()->scene->rackWidget->addModule(moduleWidget); | |||||
// Move module nearest to the mouse position | |||||
moduleWidget->box.pos = context()->scene->rackWidget->lastMousePos.minus(moduleWidget->box.size.div(2)); | |||||
context()->scene->rackWidget->requestModuleBoxNearest(moduleWidget, moduleWidget->box); | |||||
} | |||||
}; | |||||
struct AuthorItem : BrowserListItem { | |||||
std::string author; | |||||
void setAuthor(std::string author) { | |||||
clearChildren(); | |||||
this->author = author; | |||||
Label *authorLabel = createWidget<Label>(math::Vec(0, 0 + itemMargin)); | |||||
if (author.empty()) | |||||
authorLabel->text = "Show all modules"; | |||||
else | |||||
authorLabel->text = author; | |||||
addChild(authorLabel); | |||||
} | |||||
void onAction(const event::Action &e) override; | |||||
}; | |||||
struct TagItem : BrowserListItem { | |||||
std::string tag; | |||||
void setTag(std::string tag) { | |||||
clearChildren(); | |||||
this->tag = tag; | |||||
Label *tagLabel = createWidget<Label>(math::Vec(0, 0 + itemMargin)); | |||||
if (tag.empty()) | |||||
tagLabel->text = "Show all tags"; | |||||
else | |||||
tagLabel->text = tag; | |||||
addChild(tagLabel); | |||||
} | |||||
void onAction(const event::Action &e) override; | |||||
}; | |||||
struct ClearFilterItem : BrowserListItem { | |||||
ClearFilterItem() { | |||||
Label *label = createWidget<Label>(math::Vec(0, 0 + itemMargin)); | |||||
label->text = "Back"; | |||||
addChild(label); | |||||
} | |||||
void onAction(const event::Action &e) override; | |||||
}; | |||||
struct BrowserList : List { | |||||
int selected = 0; | |||||
void step() override { | |||||
incrementSelection(0); | |||||
// Find and select item | |||||
int i = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
item->selected = (i == selected); | |||||
i++; | |||||
} | |||||
} | |||||
List::step(); | |||||
} | |||||
void incrementSelection(int delta) { | |||||
selected += delta; | |||||
selected = math::clamp(selected, 0, countItems() - 1); | |||||
} | |||||
int countItems() { | |||||
int n = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
n++; | |||||
} | |||||
} | |||||
return n; | |||||
} | |||||
void selectItem(Widget *w) { | |||||
int i = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
if (child == w) { | |||||
selected = i; | |||||
break; | |||||
} | |||||
i++; | |||||
} | |||||
} | |||||
} | |||||
BrowserListItem *getSelectedItem() { | |||||
int i = 0; | |||||
for (Widget *child : children) { | |||||
BrowserListItem *item = dynamic_cast<BrowserListItem*>(child); | |||||
if (item) { | |||||
if (i == selected) { | |||||
return item; | |||||
} | |||||
i++; | |||||
} | |||||
} | |||||
return NULL; | |||||
} | |||||
void scrollSelected() { | |||||
BrowserListItem *item = getSelectedItem(); | |||||
if (item) { | |||||
ScrollWidget *parentScroll = dynamic_cast<ScrollWidget*>(parent->parent); | |||||
if (parentScroll) | |||||
parentScroll->scrollTo(item->box); | |||||
} | |||||
} | |||||
}; | |||||
struct ModuleBrowser; | |||||
struct SearchModuleField : TextField { | |||||
ModuleBrowser *moduleBrowser; | |||||
void onChange(const event::Change &e) override; | |||||
void onSelectKey(const event::SelectKey &e) override; | |||||
struct ModuleWidgetWrapper : ObstructWidget { | |||||
}; | }; | ||||
struct ModuleBrowser : OpaqueWidget { | struct ModuleBrowser : OpaqueWidget { | ||||
SearchModuleField *searchField; | |||||
ScrollWidget *moduleScroll; | |||||
BrowserList *moduleList; | |||||
std::set<std::string, string::CaseInsensitiveCompare> availableAuthors; | |||||
std::set<std::string> availableTags; | |||||
ModuleBrowser() { | ModuleBrowser() { | ||||
box.size.x = 450; | |||||
sAuthorFilter = ""; | |||||
sTagFilter = ""; | |||||
// Search | |||||
searchField = new SearchModuleField; | |||||
searchField->box.size.x = box.size.x; | |||||
searchField->moduleBrowser = this; | |||||
addChild(searchField); | |||||
moduleList = new BrowserList; | |||||
moduleList->box.size = math::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); | |||||
// Collect authors | |||||
math::Vec p; | |||||
for (Plugin *plugin : plugin::plugins) { | for (Plugin *plugin : plugin::plugins) { | ||||
// Insert author | |||||
if (!plugin->author.empty()) | |||||
availableAuthors.insert(plugin->author); | |||||
for (Model *model : plugin->models) { | for (Model *model : plugin->models) { | ||||
// Insert tag | |||||
for (std::string tag : model->tags) { | |||||
std::string allowedTag = plugin::getAllowedTag(tag); | |||||
if (!allowedTag.empty()) | |||||
availableTags.insert(tag); | |||||
} | |||||
} | |||||
} | |||||
// Trigger search update | |||||
clearSearch(); | |||||
} | |||||
void draw(NVGcontext *vg) override { | |||||
bndMenuBackground(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE); | |||||
Widget::draw(vg); | |||||
} | |||||
void clearSearch() { | |||||
searchField->setText(""); | |||||
} | |||||
bool isModelFiltered(Model *model) { | |||||
if (!sAuthorFilter.empty() && model->plugin->author != sAuthorFilter) | |||||
return false; | |||||
if (!sTagFilter.empty()) { | |||||
// TODO filter tags | |||||
} | |||||
return true; | |||||
} | |||||
void refreshSearch() { | |||||
std::string search = searchField->text; | |||||
moduleList->clearChildren(); | |||||
moduleList->selected = 0; | |||||
bool filterPage = !(sAuthorFilter.empty() && sTagFilter.empty()); | |||||
if (!filterPage) { | |||||
// Favorites | |||||
if (!sFavoriteModels.empty()) { | |||||
SeparatorItem *item = new SeparatorItem; | |||||
item->setText("Favorites"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (Model *model : sFavoriteModels) { | |||||
if (isModelFiltered(model) && isModelMatch(model, search)) { | |||||
ModelItem *item = new ModelItem; | |||||
item->setModel(model); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
// Author items | |||||
{ | |||||
SeparatorItem *item = new SeparatorItem; | |||||
item->setText("Authors"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (std::string author : availableAuthors) { | |||||
if (isMatch(author, search)) { | |||||
AuthorItem *item = new AuthorItem; | |||||
item->setAuthor(author); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
// Tag items | |||||
{ | |||||
SeparatorItem *item = new SeparatorItem; | |||||
item->setText("Tags"); | |||||
moduleList->addChild(item); | |||||
} | |||||
for (std::string tag : availableTags) { | |||||
if (isMatch(tag, search)) { | |||||
TagItem *item = new TagItem; | |||||
item->setTag(tag); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
// Clear filter | |||||
ClearFilterItem *item = new ClearFilterItem; | |||||
moduleList->addChild(item); | |||||
} | |||||
if (filterPage || !search.empty()) { | |||||
if (!search.empty()) { | |||||
SeparatorItem *item = new SeparatorItem; | |||||
item->setText("Modules"); | |||||
moduleList->addChild(item); | |||||
} | |||||
else if (filterPage) { | |||||
SeparatorItem *item = new SeparatorItem; | |||||
if (!sAuthorFilter.empty()) | |||||
item->setText(sAuthorFilter); | |||||
else if (!sTagFilter.empty()) | |||||
item->setText("Tag: " + sTagFilter); | |||||
moduleList->addChild(item); | |||||
} | |||||
// Modules | |||||
for (Plugin *plugin : plugin::plugins) { | |||||
for (Model *model : plugin->models) { | |||||
if (isModelFiltered(model) && isModelMatch(model, search)) { | |||||
ModelItem *item = new ModelItem; | |||||
item->setModel(model); | |||||
moduleList->addChild(item); | |||||
} | |||||
} | |||||
ModuleWidgetWrapper *wrapper = new ModuleWidgetWrapper; | |||||
wrapper->box.pos = p; | |||||
addChild(wrapper); | |||||
ZoomWidget *zoomWidget = new ZoomWidget; | |||||
zoomWidget->setZoom(0.5); | |||||
wrapper->addChild(zoomWidget); | |||||
ModuleWidget *moduleWidget = model->createModuleWidgetNull(); | |||||
zoomWidget->addChild(moduleWidget); | |||||
wrapper->box.size = moduleWidget->box.size.mult(zoomWidget->zoom); | |||||
p = wrapper->box.getTopRight(); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
box.pos = parent->box.size.minus(box.size).div(2).round(); | |||||
box.pos.y = 60; | |||||
box.size.y = parent->box.size.y - 2 * box.pos.y; | |||||
moduleScroll->box.size.y = std::min(box.size.y - moduleScroll->box.pos.y, moduleList->box.size.y); | |||||
box.size.y = std::min(box.size.y, moduleScroll->box.getBottomRight().y); | |||||
context()->event->selectedWidget = searchField; | |||||
Widget::step(); | |||||
} | |||||
}; | |||||
assert(parent); | |||||
// Implementations of inline methods above | |||||
box = parent->box.zeroPos().grow(math::Vec(-100, -100)); | |||||
void AuthorItem::onAction(const event::Action &e) { | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
sAuthorFilter = author; | |||||
moduleBrowser->clearSearch(); | |||||
moduleBrowser->refreshSearch(); | |||||
e.consume(this); | |||||
} | |||||
void TagItem::onAction(const event::Action &e) { | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
sTagFilter = tag; | |||||
moduleBrowser->clearSearch(); | |||||
moduleBrowser->refreshSearch(); | |||||
e.consume(this); | |||||
} | |||||
void ClearFilterItem::onAction(const event::Action &e) { | |||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
sAuthorFilter = ""; | |||||
sTagFilter = ""; | |||||
moduleBrowser->refreshSearch(); | |||||
e.consume(this); | |||||
} | |||||
void FavoriteRadioButton::onAction(const event::Action &e) { | |||||
if (!model) | |||||
return; | |||||
if (quantity->isMax()) { | |||||
sFavoriteModels.insert(model); | |||||
} | |||||
else { | |||||
auto it = sFavoriteModels.find(model); | |||||
if (it != sFavoriteModels.end()) | |||||
sFavoriteModels.erase(it); | |||||
OpaqueWidget::step(); | |||||
} | } | ||||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
if (moduleBrowser) | |||||
moduleBrowser->refreshSearch(); | |||||
} | |||||
void BrowserListItem::onDragStart(const event::DragStart &e) { | |||||
BrowserList *list = dynamic_cast<BrowserList*>(parent); | |||||
if (list) { | |||||
list->selectItem(this); | |||||
void draw(NVGcontext *vg) override { | |||||
bndTooltipBackground(vg, 0.0, 0.0, box.size.x, box.size.y); | |||||
Widget::draw(vg); | |||||
} | } | ||||
} | |||||
void SearchModuleField::onChange(const event::Change &e) { | |||||
moduleBrowser->refreshSearch(); | |||||
} | |||||
}; | |||||
void SearchModuleField::onSelectKey(const event::SelectKey &e) { | |||||
if (e.action == GLFW_PRESS) { | |||||
switch (e.key) { | |||||
case GLFW_KEY_ESCAPE: { | |||||
MenuOverlay *overlay = getAncestorOfType<MenuOverlay>(); | |||||
overlay->requestedDelete = true; | |||||
e.consume(this); | |||||
return; | |||||
} break; | |||||
case GLFW_KEY_UP: { | |||||
moduleBrowser->moduleList->incrementSelection(-1); | |||||
moduleBrowser->moduleList->scrollSelected(); | |||||
e.consume(this); | |||||
} break; | |||||
case GLFW_KEY_DOWN: { | |||||
moduleBrowser->moduleList->incrementSelection(1); | |||||
moduleBrowser->moduleList->scrollSelected(); | |||||
e.consume(this); | |||||
} break; | |||||
case GLFW_KEY_PAGE_UP: { | |||||
moduleBrowser->moduleList->incrementSelection(-5); | |||||
moduleBrowser->moduleList->scrollSelected(); | |||||
e.consume(this); | |||||
} break; | |||||
case GLFW_KEY_PAGE_DOWN: { | |||||
moduleBrowser->moduleList->incrementSelection(5); | |||||
moduleBrowser->moduleList->scrollSelected(); | |||||
e.consume(this); | |||||
} break; | |||||
case GLFW_KEY_ENTER: { | |||||
BrowserListItem *item = moduleBrowser->moduleList->getSelectedItem(); | |||||
if (item) { | |||||
item->doAction(); | |||||
e.consume(this); | |||||
} | |||||
} break; | |||||
} | |||||
} | |||||
if (!e.getConsumed()) | |||||
TextField::onSelectKey(e); | |||||
} | |||||
// Global functions | // Global functions | ||||
@@ -5,7 +5,9 @@ namespace rack { | |||||
void ModuleLightWidget::step() { | void ModuleLightWidget::step() { | ||||
assert(module); | |||||
if (!module) | |||||
return; | |||||
assert(module->lights.size() >= firstLightId + baseColors.size()); | assert(module->lights.size() >= firstLightId + baseColors.size()); | ||||
std::vector<float> values(baseColors.size()); | std::vector<float> values(baseColors.size()); | ||||
@@ -14,6 +14,8 @@ void ParamQuantity::commitSnap() { | |||||
} | } | ||||
void ParamQuantity::setValue(float value) { | void ParamQuantity::setValue(float value) { | ||||
if (!module) | |||||
return; | |||||
value = math::clamp(value, getMinValue(), getMaxValue()); | value = math::clamp(value, getMinValue(), getMaxValue()); | ||||
// TODO Smooth | // TODO Smooth | ||||
// TODO Snap | // TODO Snap | ||||
@@ -21,22 +23,32 @@ void ParamQuantity::setValue(float value) { | |||||
} | } | ||||
float ParamQuantity::getValue() { | float ParamQuantity::getValue() { | ||||
if (!module) | |||||
return Quantity::getValue(); | |||||
return getParam()->value; | return getParam()->value; | ||||
} | } | ||||
float ParamQuantity::getMinValue() { | float ParamQuantity::getMinValue() { | ||||
if (!module) | |||||
return Quantity::getMinValue(); | |||||
return getParam()->minValue; | return getParam()->minValue; | ||||
} | } | ||||
float ParamQuantity::getMaxValue() { | float ParamQuantity::getMaxValue() { | ||||
if (!module) | |||||
return Quantity::getMaxValue(); | |||||
return getParam()->maxValue; | return getParam()->maxValue; | ||||
} | } | ||||
float ParamQuantity::getDefaultValue() { | float ParamQuantity::getDefaultValue() { | ||||
if (!module) | |||||
return Quantity::getDefaultValue(); | |||||
return getParam()->defaultValue; | return getParam()->defaultValue; | ||||
} | } | ||||
float ParamQuantity::getDisplayValue() { | float ParamQuantity::getDisplayValue() { | ||||
if (!module) | |||||
return Quantity::getDisplayValue(); | |||||
if (getParam()->displayBase == 0.f) { | if (getParam()->displayBase == 0.f) { | ||||
// Linear | // Linear | ||||
return getParam()->value * getParam()->displayMultiplier; | return getParam()->value * getParam()->displayMultiplier; | ||||
@@ -52,6 +64,8 @@ float ParamQuantity::getDisplayValue() { | |||||
} | } | ||||
void ParamQuantity::setDisplayValue(float displayValue) { | void ParamQuantity::setDisplayValue(float displayValue) { | ||||
if (!module) | |||||
return; | |||||
if (getParam()->displayBase == 0.f) { | if (getParam()->displayBase == 0.f) { | ||||
// Linear | // Linear | ||||
getParam()->value = displayValue / getParam()->displayMultiplier; | getParam()->value = displayValue / getParam()->displayMultiplier; | ||||
@@ -67,14 +81,20 @@ void ParamQuantity::setDisplayValue(float displayValue) { | |||||
} | } | ||||
int ParamQuantity::getDisplayPrecision() { | int ParamQuantity::getDisplayPrecision() { | ||||
if (!module) | |||||
return Quantity::getDisplayPrecision(); | |||||
return getParam()->displayPrecision; | return getParam()->displayPrecision; | ||||
} | } | ||||
std::string ParamQuantity::getLabel() { | std::string ParamQuantity::getLabel() { | ||||
if (!module) | |||||
return Quantity::getLabel(); | |||||
return getParam()->label; | return getParam()->label; | ||||
} | } | ||||
std::string ParamQuantity::getUnit() { | std::string ParamQuantity::getUnit() { | ||||
if (!module) | |||||
return Quantity::getUnit(); | |||||
return getParam()->unit; | return getParam()->unit; | ||||
} | } | ||||
@@ -29,6 +29,9 @@ PortWidget::~PortWidget() { | |||||
} | } | ||||
void PortWidget::step() { | void PortWidget::step() { | ||||
if (!module) | |||||
return; | |||||
std::vector<float> values(2); | std::vector<float> values(2); | ||||
if (type == INPUT) { | if (type == INPUT) { | ||||
values[0] = module->inputs[portId].plugLights[0].getBrightness(); | values[0] = module->inputs[portId].plugLights[0].getBrightness(); | ||||