You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

485 lines
11KB

  1. #include <algorithm>
  2. #include <osdialog.h>
  3. #include <patch.hpp>
  4. #include <asset.hpp>
  5. #include <system.hpp>
  6. #include <engine/Engine.hpp>
  7. #include <context.hpp>
  8. #include <app/common.hpp>
  9. #include <app/Scene.hpp>
  10. #include <app/RackWidget.hpp>
  11. #include <history.hpp>
  12. #include <settings.hpp>
  13. #include <fstream>
  14. namespace rack {
  15. static const char PATCH_FILTERS[] = "VCV Rack patch (.vcv):vcv";
  16. PatchManager::PatchManager() {
  17. if (settings::devMode) {
  18. autosavePath = asset::user("autosave");
  19. }
  20. else {
  21. autosavePath = asset::user("autosave-v" + ABI_VERSION);
  22. }
  23. }
  24. PatchManager::~PatchManager() {
  25. cleanAutosave();
  26. }
  27. void PatchManager::launch(std::string pathArg) {
  28. // Load the argument if exists
  29. if (pathArg != "") {
  30. loadAction(pathArg);
  31. return;
  32. }
  33. // Try loading the autosave patch
  34. if (hasAutosave()) {
  35. try {
  36. loadAutosave();
  37. // Keep path and save state as it was stored in patch.json
  38. }
  39. catch (Exception& e) {
  40. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, e.what());
  41. }
  42. return;
  43. }
  44. // Try loading the template patch
  45. loadTemplate();
  46. }
  47. void PatchManager::clear() {
  48. path = "";
  49. if (APP->scene) {
  50. APP->scene->rack->clear();
  51. APP->scene->rackScroll->reset();
  52. }
  53. if (APP->history) {
  54. APP->history->clear();
  55. }
  56. APP->engine->clear();
  57. }
  58. static bool promptClear(std::string text) {
  59. if (APP->history->isSaved())
  60. return true;
  61. if (APP->scene->rack->isEmpty())
  62. return true;
  63. return osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, text.c_str());
  64. }
  65. void PatchManager::save(std::string path) {
  66. INFO("Saving patch %s", path.c_str());
  67. // Save patch.json
  68. saveAutosave();
  69. // Clean up autosave directory (e.g. removed modules)
  70. cleanAutosave();
  71. // Take screenshot (disabled because there is currently no way to quickly view them on any OS or website.)
  72. // APP->window->screenshot(system::join(autosavePath, "screenshot.png"));
  73. double startTime = system::getTime();
  74. // Set compression level to 1 so that a 500MB/s SSD is almost bottlenecked
  75. system::archiveFolder(path, autosavePath, 1);
  76. double endTime = system::getTime();
  77. INFO("Archived patch in %lf seconds", (endTime - startTime));
  78. }
  79. void PatchManager::saveDialog() {
  80. if (path == "") {
  81. saveAsDialog();
  82. return;
  83. }
  84. // Note: If save() fails below, this should probably be reset. But we need it so toJson() doesn't set the "unsaved" property.
  85. APP->history->setSaved();
  86. try {
  87. save(path);
  88. }
  89. catch (Exception& e) {
  90. std::string message = string::f("Could not save patch: %s", e.what());
  91. osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str());
  92. return;
  93. }
  94. }
  95. void PatchManager::saveAsDialog() {
  96. std::string dir;
  97. std::string filename;
  98. if (this->path == "") {
  99. dir = asset::user("patches");
  100. system::createDirectories(dir);
  101. filename = "Untitled.vcv";
  102. }
  103. else {
  104. dir = system::getDirectory(this->path);
  105. filename = system::getFilename(this->path);
  106. }
  107. osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS);
  108. DEFER({osdialog_filters_free(filters);});
  109. char* pathC = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters);
  110. if (!pathC) {
  111. // Cancel silently
  112. return;
  113. }
  114. DEFER({std::free(pathC);});
  115. // Append .vcv extension if no extension was given.
  116. std::string path = pathC;
  117. if (system::getExtension(path) == "") {
  118. path += ".vcv";
  119. }
  120. APP->history->setSaved();
  121. this->path = path;
  122. try {
  123. save(path);
  124. }
  125. catch (Exception& e) {
  126. std::string message = string::f("Could not save patch: %s", e.what());
  127. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
  128. return;
  129. }
  130. pushRecentPath(path);
  131. }
  132. void PatchManager::saveTemplateDialog() {
  133. // Even if <user>/template.vcv doesn't exist, this message is still valid because it overrides the <system>/template.vcv patch.
  134. if (!osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Overwrite template patch?"))
  135. return;
  136. try {
  137. save(asset::templatePath);
  138. }
  139. catch (Exception& e) {
  140. std::string message = string::f("Could not save template patch: %s", e.what());
  141. osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str());
  142. return;
  143. }
  144. }
  145. void PatchManager::saveAutosave() {
  146. std::string patchPath = system::join(autosavePath, "patch.json");
  147. INFO("Saving autosave %s", patchPath.c_str());
  148. json_t* rootJ = toJson();
  149. if (!rootJ)
  150. return;
  151. DEFER({json_decref(rootJ);});
  152. // Write to temporary path and then rename it to the correct path
  153. system::createDirectories(autosavePath);
  154. std::string tmpPath = patchPath + ".tmp";
  155. FILE* file = std::fopen(tmpPath.c_str(), "w");
  156. if (!file) {
  157. // Fail silently
  158. return;
  159. }
  160. json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9));
  161. std::fclose(file);
  162. system::remove(patchPath);
  163. system::rename(tmpPath, patchPath);
  164. }
  165. void PatchManager::cleanAutosave() {
  166. // Remove files and folders in the `autosave/modules` folder that doesn't match a module in the rack.
  167. std::string modulesDir = system::join(autosavePath, "modules");
  168. if (system::isDirectory(modulesDir)) {
  169. for (const std::string& entry : system::getEntries(modulesDir)) {
  170. try {
  171. int64_t moduleId = std::stol(system::getFilename(entry));
  172. // Ignore modules that exist in the rack
  173. if (APP->engine->getModule(moduleId))
  174. continue;
  175. }
  176. catch (std::invalid_argument& e) {
  177. }
  178. // Remove the entry.
  179. system::removeRecursively(entry);
  180. }
  181. }
  182. }
  183. static bool isPatchLegacyV1(std::string path) {
  184. FILE* f = std::fopen(path.c_str(), "rb");
  185. if (!f)
  186. return false;
  187. DEFER({std::fclose(f);});
  188. // All Zstandard frames start with this magic number.
  189. char zstdMagic[] = "\x28\xb5\x2f\xfd";
  190. char buf[4] = {};
  191. std::fread(buf, 1, sizeof(buf), f);
  192. // If the patch file doesn't begin with the magic number, it's a legacy patch.
  193. return std::memcmp(buf, zstdMagic, sizeof(buf)) != 0;
  194. }
  195. void PatchManager::load(std::string path) {
  196. INFO("Loading patch %s", path.c_str());
  197. system::removeRecursively(autosavePath);
  198. system::createDirectories(autosavePath);
  199. if (isPatchLegacyV1(path)) {
  200. // Copy the .vcv file directly to "patch.json".
  201. system::copy(path, system::join(autosavePath, "patch.json"));
  202. }
  203. else {
  204. // Extract the .vcv file as a .tar.zst archive.
  205. double startTime = system::getTime();
  206. system::unarchiveToFolder(path, autosavePath);
  207. double endTime = system::getTime();
  208. INFO("Unarchived patch in %lf seconds", (endTime - startTime));
  209. }
  210. loadAutosave();
  211. }
  212. void PatchManager::loadTemplate() {
  213. try {
  214. load(asset::templatePath);
  215. }
  216. catch (Exception& e) {
  217. // Do nothing because it's okay for the user template to not exist.
  218. try {
  219. load(asset::system("template.vcv"));
  220. }
  221. catch (Exception& e) {
  222. std::string message = string::f("Could not load system template patch, clearing rack: %s", e.what());
  223. osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str());
  224. clear();
  225. }
  226. }
  227. // load() sets the patch's original patch, but we don't want to use that.
  228. this->path = "";
  229. APP->history->setSaved();
  230. }
  231. void PatchManager::loadTemplateDialog() {
  232. if (!promptClear("The current patch is unsaved. Clear it and start a new patch?")) {
  233. return;
  234. }
  235. loadTemplate();
  236. }
  237. bool PatchManager::hasAutosave() {
  238. std::string patchPath = system::join(autosavePath, "patch.json");
  239. INFO("Loading autosave %s", patchPath.c_str());
  240. FILE* file = std::fopen(patchPath.c_str(), "r");
  241. if (!file)
  242. return false;
  243. std::fclose(file);
  244. return true;
  245. }
  246. void PatchManager::loadAutosave() {
  247. std::string patchPath = system::join(autosavePath, "patch.json");
  248. INFO("Loading autosave %s", patchPath.c_str());
  249. FILE* file = std::fopen(patchPath.c_str(), "r");
  250. if (!file)
  251. throw Exception("Could not open autosave patch %s", patchPath.c_str());
  252. DEFER({std::fclose(file);});
  253. json_error_t error;
  254. json_t* rootJ = json_loadf(file, 0, &error);
  255. if (!rootJ)
  256. throw Exception("Failed to load patch. JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
  257. DEFER({json_decref(rootJ);});
  258. fromJson(rootJ);
  259. }
  260. void PatchManager::loadAction(std::string path) {
  261. try {
  262. load(path);
  263. }
  264. catch (Exception& e) {
  265. std::string message = string::f("Could not load patch: %s", e.what());
  266. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
  267. return;
  268. }
  269. this->path = path;
  270. APP->history->setSaved();
  271. pushRecentPath(path);
  272. }
  273. void PatchManager::loadDialog() {
  274. if (!promptClear("The current patch is unsaved. Clear it and open a new patch?"))
  275. return;
  276. std::string dir;
  277. if (this->path == "") {
  278. dir = asset::user("patches");
  279. system::createDirectory(dir);
  280. }
  281. else {
  282. dir = system::getDirectory(this->path);
  283. }
  284. osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS);
  285. DEFER({osdialog_filters_free(filters);});
  286. char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters);
  287. if (!pathC) {
  288. // Fail silently
  289. return;
  290. }
  291. std::string path = pathC;
  292. std::free(pathC);
  293. loadAction(path);
  294. }
  295. void PatchManager::loadPathDialog(std::string path) {
  296. if (!promptClear("The current patch is unsaved. Clear it and open the new patch?"))
  297. return;
  298. loadAction(path);
  299. }
  300. void PatchManager::revertDialog() {
  301. if (path == "")
  302. return;
  303. if (!promptClear("Revert patch to the last saved state?"))
  304. return;
  305. loadAction(path);
  306. }
  307. void PatchManager::pushRecentPath(std::string path) {
  308. auto& recent = settings::recentPatchPaths;
  309. // Remove path from recent patches (if exists)
  310. recent.remove(path);
  311. // Add path to top of recent patches
  312. recent.push_front(path);
  313. // Limit recent patches size
  314. recent.resize(std::min((int) recent.size(), 10));
  315. }
  316. void PatchManager::disconnectDialog() {
  317. APP->scene->rack->clearCablesAction();
  318. }
  319. json_t* PatchManager::toJson() {
  320. // root
  321. json_t* rootJ = json_object();
  322. // version
  323. json_t* versionJ = json_string(APP_VERSION.c_str());
  324. json_object_set_new(rootJ, "version", versionJ);
  325. // path
  326. if (path != "") {
  327. json_t* pathJ = json_string(path.c_str());
  328. json_object_set_new(rootJ, "path", pathJ);
  329. }
  330. // unsaved
  331. if (!APP->history->isSaved())
  332. json_object_set_new(rootJ, "unsaved", json_boolean(true));
  333. // Merge with rootJ
  334. json_t* engineJ = APP->engine->toJson();
  335. json_object_update(rootJ, engineJ);
  336. json_decref(engineJ);
  337. if (APP->scene) {
  338. APP->scene->rack->mergeJson(rootJ);
  339. }
  340. return rootJ;
  341. }
  342. void PatchManager::fromJson(json_t* rootJ) {
  343. clear();
  344. // version
  345. std::string version;
  346. json_t* versionJ = json_object_get(rootJ, "version");
  347. if (versionJ)
  348. version = json_string_value(versionJ);
  349. if (version != APP_VERSION) {
  350. INFO("Patch was made with Rack v%s, current Rack version is v%s", version.c_str(), APP_VERSION.c_str());
  351. }
  352. // path
  353. json_t* pathJ = json_object_get(rootJ, "path");
  354. if (pathJ)
  355. path = json_string_value(pathJ);
  356. else
  357. path = "";
  358. // unsaved
  359. json_t* unsavedJ = json_object_get(rootJ, "unsaved");
  360. if (!unsavedJ)
  361. APP->history->setSaved();
  362. try {
  363. APP->engine->fromJson(rootJ);
  364. if (APP->scene) {
  365. APP->scene->rack->fromJson(rootJ);
  366. }
  367. }
  368. catch (Exception& e) {
  369. warningLog += "\n";
  370. warningLog += e.what();
  371. }
  372. // At this point, ModuleWidgets and CableWidgets should own all Modules and Cables.
  373. // TODO Assert this
  374. // Display a message if we have something to say.
  375. if (warningLog != "") {
  376. osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, warningLog.c_str());
  377. }
  378. warningLog = "";
  379. }
  380. void PatchManager::log(std::string msg) {
  381. warningLog += msg;
  382. warningLog += "\n";
  383. }
  384. } // namespace rack