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