Browse Source

Use various other history actions when interacting with the rack

tags/v1.0.0
Andrew Belt 6 years ago
parent
commit
c6f8153d74
10 changed files with 214 additions and 95 deletions
  1. +9
    -7
      include/app/ModuleWidget.hpp
  2. +2
    -0
      include/app/PortWidget.hpp
  3. +4
    -1
      include/app/RackWidget.hpp
  4. +9
    -0
      include/history.hpp
  5. +110
    -38
      src/app/ModuleWidget.cpp
  6. +0
    -2
      src/app/PortWidget.cpp
  7. +53
    -26
      src/app/RackWidget.cpp
  8. +0
    -6
      src/app/Scene.cpp
  9. +9
    -15
      src/engine/Engine.cpp
  10. +18
    -0
      src/history.cpp

+ 9
- 7
include/app/ModuleWidget.hpp View File

@@ -62,8 +62,8 @@ struct ModuleWidget : OpaqueWidget {


/** Serializes/unserializes the module state */ /** Serializes/unserializes the module state */
void copyClipboard(); void copyClipboard();
void pasteClipboard();
void load(std::string filename);
void pasteClipboardAction();
void loadAction(std::string filename);
void save(std::string filename); void save(std::string filename);
void loadDialog(); void loadDialog();
void saveDialog(); void saveDialog();
@@ -72,18 +72,20 @@ struct ModuleWidget : OpaqueWidget {
Called when the user clicks Disconnect Cables in the context menu. Called when the user clicks Disconnect Cables in the context menu.
*/ */
void disconnect(); void disconnect();

/** Resets the parameters of the module and calls the Module's randomize(). /** Resets the parameters of the module and calls the Module's randomize().
Called when the user clicks Initialize in the context menu. Called when the user clicks Initialize in the context menu.
*/ */
void reset();
void resetAction();
/** Randomizes the parameters of the module and calls the Module's randomize(). /** Randomizes the parameters of the module and calls the Module's randomize().
Called when the user clicks Randomize in the context menu. Called when the user clicks Randomize in the context menu.
*/ */
void randomize();

void removeAction();
void bypassAction();
void randomizeAction();
void disconnectAction();
void cloneAction(); void cloneAction();
void bypassAction();
/** Deletes `this` */
void removeAction();
void createContextMenu(); void createContextMenu();
/** Override to add context menu entries to your subclass. /** Override to add context menu entries to your subclass.
It is recommended to add a blank MenuEntry first for spacing. It is recommended to add a blank MenuEntry first for spacing.


+ 2
- 0
include/app/PortWidget.hpp View File

@@ -21,8 +21,10 @@ struct PortWidget : OpaqueWidget {


PortWidget(); PortWidget();
~PortWidget(); ~PortWidget();

void step() override; void step() override;
void draw(NVGcontext *vg) override; void draw(NVGcontext *vg) override;

void onButton(const event::Button &e) override; void onButton(const event::Button &e) override;
void onDragStart(const event::DragStart &e) override; void onDragStart(const event::DragStart &e) override;
void onDragEnd(const event::DragEnd &e) override; void onDragEnd(const event::DragEnd &e) override;


+ 4
- 1
include/app/RackWidget.hpp View File

@@ -25,6 +25,7 @@ struct RackWidget : OpaqueWidget {
void draw(NVGcontext *vg) override; void draw(NVGcontext *vg) override;


void onHover(const event::Hover &e) override; void onHover(const event::Hover &e) override;
void onHoverKey(const event::HoverKey &e) override;
void onDragHover(const event::DragHover &e) override; void onDragHover(const event::DragHover &e) override;
void onButton(const event::Button &e) override; void onButton(const event::Button &e) override;
void onZoom(const event::Zoom &e) override; void onZoom(const event::Zoom &e) override;
@@ -33,7 +34,7 @@ struct RackWidget : OpaqueWidget {
void clear(); void clear();
json_t *toJson(); json_t *toJson();
void fromJson(json_t *rootJ); void fromJson(json_t *rootJ);
void pastePresetClipboard();
void pastePresetClipboardAction();


// Module methods // Module methods


@@ -67,6 +68,8 @@ struct RackWidget : OpaqueWidget {
/** Returns the most recently added complete cable connected to the given Port, i.e. the top of the stack */ /** Returns the most recently added complete cable connected to the given Port, i.e. the top of the stack */
CableWidget *getTopCable(PortWidget *port); CableWidget *getTopCable(PortWidget *port);
CableWidget *getCable(int cableId); CableWidget *getCable(int cableId);
/** Returns all cables attached to port, complete or not */
std::list<CableWidget*> getCablesOnPort(PortWidget *port);
}; };






