| @@ -16,6 +16,7 @@ namespace history { | |||
| struct Scene; | |||
| struct Engine; | |||
| struct Window; | |||
| struct PatchManager; | |||
| /** Contains the application state */ | |||
| @@ -25,6 +26,7 @@ struct App { | |||
| Engine *engine = NULL; | |||
| Window *window = NULL; | |||
| history::State *history = NULL; | |||
| PatchManager *patch = NULL; | |||
| App(); | |||
| ~App(); | |||
| @@ -11,46 +11,30 @@ namespace rack { | |||
| struct RackWidget : OpaqueWidget { | |||
| FramebufferWidget *rails; | |||
| // Only put ModuleWidgets in here | |||
| Widget *moduleContainer; | |||
| // Only put CableWidgets in here | |||
| CableContainer *cableContainer; | |||
| /** The currently loaded patch file path */ | |||
| std::string patchPath; | |||
| /** The last mouse position in the RackWidget */ | |||
| math::Vec mousePos; | |||
| 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 */ | |||
| 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(); | |||
| void fromJson(json_t *rootJ); | |||
| 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 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_HEIGHT = 380; | |||
| 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 | |||
| @@ -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 "event.hpp" | |||
| #include "window.hpp" | |||
| #include "patch.hpp" | |||
| #include "engine/Engine.hpp" | |||
| #include "app/Scene.hpp" | |||
| #include "history.hpp" | |||
| @@ -14,12 +15,15 @@ App::App() { | |||
| history = new history::State; | |||
| window = new Window; | |||
| engine = new Engine; | |||
| patch = new PatchManager; | |||
| scene = new Scene; | |||
| event->rootWidget = scene; | |||
| } | |||
| App::~App() { | |||
| // Set pointers to NULL so other objects will segfault when attempting to access them | |||
| delete scene; scene = NULL; | |||
| delete patch; patch = NULL; | |||
| delete event; event = NULL; | |||
| delete history; history = NULL; | |||
| delete engine; engine = NULL; | |||
| @@ -23,26 +23,22 @@ void CableContainer::clear() { | |||
| void CableContainer::clearPort(PortWidget *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); | |||
| 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; | |||
| json_t *cableJ; | |||
| json_array_foreach(rootJ, cableIndex, cableJ) { | |||
| // Create a unserialize cable | |||
| CableWidget *cw = new CableWidget; | |||
| cw->fromJson(cableJ, moduleWidgets); | |||
| if (!cw->isComplete()) { | |||
| @@ -4,6 +4,7 @@ | |||
| #include "window.hpp" | |||
| #include "event.hpp" | |||
| #include "app.hpp" | |||
| #include "patch.hpp" | |||
| #include "settings.hpp" | |||
| @@ -174,8 +175,7 @@ void CableWidget::fromJson(json_t *rootJ, const std::map<int, ModuleWidget*> &mo | |||
| ModuleWidget *inputModule = inputModuleIt->second; | |||
| // 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. | |||
| setOutputPort(outputModule->outputs[outputId]); | |||
| setInputPort(inputModule->inputs[inputId]); | |||
| @@ -328,7 +328,7 @@ json_t *ModuleWidget::toJson() { | |||
| // model | |||
| json_object_set_new(rootJ, "model", json_string(model->slug.c_str())); | |||
| // Other properties | |||
| // Merge with module JSON | |||
| if (module) { | |||
| json_t *moduleJ = module->toJson(); | |||
| // Merge with rootJ | |||
| @@ -1,16 +1,16 @@ | |||
| #include <map> | |||
| #include <algorithm> | |||
| #include "app/RackWidget.hpp" | |||
| #include "app/RackRail.hpp" | |||
| #include "app/Scene.hpp" | |||
| #include "app/ModuleBrowser.hpp" | |||
| #include "osdialog.h" | |||
| #include "settings.hpp" | |||
| #include "asset.hpp" | |||
| #include "system.hpp" | |||
| #include "plugin.hpp" | |||
| #include "engine/Engine.hpp" | |||
| #include "app.hpp" | |||
| #include "asset.hpp" | |||
| #include "patch.hpp" | |||
| #include "osdialog.h" | |||
| #include <map> | |||
| #include <algorithm> | |||
| namespace rack { | |||
| @@ -92,166 +92,10 @@ void RackWidget::clear() { | |||
| 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() { | |||
| // root | |||
| json_t *rootJ = json_object(); | |||
| // version | |||
| json_t *versionJ = json_string(APP_VERSION.c_str()); | |||
| json_object_set_new(rootJ, "version", versionJ); | |||
| // modules | |||
| json_t *modulesJ = json_array(); | |||
| for (Widget *w : moduleContainer->children) { | |||
| @@ -278,30 +122,6 @@ json_t *RackWidget::toJson() { | |||
| } | |||
| 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 | |||
| json_t *modulesJ = json_object_get(rootJ, "modules"); | |||
| if (!modulesJ) | |||
| @@ -310,11 +130,6 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
| size_t moduleIndex; | |||
| json_t *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); | |||
| if (moduleWidget) { | |||
| @@ -328,7 +143,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
| double x, y; | |||
| json_unpack(posJ, "[F, F]", &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 | |||
| moduleWidget->box.pos = pos; | |||
| } | |||
| @@ -336,7 +151,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
| 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 | |||
| moduleWidgets[moduleIndex] = moduleWidget; | |||
| } | |||
| @@ -350,7 +165,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
| json_t *modelSlugJ = json_object_get(moduleJ, "model"); | |||
| std::string pluginSlug = json_string_value(pluginSlugJ); | |||
| 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"); | |||
| if (cablesJ) | |||
| 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() { | |||
| @@ -496,13 +306,6 @@ void RackWidget::step() { | |||
| 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(); | |||
| } | |||
| @@ -1,4 +1,3 @@ | |||
| #include "osdialog.h" | |||
| #include "system.hpp" | |||
| #include "network.hpp" | |||
| #include "app/Scene.hpp" | |||
| @@ -7,6 +6,9 @@ | |||
| #include "app.hpp" | |||
| #include "history.hpp" | |||
| #include "settings.hpp" | |||
| #include "patch.hpp" | |||
| #include "asset.hpp" | |||
| #include "osdialog.h" | |||
| #include <thread> | |||
| @@ -53,6 +55,13 @@ void Scene::step() { | |||
| zoomWidget->box.size = rackWidget->box.size.mult(zoomWidget->zoom); | |||
| 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 | |||
| if (app()->window->frame % 10 == 0) | |||
| zoomWidget->setZoom(settings::zoom); | |||
| @@ -85,7 +94,7 @@ void Scene::onHoverKey(const event::HoverKey &e) { | |||
| switch (e.key) { | |||
| case GLFW_KEY_N: { | |||
| if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | |||
| rackWidget->reset(); | |||
| app()->patch->resetDialog(); | |||
| e.consume(this); | |||
| } | |||
| } break; | |||
| @@ -97,21 +106,21 @@ void Scene::onHoverKey(const event::HoverKey &e) { | |||
| } break; | |||
| case GLFW_KEY_O: { | |||
| if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | |||
| rackWidget->loadDialog(); | |||
| app()->patch->loadDialog(); | |||
| e.consume(this); | |||
| } | |||
| if ((e.mods & WINDOW_MOD_MASK) == (WINDOW_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
| rackWidget->revert(); | |||
| app()->patch->revertDialog(); | |||
| e.consume(this); | |||
| } | |||
| } break; | |||
| case GLFW_KEY_S: { | |||
| if ((e.mods & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | |||
| rackWidget->saveDialog(); | |||
| app()->patch->saveDialog(); | |||
| e.consume(this); | |||
| } | |||
| if ((e.mods & WINDOW_MOD_MASK) == (WINDOW_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
| rackWidget->saveAsDialog(); | |||
| app()->patch->saveAsDialog(); | |||
| e.consume(this); | |||
| } | |||
| } break; | |||
| @@ -151,7 +160,7 @@ void Scene::onPathDrop(const event::PathDrop &e) { | |||
| if (e.paths.size() >= 1) { | |||
| const std::string &path = e.paths[0]; | |||
| if (string::extension(path) == "vcv") { | |||
| rackWidget->load(path); | |||
| app()->patch->load(path); | |||
| e.consume(this); | |||
| } | |||
| } | |||
| @@ -9,12 +9,12 @@ | |||
| #include "ui/TextField.hpp" | |||
| #include "ui/PasswordField.hpp" | |||
| #include "ui/ProgressBar.hpp" | |||
| #include "app/Scene.hpp" | |||
| #include "app.hpp" | |||
| #include "settings.hpp" | |||
| #include "helpers.hpp" | |||
| #include "system.hpp" | |||
| #include "plugin.hpp" | |||
| #include "patch.hpp" | |||
| #include <thread> | |||
| @@ -38,7 +38,7 @@ struct NewItem : MenuItem { | |||
| rightText = "(" WINDOW_MOD_CTRL_NAME "+N)"; | |||
| } | |||
| 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)"; | |||
| } | |||
| 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)"; | |||
| } | |||
| 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)"; | |||
| } | |||
| void onAction(const event::Action &e) override { | |||
| app()->scene->rackWidget->saveAsDialog(); | |||
| app()->patch->saveAsDialog(); | |||
| } | |||
| }; | |||
| @@ -81,7 +81,7 @@ struct SaveTemplateItem : MenuItem { | |||
| text = "Save template"; | |||
| } | |||
| void onAction(const event::Action &e) override { | |||
| app()->scene->rackWidget->saveTemplate(); | |||
| app()->patch->saveTemplateDialog(); | |||
| } | |||
| }; | |||
| @@ -91,7 +91,7 @@ struct RevertItem : MenuItem { | |||
| text = "Revert"; | |||
| } | |||
| void onAction(const event::Action &e) override { | |||
| app()->scene->rackWidget->revert(); | |||
| app()->patch->revertDialog(); | |||
| } | |||
| }; | |||
| @@ -101,7 +101,7 @@ struct DisconnectCablesItem : MenuItem { | |||
| text = "Disconnect cables"; | |||
| } | |||
| 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 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 | |||
| @@ -49,7 +49,7 @@ void init(bool devMode) { | |||
| systemDir = moduleBuf; | |||
| #endif | |||
| #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 = "."; | |||
| #endif | |||
| } | |||
| @@ -50,9 +50,9 @@ void Module::fromJson(json_t *rootJ) { | |||
| json_array_foreach(paramsJ, i, paramJ) { | |||
| uint32_t paramId = i; | |||
| // Get paramId | |||
| // Legacy v0.6.0 to <v1.0 | |||
| json_t *paramIdJ = json_object_get(paramJ, "paramId"); | |||
| if (paramIdJ) { | |||
| // Legacy v0.6.0 to <v1.0 | |||
| paramId = json_integer_value(paramIdJ); | |||
| } | |||
| @@ -11,6 +11,7 @@ | |||
| #include "app/Scene.hpp" | |||
| #include "plugin.hpp" | |||
| #include "app.hpp" | |||
| #include "patch.hpp" | |||
| #include "ui.hpp" | |||
| #include <unistd.h> | |||
| @@ -93,19 +94,19 @@ int main(int argc, char *argv[]) { | |||
| settings::save(asset::user("settings.json")); | |||
| 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?")) { | |||
| app()->scene->rackWidget->patchPath = ""; | |||
| app()->patch->path = ""; | |||
| } | |||
| else { | |||
| // 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 { | |||
| // Load patch | |||
| app()->scene->rackWidget->load(patchFile); | |||
| app()->scene->rackWidget->patchPath = patchFile; | |||
| app()->patch->load(patchFile); | |||
| app()->patch->path = patchFile; | |||
| } | |||
| INFO("Initialized app"); | |||
| @@ -115,7 +116,7 @@ int main(int argc, char *argv[]) { | |||
| app()->engine->stop(); | |||
| // Destroy app | |||
| app()->scene->rackWidget->save(asset::user("autosave.vcv")); | |||
| app()->patch->save(asset::user("autosave.vcv")); | |||
| settings::save(asset::user("settings.json")); | |||
| appDestroy(); | |||
| 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 "engine/Engine.hpp" | |||
| #include "app.hpp" | |||
| #include "patch.hpp" | |||
| #include <jansson.h> | |||
| @@ -53,7 +54,7 @@ static json_t *settingsToJson() { | |||
| json_object_set_new(rootJ, "sampleRate", sampleRateJ); | |||
| // 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); | |||
| // skipLoadOnLaunch | |||
| @@ -128,7 +129,7 @@ static void settingsFromJson(json_t *rootJ) { | |||
| // patchPath | |||
| json_t *patchPathJ = json_object_get(rootJ, "patchPath"); | |||
| if (patchPathJ) | |||
| app()->scene->rackWidget->patchPath = json_string_value(patchPathJ); | |||
| app()->patch->path = json_string_value(patchPathJ); | |||
| // skipLoadOnLaunch | |||
| json_t *skipLoadOnLaunchJ = json_object_get(rootJ, "skipLoadOnLaunch"); | |||
| @@ -5,6 +5,7 @@ | |||
| #include "gamepad.hpp" | |||
| #include "event.hpp" | |||
| #include "app.hpp" | |||
| #include "patch.hpp" | |||
| #include <map> | |||
| #include <queue> | |||
| @@ -316,9 +317,9 @@ void Window::run() { | |||
| windowTitle = APP_NAME; | |||
| windowTitle += " "; | |||
| windowTitle += APP_VERSION; | |||
| if (!app()->scene->rackWidget->patchPath.empty()) { | |||
| if (!app()->patch->path.empty()) { | |||
| windowTitle += " - "; | |||
| windowTitle += string::filename(app()->scene->rackWidget->patchPath); | |||
| windowTitle += string::filename(app()->patch->path); | |||
| } | |||
| if (windowTitle != internal->lastWindowTitle) { | |||
| glfwSetWindowTitle(win, windowTitle.c_str()); | |||