| @@ -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(); | ||||