diff --git a/include/app/ModuleWidget.hpp b/include/app/ModuleWidget.hpp index d21e9882..1c712b85 100644 --- a/include/app/ModuleWidget.hpp +++ b/include/app/ModuleWidget.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include namespace rack { @@ -54,12 +56,17 @@ struct ModuleWidget : widget::OpaqueWidget { ParamWidget* getParam(int paramId); PortWidget* getInput(int portId); PortWidget* getOutput(int portId); + /** Scans children widgets recursively for all ParamWidgets. */ + std::list getParams(); + std::list getPorts(); + std::list getInputs(); + std::list getOutputs(); void draw(const DrawArgs& args) override; void drawShadow(const DrawArgs& args); /** Override to add context menu entries to your subclass. - It is recommended to add a blank ui::MenuEntry first for spacing. + It is recommended to add a blank `ui::MenuSeparator` first for spacing. */ virtual void appendContextMenu(ui::Menu* menu) {} @@ -98,12 +105,14 @@ struct ModuleWidget : widget::OpaqueWidget { Called when the user clicks Randomize in the context menu. */ void randomizeAction(); + void appendDisconnectActions(history::ComplexAction* complexAction); void disconnectAction(); void cloneAction(); void bypassAction(); /** Deletes `this` */ void removeAction(); void createContextMenu(); + void createSelectionContextMenu(); INTERNAL math::Vec& dragOffset(); INTERNAL bool& dragEnabled(); diff --git a/include/app/RackWidget.hpp b/include/app/RackWidget.hpp index 9698c416..c2beee26 100644 --- a/include/app/RackWidget.hpp +++ b/include/app/RackWidget.hpp @@ -52,12 +52,12 @@ struct RackWidget : widget::OpaqueWidget { // Module methods - /** Adds a module and adds it to the Engine - Ownership rules work like add/removeChild() + /** Adds a module and adds it to the Engine, adopting ownership. */ void addModule(ModuleWidget* mw); void addModuleAtMouse(ModuleWidget* mw); - /** Removes the module and transfers ownership to the caller */ + /** Removes the module and transfers ownership to the caller. + */ void removeModule(ModuleWidget* mw); /** Sets a module's box if non-colliding. Returns true if set */ bool requestModulePos(ModuleWidget* mw, math::Vec pos); @@ -65,21 +65,31 @@ struct RackWidget : widget::OpaqueWidget { void setModulePosNearest(ModuleWidget* mw, math::Vec pos); void setModulePosForce(ModuleWidget* mw, math::Vec pos); ModuleWidget* getModule(int64_t moduleId); - bool isEmpty(); + std::list getModules(); + bool hasModules(); void updateModuleOldPositions(); history::ComplexAction* getModuleDragAction(); + void updateModuleSelections(); + int getNumSelectedModules(); + std::list getSelectedModules(); + void resetSelectedModulesAction(); + void randomizeSelectedModulesAction(); + void disconnectSelectedModulesAction(); + void bypassSelectedModulesAction(); + void deleteSelectedModulesAction(); // Cable methods void clearCables(); void clearCablesAction(); - /** Removes all complete cables connected to the port */ + /** Removes all cables connected to the port */ void clearCablesOnPort(PortWidget* port); - /** Adds a complete cable. - Ownership rules work like add/removeChild() + /** Adds a complete cable and adopts ownership. */ void addCable(CableWidget* cw); + /** Removes cable and releases ownership to caller. + */ void removeCable(CableWidget* cw); /** Takes ownership of `cw` and adds it as a child if it isn't already. */ void setIncompleteCable(CableWidget* cw); @@ -87,6 +97,7 @@ struct RackWidget : widget::OpaqueWidget { /** Returns the most recently added complete cable connected to the given Port, i.e. the top of the stack. */ CableWidget* getTopCable(PortWidget* port); CableWidget* getCable(int64_t cableId); + std::list getCompleteCables(); /** Returns all cables attached to port, complete or not. */ std::list getCablesOnPort(PortWidget* port); }; diff --git a/src/app/ModuleWidget.cpp b/src/app/ModuleWidget.cpp index 8e6db8d8..39d8fc99 100644 --- a/src/app/ModuleWidget.cpp +++ b/src/app/ModuleWidget.cpp @@ -159,6 +159,51 @@ PortWidget* ModuleWidget::getOutput(int portId) { }); } +template +void doIfTypeRecursive(widget::Widget* w, F f) { + T* t = dynamic_cast(w); + if (t) + f(t); + + for (widget::Widget* child : w->children) { + doIfTypeRecursive(child, f); + } +} + +std::list ModuleWidget::getParams() { + std::list pws; + doIfTypeRecursive(this, [&](ParamWidget* pw) { + pws.push_back(pw); + }); + return pws; +} + +std::list ModuleWidget::getPorts() { + std::list pws; + doIfTypeRecursive(this, [&](PortWidget* pw) { + pws.push_back(pw); + }); + return pws; +} + +std::list ModuleWidget::getInputs() { + std::list pws; + doIfTypeRecursive(this, [&](PortWidget* pw) { + if (pw->type == engine::Port::INPUT) + pws.push_back(pw); + }); + return pws; +} + +std::list ModuleWidget::getOutputs() { + std::list pws; + doIfTypeRecursive(this, [&](PortWidget* pw) { + if (pw->type == engine::Port::OUTPUT) + pws.push_back(pw); + }); + return pws; +} + void ModuleWidget::draw(const DrawArgs& args) { nvgScissor(args.vg, RECT_ARGS(args.clipBox)); @@ -325,7 +370,12 @@ void ModuleWidget::onButton(const ButtonEvent& e) { if (!e.isConsumed()) { // Open context menu on right-click if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) { - createContextMenu(); + if (internal->selected) { + createSelectionContextMenu(); + } + else { + createContextMenu(); + } e.consume(this); } } @@ -615,21 +665,10 @@ void ModuleWidget::saveDialog() { save(path); } -template -void doOfType(widget::Widget* w, F f) { - T* t = dynamic_cast(w); - if (t) - f(t); - - for (widget::Widget* child : w->children) { - doOfType(child, f); - } -} - void ModuleWidget::disconnect() { - doOfType(this, [&](PortWidget* pw) { + for (PortWidget* pw : getPorts()) { APP->scene->rack->clearCablesOnPort(pw); - }); + } } void ModuleWidget::resetAction() { @@ -662,30 +701,31 @@ void ModuleWidget::randomizeAction() { APP->history->push(h); } -static void disconnectActions(ModuleWidget* mw, history::ComplexAction* complexAction) { - // Add CableRemove action for all cables - doOfType(mw, [&](PortWidget* pw) { +void ModuleWidget::appendDisconnectActions(history::ComplexAction* complexAction) { + for (PortWidget* pw : getPorts()) { for (CableWidget* cw : APP->scene->rack->getCablesOnPort(pw)) { if (!cw->isComplete()) continue; - // Avoid creating duplicate actions for self-patched cables - if (pw->type == engine::Port::INPUT && cw->outputPort->module == mw->module) - continue; // history::CableRemove history::CableRemove* h = new history::CableRemove; h->setCable(cw); complexAction->push(h); + // Delete cable + APP->scene->rack->removeCable(cw); + delete cw; } - }); + }; } void ModuleWidget::disconnectAction() { history::ComplexAction* complexAction = new history::ComplexAction; complexAction->name = "disconnect cables"; - disconnectActions(this, complexAction); - APP->history->push(complexAction); + appendDisconnectActions(complexAction); - disconnect(); + if (!complexAction->isEmpty()) + APP->history->push(complexAction); + else + delete complexAction; } void ModuleWidget::cloneAction() { @@ -722,11 +762,8 @@ void ModuleWidget::cloneAction() { h->push(hma); // Clone cables attached to input ports - doOfType(this, [&](PortWidget* pw) { - if (pw->type != engine::Port::INPUT) - return; - std::list cables = APP->scene->rack->getCablesOnPort(pw); - for (CableWidget* cw : cables) { + for (PortWidget* pw : getInputs()) { + for (CableWidget* cw : APP->scene->rack->getCablesOnPort(pw)) { // Create cable attached to cloned ModuleWidget's input engine::Cable* clonedCable = new engine::Cable; clonedCable->id = -1; @@ -750,7 +787,7 @@ void ModuleWidget::cloneAction() { hca->setCable(clonedCw); h->push(hca); } - }); + } APP->history->push(h); } @@ -769,7 +806,7 @@ void ModuleWidget::bypassAction() { void ModuleWidget::removeAction() { history::ComplexAction* complexAction = new history::ComplexAction; complexAction->name = "remove module"; - disconnectActions(this, complexAction); + appendDisconnectActions(complexAction); // history::ModuleRemove history::ModuleRemove* moduleRemove = new history::ModuleRemove; @@ -778,7 +815,7 @@ void ModuleWidget::removeAction() { APP->history->push(complexAction); - // This disconnects cables, removes the module, and transfers ownership to caller + // This removes the module and transfers ownership to caller APP->scene->rack->removeModule(this); delete this; } @@ -1018,6 +1055,38 @@ void ModuleWidget::createContextMenu() { appendContextMenu(menu); } +void ModuleWidget::createSelectionContextMenu() { + ui::Menu* menu = createMenu(); + + int n = APP->scene->rack->getNumSelectedModules(); + menu->addChild(createMenuLabel(string::f("%d selected %s", n, n == 1 ? "module" : "modules"))); + + // Initialize + menu->addChild(createMenuItem("Initialize", "", [=]() { + APP->scene->rack->resetSelectedModulesAction(); + })); + + // Randomize + menu->addChild(createMenuItem("Randomize", "", [=]() { + APP->scene->rack->randomizeSelectedModulesAction(); + })); + + // Disconnect cables + menu->addChild(createMenuItem("Disconnect cables", "", [=]() { + APP->scene->rack->disconnectSelectedModulesAction(); + })); + + // Bypass + menu->addChild(createMenuItem("Bypass", "", [=]() { + APP->scene->rack->bypassSelectedModulesAction(); + })); + + // Delete + menu->addChild(createMenuItem("Delete", "", [=]() { + APP->scene->rack->deleteSelectedModulesAction(); + })); +} + math::Vec& ModuleWidget::dragOffset() { return internal->dragOffset; } diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index 8ae57f0a..b9fd9eef 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -612,7 +612,17 @@ ModuleWidget* RackWidget::getModule(int64_t moduleId) { return NULL; } -bool RackWidget::isEmpty() { +std::list RackWidget::getModules() { + std::list mws; + for (widget::Widget* w : internal->moduleContainer->children) { + ModuleWidget* mw = dynamic_cast(w); + assert(mw); + mws.push_back(mw); + } + return mws; +} + +bool RackWidget::hasModules() { return internal->moduleContainer->children.empty(); } @@ -659,6 +669,107 @@ void RackWidget::updateModuleSelections() { } } +int RackWidget::getNumSelectedModules() { + int count = 0; + for (widget::Widget* w : internal->moduleContainer->children) { + ModuleWidget* mw = dynamic_cast(w); + assert(mw); + if (mw->selected()) + count++; + } + return count; +} + +std::list RackWidget::getSelectedModules() { + std::list mws; + for (widget::Widget* w : internal->moduleContainer->children) { + ModuleWidget* mw = dynamic_cast(w); + assert(mw); + if (mw->selected()) + mws.push_back(mw); + } + return mws; +} + +void RackWidget::resetSelectedModulesAction() { + history::ComplexAction* complexAction = new history::ComplexAction; + complexAction->name = "reset modules"; + + for (ModuleWidget* mw : getSelectedModules()) { + assert(mw->module); + + // history::ModuleChange + history::ModuleChange* h = new history::ModuleChange; + h->moduleId = mw->module->id; + h->oldModuleJ = mw->toJson(); + + APP->engine->resetModule(mw->module); + + h->newModuleJ = mw->toJson(); + complexAction->push(h); + } + + APP->history->push(complexAction); +} + +void RackWidget::randomizeSelectedModulesAction() { + history::ComplexAction* complexAction = new history::ComplexAction; + complexAction->name = "randomize modules"; + + for (ModuleWidget* mw : getSelectedModules()) { + assert(mw->module); + + // history::ModuleChange + history::ModuleChange* h = new history::ModuleChange; + h->moduleId = mw->module->id; + h->oldModuleJ = mw->toJson(); + + APP->engine->randomizeModule(mw->module); + + h->newModuleJ = mw->toJson(); + complexAction->push(h); + } + + APP->history->push(complexAction); +} + +void RackWidget::disconnectSelectedModulesAction() { + history::ComplexAction* complexAction = new history::ComplexAction; + complexAction->name = "disconnect cables"; + + for (ModuleWidget* mw : getSelectedModules()) { + mw->appendDisconnectActions(complexAction); + } + + if (!complexAction->isEmpty()) + APP->history->push(complexAction); + else + delete complexAction; +} + +void RackWidget::bypassSelectedModulesAction() { + // TODO +} + +void RackWidget::deleteSelectedModulesAction() { + history::ComplexAction* complexAction = new history::ComplexAction; + complexAction->name = "remove modules"; + + for (ModuleWidget* mw : getSelectedModules()) { + mw->appendDisconnectActions(complexAction); + + // history::ModuleRemove + history::ModuleRemove* moduleRemove = new history::ModuleRemove; + moduleRemove->setModule(mw); + complexAction->push(moduleRemove); + + removeModule(mw); + delete mw; + } + + APP->history->push(complexAction); +} + void RackWidget::clearCables() { incompleteCable = NULL; internal->cableContainer->clearChildren(); @@ -669,12 +780,7 @@ void RackWidget::clearCablesAction() { history::ComplexAction* complexAction = new history::ComplexAction; complexAction->name = "clear cables"; - for (widget::Widget* w : internal->cableContainer->children) { - CableWidget* cw = dynamic_cast(w); - assert(cw); - if (!cw->isComplete()) - continue; - + for (CableWidget* cw : getCompleteCables()) { // history::CableRemove history::CableRemove* h = new history::CableRemove; h->setCable(cw); @@ -685,6 +791,7 @@ void RackWidget::clearCablesAction() { APP->history->push(complexAction); else delete complexAction; + clearCables(); } @@ -756,6 +863,17 @@ CableWidget* RackWidget::getCable(int64_t cableId) { return NULL; } +std::list RackWidget::getCompleteCables() { + std::list cws; + for (widget::Widget* w : internal->cableContainer->children) { + CableWidget* cw = dynamic_cast(w); + assert(cw); + if (cw->isComplete()) + cws.push_back(cw); + } + return cws; +} + std::list RackWidget::getCablesOnPort(PortWidget* port) { assert(port); std::list cws; diff --git a/src/patch.cpp b/src/patch.cpp index 05f6bfd2..53a87c40 100644 --- a/src/patch.cpp +++ b/src/patch.cpp @@ -79,7 +79,7 @@ void PatchManager::clear() { static bool promptClear(std::string text) { if (APP->history->isSaved()) return true; - if (APP->scene->rack->isEmpty()) + if (APP->scene->rack->hasModules()) return true; return osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, text.c_str()); }