+ 9
- 0
include/history.hpp View File

@@ -83,6 +83,15 @@ struct ModuleBypass : ModuleAction {
}; };




struct ModuleChange : ModuleAction {
json_t *oldModuleJ;
json_t *newModuleJ;
~ModuleChange();
void undo() override;
void redo() override;
};


struct ParamChange : ModuleAction { struct ParamChange : ModuleAction {
int paramId; int paramId;
float oldValue; float oldValue;


+ 110
- 38
src/app/ModuleWidget.cpp View File

@@ -22,7 +22,7 @@ struct ModuleDisconnectItem : MenuItem {
rightText = WINDOW_MOD_CTRL_NAME "+U"; rightText = WINDOW_MOD_CTRL_NAME "+U";
} }
void onAction(const event::Action &e) override { void onAction(const event::Action &e) override {
moduleWidget->disconnect();
moduleWidget->disconnectAction();
} }
}; };


@@ -33,7 +33,7 @@ struct ModuleResetItem : MenuItem {
rightText = WINDOW_MOD_CTRL_NAME "+I"; rightText = WINDOW_MOD_CTRL_NAME "+I";
} }
void onAction(const event::Action &e) override { void onAction(const event::Action &e) override {
moduleWidget->reset();
moduleWidget->resetAction();
} }
}; };


@@ -44,7 +44,7 @@ struct ModuleRandomizeItem : MenuItem {
rightText = WINDOW_MOD_CTRL_NAME "+R"; rightText = WINDOW_MOD_CTRL_NAME "+R";
} }
void onAction(const event::Action &e) override { void onAction(const event::Action &e) override {
moduleWidget->randomize();
moduleWidget->randomizeAction();
} }
}; };


@@ -66,7 +66,7 @@ struct ModulePasteItem : MenuItem {
rightText = WINDOW_MOD_CTRL_NAME "+V"; rightText = WINDOW_MOD_CTRL_NAME "+V";
} }
void onAction(const event::Action &e) override { void onAction(const event::Action &e) override {
moduleWidget->pasteClipboard();
moduleWidget->pasteClipboardAction();
} }
}; };


