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.

1076 lines
36KB

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