Browse Source

Add module preset copy, paste, load, and save

tags/v0.6.2
Andrew Belt 6 years ago
parent
commit
6c817f8d4c
7 changed files with 321 additions and 76 deletions
  1. +14
    -3
      include/app.hpp
  2. +202
    -26
      src/app/ModuleWidget.cpp
  3. +9
    -3
      src/app/RackScene.cpp
  4. +91
    -39
      src/app/RackWidget.cpp
  5. +1
    -1
      src/app/Toolbar.cpp
  6. +1
    -1
      src/engine.cpp
  7. +3
    -3
      src/main.cpp

+ 14
- 3
include/app.hpp View File

@@ -46,6 +46,8 @@ struct SVGPanel;
static const float RACK_GRID_WIDTH = 15;
static const float RACK_GRID_HEIGHT = 380;
static const Vec RACK_GRID_SIZE = Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT);
static const std::string PRESET_FILTERS = "VCV Rack module preset (.vcvm):vcvm";
static const std::string PATCH_FILTERS = "VCV Rack patch (.vcv):vcv";


struct ModuleWidget : OpaqueWidget {
@@ -68,6 +70,12 @@ struct ModuleWidget : OpaqueWidget {

virtual json_t *toJson();
virtual void fromJson(json_t *rootJ);
void copyClipboard();
void pasteClipboard();
void save(std::string filename);
void load(std::string filename);
void loadDialog();
void saveDialog();

virtual void create();
virtual void _delete();
@@ -154,17 +162,20 @@ struct RackWidget : OpaqueWidget {
void clear();
/** Clears the rack and loads the template patch */
void reset();
void openDialog();
void loadDialog();
void saveDialog();
void saveAsDialog();
/** If `lastPath` is defined, ask the user to reload it */
void revert();
/** Disconnects all wires */
void disconnect();
void savePatch(std::string filename);
void loadPatch(std::string filename);
void save(std::string filename);
void load(std::string filename);
json_t *toJson();
void fromJson(json_t *rootJ);
/** Creates a module and adds it to the rack */
ModuleWidget *moduleFromJson(json_t *moduleJ);
void pastePresetClipboard();

void addModule(ModuleWidget *m);
/** Removes the module and transfers ownership to the caller */


+ 202
- 26
src/app/ModuleWidget.cpp View File

@@ -2,6 +2,8 @@
#include "engine.hpp"
#include "plugin.hpp"
#include "window.hpp"
#include "asset.hpp"
#include "osdialog.h"


namespace rack {
@@ -68,10 +70,6 @@ json_t *ModuleWidget::toJson() {
json_object_set_new(rootJ, "version", json_string(model->plugin->version.c_str()));
// model
json_object_set_new(rootJ, "model", json_string(model->slug.c_str()));
// pos
Vec pos = box.pos.div(RACK_GRID_SIZE).round();
json_t *posJ = json_pack("[i, i]", (int) pos.x, (int) pos.y);
json_object_set_new(rootJ, "pos", posJ);
// params
json_t *paramsJ = json_array();
for (ParamWidget *paramWidget : params) {
@@ -91,24 +89,42 @@ json_t *ModuleWidget::toJson() {
}

void ModuleWidget::fromJson(json_t *rootJ) {
// Check if plugin and model are incorrect
json_t *pluginJ = json_object_get(rootJ, "plugin");
std::string pluginSlug;
if (pluginJ) {
pluginSlug = json_string_value(pluginJ);
if (pluginSlug != model->plugin->slug) {
warn("Plugin %s does not match ModuleWidget's plugin %s.", pluginSlug.c_str(), model->plugin->slug.c_str());
return;
}
}

json_t *modelJ = json_object_get(rootJ, "model");
std::string modelSlug;
if (modelJ) {
modelSlug = json_string_value(modelJ);
if (modelSlug != model->slug) {
warn("Model %s does not match ModuleWidget's model %s.", modelSlug.c_str(), model->slug.c_str());
return;
}
}

// Check plugin version
json_t *versionJ = json_object_get(rootJ, "version");
if (versionJ) {
std::string version = json_string_value(versionJ);
if (version != model->plugin->version) {
info("Patch created with %s version %s, using version %s.", pluginSlug.c_str(), version.c_str(), model->plugin->version.c_str());
}
}

// legacy
int legacy = 0;
json_t *legacyJ = json_object_get(rootJ, "legacy");
if (legacyJ)
legacy = json_integer_value(legacyJ);

// pos
json_t *posJ = json_object_get(rootJ, "pos");
double x, y;
json_unpack(posJ, "[F, F]", &x, &y);
Vec pos = Vec(x, y);
if (legacy && legacy <= 1) {
box.pos = pos;
}
else {
box.pos = pos.mult(RACK_GRID_SIZE);
}

// params
json_t *paramsJ = json_object_get(rootJ, "params");
size_t i;
@@ -146,6 +162,102 @@ void ModuleWidget::fromJson(json_t *rootJ) {
}
}

void ModuleWidget::copyClipboard() {
json_t *moduleJ = toJson();
char *moduleJson = json_dumps(moduleJ, JSON_INDENT(2) | JSON_REAL_PRECISION(9));
glfwSetClipboardString(gWindow, moduleJson);
free(moduleJson);
json_decref(moduleJ);
}

void ModuleWidget::pasteClipboard() {
const char *moduleJson = glfwGetClipboardString(gWindow);
if (!moduleJson) {
warn("Could not get text from clipboard.");
return;
}

json_error_t error;
json_t *moduleJ = json_loads(moduleJson, 0, &error);
if (moduleJ) {
fromJson(moduleJ);
json_decref(moduleJ);
}
else {
warn("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
}
}

void ModuleWidget::load(std::string filename) {
info("Loading preset %s", filename.c_str());
FILE *file = fopen(filename.c_str(), "r");
if (!file) {
// Exit silently
return;
}

json_error_t error;
json_t *moduleJ = json_loadf(file, 0, &error);
if (moduleJ) {
fromJson(moduleJ);
json_decref(moduleJ);
}
else {
std::string message = stringf("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
}

fclose(file);
}

void ModuleWidget::save(std::string filename) {
info("Saving preset %s", filename.c_str());
json_t *moduleJ = toJson();
if (!moduleJ)
return;

FILE *file = fopen(filename.c_str(), "w");
if (file) {
json_dumpf(moduleJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9));
fclose(file);
}

json_decref(moduleJ);
}

void ModuleWidget::loadDialog() {
std::string dir = assetLocal("presets");
systemCreateDirectory(dir);

osdialog_filters *filters = osdialog_filters_parse(PRESET_FILTERS.c_str());
char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters);
if (path) {
load(path);
free(path);
}
osdialog_filters_free(filters);
}

void ModuleWidget::saveDialog() {
std::string dir = assetLocal("presets");
systemCreateDirectory(dir);

osdialog_filters *filters = osdialog_filters_parse(PRESET_FILTERS.c_str());
char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), "Untitled.vcvm", filters);

if (path) {
std::string pathStr = path;
free(path);
std::string extension = stringExtension(pathStr);
if (extension.empty()) {
pathStr += ".vcvm";
}

save(pathStr);
}
osdialog_filters_free(filters);
}

void ModuleWidget::disconnect() {
for (Port *input : inputs) {
gRackWidget->wireContainer->removeAllWires(input);
@@ -268,6 +380,20 @@ void ModuleWidget::onHoverKey(EventHoverKey &e) {
return;
}
} break;
case GLFW_KEY_C: {
if (windowIsModPressed() && !windowIsShiftPressed()) {
copyClipboard();
e.consumed = true;
return;
}
} break;
case GLFW_KEY_V: {
if (windowIsModPressed() && !windowIsShiftPressed()) {
pasteClipboard();
e.consumed = true;
return;
}
} break;
case GLFW_KEY_D: {
if (windowIsModPressed() && !windowIsShiftPressed()) {
gRackWidget->cloneModule(this);
@@ -303,35 +429,63 @@ void ModuleWidget::onDragMove(EventDragMove &e) {
}


struct DisconnectMenuItem : MenuItem {
struct ModuleDisconnectItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->disconnect();
}
};

struct ResetMenuItem : MenuItem {
struct ModuleResetItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->reset();
}
};

struct RandomizeMenuItem : MenuItem {
struct ModuleRandomizeItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->randomize();
}
};

struct CloneMenuItem : MenuItem {
struct ModuleCopyItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->copyClipboard();
}
};

struct ModulePasteItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->pasteClipboard();
}
};

struct ModuleSaveItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->saveDialog();
}
};

