| @@ -16,6 +16,7 @@ namespace history { | |||||
| struct Scene; | struct Scene; | ||||
| struct Engine; | struct Engine; | ||||
| struct Window; | struct Window; | ||||
| struct PatchManager; | |||||
| /** Contains the application state */ | /** Contains the application state */ | ||||
| @@ -25,6 +26,7 @@ struct App { | |||||
| Engine *engine = NULL; | Engine *engine = NULL; | ||||
| Window *window = NULL; | Window *window = NULL; | ||||
| history::State *history = NULL; | history::State *history = NULL; | ||||
| PatchManager *patch = NULL; | |||||
| App(); | App(); | ||||
| ~App(); | ~App(); | ||||
| @@ -11,46 +11,30 @@ namespace rack { | |||||
| struct RackWidget : OpaqueWidget { | struct RackWidget : OpaqueWidget { | ||||
| FramebufferWidget *rails; | FramebufferWidget *rails; | ||||
| // Only put ModuleWidgets in here | |||||
| Widget *moduleContainer; | Widget *moduleContainer; | ||||
| // Only put CableWidgets in here | |||||
| CableContainer *cableContainer; | CableContainer *cableContainer; | ||||
| /** The currently loaded patch file path */ | |||||
| std::string patchPath; | |||||
| /** The last mouse position in the RackWidget */ | /** The last mouse position in the RackWidget */ | ||||
| math::Vec mousePos; | math::Vec mousePos; | ||||
| RackWidget(); | RackWidget(); | ||||
| ~RackWidget(); | ~RackWidget(); | ||||
| void addModule(ModuleWidget *mw); | |||||
| void addModuleAtMouse(ModuleWidget *mw); | |||||
| /** Removes the module and transfers ownership to the caller */ | |||||
| void removeModule(ModuleWidget *mw); | |||||
| /** Sets a module's box if non-colliding. Returns true if set */ | |||||
| bool requestModuleBox(ModuleWidget *mw, math::Rect requestedBox); | |||||
| /** Moves a module to the closest non-colliding position */ | |||||
| bool requestModuleBoxNearest(ModuleWidget *mw, math::Rect requestedBox); | |||||
| ModuleWidget *getModule(int moduleId); | |||||
| /** Completely clear the rack's modules and cables */ | /** Completely clear the rack's modules and cables */ | ||||
| void clear(); | void clear(); | ||||
| /** Clears the rack and loads the template patch */ | |||||
| void reset(); | |||||
| void loadDialog(); | |||||
| void saveDialog(); | |||||
| void saveAsDialog(); | |||||
| void saveTemplate(); | |||||
| /** If `lastPath` is defined, ask the user to reload it */ | |||||
| void revert(); | |||||
| /** Disconnects all cables */ | |||||
| void disconnect(); | |||||
| 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); | ||||
| void pastePresetClipboard(); | void pastePresetClipboard(); | ||||
| void addModule(ModuleWidget *m); | |||||
| void addModuleAtMouse(ModuleWidget *m); | |||||
| /** Removes the module and transfers ownership to the caller */ | |||||
| void removeModule(ModuleWidget *m); | |||||
| /** Sets a module's box if non-colliding. Returns true if set */ | |||||
| bool requestModuleBox(ModuleWidget *m, math::Rect requestedBox); | |||||
| /** Moves a module to the closest non-colliding position */ | |||||
| bool requestModuleBoxNearest(ModuleWidget *m, math::Rect requestedBox); | |||||
| ModuleWidget *getModule(int moduleId); | |||||
| void step() override; | void step() override; | ||||
| void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
| @@ -38,8 +38,8 @@ inline math::Vec mm2px(math::Vec mm) { | |||||
| 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 math::Vec RACK_GRID_SIZE = math::Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT); | static const math::Vec RACK_GRID_SIZE = math::Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT); | ||||
| extern const std::string PRESET_FILTERS; | |||||
| extern const std::string PATCH_FILTERS; | |||||
| static const std::string PRESET_FILTERS = "VCV Rack module preset (.vcvm):vcvm"; | |||||
| } // namespace rack | } // namespace rack | ||||
| @@ -0,0 +1,35 @@ | |||||
| #pragma once | |||||
| #include "common.hpp" | |||||
| #include <jansson.h> | |||||
| namespace rack { | |||||
| struct PatchManager { | |||||
| /** The currently loaded patch file path */ | |||||
| std::string path; | |||||
| /** Enables certain compatibility behavior based on the value */ | |||||
| int legacy; | |||||
| std::string warningLog; | |||||
| void reset(); | |||||
| void resetDialog(); | |||||
| void save(std::string path); | |||||
| void saveDialog(); | |||||
| void saveAsDialog(); | |||||
| void saveTemplateDialog(); | |||||
| void load(std::string path); | |||||
| void loadDialog(); | |||||
| /** If `lastPath` is defined, ask the user to reload it */ | |||||
| void revertDialog(); | |||||
| /** Disconnects all cables */ | |||||
| void disconnectDialog(); | |||||
| json_t *toJson(); | |||||
| void fromJson(json_t *rootJ); | |||||
| bool isLegacy(int level); | |||||
| }; | |||||
| } // namespace rack | |||||
| @@ -1,6 +1,7 @@ | |||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "event.hpp" | #include "event.hpp" | ||||
| #include "window.hpp" | #include "window.hpp" | ||||
| #include "patch.hpp" | |||||
| #include "engine/Engine.hpp" | #include "engine/Engine.hpp" | ||||
| #include "app/Scene.hpp" | #include "app/Scene.hpp" | ||||
| #include "history.hpp" | #include "history.hpp" | ||||
| @@ -14,12 +15,15 @@ App::App() { | |||||
| history = new history::State; | history = new history::State; | ||||
| window = new Window; | window = new Window; | ||||
| engine = new Engine; | engine = new Engine; | ||||
| patch = new PatchManager; | |||||
| scene = new Scene; | scene = new Scene; | ||||
| event->rootWidget = scene; | event->rootWidget = scene; | ||||
| } | } | ||||
| App::~App() { | App::~App() { | ||||
| // Set pointers to NULL so other objects will segfault when attempting to access them | |||||
| delete scene; scene = NULL; | delete scene; scene = NULL; | ||||
| delete patch; patch = NULL; | |||||
| delete event; event = NULL; | delete event; event = NULL; | ||||
| delete history; history = NULL; | delete history; history = NULL; | ||||
| delete engine; engine = NULL; | delete engine; engine = NULL; | ||||
| @@ -23,26 +23,22 @@ void CableContainer::clear() { | |||||
| void CableContainer::clearPort(PortWidget *port) { | void CableContainer::clearPort(PortWidget *port) { | ||||
| assert(port); | assert(port); | ||||
| // Collect cables to remove | |||||
| std::list<CableWidget*> cables; | |||||
| for (Widget *w : children) { | |||||
| std::list<Widget*> childrenCopy = children; | |||||
| for (Widget *w : childrenCopy) { | |||||
| CableWidget *cw = dynamic_cast<CableWidget*>(w); | CableWidget *cw = dynamic_cast<CableWidget*>(w); | ||||
| assert(cw); | assert(cw); | ||||
| if (cw->inputPort == port || cw->outputPort == port) { | |||||
| cables.push_back(cw); | |||||
| } | |||||
| } | |||||
| // Remove and delete the cables | |||||
| for (CableWidget *cw : cables) { | |||||
| if (cw == incompleteCable) { | |||||
| incompleteCable = NULL; | |||||
| removeChild(cw); | |||||
| } | |||||
| else { | |||||
| removeCable(cw); | |||||
| // Check if cable is connected to port | |||||
| if (cw->inputPort == port || cw->outputPort == port) { | |||||
| if (cw == incompleteCable) { | |||||
| incompleteCable = NULL; | |||||
| removeChild(cw); | |||||
| } | |||||
| else { | |||||
| removeCable(cw); | |||||
| } | |||||
| delete cw; | |||||
| } | } | ||||
| delete cw; | |||||
| } | } | ||||
| } | } | ||||
| @@ -109,6 +105,7 @@ void CableContainer::fromJson(json_t *rootJ, const std::map<int, ModuleWidget*> | |||||
| size_t cableIndex; | size_t cableIndex; | ||||
| json_t *cableJ; | json_t *cableJ; | ||||
| json_array_foreach(rootJ, cableIndex, cableJ) { | json_array_foreach(rootJ, cableIndex, cableJ) { | ||||
| // Create a unserialize cable | |||||
| CableWidget *cw = new CableWidget; | CableWidget *cw = new CableWidget; | ||||
| cw->fromJson(cableJ, moduleWidgets); | cw->fromJson(cableJ, moduleWidgets); | ||||
| if (!cw->isComplete()) { | if (!cw->isComplete()) { | ||||
| @@ -4,6 +4,7 @@ | |||||
| #include "window.hpp" | #include "window.hpp" | ||||
| #include "event.hpp" | #include "event.hpp" | ||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "patch.hpp" | |||||
| #include "settings.hpp" | #include "settings.hpp" | ||||
| @@ -174,8 +175,7 @@ void CableWidget::fromJson(json_t *rootJ, const std::map<int, ModuleWidget*> &mo | |||||
| ModuleWidget *inputModule = inputModuleIt->second; | ModuleWidget *inputModule = inputModuleIt->second; | ||||
| // Set ports | // Set ports | ||||
| // TODO | |||||
| if (false /*legacy && legacy <= 1*/) { | |||||
| if (app()->patch->isLegacy(1)) { | |||||
| // Before 0.6, the index of the "ports" array was the index of the PortWidget in the `outputs` and `inputs` vector. | // Before 0.6, the index of the "ports" array was the index of the PortWidget in the `outputs` and `inputs` vector. | ||||
| setOutputPort(outputModule->outputs[outputId]); | setOutputPort(outputModule->outputs[outputId]); | ||||
| setInputPort(inputModule->inputs[inputId]); | setInputPort(inputModule->inputs[inputId]); | ||||
| @@ -328,7 +328,7 @@ json_t *ModuleWidget::toJson() { | |||||
| // 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())); | ||||
| // Other properties | |||||
| // Merge with module JSON | |||||
| if (module) { | if (module) { | ||||
| json_t *moduleJ = module->toJson(); | json_t *moduleJ = module->toJson(); | ||||
| // Merge with rootJ | // Merge with rootJ | ||||
| @@ -1,16 +1,16 @@ | |||||
| #include <map> | |||||
| #include <algorithm> | |||||
| #include "app/RackWidget.hpp" | #include "app/RackWidget.hpp" | ||||
| #include "app/RackRail.hpp" | #include "app/RackRail.hpp" | ||||
| #include "app/Scene.hpp" | #include "app/Scene.hpp" | ||||
| #include "app/ModuleBrowser.hpp" | #include "app/ModuleBrowser.hpp" | ||||
| #include "osdialog.h" | |||||
| #include "settings.hpp" | #include "settings.hpp" | ||||
| #include "asset.hpp" | |||||
| #include "system.hpp" | |||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "engine/Engine.hpp" | #include "engine/Engine.hpp" | ||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "asset.hpp" | |||||
| #include "patch.hpp" | |||||
| #include "osdialog.h" | |||||
| #include <map> | |||||
| #include <algorithm> | |||||
| namespace rack { | namespace rack { | ||||
| @@ -92,166 +92,10 @@ void RackWidget::clear() { | |||||
| assert(cableContainer->children.empty()); | assert(cableContainer->children.empty()); | ||||
| } | } | ||||
| void RackWidget::reset() { | |||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Clear patch and start over?")) { | |||||
| clear(); | |||||
| app()->scene->scrollWidget->offset = math::Vec(0, 0); | |||||
| // Fails silently if file does not exist | |||||
| load(asset::user("template.vcv")); | |||||
| patchPath = ""; | |||||
| } | |||||
| } | |||||
| void RackWidget::loadDialog() { | |||||
| std::string dir; | |||||
| if (patchPath.empty()) { | |||||
| dir = asset::user("patches"); | |||||
| system::createDirectory(dir); | |||||
| } | |||||
| else { | |||||
| dir = string::directory(patchPath); | |||||
| } | |||||
| osdialog_filters *filters = osdialog_filters_parse(PATCH_FILTERS.c_str()); | |||||
| DEFER({ | |||||
| osdialog_filters_free(filters); | |||||
| }); | |||||
| char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters); | |||||
| if (!path) { | |||||
| // Fail silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| free(path); | |||||
| }); | |||||
| load(path); | |||||
| patchPath = path; | |||||
| } | |||||
| void RackWidget::saveDialog() { | |||||
| if (!patchPath.empty()) { | |||||
| save(patchPath); | |||||
| } | |||||
| else { | |||||
| saveAsDialog(); | |||||
| } | |||||
| } | |||||
| void RackWidget::saveAsDialog() { | |||||
| std::string dir; | |||||
| std::string filename; | |||||
| if (patchPath.empty()) { | |||||
| dir = asset::user("patches"); | |||||
| system::createDirectory(dir); | |||||
| } | |||||
| else { | |||||
| dir = string::directory(patchPath); | |||||
| filename = string::filename(patchPath); | |||||
| } | |||||
| osdialog_filters *filters = osdialog_filters_parse(PATCH_FILTERS.c_str()); | |||||
| DEFER({ | |||||
| osdialog_filters_free(filters); | |||||
| }); | |||||
| char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters); | |||||
| if (!path) { | |||||
| // Fail silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| free(path); | |||||
| }); | |||||
| std::string pathStr = path; | |||||
| if (string::extension(pathStr).empty()) { | |||||
| pathStr += ".vcv"; | |||||
| } | |||||
| save(pathStr); | |||||
| patchPath = pathStr; | |||||
| } | |||||
| void RackWidget::saveTemplate() { | |||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Overwrite template patch?")) { | |||||
| save(asset::user("template.vcv")); | |||||
| } | |||||
| } | |||||
| void RackWidget::save(std::string filename) { | |||||
| INFO("Saving patch %s", filename.c_str()); | |||||
| json_t *rootJ = toJson(); | |||||
| if (!rootJ) | |||||
| return; | |||||
| DEFER({ | |||||
| json_decref(rootJ); | |||||
| }); | |||||
| FILE *file = fopen(filename.c_str(), "w"); | |||||
| if (!file) { | |||||
| // Fail silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| fclose(file); | |||||
| }); | |||||
| json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); | |||||
| } | |||||
| 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; | |||||
| } | |||||
| DEFER({ | |||||
| fclose(file); | |||||
| }); | |||||
| json_error_t error; | |||||
| json_t *rootJ = json_loadf(file, 0, &error); | |||||
| if (!rootJ) { | |||||
| std::string message = string::f("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()); | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| json_decref(rootJ); | |||||
| }); | |||||
| clear(); | |||||
| app()->scene->scrollWidget->offset = math::Vec(0, 0); | |||||
| fromJson(rootJ); | |||||
| } | |||||
| void RackWidget::revert() { | |||||
| if (patchPath.empty()) | |||||
| return; | |||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Revert patch to the last saved state?")) { | |||||
| load(patchPath); | |||||
| } | |||||
| } | |||||
| void RackWidget::disconnect() { | |||||
| if (!osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK_CANCEL, "Remove all patch cables?")) | |||||
| return; | |||||
| cableContainer->clear(); | |||||
| } | |||||
| json_t *RackWidget::toJson() { | json_t *RackWidget::toJson() { | ||||
| // root | // root | ||||
| json_t *rootJ = json_object(); | json_t *rootJ = json_object(); | ||||
| // version | |||||
| json_t *versionJ = json_string(APP_VERSION.c_str()); | |||||
| json_object_set_new(rootJ, "version", versionJ); | |||||
| // modules | // modules | ||||
| json_t *modulesJ = json_array(); | json_t *modulesJ = json_array(); | ||||
| for (Widget *w : moduleContainer->children) { | for (Widget *w : moduleContainer->children) { | ||||
| @@ -278,30 +122,6 @@ json_t *RackWidget::toJson() { | |||||
| } | } | ||||
| void RackWidget::fromJson(json_t *rootJ) { | void RackWidget::fromJson(json_t *rootJ) { | ||||
| std::string message; | |||||
| // version | |||||
| std::string version; | |||||
| json_t *versionJ = json_object_get(rootJ, "version"); | |||||
| if (versionJ) | |||||
| version = json_string_value(versionJ); | |||||
| if (version != APP_VERSION) { | |||||
| INFO("Patch made with Rack version %s, current Rack version is %s", version.c_str(), APP_VERSION.c_str()); | |||||
| } | |||||
| // Detect old patches with ModuleWidget::params/inputs/outputs indices. | |||||
| // (We now use Module::params/inputs/outputs indices.) | |||||
| int legacy = 0; | |||||
| if (string::startsWith(version, "0.3.") || string::startsWith(version, "0.4.") || string::startsWith(version, "0.5.") || version == "" || version == "dev") { | |||||
| legacy = 1; | |||||
| } | |||||
| else if (string::startsWith(version, "0.6.")) { | |||||
| legacy = 2; | |||||
| } | |||||
| if (legacy) { | |||||
| INFO("Loading patch using legacy mode %d", legacy); | |||||
| } | |||||
| // modules | // modules | ||||
| json_t *modulesJ = json_object_get(rootJ, "modules"); | json_t *modulesJ = json_object_get(rootJ, "modules"); | ||||
| if (!modulesJ) | if (!modulesJ) | ||||
| @@ -310,11 +130,6 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
| size_t moduleIndex; | size_t moduleIndex; | ||||
| json_t *moduleJ; | json_t *moduleJ; | ||||
| json_array_foreach(modulesJ, moduleIndex, moduleJ) { | json_array_foreach(modulesJ, moduleIndex, moduleJ) { | ||||
| // Add "legacy" property if in legacy mode | |||||
| if (legacy) { | |||||
| json_object_set(moduleJ, "legacy", json_integer(legacy)); | |||||
| } | |||||
| ModuleWidget *moduleWidget = moduleFromJson(moduleJ); | ModuleWidget *moduleWidget = moduleFromJson(moduleJ); | ||||
| if (moduleWidget) { | if (moduleWidget) { | ||||
| @@ -328,7 +143,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
| double x, y; | double x, y; | ||||
| json_unpack(posJ, "[F, F]", &x, &y); | json_unpack(posJ, "[F, F]", &x, &y); | ||||
| math::Vec pos = math::Vec(x, y); | math::Vec pos = math::Vec(x, y); | ||||
| if (legacy && legacy <= 1) { | |||||
| if (app()->patch->isLegacy(1)) { | |||||
| // Before 0.6, positions were in pixel units | // Before 0.6, positions were in pixel units | ||||
| moduleWidget->box.pos = pos; | moduleWidget->box.pos = pos; | ||||
| } | } | ||||
| @@ -336,7 +151,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
| moduleWidget->box.pos = pos.mult(RACK_GRID_SIZE); | moduleWidget->box.pos = pos.mult(RACK_GRID_SIZE); | ||||
| } | } | ||||
| if (legacy && legacy <= 2) { | |||||
| if (app()->patch->isLegacy(2)) { | |||||
| // Before 1.0, the module ID was the index in the "modules" array | // Before 1.0, the module ID was the index in the "modules" array | ||||
| moduleWidgets[moduleIndex] = moduleWidget; | moduleWidgets[moduleIndex] = moduleWidget; | ||||
| } | } | ||||
| @@ -350,7 +165,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
| json_t *modelSlugJ = json_object_get(moduleJ, "model"); | json_t *modelSlugJ = json_object_get(moduleJ, "model"); | ||||
| std::string pluginSlug = json_string_value(pluginSlugJ); | std::string pluginSlug = json_string_value(pluginSlugJ); | ||||
| std::string modelSlug = json_string_value(modelSlugJ); | std::string modelSlug = json_string_value(modelSlugJ); | ||||
| message += string::f("Could not find module \"%s\" of plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); | |||||
| app()->patch->warningLog += string::f("Could not find module \"%s\" of plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); | |||||
| } | } | ||||
| } | } | ||||
| @@ -361,11 +176,6 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
| cablesJ = json_object_get(rootJ, "wires"); | cablesJ = json_object_get(rootJ, "wires"); | ||||
| if (cablesJ) | if (cablesJ) | ||||
| cableContainer->fromJson(cablesJ, moduleWidgets); | cableContainer->fromJson(cablesJ, moduleWidgets); | ||||
| // Display a message if we have something to say | |||||
| if (!message.empty()) { | |||||
| osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); | |||||
| } | |||||
| } | } | ||||
| void RackWidget::pastePresetClipboard() { | void RackWidget::pastePresetClipboard() { | ||||
| @@ -496,13 +306,6 @@ void RackWidget::step() { | |||||
| rail->box.size = rails->box.size; | rail->box.size = rails->box.size; | ||||
| } | } | ||||
| // Autosave every 15 seconds | |||||
| int frame = app()->window->frame; | |||||
| if (frame > 0 && frame % (60 * 15) == 0) { | |||||
| save(asset::user("autosave.vcv")); | |||||
| settings::save(asset::user("settings.json")); | |||||
| } | |||||
| Widget::step(); | Widget::step(); | ||||
| } | } | ||||
| @@ -1,4 +1,3 @@ | |||||
| #include "osdialog.h" | |||||
| #include "system.hpp" | #include "system.hpp" | ||||
| #include "network.hpp" | #include "network.hpp" | ||||
| #include "app/Scene.hpp" | #include "app/Scene.hpp" | ||||
| @@ -7,6 +6,9 @@ | |||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "history.hpp" | #include "history.hpp" | ||||
| #include "settings.hpp" | #include "settings.hpp" | ||||
| #include "patch.hpp" | |||||
| #include "asset.hpp" | |||||
| #include "osdialog.h" | |||||
| #include <thread> | #include <thread> | ||||
| @@ -53,6 +55,13 @@ void Scene::step() { | |||||
| zoomWidget->box.size = rackWidget->box.size.mult(zoomWidget->zoom); | zoomWidget->box.size = rackWidget->box.size.mult(zoomWidget->zoom); | ||||
| moduleBrowser->box.size = box.size; | moduleBrowser->box.size = box.size; | ||||
| // Autosave every 15 seconds | |||||
| int frame = app()->window->frame; | |||||
| if (frame > 0 && frame % (60 * 15) == 0) { | |||||
| app()->patch->save(asset::user("autosave.vcv")); | |||||
| settings::save(asset::user("settings.json")); | |||||
| } | |||||
| // Set zoom every few frames | // Set zoom every few frames | ||||
| if (app()->window->frame % 10 == 0) | if (app()->window->frame % 10 == 0) | ||||
| zoomWidget->setZoom(settings::zoom); | zoomWidget->setZoom(settings::zoom); | ||||
| @@ -85,7 +94,7 @@ void Scene::onHoverKey(const event::HoverKey &e) { | |||||
| switch (e.key) { | switch (e.key) { | ||||
| case GLFW_KEY_N: { | case GLFW_KEY_N: { | ||||
| if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | ||||
| rackWidget->reset(); | |||||
| app()->patch->resetDialog(); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| } break; | } break; | ||||
| @@ -97,21 +106,21 @@ void Scene::onHoverKey(const event::HoverKey &e) { | |||||
| } break; | } break; | ||||
| case GLFW_KEY_O: { | case GLFW_KEY_O: { | ||||
| if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | ||||
| rackWidget->loadDialog(); | |||||
| app()->patch->loadDialog(); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| if ((e.mods & WINDOW_MOD_MASK) == (WINDOW_MOD_CTRL | GLFW_MOD_SHIFT)) { | if ((e.mods & WINDOW_MOD_MASK) == (WINDOW_MOD_CTRL | GLFW_MOD_SHIFT)) { | ||||
| rackWidget->revert(); | |||||
| app()->patch->revertDialog(); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| } break; | } break; | ||||
| case GLFW_KEY_S: { | case GLFW_KEY_S: { | ||||
| if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | ||||
| rackWidget->saveDialog(); | |||||
| app()->patch->saveDialog(); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| if ((e.mods & WINDOW_MOD_MASK) == (WINDOW_MOD_CTRL | GLFW_MOD_SHIFT)) { | if ((e.mods & WINDOW_MOD_MASK) == (WINDOW_MOD_CTRL | GLFW_MOD_SHIFT)) { | ||||
| rackWidget->saveAsDialog(); | |||||
| app()->patch->saveAsDialog(); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| } break; | } break; | ||||
| @@ -151,7 +160,7 @@ void Scene::onPathDrop(const event::PathDrop &e) { | |||||
| if (e.paths.size() >= 1) { | if (e.paths.size() >= 1) { | ||||
| const std::string &path = e.paths[0]; | const std::string &path = e.paths[0]; | ||||
| if (string::extension(path) == "vcv") { | if (string::extension(path) == "vcv") { | ||||
| rackWidget->load(path); | |||||
| app()->patch->load(path); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,12 +9,12 @@ | |||||
| #include "ui/TextField.hpp" | #include "ui/TextField.hpp" | ||||
| #include "ui/PasswordField.hpp" | #include "ui/PasswordField.hpp" | ||||
| #include "ui/ProgressBar.hpp" | #include "ui/ProgressBar.hpp" | ||||
| #include "app/Scene.hpp" | |||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "settings.hpp" | #include "settings.hpp" | ||||
| #include "helpers.hpp" | #include "helpers.hpp" | ||||
| #include "system.hpp" | #include "system.hpp" | ||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "patch.hpp" | |||||
| #include <thread> | #include <thread> | ||||
| @@ -38,7 +38,7 @@ struct NewItem : MenuItem { | |||||
| rightText = "(" WINDOW_MOD_CTRL_NAME "+N)"; | rightText = "(" WINDOW_MOD_CTRL_NAME "+N)"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->reset(); | |||||
| app()->patch->resetDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -49,7 +49,7 @@ struct OpenItem : MenuItem { | |||||
| rightText = "(" WINDOW_MOD_CTRL_NAME "+O)"; | rightText = "(" WINDOW_MOD_CTRL_NAME "+O)"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->loadDialog(); | |||||
| app()->patch->loadDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -60,7 +60,7 @@ struct SaveItem : MenuItem { | |||||
| rightText = "(" WINDOW_MOD_CTRL_NAME "+S)"; | rightText = "(" WINDOW_MOD_CTRL_NAME "+S)"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->saveDialog(); | |||||
| app()->patch->saveDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -71,7 +71,7 @@ struct SaveAsItem : MenuItem { | |||||
| rightText = "(" WINDOW_MOD_CTRL_NAME "+Shift+S)"; | rightText = "(" WINDOW_MOD_CTRL_NAME "+Shift+S)"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->saveAsDialog(); | |||||
| app()->patch->saveAsDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -81,7 +81,7 @@ struct SaveTemplateItem : MenuItem { | |||||
| text = "Save template"; | text = "Save template"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->saveTemplate(); | |||||
| app()->patch->saveTemplateDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -91,7 +91,7 @@ struct RevertItem : MenuItem { | |||||
| text = "Revert"; | text = "Revert"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->revert(); | |||||
| app()->patch->revertDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -101,7 +101,7 @@ struct DisconnectCablesItem : MenuItem { | |||||
| text = "Disconnect cables"; | text = "Disconnect cables"; | ||||
| } | } | ||||
| void onAction(const event::Action &e) override { | void onAction(const event::Action &e) override { | ||||
| app()->scene->rackWidget->disconnect(); | |||||
| app()->patch->disconnectDialog(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -8,8 +8,5 @@ const std::string APP_NAME = "VCV Rack"; | |||||
| const std::string APP_VERSION = TOSTRING(VERSION); | const std::string APP_VERSION = TOSTRING(VERSION); | ||||
| const std::string API_HOST = "https://api.vcvrack.com"; | const std::string API_HOST = "https://api.vcvrack.com"; | ||||
| const std::string PRESET_FILTERS = "VCV Rack module preset (.vcvm):vcvm"; | |||||
| const std::string PATCH_FILTERS = "VCV Rack patch (.vcv):vcv"; | |||||
| } // namespace rack | } // namespace rack | ||||
| @@ -49,7 +49,7 @@ void init(bool devMode) { | |||||
| systemDir = moduleBuf; | systemDir = moduleBuf; | ||||
| #endif | #endif | ||||
| #if defined ARCH_LIN | #if defined ARCH_LIN | ||||
| // TODO For now, users should launch Rack from their terminal in the system directory | |||||
| // Users should launch Rack from their terminal in the system directory | |||||
| systemDir = "."; | systemDir = "."; | ||||
| #endif | #endif | ||||
| } | } | ||||
| @@ -50,9 +50,9 @@ void Module::fromJson(json_t *rootJ) { | |||||
| json_array_foreach(paramsJ, i, paramJ) { | json_array_foreach(paramsJ, i, paramJ) { | ||||
| uint32_t paramId = i; | uint32_t paramId = i; | ||||
| // Get paramId | // Get paramId | ||||
| // Legacy v0.6.0 to <v1.0 | |||||
| json_t *paramIdJ = json_object_get(paramJ, "paramId"); | json_t *paramIdJ = json_object_get(paramJ, "paramId"); | ||||
| if (paramIdJ) { | if (paramIdJ) { | ||||
| // Legacy v0.6.0 to <v1.0 | |||||
| paramId = json_integer_value(paramIdJ); | paramId = json_integer_value(paramIdJ); | ||||
| } | } | ||||
| @@ -11,6 +11,7 @@ | |||||
| #include "app/Scene.hpp" | #include "app/Scene.hpp" | ||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "patch.hpp" | |||||
| #include "ui.hpp" | #include "ui.hpp" | ||||
| #include <unistd.h> | #include <unistd.h> | ||||
| @@ -93,19 +94,19 @@ int main(int argc, char *argv[]) { | |||||
| settings::save(asset::user("settings.json")); | settings::save(asset::user("settings.json")); | ||||
| settings::skipLoadOnLaunch = false; | settings::skipLoadOnLaunch = false; | ||||
| if (oldSkipLoadOnLaunch && osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, "Rack has recovered from a crash, possibly caused by a faulty module in your patch. Clear your patch and start over?")) { | if (oldSkipLoadOnLaunch && osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, "Rack has recovered from a crash, possibly caused by a faulty module in your patch. Clear your patch and start over?")) { | ||||
| app()->scene->rackWidget->patchPath = ""; | |||||
| app()->patch->path = ""; | |||||
| } | } | ||||
| else { | else { | ||||
| // Load autosave | // Load autosave | ||||
| std::string oldLastPath = app()->scene->rackWidget->patchPath; | |||||
| app()->scene->rackWidget->load(asset::user("autosave.vcv")); | |||||
| app()->scene->rackWidget->patchPath = oldLastPath; | |||||
| std::string oldLastPath = app()->patch->path; | |||||
| app()->patch->load(asset::user("autosave.vcv")); | |||||
| app()->patch->path = oldLastPath; | |||||
| } | } | ||||
| } | } | ||||
| else { | else { | ||||
| // Load patch | // Load patch | ||||
| app()->scene->rackWidget->load(patchFile); | |||||
| app()->scene->rackWidget->patchPath = patchFile; | |||||
| app()->patch->load(patchFile); | |||||
| app()->patch->path = patchFile; | |||||
| } | } | ||||
| INFO("Initialized app"); | INFO("Initialized app"); | ||||
| @@ -115,7 +116,7 @@ int main(int argc, char *argv[]) { | |||||
| app()->engine->stop(); | app()->engine->stop(); | ||||
| // Destroy app | // Destroy app | ||||
| app()->scene->rackWidget->save(asset::user("autosave.vcv")); | |||||
| app()->patch->save(asset::user("autosave.vcv")); | |||||
| settings::save(asset::user("settings.json")); | settings::save(asset::user("settings.json")); | ||||
| appDestroy(); | appDestroy(); | ||||
| INFO("Cleaned up app"); | INFO("Cleaned up app"); | ||||
| @@ -0,0 +1,228 @@ | |||||
| #include "patch.hpp" | |||||
| #include "asset.hpp" | |||||
| #include "system.hpp" | |||||
| #include "app.hpp" | |||||
| #include "app/Scene.hpp" | |||||
| #include "app/RackWidget.hpp" | |||||
| #include "osdialog.h" | |||||
| namespace rack { | |||||
| static const std::string PATCH_FILTERS = "VCV Rack patch (.vcv):vcv"; | |||||
| void PatchManager::reset() { | |||||
| app()->scene->rackWidget->clear(); | |||||
| app()->scene->scrollWidget->offset = math::Vec(0, 0); | |||||
| // Fails silently if file does not exist | |||||
| load(asset::user("template.vcv")); | |||||
| legacy = 0; | |||||
| path = ""; | |||||
| } | |||||
| void PatchManager::resetDialog() { | |||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Clear patch and start over?")) { | |||||
| reset(); | |||||
| } | |||||
| } | |||||
| void PatchManager::save(std::string path) { | |||||
| INFO("Saving patch %s", path.c_str()); | |||||
| json_t *rootJ = toJson(); | |||||
| if (!rootJ) | |||||
| return; | |||||
| DEFER({ | |||||
| json_decref(rootJ); | |||||
| }); | |||||
| FILE *file = std::fopen(path.c_str(), "w"); | |||||
| if (!file) { | |||||
| // Fail silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| std::fclose(file); | |||||
| }); | |||||
| json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); | |||||
| } | |||||
| void PatchManager::saveDialog() { | |||||
| if (!path.empty()) { | |||||
| save(path); | |||||
| } | |||||
| else { | |||||
| saveAsDialog(); | |||||
| } | |||||
| } | |||||
| void PatchManager::saveAsDialog() { | |||||
| std::string dir; | |||||
| std::string filename; | |||||
| if (path.empty()) { | |||||
| dir = asset::user("patches"); | |||||
| system::createDirectory(dir); | |||||
| } | |||||
| else { | |||||
| dir = string::directory(path); | |||||
| filename = string::filename(path); | |||||
| } | |||||
| osdialog_filters *filters = osdialog_filters_parse(PATCH_FILTERS.c_str()); | |||||
| DEFER({ | |||||
| osdialog_filters_free(filters); | |||||
| }); | |||||
| char *pathC = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters); | |||||
| if (!pathC) { | |||||
| // Fail silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| free(pathC); | |||||
| }); | |||||
| std::string pathStr = pathC; | |||||
| if (string::extension(pathStr).empty()) { | |||||
| pathStr += ".vcv"; | |||||
| } | |||||
| save(pathStr); | |||||
| path = pathStr; | |||||
| } | |||||
| void PatchManager::saveTemplateDialog() { | |||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Overwrite template patch?")) { | |||||
| save(asset::user("template.vcv")); | |||||
| } | |||||
| } | |||||
| void PatchManager::load(std::string path) { | |||||
| INFO("Loading patch %s", path.c_str()); | |||||
| FILE *file = std::fopen(path.c_str(), "r"); | |||||
| if (!file) { | |||||
| // Exit silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| std::fclose(file); | |||||
| }); | |||||
| json_error_t error; | |||||
| json_t *rootJ = json_loadf(file, 0, &error); | |||||
| if (!rootJ) { | |||||
| std::string message = string::f("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()); | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| json_decref(rootJ); | |||||
| }); | |||||
| app()->scene->rackWidget->clear(); | |||||
| app()->scene->scrollWidget->offset = math::Vec(0, 0); | |||||
| fromJson(rootJ); | |||||
| } | |||||
| void PatchManager::loadDialog() { | |||||
| std::string dir; | |||||
| if (path.empty()) { | |||||
| dir = asset::user("patches"); | |||||
| system::createDirectory(dir); | |||||
| } | |||||
| else { | |||||
| dir = string::directory(path); | |||||
| } | |||||
| osdialog_filters *filters = osdialog_filters_parse(PATCH_FILTERS.c_str()); | |||||
| DEFER({ | |||||
| osdialog_filters_free(filters); | |||||
| }); | |||||
| char *pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters); | |||||
| if (!pathC) { | |||||
| // Fail silently | |||||
| return; | |||||
| } | |||||
| DEFER({ | |||||
| free(pathC); | |||||
| }); | |||||
| load(pathC); | |||||
| path = pathC; | |||||
| } | |||||
| void PatchManager::revertDialog() { | |||||
| if (path.empty()) | |||||
| return; | |||||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Revert patch to the last saved state?")) { | |||||
| load(path); | |||||
| } | |||||
| } | |||||
| void PatchManager::disconnectDialog() { | |||||
| if (!osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK_CANCEL, "Remove all patch cables?")) | |||||
| return; | |||||
| app()->scene->rackWidget->cableContainer->clear(); | |||||
| } | |||||
| json_t *PatchManager::toJson() { | |||||
| // root | |||||
| json_t *rootJ = json_object(); | |||||
| // version | |||||
| json_t *versionJ = json_string(APP_VERSION.c_str()); | |||||
| json_object_set_new(rootJ, "version", versionJ); | |||||
| // Merge with RackWidget JSON | |||||
| json_t *rackJ = app()->scene->rackWidget->toJson(); | |||||
| // Merge with rootJ | |||||
| json_object_update(rootJ, rackJ); | |||||
| json_decref(rackJ); | |||||
| return rootJ; | |||||
| } | |||||
| void PatchManager::fromJson(json_t *rootJ) { | |||||
| legacy = 0; | |||||
| // version | |||||
| std::string version; | |||||
| json_t *versionJ = json_object_get(rootJ, "version"); | |||||
| if (versionJ) | |||||
| version = json_string_value(versionJ); | |||||
| if (version != APP_VERSION) { | |||||
| INFO("Patch made with Rack version %s, current Rack version is %s", version.c_str(), APP_VERSION.c_str()); | |||||
| } | |||||
| // Detect old patches with ModuleWidget::params/inputs/outputs indices. | |||||
| // (We now use Module::params/inputs/outputs indices.) | |||||
| if (string::startsWith(version, "0.3.") || string::startsWith(version, "0.4.") || string::startsWith(version, "0.5.") || version == "" || version == "dev") { | |||||
| legacy = 1; | |||||
| } | |||||
| else if (string::startsWith(version, "0.6.")) { | |||||
| legacy = 2; | |||||
| } | |||||
| if (legacy) { | |||||
| INFO("Loading patch using legacy mode %d", legacy); | |||||
| } | |||||
| app()->scene->rackWidget->fromJson(rootJ); | |||||
| // Display a message if we have something to say | |||||
| if (!warningLog.empty()) { | |||||
| osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, warningLog.c_str()); | |||||
| } | |||||
| warningLog = ""; | |||||
| } | |||||
| bool PatchManager::isLegacy(int level) { | |||||
| return legacy && legacy <= level; | |||||
| } | |||||
| } // namespace rack | |||||
| @@ -5,6 +5,7 @@ | |||||
| #include "app/ModuleBrowser.hpp" | #include "app/ModuleBrowser.hpp" | ||||
| #include "engine/Engine.hpp" | #include "engine/Engine.hpp" | ||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "patch.hpp" | |||||
| #include <jansson.h> | #include <jansson.h> | ||||
| @@ -53,7 +54,7 @@ static json_t *settingsToJson() { | |||||
| json_object_set_new(rootJ, "sampleRate", sampleRateJ); | json_object_set_new(rootJ, "sampleRate", sampleRateJ); | ||||
| // patchPath | // patchPath | ||||
| json_t *patchPathJ = json_string(app()->scene->rackWidget->patchPath.c_str()); | |||||
| json_t *patchPathJ = json_string(app()->patch->path.c_str()); | |||||
| json_object_set_new(rootJ, "patchPath", patchPathJ); | json_object_set_new(rootJ, "patchPath", patchPathJ); | ||||
| // skipLoadOnLaunch | // skipLoadOnLaunch | ||||
| @@ -128,7 +129,7 @@ static void settingsFromJson(json_t *rootJ) { | |||||
| // patchPath | // patchPath | ||||
| json_t *patchPathJ = json_object_get(rootJ, "patchPath"); | json_t *patchPathJ = json_object_get(rootJ, "patchPath"); | ||||
| if (patchPathJ) | if (patchPathJ) | ||||
| app()->scene->rackWidget->patchPath = json_string_value(patchPathJ); | |||||
| app()->patch->path = json_string_value(patchPathJ); | |||||
| // skipLoadOnLaunch | // skipLoadOnLaunch | ||||
| json_t *skipLoadOnLaunchJ = json_object_get(rootJ, "skipLoadOnLaunch"); | json_t *skipLoadOnLaunchJ = json_object_get(rootJ, "skipLoadOnLaunch"); | ||||
| @@ -5,6 +5,7 @@ | |||||
| #include "gamepad.hpp" | #include "gamepad.hpp" | ||||
| #include "event.hpp" | #include "event.hpp" | ||||
| #include "app.hpp" | #include "app.hpp" | ||||
| #include "patch.hpp" | |||||
| #include <map> | #include <map> | ||||
| #include <queue> | #include <queue> | ||||
| @@ -316,9 +317,9 @@ void Window::run() { | |||||
| windowTitle = APP_NAME; | windowTitle = APP_NAME; | ||||
| windowTitle += " "; | windowTitle += " "; | ||||
| windowTitle += APP_VERSION; | windowTitle += APP_VERSION; | ||||
| if (!app()->scene->rackWidget->patchPath.empty()) { | |||||
| if (!app()->patch->path.empty()) { | |||||
| windowTitle += " - "; | windowTitle += " - "; | ||||
| windowTitle += string::filename(app()->scene->rackWidget->patchPath); | |||||
| windowTitle += string::filename(app()->patch->path); | |||||
| } | } | ||||
| if (windowTitle != internal->lastWindowTitle) { | if (windowTitle != internal->lastWindowTitle) { | ||||
| glfwSetWindowTitle(win, windowTitle.c_str()); | glfwSetWindowTitle(win, windowTitle.c_str()); | ||||