@@ -46,6 +46,8 @@ struct SVGPanel; | |||||
static const float RACK_GRID_WIDTH = 15; | static const float RACK_GRID_WIDTH = 15; | ||||
static const float RACK_GRID_HEIGHT = 380; | static const float RACK_GRID_HEIGHT = 380; | ||||
static const Vec RACK_GRID_SIZE = Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT); | 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 { | struct ModuleWidget : OpaqueWidget { | ||||
@@ -68,6 +70,12 @@ struct ModuleWidget : OpaqueWidget { | |||||
virtual json_t *toJson(); | virtual json_t *toJson(); | ||||
virtual void fromJson(json_t *rootJ); | 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 create(); | ||||
virtual void _delete(); | virtual void _delete(); | ||||
@@ -154,17 +162,20 @@ struct RackWidget : OpaqueWidget { | |||||
void clear(); | void clear(); | ||||
/** Clears the rack and loads the template patch */ | /** Clears the rack and loads the template patch */ | ||||
void reset(); | void reset(); | ||||
void openDialog(); | |||||
void loadDialog(); | |||||
void saveDialog(); | void saveDialog(); | ||||
void saveAsDialog(); | void saveAsDialog(); | ||||
/** If `lastPath` is defined, ask the user to reload it */ | /** If `lastPath` is defined, ask the user to reload it */ | ||||
void revert(); | void revert(); | ||||
/** Disconnects all wires */ | /** Disconnects all wires */ | ||||
void disconnect(); | 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(); | json_t *toJson(); | ||||
void fromJson(json_t *rootJ); | 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); | void addModule(ModuleWidget *m); | ||||
/** Removes the module and transfers ownership to the caller */ | /** Removes the module and transfers ownership to the caller */ | ||||
@@ -2,6 +2,8 @@ | |||||
#include "engine.hpp" | #include "engine.hpp" | ||||
#include "plugin.hpp" | #include "plugin.hpp" | ||||
#include "window.hpp" | #include "window.hpp" | ||||
#include "asset.hpp" | |||||
#include "osdialog.h" | |||||
namespace rack { | namespace rack { | ||||
@@ -68,10 +70,6 @@ json_t *ModuleWidget::toJson() { | |||||
json_object_set_new(rootJ, "version", json_string(model->plugin->version.c_str())); | json_object_set_new(rootJ, "version", json_string(model->plugin->version.c_str())); | ||||
// model | // model | ||||
json_object_set_new(rootJ, "model", json_string(model->slug.c_str())); | 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 | // params | ||||
json_t *paramsJ = json_array(); | json_t *paramsJ = json_array(); | ||||
for (ParamWidget *paramWidget : params) { | for (ParamWidget *paramWidget : params) { | ||||
@@ -91,24 +89,42 @@ json_t *ModuleWidget::toJson() { | |||||
} | } | ||||
void ModuleWidget::fromJson(json_t *rootJ) { | 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 | // legacy | ||||
int legacy = 0; | int legacy = 0; | ||||
json_t *legacyJ = json_object_get(rootJ, "legacy"); | json_t *legacyJ = json_object_get(rootJ, "legacy"); | ||||
if (legacyJ) | if (legacyJ) | ||||
legacy = json_integer_value(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 | // params | ||||
json_t *paramsJ = json_object_get(rootJ, "params"); | json_t *paramsJ = json_object_get(rootJ, "params"); | ||||
size_t i; | 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() { | void ModuleWidget::disconnect() { | ||||
for (Port *input : inputs) { | for (Port *input : inputs) { | ||||
gRackWidget->wireContainer->removeAllWires(input); | gRackWidget->wireContainer->removeAllWires(input); | ||||
@@ -268,6 +380,20 @@ void ModuleWidget::onHoverKey(EventHoverKey &e) { | |||||
return; | return; | ||||
} | } | ||||
} break; | } 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: { | case GLFW_KEY_D: { | ||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | if (windowIsModPressed() && !windowIsShiftPressed()) { | ||||
gRackWidget->cloneModule(this); | gRackWidget->cloneModule(this); | ||||
@@ -303,35 +429,63 @@ void ModuleWidget::onDragMove(EventDragMove &e) { | |||||
} | } | ||||
struct DisconnectMenuItem : MenuItem { | |||||
struct ModuleDisconnectItem : MenuItem { | |||||
ModuleWidget *moduleWidget; | ModuleWidget *moduleWidget; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
moduleWidget->disconnect(); | moduleWidget->disconnect(); | ||||
} | } | ||||
}; | }; | ||||
struct ResetMenuItem : MenuItem { | |||||
struct ModuleResetItem : MenuItem { | |||||
ModuleWidget *moduleWidget; | ModuleWidget *moduleWidget; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
moduleWidget->reset(); | moduleWidget->reset(); | ||||
} | } | ||||
}; | }; | ||||
struct RandomizeMenuItem : MenuItem { | |||||
struct ModuleRandomizeItem : MenuItem { | |||||
ModuleWidget *moduleWidget; | ModuleWidget *moduleWidget; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
moduleWidget->randomize(); | 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; | ModuleWidget *moduleWidget; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
gRackWidget->cloneModule(moduleWidget); | gRackWidget->cloneModule(moduleWidget); | ||||
} | } | ||||
}; | }; | ||||
struct DeleteMenuItem : MenuItem { | |||||
struct ModuleDeleteItem : MenuItem { | |||||
ModuleWidget *moduleWidget; | ModuleWidget *moduleWidget; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
gRackWidget->deleteModule(moduleWidget); | gRackWidget->deleteModule(moduleWidget); | ||||
@@ -347,31 +501,53 @@ Menu *ModuleWidget::createContextMenu() { | |||||
menuLabel->text = model->author + " " + model->name + " " + model->plugin->version; | menuLabel->text = model->author + " " + model->name + " " + model->plugin->version; | ||||
menu->addChild(menuLabel); | menu->addChild(menuLabel); | ||||
ResetMenuItem *resetItem = new ResetMenuItem(); | |||||
ModuleResetItem *resetItem = new ModuleResetItem(); | |||||
resetItem->text = "Initialize"; | resetItem->text = "Initialize"; | ||||
resetItem->rightText = WINDOW_MOD_KEY_NAME "+I"; | resetItem->rightText = WINDOW_MOD_KEY_NAME "+I"; | ||||
resetItem->moduleWidget = this; | resetItem->moduleWidget = this; | ||||
menu->addChild(resetItem); | menu->addChild(resetItem); | ||||
RandomizeMenuItem *randomizeItem = new RandomizeMenuItem(); | |||||
ModuleRandomizeItem *randomizeItem = new ModuleRandomizeItem(); | |||||
randomizeItem->text = "Randomize"; | randomizeItem->text = "Randomize"; | ||||
randomizeItem->rightText = WINDOW_MOD_KEY_NAME "+R"; | randomizeItem->rightText = WINDOW_MOD_KEY_NAME "+R"; | ||||
randomizeItem->moduleWidget = this; | randomizeItem->moduleWidget = this; | ||||
menu->addChild(randomizeItem); | menu->addChild(randomizeItem); | ||||
DisconnectMenuItem *disconnectItem = new DisconnectMenuItem(); | |||||
ModuleDisconnectItem *disconnectItem = new ModuleDisconnectItem(); | |||||
disconnectItem->text = "Disconnect cables"; | disconnectItem->text = "Disconnect cables"; | ||||
disconnectItem->rightText = WINDOW_MOD_KEY_NAME "+U"; | disconnectItem->rightText = WINDOW_MOD_KEY_NAME "+U"; | ||||
disconnectItem->moduleWidget = this; | disconnectItem->moduleWidget = this; | ||||
menu->addChild(disconnectItem); | menu->addChild(disconnectItem); | ||||
CloneMenuItem *cloneItem = new CloneMenuItem(); | |||||
ModuleCloneItem *cloneItem = new ModuleCloneItem(); | |||||
cloneItem->text = "Duplicate"; | cloneItem->text = "Duplicate"; | ||||
cloneItem->rightText = WINDOW_MOD_KEY_NAME "+D"; | cloneItem->rightText = WINDOW_MOD_KEY_NAME "+D"; | ||||
cloneItem->moduleWidget = this; | cloneItem->moduleWidget = this; | ||||
menu->addChild(cloneItem); | 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->text = "Delete"; | ||||
deleteItem->rightText = "Backspace/Delete"; | deleteItem->rightText = "Backspace/Delete"; | ||||
deleteItem->moduleWidget = this; | deleteItem->moduleWidget = this; | ||||
@@ -77,7 +77,7 @@ void RackScene::onHoverKey(EventHoverKey &e) { | |||||
} break; | } break; | ||||
case GLFW_KEY_O: { | case GLFW_KEY_O: { | ||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | if (windowIsModPressed() && !windowIsShiftPressed()) { | ||||
gRackWidget->openDialog(); | |||||
gRackWidget->loadDialog(); | |||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
if (windowIsModPressed() && windowIsShiftPressed()) { | if (windowIsModPressed() && windowIsShiftPressed()) { | ||||
@@ -95,6 +95,12 @@ void RackScene::onHoverKey(EventHoverKey &e) { | |||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
} break; | } break; | ||||
case GLFW_KEY_V: { | |||||
if (windowIsModPressed() && !windowIsShiftPressed()) { | |||||
gRackWidget->pastePresetClipboard(); | |||||
e.consumed = true; | |||||
} | |||||
} break; | |||||
case GLFW_KEY_ENTER: | case GLFW_KEY_ENTER: | ||||
case GLFW_KEY_KP_ENTER: { | case GLFW_KEY_KP_ENTER: { | ||||
appModuleBrowserCreate(); | appModuleBrowserCreate(); | ||||
@@ -109,9 +115,9 @@ void RackScene::onHoverKey(EventHoverKey &e) { | |||||
void RackScene::onPathDrop(EventPathDrop &e) { | void RackScene::onPathDrop(EventPathDrop &e) { | ||||
if (e.paths.size() >= 1) { | if (e.paths.size() >= 1) { | ||||
const std::string& firstPath = e.paths.front(); | |||||
const std::string &firstPath = e.paths.front(); | |||||
if (stringExtension(firstPath) == "vcv") { | if (stringExtension(firstPath) == "vcv") { | ||||
gRackWidget->loadPatch(firstPath); | |||||
gRackWidget->load(firstPath); | |||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
} | } | ||||
@@ -12,9 +12,6 @@ | |||||
namespace rack { | namespace rack { | ||||
static const char *FILTERS = "VCV Rack patch (.vcv):vcv"; | |||||
struct ModuleContainer : Widget { | struct ModuleContainer : Widget { | ||||
void draw(NVGcontext *vg) override { | void draw(NVGcontext *vg) override { | ||||
// Draw shadows behind each ModuleWidget first, so the shadow doesn't overlap the front. | // 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?")) { | if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Clear patch and start over?")) { | ||||
clear(); | clear(); | ||||
// Fails silently if file does not exist | // Fails silently if file does not exist | ||||
loadPatch(assetLocal("template.vcv")); | |||||
load(assetLocal("template.vcv")); | |||||
lastPath = ""; | lastPath = ""; | ||||
} | } | ||||
} | } | ||||
void RackWidget::openDialog() { | |||||
void RackWidget::loadDialog() { | |||||
std::string dir; | std::string dir; | ||||
if (lastPath.empty()) { | if (lastPath.empty()) { | ||||
dir = assetLocal("patches"); | dir = assetLocal("patches"); | ||||
@@ -80,10 +77,10 @@ void RackWidget::openDialog() { | |||||
else { | else { | ||||
dir = stringDirectory(lastPath); | 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); | char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters); | ||||
if (path) { | if (path) { | ||||
loadPatch(path); | |||||
load(path); | |||||
lastPath = path; | lastPath = path; | ||||
free(path); | free(path); | ||||
} | } | ||||
@@ -92,7 +89,7 @@ void RackWidget::openDialog() { | |||||
void RackWidget::saveDialog() { | void RackWidget::saveDialog() { | ||||
if (!lastPath.empty()) { | if (!lastPath.empty()) { | ||||
savePatch(lastPath); | |||||
save(lastPath); | |||||
} | } | ||||
else { | else { | ||||
saveAsDialog(); | saveAsDialog(); | ||||
@@ -110,7 +107,7 @@ void RackWidget::saveAsDialog() { | |||||
dir = stringDirectory(lastPath); | dir = stringDirectory(lastPath); | ||||
filename = stringFilename(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); | char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters); | ||||
if (path) { | if (path) { | ||||
@@ -121,19 +118,19 @@ void RackWidget::saveAsDialog() { | |||||
pathStr += ".vcv"; | pathStr += ".vcv"; | ||||
} | } | ||||
savePatch(pathStr); | |||||
save(pathStr); | |||||
lastPath = pathStr; | lastPath = pathStr; | ||||
} | } | ||||
osdialog_filters_free(filters); | 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(); | json_t *rootJ = toJson(); | ||||
if (!rootJ) | if (!rootJ) | ||||
return; | return; | ||||
FILE *file = fopen(path.c_str(), "w"); | |||||
FILE *file = fopen(filename.c_str(), "w"); | |||||
if (file) { | if (file) { | ||||
json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); | json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); | ||||
fclose(file); | fclose(file); | ||||
@@ -142,9 +139,9 @@ void RackWidget::savePatch(std::string path) { | |||||
json_decref(rootJ); | 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) { | if (!file) { | ||||
// Exit silently | // Exit silently | ||||
return; | return; | ||||
@@ -158,7 +155,7 @@ void RackWidget::loadPatch(std::string path) { | |||||
json_decref(rootJ); | json_decref(rootJ); | ||||
} | } | ||||
else { | 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()); | osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); | ||||
} | } | ||||
@@ -169,7 +166,7 @@ void RackWidget::revert() { | |||||
if (lastPath.empty()) | if (lastPath.empty()) | ||||
return; | return; | ||||
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Revert patch to the last saved state?")) { | 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++; | moduleId++; | ||||
// module | // module | ||||
json_t *moduleJ = moduleWidget->toJson(); | 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_array_append_new(modulesJ, moduleJ); | ||||
} | } | ||||
json_object_set_new(rootJ, "modules", modulesJ); | 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_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()); | 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 | // 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) { | void RackWidget::addModule(ModuleWidget *m) { | ||||
moduleContainer->addChild(m); | moduleContainer->addChild(m); | ||||
m->create(); | m->create(); | ||||
@@ -366,16 +421,13 @@ void RackWidget::deleteModule(ModuleWidget *m) { | |||||
} | } | ||||
void RackWidget::cloneModule(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 serialization is the most straightforward way to do this | ||||
json_t *moduleJ = m->toJson(); | json_t *moduleJ = m->toJson(); | ||||
clonedModuleWidget->fromJson(moduleJ); | |||||
ModuleWidget *clonedModuleWidget = moduleFromJson(moduleJ); | |||||
json_decref(moduleJ); | json_decref(moduleJ); | ||||
Rect clonedBox = clonedModuleWidget->box; | Rect clonedBox = clonedModuleWidget->box; | ||||
clonedBox.pos = m->box.pos; | clonedBox.pos = m->box.pos; | ||||
requestModuleBoxNearest(clonedModuleWidget, clonedBox); | requestModuleBoxNearest(clonedModuleWidget, clonedBox); | ||||
addModule(clonedModuleWidget); | |||||
} | } | ||||
bool RackWidget::requestModuleBox(ModuleWidget *m, Rect box) { | bool RackWidget::requestModuleBox(ModuleWidget *m, Rect box) { | ||||
@@ -438,7 +490,7 @@ void RackWidget::step() { | |||||
// Autosave every 15 seconds | // Autosave every 15 seconds | ||||
if (gGuiFrame % (60 * 15) == 0) { | if (gGuiFrame % (60 * 15) == 0) { | ||||
savePatch(assetLocal("autosave.vcv")); | |||||
save(assetLocal("autosave.vcv")); | |||||
settingsSave(assetLocal("settings.json")); | settingsSave(assetLocal("settings.json")); | ||||
} | } | ||||
@@ -45,7 +45,7 @@ struct OpenButton : TooltipIconButton { | |||||
tooltipText = "Open (" WINDOW_MOD_KEY_NAME "+O)"; | tooltipText = "Open (" WINDOW_MOD_KEY_NAME "+O)"; | ||||
} | } | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
gRackWidget->openDialog(); | |||||
gRackWidget->loadDialog(); | |||||
} | } | ||||
}; | }; | ||||
@@ -295,9 +295,9 @@ void engineSetParamSmooth(Module *module, int paramId, float value) { | |||||
if (smoothModule && !(smoothModule == module && smoothParamId == paramId)) { | if (smoothModule && !(smoothModule == module && smoothParamId == paramId)) { | ||||
smoothModule->params[smoothParamId].value = smoothValue; | smoothModule->params[smoothParamId].value = smoothValue; | ||||
} | } | ||||
smoothModule = module; | |||||
smoothParamId = paramId; | smoothParamId = paramId; | ||||
smoothValue = value; | smoothValue = value; | ||||
smoothModule = module; | |||||
} | } | ||||
void engineSetSampleRate(float newSampleRate) { | void engineSetSampleRate(float newSampleRate) { | ||||
@@ -73,13 +73,13 @@ int main(int argc, char* argv[]) { | |||||
else { | else { | ||||
// Load autosave | // Load autosave | ||||
std::string oldLastPath = gRackWidget->lastPath; | std::string oldLastPath = gRackWidget->lastPath; | ||||
gRackWidget->loadPatch(assetLocal("autosave.vcv")); | |||||
gRackWidget->load(assetLocal("autosave.vcv")); | |||||
gRackWidget->lastPath = oldLastPath; | gRackWidget->lastPath = oldLastPath; | ||||
} | } | ||||
} | } | ||||
else { | else { | ||||
// Load patch | // Load patch | ||||
gRackWidget->loadPatch(patchFile); | |||||
gRackWidget->load(patchFile); | |||||
} | } | ||||
engineStart(); | engineStart(); | ||||
@@ -87,7 +87,7 @@ int main(int argc, char* argv[]) { | |||||
engineStop(); | engineStop(); | ||||
// Destroy namespaces | // Destroy namespaces | ||||
gRackWidget->savePatch(assetLocal("autosave.vcv")); | |||||
gRackWidget->save(assetLocal("autosave.vcv")); | |||||
settingsSave(assetLocal("settings.json")); | settingsSave(assetLocal("settings.json")); | ||||
appDestroy(); | appDestroy(); | ||||
windowDestroy(); | windowDestroy(); | ||||