@@ -6,6 +6,8 @@ | |||
#include <app/ParamWidget.hpp> | |||
#include <plugin/Model.hpp> | |||
#include <engine/Module.hpp> | |||
#include <history.hpp> | |||
#include <list> | |||
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<ParamWidget*> getParams(); | |||
std::list<PortWidget*> getPorts(); | |||
std::list<PortWidget*> getInputs(); | |||
std::list<PortWidget*> 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(); | |||
@@ -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<ModuleWidget*> getModules(); | |||
bool hasModules(); | |||
void updateModuleOldPositions(); | |||
history::ComplexAction* getModuleDragAction(); | |||
void updateModuleSelections(); | |||
int getNumSelectedModules(); | |||
std::list<ModuleWidget*> 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<CableWidget*> getCompleteCables(); | |||
/** Returns all cables attached to port, complete or not. */ | |||
std::list<CableWidget*> getCablesOnPort(PortWidget* port); | |||
}; | |||
@@ -159,6 +159,51 @@ PortWidget* ModuleWidget::getOutput(int portId) { | |||
}); | |||
} | |||
template <class T, typename F> | |||
void doIfTypeRecursive(widget::Widget* w, F f) { | |||
T* t = dynamic_cast<T*>(w); | |||
if (t) | |||
f(t); | |||
for (widget::Widget* child : w->children) { | |||
doIfTypeRecursive<T>(child, f); | |||
} | |||
} | |||
std::list<ParamWidget*> ModuleWidget::getParams() { | |||
std::list<ParamWidget*> pws; | |||
doIfTypeRecursive<ParamWidget>(this, [&](ParamWidget* pw) { | |||
pws.push_back(pw); | |||
}); | |||
return pws; | |||
} | |||
std::list<PortWidget*> ModuleWidget::getPorts() { | |||
std::list<PortWidget*> pws; | |||
doIfTypeRecursive<PortWidget>(this, [&](PortWidget* pw) { | |||
pws.push_back(pw); | |||
}); | |||
return pws; | |||
} | |||
std::list<PortWidget*> ModuleWidget::getInputs() { | |||
std::list<PortWidget*> pws; | |||
doIfTypeRecursive<PortWidget>(this, [&](PortWidget* pw) { | |||
if (pw->type == engine::Port::INPUT) | |||
pws.push_back(pw); | |||
}); | |||
return pws; | |||
} | |||
std::list<PortWidget*> ModuleWidget::getOutputs() { | |||
std::list<PortWidget*> pws; | |||
doIfTypeRecursive<PortWidget>(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 <class T, typename F> | |||
void doOfType(widget::Widget* w, F f) { | |||
T* t = dynamic_cast<T*>(w); | |||
if (t) | |||
f(t); | |||
for (widget::Widget* child : w->children) { | |||
doOfType<T>(child, f); | |||
} | |||
} | |||
void ModuleWidget::disconnect() { | |||
doOfType<PortWidget>(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<PortWidget>(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<PortWidget>(this, [&](PortWidget* pw) { | |||
if (pw->type != engine::Port::INPUT) | |||
return; | |||
std::list<CableWidget*> 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; | |||
} | |||
@@ -612,7 +612,17 @@ ModuleWidget* RackWidget::getModule(int64_t moduleId) { | |||
return NULL; | |||
} | |||
bool RackWidget::isEmpty() { | |||
std::list<ModuleWidget*> RackWidget::getModules() { | |||
std::list<ModuleWidget*> mws; | |||
for (widget::Widget* w : internal->moduleContainer->children) { | |||
ModuleWidget* mw = dynamic_cast<ModuleWidget*>(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<ModuleWidget*>(w); | |||
assert(mw); | |||
if (mw->selected()) | |||
count++; | |||
} | |||
return count; | |||
} | |||
std::list<ModuleWidget*> RackWidget::getSelectedModules() { | |||
std::list<ModuleWidget*> mws; | |||
for (widget::Widget* w : internal->moduleContainer->children) { | |||
ModuleWidget* mw = dynamic_cast<ModuleWidget*>(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<CableWidget*>(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<CableWidget*> RackWidget::getCompleteCables() { | |||
std::list<CableWidget*> cws; | |||
for (widget::Widget* w : internal->cableContainer->children) { | |||
CableWidget* cw = dynamic_cast<CableWidget*>(w); | |||
assert(cw); | |||
if (cw->isComplete()) | |||
cws.push_back(cw); | |||
} | |||
return cws; | |||
} | |||
std::list<CableWidget*> RackWidget::getCablesOnPort(PortWidget* port) { | |||
assert(port); | |||
std::list<CableWidget*> cws; | |||
@@ -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()); | |||
} | |||