@@ -214,13 +214,13 @@ void ModuleWidget::onHoverKey(const event::HoverKey &e) {
switch (e.key) { switch (e.key) {
case GLFW_KEY_I: { case GLFW_KEY_I: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
reset();
resetAction();
e.consume(this); e.consume(this);
} }
} break; } break;
case GLFW_KEY_R: { case GLFW_KEY_R: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
randomize();
randomizeAction();
e.consume(this); e.consume(this);
} }
} break; } break;
@@ -232,18 +232,19 @@ void ModuleWidget::onHoverKey(const event::HoverKey &e) {
} break; } break;
case GLFW_KEY_V: { case GLFW_KEY_V: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
pasteClipboard();
pasteClipboardAction();
e.consume(this); e.consume(this);
} }
} break; } break;
case GLFW_KEY_D: { case GLFW_KEY_D: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
cloneAction(); cloneAction();
e.consume(this);
} }
} break; } break;
case GLFW_KEY_U: { case GLFW_KEY_U: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
disconnect();
disconnectAction();
e.consume(this); e.consume(this);
} }
} break; } break;
@@ -267,7 +268,7 @@ void ModuleWidget::onDragStart(const event::DragStart &e) {


void ModuleWidget::onDragEnd(const event::DragEnd &e) { void ModuleWidget::onDragEnd(const event::DragEnd &e) {
if (!box.pos.isEqual(oldPos)) { if (!box.pos.isEqual(oldPos)) {
// Push ModuleMove history action
// history::ModuleMove
history::ModuleMove *h = new history::ModuleMove; history::ModuleMove *h = new history::ModuleMove;
h->moduleId = module->id; h->moduleId = module->id;
h->oldPos = oldPos; h->oldPos = oldPos;
@@ -412,7 +413,7 @@ void ModuleWidget::copyClipboard() {
glfwSetClipboardString(app()->window->win, moduleJson); glfwSetClipboardString(app()->window->win, moduleJson);
} }


void ModuleWidget::pasteClipboard() {
void ModuleWidget::pasteClipboardAction() {
const char *moduleJson = glfwGetClipboardString(app()->window->win); const char *moduleJson = glfwGetClipboardString(app()->window->win);
if (!moduleJson) { if (!moduleJson) {
WARN("Could not get text from clipboard."); WARN("Could not get text from clipboard.");
@@ -429,10 +430,18 @@ void ModuleWidget::pasteClipboard() {
json_decref(moduleJ); json_decref(moduleJ);
}); });


// history::ModuleChange
history::ModuleChange *h = new history::ModuleChange;
h->moduleId = module->id;
h->oldModuleJ = toJson();

fromJson(moduleJ); fromJson(moduleJ);

h->newModuleJ = toJson();
app()->history->push(h);
} }


void ModuleWidget::load(std::string filename) {
void ModuleWidget::loadAction(std::string filename) {
INFO("Loading preset %s", filename.c_str()); INFO("Loading preset %s", filename.c_str());


FILE *file = fopen(filename.c_str(), "r"); FILE *file = fopen(filename.c_str(), "r");
@@ -455,7 +464,15 @@ void ModuleWidget::load(std::string filename) {
json_decref(moduleJ); json_decref(moduleJ);
}); });


// history::ModuleChange
history::ModuleChange *h = new history::ModuleChange;
h->moduleId = module->id;
h->oldModuleJ = toJson();

fromJson(moduleJ); fromJson(moduleJ);

h->newModuleJ = toJson();
app()->history->push(h);
} }


void ModuleWidget::save(std::string filename) { void ModuleWidget::save(std::string filename) {
@@ -496,7 +513,7 @@ void ModuleWidget::loadDialog() {
free(path); free(path);
}); });


load(path);
loadAction(path);
} }


void ModuleWidget::saveDialog() { void ModuleWidget::saveDialog() {
@@ -535,39 +552,68 @@ void ModuleWidget::disconnect() {
} }
} }


void ModuleWidget::reset() {
if (module) {
app()->engine->resetModule(module);
}
}
void ModuleWidget::resetAction() {
assert(module);


void ModuleWidget::randomize() {
if (module) {
app()->engine->randomizeModule(module);
}
// history::ModuleChange
history::ModuleChange *h = new history::ModuleChange;
h->moduleId = module->id;
h->oldModuleJ = toJson();

app()->engine->resetModule(module);

h->newModuleJ = toJson();
app()->history->push(h);
} }


void ModuleWidget::removeAction() {
history::ComplexAction *complexAction = new history::ComplexAction;
void ModuleWidget::randomizeAction() {
assert(module);


// Push ModuleRemove history action
history::ModuleRemove *moduleRemove = new history::ModuleRemove;
moduleRemove->setModule(this);
complexAction->push(moduleRemove);
// history::ModuleChange
history::ModuleChange *h = new history::ModuleChange;
h->moduleId = module->id;
h->oldModuleJ = toJson();


app()->history->push(complexAction);
app()->engine->randomizeModule(module);


app()->scene->rackWidget->removeModule(this);
delete this;
h->newModuleJ = toJson();
app()->history->push(h);
} }


