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.

453 lines
11KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
  4. *
  5. * This program is free software; you can redistribute it and/or
  6. * modify it under the terms of the GNU General Public License as
  7. * published by the Free Software Foundation; either version 3 of
  8. * the License, or any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * For a full copy of the GNU General Public License see the LICENSE file.
  16. */
  17. /**
  18. * This file is partially based on VCVRack's patch.cpp
  19. * Copyright (C) 2016-2021 VCV.
  20. *
  21. * This program is free software: you can redistribute it and/or
  22. * modify it under the terms of the GNU General Public License as
  23. * published by the Free Software Foundation; either version 3 of
  24. * the License, or (at your option) any later version.
  25. */
  26. #include "CardinalCommon.hpp"
  27. #include "AsyncDialog.hpp"
  28. #include "PluginContext.hpp"
  29. #include "DistrhoPluginUtils.hpp"
  30. #include <asset.hpp>
  31. #include <context.hpp>
  32. #include <history.hpp>
  33. #include <patch.hpp>
  34. #include <settings.hpp>
  35. #include <string.hpp>
  36. #include <system.hpp>
  37. #include <app/Scene.hpp>
  38. #include <window/Window.hpp>
  39. #ifdef NDEBUG
  40. # undef DEBUG
  41. #endif
  42. // for finding special paths
  43. #ifdef ARCH_WIN
  44. # include <shlobj.h>
  45. #else
  46. # include <pwd.h>
  47. # include <unistd.h>
  48. #endif
  49. #ifdef DISTRHO_OS_WASM
  50. # include <emscripten/emscripten.h>
  51. #endif
  52. const std::string CARDINAL_VERSION = "22.12";
  53. START_NAMESPACE_DISTRHO
  54. // -----------------------------------------------------------------------------------------------------------
  55. void handleHostParameterDrag(const CardinalPluginContext* pcontext, uint index, bool started)
  56. {
  57. DISTRHO_SAFE_ASSERT_RETURN(pcontext->ui != nullptr,);
  58. if (started)
  59. {
  60. pcontext->ui->editParameter(index, true);
  61. pcontext->ui->setParameterValue(index, pcontext->parameters[index]);
  62. }
  63. else
  64. {
  65. pcontext->ui->editParameter(index, false);
  66. }
  67. }
  68. // --------------------------------------------------------------------------------------------------------------------
  69. bool CardinalPluginContext::addIdleCallback(IdleCallback* const cb) const
  70. {
  71. if (ui == nullptr)
  72. return false;
  73. ui->addIdleCallback(cb);
  74. return true;
  75. }
  76. void CardinalPluginContext::removeIdleCallback(IdleCallback* const cb) const
  77. {
  78. if (ui == nullptr)
  79. return;
  80. ui->removeIdleCallback(cb);
  81. }
  82. void CardinalPluginContext::writeMidiMessage(const rack::midi::Message& message, const uint8_t channel)
  83. {
  84. if (bypassed)
  85. return;
  86. const size_t size = message.bytes.size();
  87. DISTRHO_SAFE_ASSERT_RETURN(size > 0,);
  88. DISTRHO_SAFE_ASSERT_RETURN(message.frame >= 0,);
  89. MidiEvent event;
  90. event.frame = message.frame;
  91. switch (message.bytes[0] & 0xF0)
  92. {
  93. case 0x80:
  94. case 0x90:
  95. case 0xA0:
  96. case 0xB0:
  97. case 0xE0:
  98. event.size = 3;
  99. break;
  100. case 0xC0:
  101. case 0xD0:
  102. event.size = 2;
  103. break;
  104. case 0xF0:
  105. switch (message.bytes[0] & 0x0F)
  106. {
  107. case 0x0:
  108. case 0x4:
  109. case 0x5:
  110. case 0x7:
  111. case 0x9:
  112. case 0xD:
  113. // unsupported
  114. return;
  115. case 0x1:
  116. case 0x2:
  117. case 0x3:
  118. case 0xE:
  119. event.size = 3;
  120. break;
  121. case 0x6:
  122. case 0x8:
  123. case 0xA:
  124. case 0xB:
  125. case 0xC:
  126. case 0xF:
  127. event.size = 1;
  128. break;
  129. }
  130. break;
  131. default:
  132. // invalid
  133. return;
  134. }
  135. DISTRHO_SAFE_ASSERT_RETURN(size >= event.size,);
  136. std::memcpy(event.data, message.bytes.data(), event.size);
  137. if (channel != 0 && event.data[0] < 0xF0)
  138. event.data[0] |= channel & 0x0F;
  139. plugin->writeMidiEvent(event);
  140. }
  141. END_NAMESPACE_DISTRHO
  142. // --------------------------------------------------------------------------------------------------------------------
  143. namespace rack {
  144. bool isStandalone()
  145. {
  146. return std::strstr(getPluginFormatName(), "Standalone") != nullptr;
  147. }
  148. #ifdef ARCH_WIN
  149. std::string getSpecialPath(const SpecialPath type)
  150. {
  151. int csidl;
  152. switch (type)
  153. {
  154. case kSpecialPathUserProfile:
  155. csidl = CSIDL_PROFILE;
  156. break;
  157. case kSpecialPathCommonProgramFiles:
  158. csidl = CSIDL_PROGRAM_FILES_COMMON;
  159. break;
  160. case kSpecialPathProgramFiles:
  161. csidl = CSIDL_PROGRAM_FILES;
  162. break;
  163. case kSpecialPathAppData:
  164. csidl = CSIDL_APPDATA;
  165. break;
  166. default:
  167. return {};
  168. }
  169. WCHAR path[MAX_PATH + 256];
  170. if (SHGetSpecialFolderPathW(nullptr, path, csidl, FALSE))
  171. return string::UTF16toUTF8(path);
  172. return {};
  173. }
  174. #endif
  175. #ifdef DISTRHO_OS_WASM
  176. char* patchFromURL = nullptr;
  177. char* patchRemoteURL = nullptr;
  178. char* patchStorageSlug = nullptr;
  179. #endif
  180. std::string homeDir()
  181. {
  182. # ifdef ARCH_WIN
  183. return getSpecialPath(kSpecialPathUserProfile);
  184. # else
  185. if (const char* const home = getenv("HOME"))
  186. return home;
  187. if (struct passwd* const pwd = getpwuid(getuid()))
  188. return pwd->pw_dir;
  189. # endif
  190. return {};
  191. }
  192. } // namespace rack
  193. // --------------------------------------------------------------------------------------------------------------------
  194. namespace patchUtils
  195. {
  196. using namespace rack;
  197. #ifndef HEADLESS
  198. static void promptClear(const char* const message, const std::function<void()> action)
  199. {
  200. if (APP->history->isSaved() || APP->scene->rack->hasModules())
  201. return action();
  202. asyncDialog::create(message, action);
  203. }
  204. #endif
  205. void loadDialog()
  206. {
  207. #ifndef HEADLESS
  208. promptClear("The current patch is unsaved. Clear it and open a new patch?", []() {
  209. std::string dir;
  210. if (! APP->patch->path.empty())
  211. dir = system::getDirectory(APP->patch->path);
  212. else
  213. dir = homeDir();
  214. CardinalPluginContext* const pcontext = static_cast<CardinalPluginContext*>(APP);
  215. DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,);
  216. CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(pcontext->ui);
  217. DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,);
  218. DISTRHO_NAMESPACE::FileBrowserOptions opts;
  219. opts.saving = ui->saving = false;
  220. opts.startDir = dir.c_str();
  221. opts.title = "Open patch";
  222. ui->openFileBrowser(opts);
  223. });
  224. #endif
  225. }
  226. void loadPathDialog(const std::string& path, const bool asTemplate)
  227. {
  228. #ifndef HEADLESS
  229. promptClear("The current patch is unsaved. Clear it and open the new patch?", [path, asTemplate]() {
  230. APP->patch->loadAction(path);
  231. if (asTemplate)
  232. {
  233. APP->patch->path = "";
  234. APP->history->setSaved();
  235. }
  236. });
  237. #endif
  238. }
  239. void loadSelectionDialog()
  240. {
  241. app::RackWidget* const w = APP->scene->rack;
  242. std::string selectionDir = asset::user("selections");
  243. system::createDirectories(selectionDir);
  244. async_dialog_filebrowser(false, nullptr, selectionDir.c_str(), "Import selection", [w](char* pathC) {
  245. if (!pathC) {
  246. // No path selected
  247. return;
  248. }
  249. try {
  250. w->loadSelection(pathC);
  251. }
  252. catch (Exception& e) {
  253. async_dialog_message(e.what());
  254. }
  255. std::free(pathC);
  256. });
  257. }
  258. void loadTemplateDialog()
  259. {
  260. #ifndef HEADLESS
  261. promptClear("The current patch is unsaved. Clear it and start a new patch?", []() {
  262. APP->patch->loadTemplate();
  263. });
  264. #endif
  265. }
  266. void revertDialog()
  267. {
  268. #ifndef HEADLESS
  269. if (APP->patch->path.empty())
  270. return;
  271. promptClear("Revert patch to the last saved state?", []{
  272. APP->patch->loadAction(APP->patch->path);
  273. });
  274. #endif
  275. }
  276. void saveDialog(const std::string& path)
  277. {
  278. #ifndef HEADLESS
  279. if (path.empty()) {
  280. return;
  281. }
  282. // Note: If save() fails below, this should probably be reset. But we need it so toJson() doesn't set the "unsaved" property.
  283. APP->history->setSaved();
  284. try {
  285. APP->patch->save(path);
  286. }
  287. catch (Exception& e) {
  288. asyncDialog::create(string::f("Could not save patch: %s", e.what()).c_str());
  289. return;
  290. }
  291. #endif
  292. }
  293. #ifndef HEADLESS
  294. static void saveAsDialog(const bool uncompressed)
  295. {
  296. std::string dir;
  297. if (! APP->patch->path.empty())
  298. dir = system::getDirectory(APP->patch->path);
  299. else
  300. dir = homeDir();
  301. CardinalPluginContext* const pcontext = static_cast<CardinalPluginContext*>(APP);
  302. DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,);
  303. CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(pcontext->ui);
  304. DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,);
  305. DISTRHO_NAMESPACE::FileBrowserOptions opts;
  306. opts.saving = ui->saving = true;
  307. opts.defaultName = "patch.vcv";
  308. opts.startDir = dir.c_str();
  309. opts.title = "Save patch";
  310. ui->savingUncompressed = uncompressed;
  311. ui->openFileBrowser(opts);
  312. }
  313. #endif
  314. void saveAsDialog()
  315. {
  316. #ifndef HEADLESS
  317. saveAsDialog(false);
  318. #endif
  319. }
  320. void saveAsDialogUncompressed()
  321. {
  322. #ifndef HEADLESS
  323. saveAsDialog(true);
  324. #endif
  325. }
  326. void openBrowser(const std::string& url)
  327. {
  328. #ifdef DISTRHO_OS_WASM
  329. EM_ASM({
  330. window.open(UTF8ToString($0), '_blank');
  331. }, url.c_str());
  332. #else
  333. system::openBrowser(url);
  334. #endif
  335. }
  336. }
  337. // --------------------------------------------------------------------------------------------------------------------
  338. void async_dialog_filebrowser(const bool saving,
  339. const char* const defaultName,
  340. const char* const startDir,
  341. const char* const title,
  342. const std::function<void(char* path)> action)
  343. {
  344. #ifndef HEADLESS
  345. CardinalPluginContext* const pcontext = static_cast<CardinalPluginContext*>(APP);
  346. DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,);
  347. CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(pcontext->ui);
  348. DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,);
  349. // only 1 dialog possible at a time
  350. DISTRHO_SAFE_ASSERT_RETURN(ui->filebrowserhandle == nullptr,);
  351. DISTRHO_NAMESPACE::FileBrowserOptions opts;
  352. opts.saving = saving;
  353. opts.defaultName = defaultName;
  354. opts.startDir = startDir;
  355. opts.title = title;
  356. ui->filebrowseraction = action;
  357. ui->filebrowserhandle = fileBrowserCreate(true, pcontext->nativeWindowId, pcontext->window->pixelRatio, opts);
  358. #endif
  359. }
  360. void async_dialog_message(const char* const message)
  361. {
  362. #ifndef HEADLESS
  363. asyncDialog::create(message);
  364. #endif
  365. }
  366. void async_dialog_message(const char* const message, const std::function<void()> action)
  367. {
  368. #ifndef HEADLESS
  369. asyncDialog::create(message, action);
  370. #endif
  371. }
  372. void async_dialog_text_input(const char* const message, const char* const text,
  373. const std::function<void(char* newText)> action)
  374. {
  375. #ifndef HEADLESS
  376. asyncDialog::textInput(message, text, action);
  377. #endif
  378. }