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.

482 lines
13KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021-2025 Filipe Coelho <falktx@falktx.com>
  4. * SPDX-License-Identifier: GPL-3.0-or-later
  5. */
  6. /**
  7. * This file is an edited version of VCVRack's app/Scene.cpp
  8. * Copyright (C) 2016-2023 VCV.
  9. *
  10. * This program is free software: you can redistribute it and/or
  11. * modify it under the terms of the GNU General Public License as
  12. * published by the Free Software Foundation; either version 3 of
  13. * the License, or (at your option) any later version.
  14. */
  15. #include <app/Scene.hpp>
  16. #include <app/Browser.hpp>
  17. #include <app/TipWindow.hpp>
  18. #include <app/MenuBar.hpp>
  19. #include <context.hpp>
  20. #include <engine/Engine.hpp>
  21. #include <system.hpp>
  22. #include <network.hpp>
  23. #include <history.hpp>
  24. #include <settings.hpp>
  25. #include <patch.hpp>
  26. #include <asset.hpp>
  27. #ifdef NDEBUG
  28. # undef DEBUG
  29. #endif
  30. #include "../CardinalCommon.hpp"
  31. #include "../CardinalRemote.hpp"
  32. #include <algorithm>
  33. namespace rack {
  34. namespace app {
  35. struct ResizeHandle : widget::OpaqueWidget {
  36. math::Vec size;
  37. void draw(const DrawArgs& args) override {
  38. nvgStrokeColor(args.vg, nvgRGBf(1, 1, 1));
  39. nvgStrokeWidth(args.vg, 1);
  40. nvgBeginPath(args.vg);
  41. nvgMoveTo(args.vg, box.size.x, 0);
  42. nvgLineTo(args.vg, 0, box.size.y);
  43. nvgStroke(args.vg);
  44. nvgBeginPath(args.vg);
  45. nvgMoveTo(args.vg, box.size.x + 5, 0);
  46. nvgLineTo(args.vg, 0, box.size.y + 5);
  47. nvgStroke(args.vg);
  48. nvgBeginPath(args.vg);
  49. nvgMoveTo(args.vg, box.size.x + 10, 0);
  50. nvgLineTo(args.vg, 0, box.size.y + 10);
  51. nvgStroke(args.vg);
  52. nvgStrokeColor(args.vg, nvgRGBf(0, 0, 0));
  53. nvgBeginPath(args.vg);
  54. nvgMoveTo(args.vg, box.size.x+1, 0);
  55. nvgLineTo(args.vg, 0, box.size.y+1);
  56. nvgStroke(args.vg);
  57. nvgBeginPath(args.vg);
  58. nvgMoveTo(args.vg, box.size.x + 6, 0);
  59. nvgLineTo(args.vg, 0, box.size.y + 6);
  60. nvgStroke(args.vg);
  61. nvgBeginPath(args.vg);
  62. nvgMoveTo(args.vg, box.size.x + 11, 0);
  63. nvgLineTo(args.vg, 0, box.size.y + 11);
  64. nvgStroke(args.vg);
  65. }
  66. void onHover(const HoverEvent& e) override {
  67. e.consume(this);
  68. }
  69. void onEnter(const EnterEvent& e) override {
  70. glfwSetCursor(APP->window->win, glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR));
  71. }
  72. void onLeave(const LeaveEvent& e) override {
  73. glfwSetCursor(APP->window->win, nullptr);
  74. }
  75. void onDragStart(const DragStartEvent&) override {
  76. size = APP->window->getSize();
  77. }
  78. void onDragMove(const DragMoveEvent& e) override {
  79. size = size.plus(e.mouseDelta.mult(APP->window->pixelRatio));
  80. APP->window->setSize(size.round());
  81. }
  82. };
  83. struct Scene::Internal {
  84. ResizeHandle* resizeHandle = nullptr;
  85. bool heldArrowKeys[4] = {};
  86. double lastSceneChangeTime = 0.0;
  87. int historyActionIndex = -1;
  88. };
  89. Scene::Scene() {
  90. internal = new Internal;
  91. rackScroll = new RackScrollWidget;
  92. addChild(rackScroll);
  93. rack = rackScroll->rackWidget;
  94. menuBar = createMenuBar();
  95. addChild(menuBar);
  96. browser = browserCreate();
  97. browser->hide();
  98. addChild(browser);
  99. if (isStandalone() || isMini())
  100. return;
  101. internal->resizeHandle = new ResizeHandle;
  102. internal->resizeHandle->box.size = math::Vec(16, 16);
  103. addChild(internal->resizeHandle);
  104. }
  105. Scene::~Scene() {
  106. delete internal;
  107. }
  108. math::Vec Scene::getMousePos() {
  109. return mousePos;
  110. }
  111. void Scene::step() {
  112. if (APP->window->isFullScreen()) {
  113. // Expand RackScrollWidget to cover entire screen if fullscreen
  114. rackScroll->box.pos.y = 0;
  115. }
  116. else {
  117. // Always show MenuBar if not fullscreen
  118. menuBar->show();
  119. rackScroll->box.pos.y = menuBar->box.size.y;
  120. }
  121. if (internal->resizeHandle != nullptr)
  122. internal->resizeHandle->box.pos = box.size.minus(internal->resizeHandle->box.size);
  123. // Resize owned descendants
  124. menuBar->box.size.x = box.size.x;
  125. rackScroll->box.size = box.size.minus(rackScroll->box.pos);
  126. // Scroll RackScrollWidget with arrow keys
  127. math::Vec arrowDelta;
  128. if (internal->heldArrowKeys[0]) {
  129. arrowDelta.x -= 1;
  130. }
  131. if (internal->heldArrowKeys[1]) {
  132. arrowDelta.x += 1;
  133. }
  134. if (internal->heldArrowKeys[2]) {
  135. arrowDelta.y -= 1;
  136. }
  137. if (internal->heldArrowKeys[3]) {
  138. arrowDelta.y += 1;
  139. }
  140. if (!arrowDelta.isZero()) {
  141. int mods = APP->window->getMods();
  142. float arrowSpeed = 32.f;
  143. if ((mods & RACK_MOD_MASK) == RACK_MOD_CTRL)
  144. arrowSpeed /= 4.f;
  145. if ((mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT)
  146. arrowSpeed *= 4.f;
  147. if ((mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT))
  148. arrowSpeed /= 16.f;
  149. rackScroll->offset += arrowDelta * arrowSpeed;
  150. }
  151. if (remoteUtils::RemoteDetails* const remoteDetails = remoteUtils::getRemote()) {
  152. idleRemote(remoteDetails);
  153. if (remoteDetails->autoDeploy && remoteDetails->connected) {
  154. const int actionIndex = APP->history->actionIndex;
  155. const double time = system::getTime();
  156. if (internal->historyActionIndex == -1) {
  157. internal->historyActionIndex = actionIndex;
  158. internal->lastSceneChangeTime = time;
  159. } else if (remoteDetails->first ||
  160. (internal->historyActionIndex != actionIndex
  161. && actionIndex > 0
  162. && time - internal->lastSceneChangeTime >= 1.0)) {
  163. if (remoteDetails->first) {
  164. remoteDetails->first = false;
  165. remoteUtils::sendFullPatchToRemote(remoteDetails);
  166. } else {
  167. const std::string& name(APP->history->actions[actionIndex - 1]->name);
  168. static const std::vector<std::string> ignoredNames = {
  169. "move knob",
  170. "move modules",
  171. "move switch",
  172. };
  173. if (std::find(ignoredNames.cbegin(), ignoredNames.cend(), name) == ignoredNames.cend()) {
  174. d_debug("action '%s'\n", APP->history->actions[actionIndex - 1]->name.c_str());
  175. remoteUtils::sendFullPatchToRemote(remoteDetails);
  176. if (remoteDetails->screenshot) {
  177. window::generateScreenshot();
  178. }
  179. }
  180. }
  181. internal->historyActionIndex = actionIndex;
  182. internal->lastSceneChangeTime = time;
  183. }
  184. }
  185. }
  186. Widget::step();
  187. }
  188. void Scene::draw(const DrawArgs& args) {
  189. Widget::draw(args);
  190. }
  191. void Scene::onHover(const HoverEvent& e) {
  192. mousePos = e.pos;
  193. if (mousePos.y < menuBar->box.size.y) {
  194. menuBar->show();
  195. }
  196. OpaqueWidget::onHover(e);
  197. }
  198. void Scene::onDragHover(const DragHoverEvent& e) {
  199. mousePos = e.pos;
  200. OpaqueWidget::onDragHover(e);
  201. }
  202. void Scene::onHoverKey(const HoverKeyEvent& e) {
  203. // Key commands that override children
  204. if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
  205. // DEBUG("key '%d '%c' scancode %d '%c' keyName '%s'", e.key, e.key, e.scancode, e.scancode, e.keyName.c_str());
  206. if (e.keyName == "n" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  207. patchUtils::loadTemplateDialog(false);
  208. e.consume(this);
  209. }
  210. if (e.keyName == "q" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  211. APP->window->close();
  212. e.consume(this);
  213. }
  214. if (e.keyName == "o" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  215. patchUtils::loadDialog();
  216. e.consume(this);
  217. }
  218. if (e.keyName == "o" && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) {
  219. patchUtils::revertDialog();
  220. e.consume(this);
  221. }
  222. if (e.keyName == "s" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  223. // NOTE: for plugin versions it will do nothing if path is empty, intentionally
  224. if (APP->patch->path.empty()) {
  225. if (isStandalone())
  226. patchUtils::saveAsDialog();
  227. } else {
  228. patchUtils::saveDialog(APP->patch->path);
  229. }
  230. e.consume(this);
  231. }
  232. if (e.keyName == "s" && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) {
  233. patchUtils::saveAsDialog();
  234. e.consume(this);
  235. }
  236. if (e.keyName == "z" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  237. APP->history->undo();
  238. e.consume(this);
  239. }
  240. if (e.keyName == "z" && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) {
  241. APP->history->redo();
  242. e.consume(this);
  243. }
  244. if (e.keyName == "-" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  245. float zoom = std::log2(APP->scene->rackScroll->getZoom());
  246. zoom *= 2;
  247. zoom = std::ceil(zoom - 0.01f) - 1;
  248. zoom /= 2;
  249. APP->scene->rackScroll->setZoom(std::pow(2.f, zoom));
  250. e.consume(this);
  251. }
  252. // Numpad has a "+" key, but the main keyboard section hides it under "="
  253. if ((e.keyName == "=" || e.keyName == "+") && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  254. float zoom = std::log2(APP->scene->rackScroll->getZoom());
  255. zoom *= 2;
  256. zoom = std::floor(zoom + 0.01f) + 1;
  257. zoom /= 2;
  258. APP->scene->rackScroll->setZoom(std::pow(2.f, zoom));
  259. e.consume(this);
  260. }
  261. if ((e.keyName == "0" || e.keyName == "1") && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  262. APP->scene->rackScroll->setZoom(1.f);
  263. e.consume(this);
  264. }
  265. if (e.keyName == "2" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  266. APP->scene->rackScroll->setZoom(2.f);
  267. e.consume(this);
  268. }
  269. if (e.key == GLFW_KEY_F1 && (e.mods & RACK_MOD_MASK) == 0) {
  270. patchUtils::openBrowser("https://vcvrack.com/manual/");
  271. e.consume(this);
  272. }
  273. if (e.key == GLFW_KEY_F3 && (e.mods & RACK_MOD_MASK) == 0) {
  274. settings::cpuMeter ^= true;
  275. e.consume(this);
  276. }
  277. if (e.key == GLFW_KEY_F7 && (e.mods & RACK_MOD_MASK) == 0) {
  278. if (remoteUtils::RemoteDetails* const remoteDetails = remoteUtils::getRemote())
  279. {
  280. if (remoteDetails->connected) {
  281. remoteUtils::sendFullPatchToRemote(remoteDetails);
  282. if (remoteDetails->screenshot) {
  283. window::generateScreenshot();
  284. }
  285. }
  286. }
  287. e.consume(this);
  288. }
  289. if (e.key == GLFW_KEY_F9 && (e.mods & RACK_MOD_MASK) == 0) {
  290. window::generateScreenshot();
  291. e.consume(this);
  292. }
  293. #ifdef DISTRHO_OS_WASM
  294. if (e.key == GLFW_KEY_F11 && (e.mods & RACK_MOD_MASK) == 0) {
  295. APP->window->setFullScreen(!APP->window->isFullScreen());
  296. // The MenuBar will be hidden when the mouse moves over the RackScrollWidget.
  297. // menuBar->hide();
  298. e.consume(this);
  299. }
  300. #endif
  301. // Module selections
  302. if (e.keyName == "a" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  303. rack->selectAll();
  304. e.consume(this);
  305. }
  306. if (e.keyName == "a" && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) {
  307. rack->deselectAll();
  308. e.consume(this);
  309. }
  310. if (e.keyName == "c" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  311. if (rack->hasSelection()) {
  312. rack->copyClipboardSelection();
  313. e.consume(this);
  314. }
  315. }
  316. if (e.keyName == "i" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  317. if (rack->hasSelection()) {
  318. rack->resetSelectionAction();
  319. e.consume(this);
  320. }
  321. }
  322. if (e.keyName == "r" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  323. if (rack->hasSelection()) {
  324. rack->randomizeSelectionAction();
  325. e.consume(this);
  326. }
  327. }
  328. if (e.keyName == "u" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  329. if (rack->hasSelection()) {
  330. rack->disconnectSelectionAction();
  331. e.consume(this);
  332. }
  333. }
  334. if (e.keyName == "e" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  335. if (rack->hasSelection()) {
  336. rack->bypassSelectionAction(!rack->isSelectionBypassed());
  337. e.consume(this);
  338. }
  339. }
  340. if (e.keyName == "d" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  341. if (rack->hasSelection()) {
  342. rack->cloneSelectionAction(false);
  343. e.consume(this);
  344. }
  345. }
  346. if (e.keyName == "d" && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) {
  347. if (rack->hasSelection()) {
  348. rack->cloneSelectionAction(true);
  349. e.consume(this);
  350. }
  351. }
  352. if ((e.key == GLFW_KEY_DELETE || e.key == GLFW_KEY_BACKSPACE) && (e.mods & RACK_MOD_MASK) == 0) {
  353. if (rack->hasSelection()) {
  354. rack->deleteSelectionAction();
  355. e.consume(this);
  356. }
  357. }
  358. }
  359. // Scroll RackScrollWidget with arrow keys
  360. if (e.action == GLFW_PRESS || e.action == GLFW_RELEASE) {
  361. if (e.key == GLFW_KEY_LEFT) {
  362. internal->heldArrowKeys[0] = (e.action == GLFW_PRESS);
  363. e.consume(this);
  364. }
  365. if (e.key == GLFW_KEY_RIGHT) {
  366. internal->heldArrowKeys[1] = (e.action == GLFW_PRESS);
  367. e.consume(this);
  368. }
  369. if (e.key == GLFW_KEY_UP) {
  370. internal->heldArrowKeys[2] = (e.action == GLFW_PRESS);
  371. e.consume(this);
  372. }
  373. if (e.key == GLFW_KEY_DOWN) {
  374. internal->heldArrowKeys[3] = (e.action == GLFW_PRESS);
  375. e.consume(this);
  376. }
  377. }
  378. if (e.isConsumed())
  379. return;
  380. OpaqueWidget::onHoverKey(e);
  381. if (e.isConsumed())
  382. return;
  383. // Key commands that can be overridden by children
  384. if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
  385. if (e.keyName == "v" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  386. rack->pasteClipboardAction();
  387. e.consume(this);
  388. }
  389. if ((e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER) && (e.mods & RACK_MOD_MASK) == 0) {
  390. browser->show();
  391. e.consume(this);
  392. }
  393. }
  394. }
  395. void Scene::onPathDrop(const PathDropEvent& e) {
  396. if (e.paths.size() >= 1) {
  397. const std::string& path = e.paths[0];
  398. std::string extension = system::getExtension(path);
  399. if (extension == ".vcv") {
  400. patchUtils::loadPathDialog(path);
  401. e.consume(this);
  402. return;
  403. }
  404. if (extension == ".vcvs") {
  405. APP->scene->rack->loadSelection(path);
  406. e.consume(this);
  407. return;
  408. }
  409. }
  410. OpaqueWidget::onPathDrop(e);
  411. }
  412. } // namespace app
  413. } // namespace rack