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.

1250 lines
43KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021-2023 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. #include <app/MenuBar.hpp>
  18. #include <app/Scene.hpp>
  19. #include <asset.hpp>
  20. #include <context.hpp>
  21. #include <engine/Engine.hpp>
  22. #include <helpers.hpp>
  23. #include <patch.hpp>
  24. #include <settings.hpp>
  25. #include <string.hpp>
  26. #include <system.hpp>
  27. #include <ui/Button.hpp>
  28. #include <ui/MenuItem.hpp>
  29. #include <ui/MenuSeparator.hpp>
  30. #include <window/Window.hpp>
  31. #ifdef DPF_RUNTIME_TESTING
  32. # include <plugin.hpp>
  33. #endif
  34. #ifdef DISTRHO_OS_WASM
  35. # include <ui/Button.hpp>
  36. # include <ui/Label.hpp>
  37. # include <ui/MenuOverlay.hpp>
  38. # include <ui/SequentialLayout.hpp>
  39. # include <emscripten/emscripten.h>
  40. #endif
  41. #ifdef NDEBUG
  42. # undef DEBUG
  43. #endif
  44. #include "Application.hpp"
  45. #include "AsyncDialog.hpp"
  46. #include "CardinalCommon.hpp"
  47. #include "PluginContext.hpp"
  48. #include "WindowParameters.hpp"
  49. #include "extra/Base64.hpp"
  50. #ifndef DISTRHO_OS_WASM
  51. # include "extra/SharedResourcePointer.hpp"
  52. #endif
  53. #ifndef HEADLESS
  54. # include "extra/ScopedValueSetter.hpp"
  55. #endif
  56. namespace rack {
  57. namespace engine {
  58. void Engine_setAboutToClose(Engine*);
  59. void Engine_setRemoteDetails(Engine*, remoteUtils::RemoteDetails*);
  60. }
  61. namespace window {
  62. void WindowSetPluginUI(Window* window, DISTRHO_NAMESPACE::UI* ui);
  63. void WindowSetMods(Window* window, int mods);
  64. void WindowSetInternalSize(rack::window::Window* window, math::Vec size);
  65. }
  66. }
  67. START_NAMESPACE_DISTRHO
  68. // --------------------------------------------------------------------------------------------------------------------
  69. #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  70. uint32_t Plugin::getBufferSize() const noexcept { return 0; }
  71. double Plugin::getSampleRate() const noexcept { return 0.0; }
  72. const char* Plugin::getBundlePath() const noexcept { return nullptr; }
  73. bool Plugin::isSelfTestInstance() const noexcept { return false; }
  74. bool Plugin::writeMidiEvent(const MidiEvent&) noexcept { return false; }
  75. #endif
  76. // --------------------------------------------------------------------------------------------------------------------
  77. #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
  78. struct WasmWelcomeDialog : rack::widget::OpaqueWidget
  79. {
  80. static const constexpr float margin = 10;
  81. static const constexpr float buttonWidth = 110;
  82. WasmWelcomeDialog()
  83. {
  84. using rack::ui::Button;
  85. using rack::ui::Label;
  86. using rack::ui::MenuOverlay;
  87. using rack::ui::SequentialLayout;
  88. box.size = rack::math::Vec(550, 310);
  89. SequentialLayout* const layout = new SequentialLayout;
  90. layout->box.pos = rack::math::Vec(0, 0);
  91. layout->box.size = box.size;
  92. layout->orientation = SequentialLayout::VERTICAL_ORIENTATION;
  93. layout->margin = rack::math::Vec(margin, margin);
  94. layout->spacing = rack::math::Vec(margin, margin);
  95. layout->wrap = false;
  96. addChild(layout);
  97. SequentialLayout* const contentLayout = new SequentialLayout;
  98. contentLayout->spacing = rack::math::Vec(margin, margin);
  99. layout->addChild(contentLayout);
  100. SequentialLayout* const buttonLayout = new SequentialLayout;
  101. buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT;
  102. buttonLayout->box.size = box.size;
  103. buttonLayout->spacing = rack::math::Vec(margin, margin);
  104. layout->addChild(buttonLayout);
  105. Label* const label = new Label;
  106. label->box.size.x = box.size.x - 2*margin;
  107. label->box.size.y = box.size.y - 2*margin - 40;
  108. label->fontSize = 20;
  109. label->text = ""
  110. "Welcome to Cardinal on the Web!\n"
  111. "\n"
  112. "If using mobile/touch devices, please note:\n"
  113. " - Single quick press does simple mouse click\n"
  114. " - Press & move does click & drag action\n"
  115. " - Press & hold does right-click (and opens module browser)\n"
  116. "\n"
  117. "Still a bit experimental, so proceed with caution.\n"
  118. "Have fun!";
  119. contentLayout->addChild(label);
  120. struct JoinDiscussionButton : Button {
  121. WasmWelcomeDialog* dialog;
  122. void onAction(const ActionEvent& e) override {
  123. patchUtils::openBrowser("https://github.com/DISTRHO/Cardinal/issues/287");
  124. dialog->getParent()->requestDelete();
  125. }
  126. };
  127. JoinDiscussionButton* const discussionButton = new JoinDiscussionButton;
  128. discussionButton->box.size.x = buttonWidth;
  129. discussionButton->text = "Join discussion";
  130. discussionButton->dialog = this;
  131. buttonLayout->addChild(discussionButton);
  132. struct DismissButton : Button {
  133. WasmWelcomeDialog* dialog;
  134. void onAction(const ActionEvent& e) override {
  135. dialog->getParent()->requestDelete();
  136. }
  137. };
  138. DismissButton* const dismissButton = new DismissButton;
  139. dismissButton->box.size.x = buttonWidth;
  140. dismissButton->text = "Dismiss";
  141. dismissButton->dialog = this;
  142. buttonLayout->addChild(dismissButton);
  143. MenuOverlay* const overlay = new MenuOverlay;
  144. overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33);
  145. overlay->addChild(this);
  146. APP->scene->addChild(overlay);
  147. }
  148. void step() override
  149. {
  150. OpaqueWidget::step();
  151. box.pos = parent->box.size.minus(box.size).div(2).round();
  152. }
  153. void draw(const DrawArgs& args) override
  154. {
  155. bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0);
  156. Widget::draw(args);
  157. }
  158. };
  159. struct WasmRemotePatchLoadingDialog : rack::widget::OpaqueWidget
  160. {
  161. static const constexpr float margin = 10;
  162. rack::ui::MenuOverlay* overlay;
  163. WasmRemotePatchLoadingDialog(const bool isFromPatchStorage)
  164. {
  165. using rack::ui::Label;
  166. using rack::ui::MenuOverlay;
  167. using rack::ui::SequentialLayout;
  168. box.size = rack::math::Vec(300, 40);
  169. SequentialLayout* const layout = new SequentialLayout;
  170. layout->box.pos = rack::math::Vec(0, 0);
  171. layout->box.size = box.size;
  172. layout->alignment = SequentialLayout::CENTER_ALIGNMENT;
  173. layout->margin = rack::math::Vec(margin, margin);
  174. layout->spacing = rack::math::Vec(margin, margin);
  175. layout->wrap = false;
  176. addChild(layout);
  177. Label* const label = new Label;
  178. label->box.size.x = box.size.x - 2*margin;
  179. label->box.size.y = box.size.y - 2*margin;
  180. label->fontSize = 16;
  181. label->text = isFromPatchStorage
  182. ? "Loading patch from PatchStorage...\n"
  183. : "Loading remote patch...\n";
  184. layout->addChild(label);
  185. overlay = new MenuOverlay;
  186. overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33);
  187. overlay->addChild(this);
  188. APP->scene->addChild(overlay);
  189. }
  190. void step() override
  191. {
  192. OpaqueWidget::step();
  193. box.pos = parent->box.size.minus(box.size).div(2).round();
  194. }
  195. void draw(const DrawArgs& args) override
  196. {
  197. bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0);
  198. Widget::draw(args);
  199. }
  200. };
  201. static void downloadRemotePatchFailed(const char* const filename)
  202. {
  203. d_stdout("downloadRemotePatchFailed %s", filename);
  204. CardinalPluginContext* const context = static_cast<CardinalPluginContext*>(APP);
  205. CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(context->ui);
  206. if (ui->psDialog != nullptr)
  207. {
  208. ui->psDialog->overlay->requestDelete();
  209. ui->psDialog = nullptr;
  210. asyncDialog::create("Failed to fetch remote patch");
  211. }
  212. using namespace rack;
  213. context->patch->templatePath = system::join(asset::systemDir, "init/wasm.vcv"); // FIXME
  214. context->patch->loadTemplate();
  215. context->scene->rackScroll->reset();
  216. }
  217. static void downloadRemotePatchSucceeded(const char* const filename)
  218. {
  219. d_stdout("downloadRemotePatchSucceeded %s | %s", filename, APP->patch->templatePath.c_str());
  220. CardinalPluginContext* const context = static_cast<CardinalPluginContext*>(APP);
  221. CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(context->ui);
  222. ui->psDialog->overlay->requestDelete();
  223. ui->psDialog = nullptr;
  224. if (FILE* f = fopen(filename, "r"))
  225. {
  226. uint8_t buf[8] = {};
  227. fread(buf, 8, 1, f);
  228. d_stdout("read patch %x %x %x %x %x %x %x %x",
  229. buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7]);
  230. fclose(f);
  231. }
  232. try {
  233. context->patch->load(filename);
  234. } catch (rack::Exception& e) {
  235. const std::string message = rack::string::f("Could not load patch: %s", e.what());
  236. asyncDialog::create(message.c_str());
  237. return;
  238. }
  239. context->scene->rackScroll->reset();
  240. context->patch->path = "";
  241. context->history->setSaved();
  242. }
  243. #endif
  244. // -----------------------------------------------------------------------------------------------------------
  245. class CardinalUI : public CardinalBaseUI,
  246. public WindowParametersCallback
  247. {
  248. #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  249. #ifdef DISTRHO_OS_WASM
  250. ScopedPointer<Initializer> fInitializer;
  251. #else
  252. SharedResourcePointer<Initializer> fInitializer;
  253. #endif
  254. std::string fAutosavePath;
  255. #endif
  256. rack::math::Vec lastMousePos;
  257. WindowParameters windowParameters;
  258. int rateLimitStep = 0;
  259. #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
  260. int8_t counterForFirstIdlePoint = 0;
  261. #endif
  262. #ifdef DPF_RUNTIME_TESTING
  263. bool inSelfTest = false;
  264. #endif
  265. struct ScopedContext {
  266. CardinalPluginContext* const context;
  267. ScopedContext(CardinalUI* const ui)
  268. : context(ui->context)
  269. {
  270. rack::contextSet(context);
  271. WindowParametersRestore(context->window);
  272. }
  273. ScopedContext(CardinalUI* const ui, const int mods)
  274. : context(ui->context)
  275. {
  276. rack::contextSet(context);
  277. rack::window::WindowSetMods(context->window, mods);
  278. WindowParametersRestore(context->window);
  279. }
  280. ~ScopedContext()
  281. {
  282. if (context->window != nullptr)
  283. WindowParametersSave(context->window);
  284. }
  285. };
  286. public:
  287. CardinalUI()
  288. : CardinalBaseUI(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT),
  289. #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  290. #ifdef DISTRHO_OS_WASM
  291. fInitializer(new Initializer(static_cast<const CardinalBasePlugin*>(nullptr), this)),
  292. #else
  293. fInitializer(static_cast<const CardinalBasePlugin*>(nullptr), this),
  294. #endif
  295. #endif
  296. lastMousePos()
  297. {
  298. rack::contextSet(context);
  299. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  300. // create unique temporary path for this instance
  301. try {
  302. char uidBuf[24];
  303. const std::string tmp = rack::system::getTempDirectory();
  304. for (int i=1;; ++i)
  305. {
  306. std::snprintf(uidBuf, sizeof(uidBuf), "Cardinal.%04d", i);
  307. const std::string trypath = rack::system::join(tmp, uidBuf);
  308. if (! rack::system::exists(trypath))
  309. {
  310. if (rack::system::createDirectories(trypath))
  311. fAutosavePath = trypath;
  312. break;
  313. }
  314. }
  315. } DISTRHO_SAFE_EXCEPTION("create unique temporary path");
  316. const float sampleRate = 60; // fake audio running at 60 fps
  317. rack::settings::sampleRate = sampleRate;
  318. context->dataIns = new const float*[DISTRHO_PLUGIN_NUM_INPUTS];
  319. context->dataOuts = new float*[DISTRHO_PLUGIN_NUM_OUTPUTS];
  320. for (uint32_t i=0; i<DISTRHO_PLUGIN_NUM_INPUTS;++i)
  321. {
  322. float** const bufferptr = const_cast<float**>(&context->dataIns[i]);
  323. *bufferptr = new float[1];
  324. (*bufferptr)[0] = 0.f;
  325. }
  326. for (uint32_t i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS;++i)
  327. context->dataOuts[i] = new float[1];
  328. context->bufferSize = 1;
  329. context->sampleRate = sampleRate;
  330. context->engine = new rack::engine::Engine;
  331. context->engine->setSampleRate(sampleRate);
  332. context->history = new rack::history::State;
  333. context->patch = new rack::patch::Manager;
  334. context->patch->autosavePath = fAutosavePath;
  335. context->patch->templatePath = context->patch->factoryTemplatePath = fInitializer->factoryTemplatePath;
  336. context->event = new rack::widget::EventState;
  337. context->scene = new rack::app::Scene;
  338. context->event->rootWidget = context->scene;
  339. context->window = new rack::window::Window;
  340. context->patch->loadTemplate();
  341. context->scene->rackScroll->reset();
  342. DISTRHO_SAFE_ASSERT(remoteUtils::connectToRemote());
  343. Engine_setRemoteDetails(context->engine, remoteDetails);
  344. #endif
  345. Window& window(getWindow());
  346. window.setIgnoringKeyRepeat(true);
  347. context->nativeWindowId = window.getNativeWindowHandle();
  348. const double scaleFactor = getScaleFactor();
  349. setGeometryConstraints(648 * scaleFactor, 538 * scaleFactor);
  350. if (scaleFactor != 1.0)
  351. setSize(DISTRHO_UI_DEFAULT_WIDTH * scaleFactor, DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor);
  352. rack::window::WindowSetPluginUI(context->window, this);
  353. // hide "Browse VCV Library" button
  354. rack::widget::Widget* const browser = context->scene->browser->children.back();
  355. rack::widget::Widget* const headerLayout = browser->children.front();
  356. rack::widget::Widget* const libraryButton = headerLayout->children.back();
  357. libraryButton->hide();
  358. // Report to user if something is wrong with the installation
  359. std::string errorMessage;
  360. if (rack::asset::systemDir.empty())
  361. {
  362. errorMessage = "Failed to locate Cardinal plugin bundle.\n"
  363. "Install Cardinal with its plugin bundle folder intact and try again.";
  364. }
  365. else if (! rack::system::exists(rack::asset::systemDir))
  366. {
  367. errorMessage = rack::string::f("System directory \"%s\" does not exist. "
  368. "Make sure Cardinal was downloaded and installed correctly.",
  369. rack::asset::systemDir.c_str());
  370. }
  371. if (! errorMessage.empty())
  372. {
  373. static bool shown = false;
  374. if (! shown)
  375. {
  376. shown = true;
  377. asyncDialog::create(errorMessage.c_str());
  378. }
  379. }
  380. #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
  381. if (rack::patchStorageSlug != nullptr)
  382. {
  383. psDialog = new WasmRemotePatchLoadingDialog(true);
  384. }
  385. else if (rack::patchRemoteURL != nullptr)
  386. {
  387. psDialog = new WasmRemotePatchLoadingDialog(false);
  388. }
  389. else if (rack::patchFromURL != nullptr)
  390. {
  391. static_cast<CardinalBasePlugin*>(context->plugin)->setState("patch", rack::patchFromURL);
  392. rack::contextSet(context);
  393. }
  394. else
  395. {
  396. new WasmWelcomeDialog();
  397. }
  398. #endif
  399. context->window->step();
  400. rack::contextSet(nullptr);
  401. WindowParametersSetCallback(context->window, this);
  402. }
  403. ~CardinalUI() override
  404. {
  405. rack::contextSet(context);
  406. context->nativeWindowId = 0;
  407. rack::window::WindowSetPluginUI(context->window, nullptr);
  408. context->tlw = nullptr;
  409. context->ui = nullptr;
  410. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  411. {
  412. const ScopedContext sc(this);
  413. context->patch->clear();
  414. // do a little dance to prevent context scene deletion from saving to temp dir
  415. const ScopedValueSetter<bool> svs(rack::settings::headless, true);
  416. Engine_setAboutToClose(context->engine);
  417. delete context;
  418. }
  419. if (! fAutosavePath.empty())
  420. rack::system::removeRecursively(fAutosavePath);
  421. #endif
  422. rack::contextSet(nullptr);
  423. }
  424. void onNanoDisplay() override
  425. {
  426. const ScopedContext sc(this);
  427. context->window->step();
  428. }
  429. void uiIdle() override
  430. {
  431. #ifdef DPF_RUNTIME_TESTING
  432. if (inSelfTest)
  433. {
  434. context->window->step();
  435. return;
  436. }
  437. if (context->plugin->isSelfTestInstance())
  438. {
  439. inSelfTest = true;
  440. Application& app(getApp());
  441. const ScopedContext sc(this);
  442. context->patch->clear();
  443. app.idle();
  444. const rack::math::Vec mousePos(getWidth()/2,getHeight()/2);
  445. context->event->handleButton(mousePos, GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0x0);
  446. context->event->handleHover(mousePos, rack::math::Vec(0,0));
  447. app.idle();
  448. for (rack::plugin::Plugin* p : rack::plugin::plugins)
  449. {
  450. for (rack::plugin::Model* m : p->models)
  451. {
  452. d_stdout(">>>>>>>>>>>>>>>>> LOADING module %s : %s", p->slug.c_str(), m->slug.c_str());
  453. rack::engine::Module* const module = m->createModule();
  454. DISTRHO_SAFE_ASSERT_CONTINUE(module != nullptr);
  455. rack::CardinalPluginModelHelper* const helper = dynamic_cast<rack::CardinalPluginModelHelper*>(m);
  456. DISTRHO_SAFE_ASSERT_CONTINUE(helper != nullptr);
  457. d_stdout(">>>>>>>>>>>>>>>>> LOADING moduleWidget %s : %s", p->slug.c_str(), m->slug.c_str());
  458. rack::app::ModuleWidget* const moduleWidget = helper->createModuleWidget(module);
  459. DISTRHO_SAFE_ASSERT_CONTINUE(moduleWidget != nullptr);
  460. d_stdout(">>>>>>>>>>>>>>>>> ADDING TO ENGINE %s : %s", p->slug.c_str(), m->slug.c_str());
  461. context->engine->addModule(module);
  462. d_stdout(">>>>>>>>>>>>>>>>> ADDING TO RACK VIEW %s : %s", p->slug.c_str(), m->slug.c_str());
  463. context->scene->rack->addModuleAtMouse(moduleWidget);
  464. for (int i=5; --i>=0;)
  465. app.idle();
  466. d_stdout(">>>>>>>>>>>>>>>>> REMOVING FROM RACK VIEW %s : %s", p->slug.c_str(), m->slug.c_str());
  467. context->scene->rack->removeModule(moduleWidget);
  468. app.idle();
  469. d_stdout(">>>>>>>>>>>>>>>>> DELETING module + moduleWidget %s : %s", p->slug.c_str(), m->slug.c_str());
  470. delete moduleWidget;
  471. app.idle();
  472. }
  473. }
  474. inSelfTest = false;
  475. }
  476. #endif
  477. #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
  478. if (counterForFirstIdlePoint >= 0 && ++counterForFirstIdlePoint == 30)
  479. {
  480. counterForFirstIdlePoint = -1;
  481. if (rack::patchStorageSlug != nullptr)
  482. {
  483. std::string url("/patchstorage.php?slug=");
  484. url += rack::patchStorageSlug;
  485. std::free(rack::patchStorageSlug);
  486. rack::patchStorageSlug = nullptr;
  487. emscripten_async_wget(url.c_str(), context->patch->templatePath.c_str(),
  488. downloadRemotePatchSucceeded, downloadRemotePatchFailed);
  489. }
  490. else if (rack::patchRemoteURL != nullptr)
  491. {
  492. std::string url("/patchurl.php?url=");
  493. url += rack::patchRemoteURL;
  494. std::free(rack::patchRemoteURL);
  495. rack::patchRemoteURL = nullptr;
  496. emscripten_async_wget(url.c_str(), context->patch->templatePath.c_str(),
  497. downloadRemotePatchSucceeded, downloadRemotePatchFailed);
  498. }
  499. }
  500. #endif
  501. if (filebrowserhandle != nullptr && fileBrowserIdle(filebrowserhandle))
  502. {
  503. {
  504. const char* const path = fileBrowserGetPath(filebrowserhandle);
  505. const ScopedContext sc(this);
  506. filebrowseraction(path != nullptr ? strdup(path) : nullptr);
  507. }
  508. fileBrowserClose(filebrowserhandle);
  509. filebrowseraction = nullptr;
  510. filebrowserhandle = nullptr;
  511. }
  512. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  513. {
  514. const ScopedContext sc(this);
  515. for (uint32_t i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS;++i)
  516. context->dataOuts[i][0] = 0.f;
  517. ++context->processCounter;
  518. context->engine->stepBlock(1);
  519. }
  520. #endif
  521. if (windowParameters.rateLimit != 0 && ++rateLimitStep % (windowParameters.rateLimit * 2))
  522. return;
  523. rateLimitStep = 0;
  524. repaint();
  525. }
  526. void WindowParametersChanged(const WindowParameterList param, float value) override
  527. {
  528. float mult = 1.0f;
  529. switch (param)
  530. {
  531. case kWindowParameterShowTooltips:
  532. windowParameters.tooltips = value > 0.5f;
  533. break;
  534. case kWindowParameterCableOpacity:
  535. mult = 100.0f;
  536. windowParameters.cableOpacity = value;
  537. break;
  538. case kWindowParameterCableTension:
  539. mult = 100.0f;
  540. windowParameters.cableTension = value;
  541. break;
  542. case kWindowParameterRackBrightness:
  543. mult = 100.0f;
  544. windowParameters.rackBrightness = value;
  545. break;
  546. case kWindowParameterHaloBrightness:
  547. mult = 100.0f;
  548. windowParameters.haloBrightness = value;
  549. break;
  550. case kWindowParameterKnobMode:
  551. switch (static_cast<int>(value + 0.5f))
  552. {
  553. case rack::settings::KNOB_MODE_LINEAR:
  554. value = 0;
  555. windowParameters.knobMode = rack::settings::KNOB_MODE_LINEAR;
  556. break;
  557. case rack::settings::KNOB_MODE_ROTARY_ABSOLUTE:
  558. value = 1;
  559. windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_ABSOLUTE;
  560. break;
  561. case rack::settings::KNOB_MODE_ROTARY_RELATIVE:
  562. value = 2;
  563. windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_RELATIVE;
  564. break;
  565. }
  566. break;
  567. case kWindowParameterWheelKnobControl:
  568. windowParameters.knobScroll = value > 0.5f;
  569. break;
  570. case kWindowParameterWheelSensitivity:
  571. mult = 1000.0f;
  572. windowParameters.knobScrollSensitivity = value;
  573. break;
  574. case kWindowParameterLockModulePositions:
  575. windowParameters.lockModules = value > 0.5f;
  576. break;
  577. case kWindowParameterUpdateRateLimit:
  578. windowParameters.rateLimit = static_cast<int>(value + 0.5f);
  579. rateLimitStep = 0;
  580. break;
  581. case kWindowParameterBrowserSort:
  582. windowParameters.browserSort = static_cast<int>(value + 0.5f);
  583. break;
  584. case kWindowParameterBrowserZoom:
  585. windowParameters.browserZoom = value;
  586. value = std::pow(2.f, value) * 100.0f;
  587. break;
  588. case kWindowParameterInvertZoom:
  589. windowParameters.invertZoom = value > 0.5f;
  590. break;
  591. case kWindowParameterSqueezeModulePositions:
  592. windowParameters.squeezeModules = value > 0.5f;
  593. break;
  594. default:
  595. return;
  596. }
  597. setParameterValue(kCardinalParameterStartWindow + param, value * mult);
  598. }
  599. protected:
  600. /* --------------------------------------------------------------------------------------------------------
  601. * DSP/Plugin Callbacks */
  602. /**
  603. A parameter has changed on the plugin side.
  604. This is called by the host to inform the UI about parameter changes.
  605. */
  606. void parameterChanged(const uint32_t index, const float value) override
  607. {
  608. // host mapped parameters
  609. if (index < kCardinalParameterCountAtModules)
  610. {
  611. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  612. context->parameters[index] = value;
  613. #endif
  614. return;
  615. }
  616. // bypass
  617. if (index == kCardinalParameterBypass)
  618. {
  619. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  620. context->bypassed = value > 0.5f;
  621. #endif
  622. return;
  623. }
  624. if (index < kCardinalParameterCountAtWindow)
  625. {
  626. switch (index - kCardinalParameterStartWindow)
  627. {
  628. case kWindowParameterShowTooltips:
  629. windowParameters.tooltips = value > 0.5f;
  630. break;
  631. case kWindowParameterCableOpacity:
  632. windowParameters.cableOpacity = value / 100.0f;
  633. break;
  634. case kWindowParameterCableTension:
  635. windowParameters.cableTension = value / 100.0f;
  636. break;
  637. case kWindowParameterRackBrightness:
  638. windowParameters.rackBrightness = value / 100.0f;
  639. break;
  640. case kWindowParameterHaloBrightness:
  641. windowParameters.haloBrightness = value / 100.0f;
  642. break;
  643. case kWindowParameterKnobMode:
  644. switch (static_cast<int>(value + 0.5f))
  645. {
  646. case 0:
  647. windowParameters.knobMode = rack::settings::KNOB_MODE_LINEAR;
  648. break;
  649. case 1:
  650. windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_ABSOLUTE;
  651. break;
  652. case 2:
  653. windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_RELATIVE;
  654. break;
  655. }
  656. break;
  657. case kWindowParameterWheelKnobControl:
  658. windowParameters.knobScroll = value > 0.5f;
  659. break;
  660. case kWindowParameterWheelSensitivity:
  661. windowParameters.knobScrollSensitivity = value / 1000.0f;
  662. break;
  663. case kWindowParameterLockModulePositions:
  664. windowParameters.lockModules = value > 0.5f;
  665. break;
  666. case kWindowParameterUpdateRateLimit:
  667. windowParameters.rateLimit = static_cast<int>(value + 0.5f);
  668. rateLimitStep = 0;
  669. break;
  670. case kWindowParameterBrowserSort:
  671. windowParameters.browserSort = static_cast<int>(value + 0.5f);
  672. break;
  673. case kWindowParameterBrowserZoom:
  674. // round up to nearest valid value
  675. {
  676. float rvalue = value - 1.0f;
  677. if (rvalue <= 25.0f)
  678. rvalue = -2.0f;
  679. else if (rvalue <= 35.0f)
  680. rvalue = -1.5f;
  681. else if (rvalue <= 50.0f)
  682. rvalue = -1.0f;
  683. else if (rvalue <= 71.0f)
  684. rvalue = -0.5f;
  685. else if (rvalue <= 100.0f)
  686. rvalue = 0.0f;
  687. else if (rvalue <= 141.0f)
  688. rvalue = 0.5f;
  689. else if (rvalue <= 200.0f)
  690. rvalue = 1.0f;
  691. else
  692. rvalue = 0.0f;
  693. windowParameters.browserZoom = rvalue;
  694. }
  695. break;
  696. case kWindowParameterInvertZoom:
  697. windowParameters.invertZoom = value > 0.5f;
  698. break;
  699. case kWindowParameterSqueezeModulePositions:
  700. windowParameters.squeezeModules = value > 0.5f;
  701. break;
  702. default:
  703. return;
  704. }
  705. WindowParametersSetValues(context->window, windowParameters);
  706. return;
  707. }
  708. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  709. if (index < kCardinalParameterCountAtMiniBuffers)
  710. {
  711. float* const buffer = *const_cast<float**>(&context->dataIns[index - kCardinalParameterStartMiniBuffers]);
  712. buffer[0] = value;
  713. return;
  714. }
  715. switch (index)
  716. {
  717. case kCardinalParameterMiniTimeFlags: {
  718. const int32_t flags = static_cast<int32_t>(value + 0.5f);
  719. context->playing = flags & 0x1;
  720. context->bbtValid = flags & 0x2;
  721. context->reset = flags & 0x4;
  722. return;
  723. }
  724. case kCardinalParameterMiniTimeBar:
  725. context->bar = static_cast<int32_t>(value + 0.5f);
  726. return;
  727. case kCardinalParameterMiniTimeBeat:
  728. context->beat = static_cast<int32_t>(value + 0.5f);
  729. return;
  730. case kCardinalParameterMiniTimeBeatsPerBar:
  731. context->beatsPerBar = static_cast<int32_t>(value + 0.5f);
  732. return;
  733. case kCardinalParameterMiniTimeBeatType:
  734. context->beatType = static_cast<int32_t>(value + 0.5f);
  735. context->ticksPerClock = context->ticksPerBeat / context->beatType;
  736. context->tickClock = std::fmod(context->tick, context->ticksPerClock);
  737. return;
  738. case kCardinalParameterMiniTimeFrame:
  739. context->frame = static_cast<uint64_t>(value * context->sampleRate + 0.5f);
  740. return;
  741. case kCardinalParameterMiniTimeBarStartTick:
  742. context->barStartTick = value;
  743. return;
  744. case kCardinalParameterMiniTimeBeatsPerMinute:
  745. context->beatsPerMinute = value;
  746. context->ticksPerFrame = 1.0 / (60.0 * context->sampleRate / context->beatsPerMinute / context->ticksPerBeat);
  747. return;
  748. case kCardinalParameterMiniTimeTick:
  749. context->tick = value;
  750. context->tickClock = std::fmod(context->tick, context->ticksPerClock);
  751. return;
  752. case kCardinalParameterMiniTimeTicksPerBeat:
  753. context->ticksPerBeat = value;
  754. context->ticksPerClock = context->ticksPerBeat / context->beatType;
  755. context->ticksPerFrame = 1.0 / (60.0 * context->sampleRate / context->beatsPerMinute / context->ticksPerBeat);
  756. context->tickClock = std::fmod(context->tick, context->ticksPerClock);
  757. return;
  758. }
  759. #endif
  760. }
  761. void stateChanged(const char* const key, const char* const value) override
  762. {
  763. #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
  764. if (std::strcmp(key, "patch") == 0)
  765. {
  766. if (fAutosavePath.empty())
  767. return;
  768. rack::system::removeRecursively(fAutosavePath);
  769. rack::system::createDirectories(fAutosavePath);
  770. FILE* const f = std::fopen(rack::system::join(fAutosavePath, "patch.json").c_str(), "w");
  771. DISTRHO_SAFE_ASSERT_RETURN(f != nullptr,);
  772. std::fwrite(value, std::strlen(value), 1, f);
  773. std::fclose(f);
  774. const ScopedContext sc(this);
  775. try {
  776. context->patch->loadAutosave();
  777. } catch(const rack::Exception& e) {
  778. d_stderr(e.what());
  779. } DISTRHO_SAFE_EXCEPTION_RETURN("setState loadAutosave",);
  780. return;
  781. }
  782. #endif
  783. if (std::strcmp(key, "windowSize") == 0)
  784. {
  785. int width = 0;
  786. int height = 0;
  787. std::sscanf(value, "%d:%d", &width, &height);
  788. if (width > 0 && height > 0)
  789. {
  790. const double scaleFactor = getScaleFactor();
  791. setSize(width * scaleFactor, height * scaleFactor);
  792. }
  793. return;
  794. }
  795. }
  796. // -------------------------------------------------------------------------------------------------------
  797. static int glfwMods(const uint mod) noexcept
  798. {
  799. int mods = 0;
  800. if (mod & kModifierControl)
  801. mods |= GLFW_MOD_CONTROL;
  802. if (mod & kModifierShift)
  803. mods |= GLFW_MOD_SHIFT;
  804. if (mod & kModifierAlt)
  805. mods |= GLFW_MOD_ALT;
  806. if (mod & kModifierSuper)
  807. mods |= GLFW_MOD_SUPER;
  808. /*
  809. if (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)
  810. mods |= GLFW_MOD_SHIFT;
  811. if (glfwGetKey(win, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS)
  812. mods |= GLFW_MOD_CONTROL;
  813. if (glfwGetKey(win, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS)
  814. mods |= GLFW_MOD_ALT;
  815. if (glfwGetKey(win, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS)
  816. mods |= GLFW_MOD_SUPER;
  817. */
  818. return mods;
  819. }
  820. bool onMouse(const MouseEvent& ev) override
  821. {
  822. #ifdef DPF_RUNTIME_TESTING
  823. if (inSelfTest) return false;
  824. #endif
  825. if (ev.press)
  826. getWindow().focus();
  827. const int action = ev.press ? GLFW_PRESS : GLFW_RELEASE;
  828. int mods = glfwMods(ev.mod);
  829. int button;
  830. switch (ev.button)
  831. {
  832. case kMouseButtonLeft: button = GLFW_MOUSE_BUTTON_LEFT; break;
  833. case kMouseButtonRight: button = GLFW_MOUSE_BUTTON_RIGHT; break;
  834. case kMouseButtonMiddle: button = GLFW_MOUSE_BUTTON_MIDDLE; break;
  835. default:
  836. button = ev.button;
  837. break;
  838. }
  839. #ifdef DISTRHO_OS_MAC
  840. // Remap Ctrl-left click to right click on macOS
  841. if (button == GLFW_MOUSE_BUTTON_LEFT && (mods & RACK_MOD_MASK) == GLFW_MOD_CONTROL) {
  842. button = GLFW_MOUSE_BUTTON_RIGHT;
  843. mods &= ~GLFW_MOD_CONTROL;
  844. }
  845. // Remap Ctrl-shift-left click to middle click on macOS
  846. if (button == GLFW_MOUSE_BUTTON_LEFT && (mods & RACK_MOD_MASK) == (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT)) {
  847. button = GLFW_MOUSE_BUTTON_MIDDLE;
  848. mods &= ~(GLFW_MOD_CONTROL | GLFW_MOD_SHIFT);
  849. }
  850. #endif
  851. const ScopedContext sc(this, mods);
  852. return context->event->handleButton(lastMousePos, button, action, mods);
  853. }
  854. bool onMotion(const MotionEvent& ev) override
  855. {
  856. #ifdef DPF_RUNTIME_TESTING
  857. if (inSelfTest) return false;
  858. #endif
  859. const rack::math::Vec mousePos = rack::math::Vec(ev.pos.getX(), ev.pos.getY()).div(getScaleFactor()).round();
  860. const rack::math::Vec mouseDelta = mousePos.minus(lastMousePos);
  861. lastMousePos = mousePos;
  862. const ScopedContext sc(this, glfwMods(ev.mod));
  863. return context->event->handleHover(mousePos, mouseDelta);
  864. }
  865. bool onScroll(const ScrollEvent& ev) override
  866. {
  867. #ifdef DPF_RUNTIME_TESTING
  868. if (inSelfTest) return false;
  869. #endif
  870. rack::math::Vec scrollDelta = rack::math::Vec(-ev.delta.getX(), ev.delta.getY());
  871. #ifndef DISTRHO_OS_MAC
  872. scrollDelta = scrollDelta.mult(50.0);
  873. #endif
  874. const int mods = glfwMods(ev.mod);
  875. const ScopedContext sc(this, mods);
  876. return context->event->handleScroll(lastMousePos, scrollDelta);
  877. }
  878. bool onCharacterInput(const CharacterInputEvent& ev) override
  879. {
  880. #ifdef DPF_RUNTIME_TESTING
  881. if (inSelfTest) return false;
  882. #endif
  883. if (ev.character < ' ' || ev.character >= kKeyDelete)
  884. return false;
  885. const int mods = glfwMods(ev.mod);
  886. const ScopedContext sc(this, mods);
  887. return context->event->handleText(lastMousePos, ev.character);
  888. }
  889. bool onKeyboard(const KeyboardEvent& ev) override
  890. {
  891. #ifdef DPF_RUNTIME_TESTING
  892. if (inSelfTest) return false;
  893. #endif
  894. const int action = ev.press ? GLFW_PRESS : GLFW_RELEASE;
  895. const int mods = glfwMods(ev.mod);
  896. /* These are unsupported in pugl right now
  897. #define GLFW_KEY_KP_0 320
  898. #define GLFW_KEY_KP_1 321
  899. #define GLFW_KEY_KP_2 322
  900. #define GLFW_KEY_KP_3 323
  901. #define GLFW_KEY_KP_4 324
  902. #define GLFW_KEY_KP_5 325
  903. #define GLFW_KEY_KP_6 326
  904. #define GLFW_KEY_KP_7 327
  905. #define GLFW_KEY_KP_8 328
  906. #define GLFW_KEY_KP_9 329
  907. #define GLFW_KEY_KP_DECIMAL 330
  908. #define GLFW_KEY_KP_DIVIDE 331
  909. #define GLFW_KEY_KP_MULTIPLY 332
  910. #define GLFW_KEY_KP_SUBTRACT 333
  911. #define GLFW_KEY_KP_ADD 334
  912. #define GLFW_KEY_KP_ENTER 335
  913. #define GLFW_KEY_KP_EQUAL 336
  914. */
  915. int key;
  916. switch (ev.key)
  917. {
  918. case '\r': key = GLFW_KEY_ENTER; break;
  919. case '\t': key = GLFW_KEY_TAB; break;
  920. case kKeyBackspace: key = GLFW_KEY_BACKSPACE; break;
  921. case kKeyEscape: key = GLFW_KEY_ESCAPE; break;
  922. case kKeyDelete: key = GLFW_KEY_DELETE; break;
  923. case kKeyF1: key = GLFW_KEY_F1; break;
  924. case kKeyF2: key = GLFW_KEY_F2; break;
  925. case kKeyF3: key = GLFW_KEY_F3; break;
  926. case kKeyF4: key = GLFW_KEY_F4; break;
  927. case kKeyF5: key = GLFW_KEY_F5; break;
  928. case kKeyF6: key = GLFW_KEY_F6; break;
  929. case kKeyF7: key = GLFW_KEY_F7; break;
  930. case kKeyF8: key = GLFW_KEY_F8; break;
  931. case kKeyF9: key = GLFW_KEY_F9; break;
  932. case kKeyF10: key = GLFW_KEY_F10; break;
  933. case kKeyF11: key = GLFW_KEY_F11; break;
  934. case kKeyF12: key = GLFW_KEY_F12; break;
  935. case kKeyLeft: key = GLFW_KEY_LEFT; break;
  936. case kKeyUp: key = GLFW_KEY_UP; break;
  937. case kKeyRight: key = GLFW_KEY_RIGHT; break;
  938. case kKeyDown: key = GLFW_KEY_DOWN; break;
  939. case kKeyPageUp: key = GLFW_KEY_PAGE_UP; break;
  940. case kKeyPageDown: key = GLFW_KEY_PAGE_DOWN; break;
  941. case kKeyHome: key = GLFW_KEY_HOME; break;
  942. case kKeyEnd: key = GLFW_KEY_END; break;
  943. case kKeyInsert: key = GLFW_KEY_INSERT; break;
  944. case kKeyShiftL: key = GLFW_KEY_LEFT_SHIFT; break;
  945. case kKeyShiftR: key = GLFW_KEY_RIGHT_SHIFT; break;
  946. case kKeyControlL: key = GLFW_KEY_LEFT_CONTROL; break;
  947. case kKeyControlR: key = GLFW_KEY_RIGHT_CONTROL; break;
  948. case kKeyAltL: key = GLFW_KEY_LEFT_ALT; break;
  949. case kKeyAltR: key = GLFW_KEY_RIGHT_ALT; break;
  950. case kKeySuperL: key = GLFW_KEY_LEFT_SUPER; break;
  951. case kKeySuperR: key = GLFW_KEY_RIGHT_SUPER; break;
  952. case kKeyMenu: key = GLFW_KEY_MENU; break;
  953. case kKeyCapsLock: key = GLFW_KEY_CAPS_LOCK; break;
  954. case kKeyScrollLock: key = GLFW_KEY_SCROLL_LOCK; break;
  955. case kKeyNumLock: key = GLFW_KEY_NUM_LOCK; break;
  956. case kKeyPrintScreen: key = GLFW_KEY_PRINT_SCREEN; break;
  957. case kKeyPause: key = GLFW_KEY_PAUSE; break;
  958. default:
  959. // glfw expects uppercase
  960. if (ev.key >= 'a' && ev.key <= 'z')
  961. key = ev.key - ('a' - 'A');
  962. else
  963. key = ev.key;
  964. break;
  965. }
  966. const ScopedContext sc(this, mods);
  967. return context->event->handleKey(lastMousePos, key, ev.keycode, action, mods);
  968. }
  969. void onResize(const ResizeEvent& ev) override
  970. {
  971. UI::onResize(ev);
  972. if (context->window != nullptr)
  973. WindowSetInternalSize(context->window, rack::math::Vec(ev.size.getWidth(), ev.size.getHeight()));
  974. const double scaleFactor = getScaleFactor();
  975. char sizeString[64];
  976. std::snprintf(sizeString, sizeof(sizeString), "%d:%d",
  977. (int)(ev.size.getWidth() / scaleFactor), (int)(ev.size.getHeight() / scaleFactor));
  978. setState("windowSize", sizeString);
  979. }
  980. void uiFocus(const bool focus, CrossingMode) override
  981. {
  982. #ifdef DPF_RUNTIME_TESTING
  983. if (inSelfTest) return;
  984. #endif
  985. if (!focus)
  986. {
  987. const ScopedContext sc(this, 0);
  988. context->event->handleLeave();
  989. }
  990. }
  991. void uiFileBrowserSelected(const char* const filename) override
  992. {
  993. if (filename == nullptr)
  994. return;
  995. rack::contextSet(context);
  996. WindowParametersRestore(context->window);
  997. std::string sfilename = filename;
  998. if (saving)
  999. {
  1000. const bool uncompressed = savingUncompressed;
  1001. savingUncompressed = false;
  1002. if (rack::system::getExtension(sfilename) != ".vcv")
  1003. sfilename += ".vcv";
  1004. try {
  1005. if (uncompressed)
  1006. {
  1007. context->engine->prepareSave();
  1008. if (json_t* const rootJ = context->patch->toJson())
  1009. {
  1010. if (FILE* const file = std::fopen(sfilename.c_str(), "w"))
  1011. {
  1012. json_dumpf(rootJ, file, JSON_INDENT(2));
  1013. std::fclose(file);
  1014. }
  1015. json_decref(rootJ);
  1016. }
  1017. }
  1018. else
  1019. {
  1020. context->patch->save(sfilename);
  1021. }
  1022. }
  1023. catch (rack::Exception& e) {
  1024. std::string message = rack::string::f("Could not save patch: %s", e.what());
  1025. asyncDialog::create(message.c_str());
  1026. return;
  1027. }
  1028. }
  1029. else
  1030. {
  1031. try {
  1032. context->patch->load(sfilename);
  1033. } catch (rack::Exception& e) {
  1034. std::string message = rack::string::f("Could not load patch: %s", e.what());
  1035. asyncDialog::create(message.c_str());
  1036. return;
  1037. }
  1038. }
  1039. context->patch->path = sfilename;
  1040. context->history->setSaved();
  1041. }
  1042. #if 0
  1043. void uiReshape(const uint width, const uint height) override
  1044. {
  1045. glEnable(GL_BLEND);
  1046. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  1047. glMatrixMode(GL_PROJECTION);
  1048. glLoadIdentity();
  1049. glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
  1050. glViewport(0, 0, width, height);
  1051. glMatrixMode(GL_MODELVIEW);
  1052. glLoadIdentity();
  1053. }
  1054. #endif
  1055. private:
  1056. /**
  1057. Set our UI class as non-copyable and add a leak detector just in case.
  1058. */
  1059. DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CardinalUI)
  1060. };
  1061. /* ------------------------------------------------------------------------------------------------------------
  1062. * UI entry point, called by DPF to create a new UI instance. */
  1063. UI* createUI()
  1064. {
  1065. return new CardinalUI();
  1066. }
  1067. // -----------------------------------------------------------------------------------------------------------
  1068. END_NAMESPACE_DISTRHO