void ModuleWidget::bypassAction() {
// Push ModuleBypass history action
history::ModuleBypass *h = new history::ModuleBypass;
h->moduleId = module->id;
h->bypass = !module->bypass;
app()->history->push(h);
h->redo();
static void disconnectActions(ModuleWidget *mw, history::ComplexAction *complexAction) {
// Add CableRemove action for all cables attached to outputs
for (PortWidget* output : mw->outputs) {
for (CableWidget *cw : app()->scene->rackWidget->getCablesOnPort(output)) {
if (!cw->isComplete())
continue;
// history::CableRemove
history::CableRemove *h = new history::CableRemove;
h->setCable(cw);
complexAction->push(h);
}
}
// Add CableRemove action for all cables attached to inputs
for (PortWidget* input : mw->inputs) {
for (CableWidget *cw : app()->scene->rackWidget->getCablesOnPort(input)) {
if (!cw->isComplete())
continue;
// Avoid creating duplicate actions for self-patched cables
if (cw->outputPort->module == mw->module)
continue;
// history::CableRemove
history::CableRemove *h = new history::CableRemove;
h->setCable(cw);
complexAction->push(h);
}
}
}

void ModuleWidget::disconnectAction() {
history::ComplexAction *complexAction = new history::ComplexAction;
disconnectActions(this, complexAction);
app()->history->push(complexAction);

disconnect();
} }


