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