struct ModuleLoadItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
moduleWidget->loadDialog();
}
};

struct ModuleCloneItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
gRackWidget->cloneModule(moduleWidget);
}
};

struct DeleteMenuItem : MenuItem {
struct ModuleDeleteItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction(EventAction &e) override {
gRackWidget->deleteModule(moduleWidget);
@@ -347,31 +501,53 @@ Menu *ModuleWidget::createContextMenu() {
menuLabel->text = model->author + " " + model->name + " " + model->plugin->version;
menu->addChild(menuLabel);

ResetMenuItem *resetItem = new ResetMenuItem();
ModuleResetItem *resetItem = new ModuleResetItem();
resetItem->text = "Initialize";
resetItem->rightText = WINDOW_MOD_KEY_NAME "+I";
resetItem->moduleWidget = this;
menu->addChild(resetItem);

RandomizeMenuItem *randomizeItem = new RandomizeMenuItem();
ModuleRandomizeItem *randomizeItem = new ModuleRandomizeItem();
randomizeItem->text = "Randomize";
randomizeItem->rightText = WINDOW_MOD_KEY_NAME "+R";
randomizeItem->moduleWidget = this;
menu->addChild(randomizeItem);

DisconnectMenuItem *disconnectItem = new DisconnectMenuItem();
ModuleDisconnectItem *disconnectItem = new ModuleDisconnectItem();
disconnectItem->text = "Disconnect cables";
disconnectItem->rightText = WINDOW_MOD_KEY_NAME "+U";
disconnectItem->moduleWidget = this;
menu->addChild(disconnectItem);

CloneMenuItem *cloneItem = new CloneMenuItem();
ModuleCloneItem *cloneItem = new ModuleCloneItem();
cloneItem->text = "Duplicate";
cloneItem->rightText = WINDOW_MOD_KEY_NAME "+D";
cloneItem->moduleWidget = this;
menu->addChild(cloneItem);

DeleteMenuItem *deleteItem = new DeleteMenuItem();
ModuleCopyItem *copyItem = new ModuleCopyItem();
copyItem->text = "Copy preset";
copyItem->rightText = WINDOW_MOD_KEY_NAME "+C";
copyItem->moduleWidget = this;
menu->addChild(copyItem);

ModulePasteItem *pasteItem = new ModulePasteItem();
pasteItem->text = "Paste preset";
pasteItem->rightText = WINDOW_MOD_KEY_NAME "+V";
pasteItem->moduleWidget = this;
menu->addChild(pasteItem);

ModuleLoadItem *loadItem = new ModuleLoadItem();
loadItem->text = "Load preset";
loadItem->moduleWidget = this;
menu->addChild(loadItem);

ModuleSaveItem *saveItem = new ModuleSaveItem();
saveItem->text = "Save preset";
saveItem->moduleWidget = this;
menu->addChild(saveItem);

ModuleDeleteItem *deleteItem = new ModuleDeleteItem();
deleteItem->text = "Delete";
deleteItem->rightText = "Backspace/Delete";
deleteItem->moduleWidget = this;


+ 9
- 3
src/app/RackScene.cpp View File

@@ -77,7 +77,7 @@ void RackScene::onHoverKey(EventHoverKey &e) {
} break;
case GLFW_KEY_O: {
if (windowIsModPressed() && !windowIsShiftPressed()) {
gRackWidget->openDialog();
gRackWidget->loadDialog();
e.consumed = true;
}
if (windowIsModPressed() && windowIsShiftPressed()) {
@@ -95,6 +95,12 @@ void RackScene::onHoverKey(EventHoverKey &e) {
e.consumed = true;
}
} break;
case GLFW_KEY_V: {
if (windowIsModPressed() && !windowIsShiftPressed()) {
gRackWidget->pastePresetClipboard();
e.consumed = true;
}
} break;
case GLFW_KEY_ENTER:
case GLFW_KEY_KP_ENTER: {
appModuleBrowserCreate();
@@ -109,9 +115,9 @@ void RackScene::onHoverKey(EventHoverKey &e) {

void RackScene::onPathDrop(EventPathDrop &e) {
if (e.paths.size() >= 1) {
const std::string& firstPath = e.paths.front();
const std::string &firstPath = e.paths.front();
if (stringExtension(firstPath) == "vcv") {
gRackWidget->loadPatch(firstPath);
gRackWidget->load(firstPath);
e.consumed = true;
}
}


+ 91
- 39
src/app/RackWidget.cpp View File

@@ -12,9 +12,6 @@
namespace rack {


static const char *FILTERS = "VCV Rack patch (.vcv):vcv";


struct ModuleContainer : Widget {
void draw(NVGcontext *vg) override {
// Draw shadows behind each ModuleWidget first, so the shadow doesn't overlap the front.
@@ -66,12 +63,12 @@ void RackWidget::reset() {
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Clear patch and start over?")) {
clear();
// Fails silently if file does not exist
loadPatch(assetLocal("template.vcv"));
load(assetLocal("template.vcv"));
lastPath = "";
}
}

void RackWidget::openDialog() {
void RackWidget::loadDialog() {
std::string dir;
if (lastPath.empty()) {
dir = assetLocal("patches");
@@ -80,10 +77,10 @@ void RackWidget::openDialog() {
else {
dir = stringDirectory(lastPath);
}
osdialog_filters *filters = osdialog_filters_parse(FILTERS);
osdialog_filters *filters = osdialog_filters_parse(PATCH_FILTERS.c_str());
char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters);
if (path) {
loadPatch(path);
load(path);
lastPath = path;
free(path);
}
@@ -92,7 +89,7 @@ void RackWidget::openDialog() {

void RackWidget::saveDialog() {
if (!lastPath.empty()) {
savePatch(lastPath);
save(lastPath);
}
else {
saveAsDialog();
@@ -110,7 +107,7 @@ void RackWidget::saveAsDialog() {
dir = stringDirectory(lastPath);
filename = stringFilename(lastPath);
}
osdialog_filters *filters = osdialog_filters_parse(FILTERS);
osdialog_filters *filters = osdialog_filters_parse(PATCH_FILTERS.c_str());
char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters);

if (path) {
@@ -121,19 +118,19 @@ void RackWidget::saveAsDialog() {
pathStr += ".vcv";
}

savePatch(pathStr);
save(pathStr);
lastPath = pathStr;
}
osdialog_filters_free(filters);
}

void RackWidget::savePatch(std::string path) {
info("Saving patch %s", path.c_str());
void RackWidget::save(std::string filename) {
info("Saving patch %s", filename.c_str());
json_t *rootJ = toJson();
if (!rootJ)
return;

FILE *file = fopen(path.c_str(), "w");
FILE *file = fopen(filename.c_str(), "w");
if (file) {
json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9));
fclose(file);
@@ -142,9 +139,9 @@ void RackWidget::savePatch(std::string path) {
json_decref(rootJ);
}

void RackWidget::loadPatch(std::string path) {
info("Loading patch %s", path.c_str());
FILE *file = fopen(path.c_str(), "r");
void RackWidget::load(std::string filename) {
info("Loading patch %s", filename.c_str());
FILE *file = fopen(filename.c_str(), "r");
if (!file) {
// Exit silently
return;
@@ -158,7 +155,7 @@ void RackWidget::loadPatch(std::string path) {
json_decref(rootJ);
}
else {
std::string message = stringf("JSON parsing error at %s %d:%d %s\n", error.source, error.line, error.column, error.text);
std::string message = stringf("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
}

@@ -169,7 +166,7 @@ void RackWidget::revert() {
if (lastPath.empty())
return;
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Revert patch to the last saved state?")) {
loadPatch(lastPath);
load(lastPath);
}
}

@@ -203,6 +200,12 @@ json_t *RackWidget::toJson() {
moduleId++;
// module
json_t *moduleJ = moduleWidget->toJson();
{
// pos
Vec pos = moduleWidget->box.pos.div(RACK_GRID_SIZE).round();
json_t *posJ = json_pack("[i, i]", (int) pos.x, (int) pos.y);
json_object_set_new(moduleJ, "pos", posJ);
}
json_array_append_new(modulesJ, moduleJ);
}
json_object_set_new(rootJ, "modules", modulesJ);
@@ -275,25 +278,30 @@ void RackWidget::fromJson(json_t *rootJ) {
json_object_set(moduleJ, "legacy", json_integer(legacy));
}

json_t *pluginSlugJ = json_object_get(moduleJ, "plugin");
if (!pluginSlugJ) continue;
json_t *modelSlugJ = json_object_get(moduleJ, "model");
if (!modelSlugJ) continue;
std::string pluginSlug = json_string_value(pluginSlugJ);
std::string modelSlug = json_string_value(modelSlugJ);
ModuleWidget *moduleWidget = moduleFromJson(moduleJ);

if (moduleWidget) {
// pos
json_t *posJ = json_object_get(moduleJ, "pos");
double x, y;
json_unpack(posJ, "[F, F]", &x, &y);
Vec pos = Vec(x, y);
if (legacy && legacy <= 1) {
moduleWidget->box.pos = pos;
}
else {
moduleWidget->box.pos = pos.mult(RACK_GRID_SIZE);
}

Model *model = pluginGetModel(pluginSlug, modelSlug);
if (!model) {
moduleWidgets[moduleId] = moduleWidget;
}
else {
json_t *pluginSlugJ = json_object_get(moduleJ, "plugin");
json_t *modelSlugJ = json_object_get(moduleJ, "model");
std::string pluginSlug = json_string_value(pluginSlugJ);
std::string modelSlug = json_string_value(modelSlugJ);
message += stringf("Could not find module \"%s\" of plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str());
continue;
}

// Create ModuleWidget
ModuleWidget *moduleWidget = model->createModuleWidget();
assert(moduleWidget);
moduleWidget->fromJson(moduleJ);
moduleContainer->addChild(moduleWidget);
moduleWidgets[moduleId] = moduleWidget;
}

// wires
@@ -355,6 +363,53 @@ void RackWidget::fromJson(json_t *rootJ) {
}
}

ModuleWidget *RackWidget::moduleFromJson(json_t *moduleJ) {
// Get slugs
json_t *pluginSlugJ = json_object_get(moduleJ, "plugin");
if (!pluginSlugJ)
return NULL;
json_t *modelSlugJ = json_object_get(moduleJ, "model");
if (!modelSlugJ)
return NULL;
std::string pluginSlug = json_string_value(pluginSlugJ);
std::string modelSlug = json_string_value(modelSlugJ);

// Get Model
Model *model = pluginGetModel(pluginSlug, modelSlug);
if (!model)
return NULL;

// Create ModuleWidget
ModuleWidget *moduleWidget = model->createModuleWidget();
assert(moduleWidget);
moduleWidget->fromJson(moduleJ);
moduleContainer->addChild(moduleWidget);
return moduleWidget;
}

void RackWidget::pastePresetClipboard() {
const char *moduleJson = glfwGetClipboardString(gWindow);
if (!moduleJson) {
warn("Could not get text from clipboard.");
return;
}

json_error_t error;
json_t *moduleJ = json_loads(moduleJson, 0, &error);
if (moduleJ) {
ModuleWidget *moduleWidget = moduleFromJson(moduleJ);
// Set moduleWidget position
Rect newBox = moduleWidget->box;
newBox.pos = lastMousePos.minus(newBox.size.div(2));
requestModuleBoxNearest(moduleWidget, newBox);

json_decref(moduleJ);
}
else {
warn("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
}
}

void RackWidget::addModule(ModuleWidget *m) {
moduleContainer->addChild(m);
m->create();
@@ -366,16 +421,13 @@ void RackWidget::deleteModule(ModuleWidget *m) {
}

void RackWidget::cloneModule(ModuleWidget *m) {
// Create new module from model
ModuleWidget *clonedModuleWidget = m->model->createModuleWidget();
// JSON serialization is the most straightforward way to do this
json_t *moduleJ = m->toJson();
clonedModuleWidget->fromJson(moduleJ);
ModuleWidget *clonedModuleWidget = moduleFromJson(moduleJ);
json_decref(moduleJ);
Rect clonedBox = clonedModuleWidget->box;
clonedBox.pos = m->box.pos;
requestModuleBoxNearest(clonedModuleWidget, clonedBox);
addModule(clonedModuleWidget);
}

bool RackWidget::requestModuleBox(ModuleWidget *m, Rect box) {
@@ -438,7 +490,7 @@ void RackWidget::step() {

// Autosave every 15 seconds
if (gGuiFrame % (60 * 15) == 0) {
savePatch(assetLocal("autosave.vcv"));
save(assetLocal("autosave.vcv"));
settingsSave(assetLocal("settings.json"));
}



+ 1
- 1
src/app/Toolbar.cpp View File

@@ -45,7 +45,7 @@ struct OpenButton : TooltipIconButton {
tooltipText = "Open (" WINDOW_MOD_KEY_NAME "+O)";
}
void onAction(EventAction &e) override {
gRackWidget->openDialog();
gRackWidget->loadDialog();
}
};



+ 1
- 1
src/engine.cpp View File

@@ -295,9 +295,9 @@ void engineSetParamSmooth(Module *module, int paramId, float value) {
if (smoothModule && !(smoothModule == module && smoothParamId == paramId)) {
smoothModule->params[smoothParamId].value = smoothValue;
}
smoothModule = module;
smoothParamId = paramId;
smoothValue = value;
smoothModule = module;
}

void engineSetSampleRate(float newSampleRate) {


+ 3
- 3
src/main.cpp View File

@@ -73,13 +73,13 @@ int main(int argc, char* argv[]) {
else {
// Load autosave
std::string oldLastPath = gRackWidget->lastPath;
gRackWidget->loadPatch(assetLocal("autosave.vcv"));
gRackWidget->load(assetLocal("autosave.vcv"));
gRackWidget->lastPath = oldLastPath;
}
}
else {
// Load patch
gRackWidget->loadPatch(patchFile);
gRackWidget->load(patchFile);
}

engineStart();
@@ -87,7 +87,7 @@ int main(int argc, char* argv[]) {
engineStop();

// Destroy namespaces
gRackWidget->savePatch(assetLocal("autosave.vcv"));
gRackWidget->save(assetLocal("autosave.vcv"));
settingsSave(assetLocal("settings.json"));
appDestroy();
windowDestroy();


Loading…
Cancel
Save