void ModuleWidget::cloneAction() { void ModuleWidget::cloneAction() {
@@ -580,12 +626,38 @@ void ModuleWidget::cloneAction() {


app()->scene->rackWidget->addModuleAtMouse(clonedModuleWidget); app()->scene->rackWidget->addModuleAtMouse(clonedModuleWidget);


// Push ModuleAdd history action
// history::ModuleAdd
history::ModuleAdd *h = new history::ModuleAdd; history::ModuleAdd *h = new history::ModuleAdd;
h->setModule(clonedModuleWidget); h->setModule(clonedModuleWidget);
app()->history->push(h); app()->history->push(h);
} }


void ModuleWidget::bypassAction() {
assert(module);
// history::ModuleBypass
history::ModuleBypass *h = new history::ModuleBypass;
h->moduleId = module->id;
h->bypass = !module->bypass;
app()->history->push(h);
h->redo();
}

void ModuleWidget::removeAction() {
history::ComplexAction *complexAction = new history::ComplexAction;
disconnectActions(this, complexAction);

// history::ModuleRemove
history::ModuleRemove *moduleRemove = new history::ModuleRemove;
moduleRemove->setModule(this);
complexAction->push(moduleRemove);

app()->history->push(complexAction);

// This disconnects cables, removes the module, and transfers ownership to caller
app()->scene->rackWidget->removeModule(this);
delete this;
}

void ModuleWidget::createContextMenu() { void ModuleWidget::createContextMenu() {
Menu *menu = createMenu(); Menu *menu = createMenu();
assert(model); assert(model);


+ 0
- 2
src/app/PortWidget.cpp View File

@@ -108,8 +108,6 @@ void PortWidget::onDragStart(const event::DragStart &e) {
} }


void PortWidget::onDragEnd(const event::DragEnd &e) { void PortWidget::onDragEnd(const event::DragEnd &e) {
// FIXME
// If the source PortWidget is deleted, this will be called, removing the cable
CableWidget *cw = app()->scene->rackWidget->releaseIncompleteCable(); CableWidget *cw = app()->scene->rackWidget->releaseIncompleteCable();
if (cw->isComplete()) { if (cw->isComplete()) {
app()->scene->rackWidget->addCable(cw); app()->scene->rackWidget->addCable(cw);


+ 53
- 26
src/app/RackWidget.cpp View File

@@ -148,6 +148,22 @@ void RackWidget::onHover(const event::Hover &e) {
mousePos = e.pos; mousePos = e.pos;
} }


void RackWidget::onHoverKey(const event::HoverKey &e) {
OpaqueWidget::onHoverKey(e);
if (e.getConsumed() != this)
return;

if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
switch (e.key) {
case GLFW_KEY_V: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
pastePresetClipboardAction();
}
} break;
}
}
}

void RackWidget::onDragHover(const event::DragHover &e) { void RackWidget::onDragHover(const event::DragHover &e) {
OpaqueWidget::onDragHover(e); OpaqueWidget::onDragHover(e);
mousePos = e.pos; mousePos = e.pos;
@@ -287,7 +303,7 @@ void RackWidget::fromJson(json_t *rootJ) {
} }
} }


void RackWidget::pastePresetClipboard() {
void RackWidget::pastePresetClipboardAction() {
const char *moduleJson = glfwGetClipboardString(app()->window->win); const char *moduleJson = glfwGetClipboardString(app()->window->win);
if (!moduleJson) { if (!moduleJson) {
WARN("Could not get text from clipboard."); WARN("Could not get text from clipboard.");
@@ -297,13 +313,14 @@ void RackWidget::pastePresetClipboard() {
json_error_t error; json_error_t error;
json_t *moduleJ = json_loads(moduleJson, 0, &error); json_t *moduleJ = json_loads(moduleJson, 0, &error);
if (moduleJ) { if (moduleJ) {
ModuleWidget *moduleWidget = moduleFromJson(moduleJ);
ModuleWidget *mw = moduleFromJson(moduleJ);
json_decref(moduleJ); json_decref(moduleJ);
addModule(moduleWidget);
// Set moduleWidget position
math::Rect newBox = moduleWidget->box;
newBox.pos = mousePos.minus(newBox.size.div(2));
requestModuleBoxNearest(moduleWidget, newBox);
addModuleAtMouse(mw);

// history::ModuleAdd
history::ModuleAdd *h = new history::ModuleAdd;
h->setModule(mw);
app()->history->push(h);
} }
else { else {
WARN("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text); WARN("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
@@ -349,7 +366,9 @@ bool RackWidget::requestModuleBox(ModuleWidget *m, math::Rect requestedBox) {


// Check intersection with other modules // Check intersection with other modules
for (Widget *m2 : moduleContainer->children) { for (Widget *m2 : moduleContainer->children) {
if (m == m2) continue;
// Don't intersect with self
if (m == m2)
continue;
if (requestedBox.intersects(m2->box)) { if (requestedBox.intersects(m2->box)) {
return false; return false;
} }
@@ -401,8 +420,10 @@ void RackWidget::clearCables() {
for (Widget *w : cableContainer->children) { for (Widget *w : cableContainer->children) {
CableWidget *cw = dynamic_cast<CableWidget*>(w); CableWidget *cw = dynamic_cast<CableWidget*>(w);
assert(cw); assert(cw);
if (cw != incompleteCable)
app()->engine->removeCable(cw->cable);
if (!cw->isComplete())
continue;

app()->engine->removeCable(cw->cable);
} }
incompleteCable = NULL; incompleteCable = NULL;
cableContainer->clearChildren(); cableContainer->clearChildren();
@@ -415,7 +436,7 @@ void RackWidget::clearCablesAction() {
for (Widget *w : cableContainer->children) { for (Widget *w : cableContainer->children) {
CableWidget *cw = dynamic_cast<CableWidget*>(w); CableWidget *cw = dynamic_cast<CableWidget*>(w);
assert(cw); assert(cw);
if (cw == incompleteCable)
if (!cw->isComplete())
continue; continue;


// history::CableRemove // history::CableRemove
@@ -429,23 +450,16 @@ void RackWidget::clearCablesAction() {
} }


void RackWidget::clearCablesOnPort(PortWidget *port) { void RackWidget::clearCablesOnPort(PortWidget *port) {
assert(port);
std::list<Widget*> childrenCopy = cableContainer->children;
for (Widget *w : childrenCopy) {
CableWidget *cw = dynamic_cast<CableWidget*>(w);
assert(cw);

for (CableWidget *cw : getCablesOnPort(port)) {
// Check if cable is connected to port // Check if cable is connected to port
if (cw->inputPort == port || cw->outputPort == port) {
if (cw == incompleteCable) {
incompleteCable = NULL;
cableContainer->removeChild(cw);
}
else {
removeCable(cw);
}
delete cw;
if (cw == incompleteCable) {
incompleteCable = NULL;
cableContainer->removeChild(cw);
}
else {
removeCable(cw);
} }
delete cw;
} }
} }


@@ -503,5 +517,18 @@ CableWidget *RackWidget::getCable(int cableId) {
return NULL; return NULL;
} }


std::list<CableWidget*> RackWidget::getCablesOnPort(PortWidget *port) {
assert(port);
std::list<CableWidget*> cables;
for (Widget *w : cableContainer->children) {
CableWidget *cw = dynamic_cast<CableWidget*>(w);
assert(cw);
if (cw->inputPort == port || cw->outputPort == port) {
cables.push_back(cw);
}
}
return cables;
}



} // namespace rack } // namespace rack

+ 0
- 6
src/app/Scene.cpp View File

@@ -124,12 +124,6 @@ void Scene::onHoverKey(const event::HoverKey &e) {
e.consume(this); e.consume(this);
} }
} break; } break;
case GLFW_KEY_V: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
rackWidget->pastePresetClipboard();
e.consume(this);
}
} break;
case GLFW_KEY_Z: { case GLFW_KEY_Z: {
if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) {
app()->history->undo(); app()->history->undo();


+ 9
- 15
src/engine/Engine.cpp View File

@@ -50,8 +50,6 @@ struct Engine::Internal {
float sampleTime; float sampleTime;
float sampleRateRequested; float sampleRateRequested;


Module *resetModule = NULL;
Module *randomizeModule = NULL;
int nextModuleId = 1; int nextModuleId = 1;
int nextCableId = 1; int nextCableId = 1;


@@ -93,16 +91,6 @@ static void Engine_step(Engine *engine) {
} }
} }


// Events
if (engine->internal->resetModule) {
engine->internal->resetModule->reset();
engine->internal->resetModule = NULL;
}
if (engine->internal->randomizeModule) {
engine->internal->randomizeModule->randomize();
engine->internal->randomizeModule = NULL;
}

// Param smoothing // Param smoothing
{ {
Module *smoothModule = engine->internal->smoothModule; Module *smoothModule = engine->internal->smoothModule;
@@ -273,11 +261,17 @@ void Engine::removeModule(Module *module) {
} }


void Engine::resetModule(Module *module) { void Engine::resetModule(Module *module) {
internal->resetModule = module;
assert(module);
VIPLock vipLock(internal->vipMutex);
std::lock_guard<std::mutex> lock(internal->mutex);
module->reset();
} }


void Engine::randomizeModule(Module *module) { void Engine::randomizeModule(Module *module) {
internal->randomizeModule = module;
assert(module);
VIPLock vipLock(internal->vipMutex);
std::lock_guard<std::mutex> lock(internal->mutex);
module->randomize();
} }


static void Engine_updateActive(Engine *engine) { static void Engine_updateActive(Engine *engine) {
@@ -344,7 +338,7 @@ void Engine::removeCable(Cable *cable) {
} }


void Engine::setParam(Module *module, int paramId, float value) { void Engine::setParam(Module *module, int paramId, float value) {
// TODO Make thread safe
// TODO Does this need to be thread-safe?
module->params[paramId].value = value; module->params[paramId].value = value;
} }




+ 18
- 0
src/history.cpp View File

@@ -90,6 +90,24 @@ void ModuleBypass::redo() {
} }




ModuleChange::~ModuleChange() {
json_decref(oldModuleJ);
json_decref(newModuleJ);
}

void ModuleChange::undo() {
ModuleWidget *mw = app()->scene->rackWidget->getModule(moduleId);
assert(mw);
mw->fromJson(oldModuleJ);
}

void ModuleChange::redo() {
ModuleWidget *mw = app()->scene->rackWidget->getModule(moduleId);
assert(mw);
mw->fromJson(newModuleJ);
}


void ParamChange::undo() { void ParamChange::undo() {
ModuleWidget *mw = app()->scene->rackWidget->getModule(moduleId); ModuleWidget *mw = app()->scene->rackWidget->getModule(moduleId);
assert(mw); assert(mw);


Loading…
Cancel
Save