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.

465 lines
14KB

  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 ModuleWidget.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 <regex>
  28. #include <app/ModuleWidget.hpp>
  29. #include <app/RackWidget.hpp>
  30. #include <app/Scene.hpp>
  31. #include <engine/Engine.hpp>
  32. #include <ui/MenuSeparator.hpp>
  33. #include <asset.hpp>
  34. #include <context.hpp>
  35. #include <helpers.hpp>
  36. #include <settings.hpp>
  37. #include <system.hpp>
  38. namespace rack {
  39. namespace app {
  40. struct CardinalModuleWidget : ModuleWidget {
  41. CardinalModuleWidget() : ModuleWidget() {}
  42. DEPRECATED CardinalModuleWidget(engine::Module* module) : ModuleWidget() {
  43. setModule(module);
  44. }
  45. void onButton(const ButtonEvent& e) override;
  46. };
  47. struct ModuleWidget::Internal {
  48. math::Vec dragOffset;
  49. math::Vec dragRackPos;
  50. bool dragEnabled;
  51. widget::Widget* panel;
  52. };
  53. static void CardinalModuleWidget__loadDialog(ModuleWidget* const w)
  54. {
  55. std::string presetDir = w->model->getUserPresetDirectory();
  56. system::createDirectories(presetDir);
  57. WeakPtr<ModuleWidget> weakThis = w;
  58. async_dialog_filebrowser(false, nullptr, presetDir.c_str(), "Load preset", [=](char* pathC) {
  59. // Delete directories if empty
  60. DEFER({
  61. try {
  62. system::remove(presetDir);
  63. system::remove(system::getDirectory(presetDir));
  64. }
  65. catch (Exception& e) {
  66. // Ignore exceptions if directory cannot be removed.
  67. }
  68. });
  69. if (!weakThis)
  70. return;
  71. if (!pathC)
  72. return;
  73. try {
  74. weakThis->loadAction(pathC);
  75. }
  76. catch (Exception& e) {
  77. async_dialog_message(e.what());
  78. }
  79. std::free(pathC);
  80. });
  81. }
  82. void CardinalModuleWidget__saveDialog(ModuleWidget* const w)
  83. {
  84. const std::string presetDir = w->model->getUserPresetDirectory();
  85. system::createDirectories(presetDir);
  86. WeakPtr<ModuleWidget> weakThis = w;
  87. async_dialog_filebrowser(true, "preset.vcvm", presetDir.c_str(), "Save preset", [=](char* pathC) {
  88. // Delete directories if empty
  89. DEFER({
  90. try {
  91. system::remove(presetDir);
  92. system::remove(system::getDirectory(presetDir));
  93. }
  94. catch (Exception& e) {
  95. // Ignore exceptions if directory cannot be removed.
  96. }
  97. });
  98. if (!weakThis)
  99. return;
  100. if (!pathC)
  101. return;
  102. std::string path = pathC;
  103. std::free(pathC);
  104. // Automatically append .vcvm extension
  105. if (system::getExtension(path) != ".vcvm")
  106. path += ".vcvm";
  107. weakThis->save(path);
  108. });
  109. }
  110. // Create ModulePresetPathItems for each patch in a directory.
  111. static void appendPresetItems(ui::Menu* menu, WeakPtr<ModuleWidget> moduleWidget, std::string presetDir) {
  112. bool foundPresets = false;
  113. if (system::isDirectory(presetDir))
  114. {
  115. // Note: This is not cached, so opening this menu each time might have a bit of latency.
  116. std::vector<std::string> entries = system::getEntries(presetDir);
  117. std::sort(entries.begin(), entries.end());
  118. for (std::string path : entries) {
  119. std::string name = system::getStem(path);
  120. // Remove "1_", "42_", "001_", etc at the beginning of preset filenames
  121. std::regex r("^\\d+_");
  122. name = std::regex_replace(name, r, "");
  123. if (system::getExtension(path) == ".vcvm" && name != "template")
  124. {
  125. if (!foundPresets)
  126. menu->addChild(new ui::MenuSeparator);
  127. foundPresets = true;
  128. menu->addChild(createMenuItem(name, "", [=]() {
  129. if (!moduleWidget)
  130. return;
  131. try {
  132. moduleWidget->loadAction(path);
  133. }
  134. catch (Exception& e) {
  135. async_dialog_message(e.what());
  136. }
  137. }));
  138. }
  139. }
  140. }
  141. };
  142. static void CardinalModuleWidget__createContextMenu(ModuleWidget* const w,
  143. plugin::Model* const model,
  144. engine::Module* const module) {
  145. DISTRHO_SAFE_ASSERT_RETURN(model != nullptr,);
  146. ui::Menu* menu = createMenu();
  147. WeakPtr<ModuleWidget> weakThis = w;
  148. // Brand and module name
  149. menu->addChild(createMenuLabel(model->name));
  150. menu->addChild(createMenuLabel(model->plugin->brand));
  151. // Info
  152. menu->addChild(createSubmenuItem("Info", "", [model](ui::Menu* menu) {
  153. model->appendContextMenu(menu);
  154. }));
  155. // Preset
  156. menu->addChild(createSubmenuItem("Preset", "", [weakThis](ui::Menu* menu) {
  157. menu->addChild(createMenuItem("Copy", RACK_MOD_CTRL_NAME "+C", [weakThis]() {
  158. if (!weakThis)
  159. return;
  160. weakThis->copyClipboard();
  161. }));
  162. menu->addChild(createMenuItem("Paste", RACK_MOD_CTRL_NAME "+V", [weakThis]() {
  163. if (!weakThis)
  164. return;
  165. weakThis->pasteClipboardAction();
  166. }));
  167. menu->addChild(createMenuItem("Open", "", [weakThis]() {
  168. if (!weakThis)
  169. return;
  170. CardinalModuleWidget__loadDialog(weakThis);
  171. }));
  172. /* TODO requires setting up user dir
  173. menu->addChild(createMenuItem("Save as", "", [weakThis]() {
  174. if (!weakThis)
  175. return;
  176. CardinalModuleWidget__saveDialog(weakThis);
  177. }));
  178. // Scan `<user dir>/presets/<plugin slug>/<module slug>` for presets.
  179. menu->addChild(new ui::MenuSeparator);
  180. menu->addChild(createMenuLabel("User presets"));
  181. appendPresetItems(menu, weakThis, weakThis->model->getUserPresetDirectory());
  182. */
  183. // Scan `<plugin dir>/presets/<module slug>` for presets.
  184. appendPresetItems(menu, weakThis, weakThis->model->getFactoryPresetDirectory());
  185. }));
  186. // Initialize
  187. menu->addChild(createMenuItem("Initialize", RACK_MOD_CTRL_NAME "+I", [weakThis]() {
  188. if (!weakThis)
  189. return;
  190. weakThis->resetAction();
  191. }));
  192. // Randomize
  193. menu->addChild(createMenuItem("Randomize", RACK_MOD_CTRL_NAME "+R", [weakThis]() {
  194. if (!weakThis)
  195. return;
  196. weakThis->randomizeAction();
  197. }));
  198. // Disconnect cables
  199. menu->addChild(createMenuItem("Disconnect cables", RACK_MOD_CTRL_NAME "+U", [weakThis]() {
  200. if (!weakThis)
  201. return;
  202. weakThis->disconnectAction();
  203. }));
  204. // Bypass
  205. std::string bypassText = RACK_MOD_CTRL_NAME "+E";
  206. bool bypassed = module && module->isBypassed();
  207. if (bypassed)
  208. bypassText += " " CHECKMARK_STRING;
  209. menu->addChild(createMenuItem("Bypass", bypassText, [weakThis, bypassed]() {
  210. if (!weakThis)
  211. return;
  212. weakThis->bypassAction(!bypassed);
  213. }));
  214. // Duplicate
  215. menu->addChild(createMenuItem("Duplicate", RACK_MOD_CTRL_NAME "+D", [weakThis]() {
  216. if (!weakThis)
  217. return;
  218. weakThis->cloneAction(false);
  219. }));
  220. // Duplicate with cables
  221. menu->addChild(createMenuItem("└ with cables", RACK_MOD_SHIFT_NAME "+" RACK_MOD_CTRL_NAME "+D", [weakThis]() {
  222. if (!weakThis)
  223. return;
  224. weakThis->cloneAction(true);
  225. }));
  226. // Delete
  227. menu->addChild(createMenuItem("Delete", "Backspace/Delete", [weakThis]() {
  228. if (!weakThis)
  229. return;
  230. weakThis->removeAction();
  231. }, false, true));
  232. w->appendContextMenu(menu);
  233. }
  234. static void CardinalModuleWidget__saveSelectionDialog(RackWidget* const w)
  235. {
  236. std::string selectionDir = asset::user("selections");
  237. system::createDirectories(selectionDir);
  238. async_dialog_filebrowser(true, "selection.vcvs", selectionDir.c_str(),
  239. #ifdef DISTRHO_OS_WASM
  240. "Save selection",
  241. #else
  242. "Save selection as...",
  243. #endif
  244. [w](char* pathC) {
  245. if (!pathC) {
  246. // No path selected
  247. return;
  248. }
  249. std::string path = pathC;
  250. std::free(pathC);
  251. // Automatically append .vcvs extension
  252. if (system::getExtension(path) != ".vcvs")
  253. path += ".vcvs";
  254. w->saveSelection(path);
  255. });
  256. }
  257. void CardinalModuleWidget::onButton(const ButtonEvent& e)
  258. {
  259. const bool selected = APP->scene->rack->isSelected(this);
  260. if (selected) {
  261. if (e.button == GLFW_MOUSE_BUTTON_RIGHT) {
  262. if (e.action == GLFW_PRESS) {
  263. // Open selection context menu on right-click
  264. ui::Menu* menu = createMenu();
  265. patchUtils::appendSelectionContextMenu(menu);
  266. }
  267. e.consume(this);
  268. }
  269. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  270. if (e.action == GLFW_PRESS) {
  271. // Toggle selection on Shift-click
  272. if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) {
  273. APP->scene->rack->select(this, false);
  274. e.consume(NULL);
  275. return;
  276. }
  277. // If module positions are locked, don't consume left-click
  278. if (settings::lockModules) {
  279. return;
  280. }
  281. internal->dragOffset = e.pos;
  282. }
  283. e.consume(this);
  284. }
  285. return;
  286. }
  287. // Dispatch event to children
  288. Widget::onButton(e);
  289. e.stopPropagating();
  290. if (e.isConsumed())
  291. return;
  292. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  293. if (e.action == GLFW_PRESS) {
  294. // Toggle selection on Shift-click
  295. if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) {
  296. APP->scene->rack->select(this, true);
  297. e.consume(NULL);
  298. return;
  299. }
  300. // If module positions are locked, don't consume left-click
  301. if (settings::lockModules) {
  302. return;
  303. }
  304. internal->dragOffset = e.pos;
  305. }
  306. e.consume(this);
  307. }
  308. // Open context menu on right-click
  309. if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) {
  310. CardinalModuleWidget__createContextMenu(this, model, module);
  311. e.consume(this);
  312. }
  313. }
  314. }
  315. }
  316. namespace patchUtils
  317. {
  318. using namespace rack;
  319. void appendSelectionContextMenu(ui::Menu* const menu)
  320. {
  321. app::RackWidget* const w = APP->scene->rack;
  322. int n = w->getSelected().size();
  323. menu->addChild(createMenuLabel(string::f("%d selected %s", n, n == 1 ? "module" : "modules")));
  324. // Enable alwaysConsume of menu items if the number of selected modules changes
  325. // Select all
  326. menu->addChild(createMenuItem("Select all", RACK_MOD_CTRL_NAME "+A", [w]() {
  327. w->selectAll();
  328. }, false, true));
  329. // Deselect
  330. menu->addChild(createMenuItem("Deselect", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+A", [w]() {
  331. w->deselectAll();
  332. }, n == 0, true));
  333. // Copy
  334. menu->addChild(createMenuItem("Copy", RACK_MOD_CTRL_NAME "+C", [w]() {
  335. w->copyClipboardSelection();
  336. }, n == 0));
  337. // Paste
  338. menu->addChild(createMenuItem("Paste", RACK_MOD_CTRL_NAME "+V", [w]() {
  339. w->pasteClipboardAction();
  340. }, false, true));
  341. // Save
  342. menu->addChild(createMenuItem(
  343. #ifdef DISTRHO_OS_WASM
  344. "Save selection",
  345. #else
  346. "Save selection as...",
  347. #endif
  348. "", [w]() {
  349. CardinalModuleWidget__saveSelectionDialog(w);
  350. }, n == 0));
  351. // Initialize
  352. menu->addChild(createMenuItem("Initialize", RACK_MOD_CTRL_NAME "+I", [w]() {
  353. w->resetSelectionAction();
  354. }, n == 0));
  355. // Randomize
  356. menu->addChild(createMenuItem("Randomize", RACK_MOD_CTRL_NAME "+R", [w]() {
  357. w->randomizeSelectionAction();
  358. }, n == 0));
  359. // Disconnect cables
  360. menu->addChild(createMenuItem("Disconnect cables", RACK_MOD_CTRL_NAME "+U", [w]() {
  361. w->disconnectSelectionAction();
  362. }, n == 0));
  363. // Bypass
  364. std::string bypassText = RACK_MOD_CTRL_NAME "+E";
  365. bool bypassed = (n > 0) && w->isSelectionBypassed();
  366. if (bypassed)
  367. bypassText += " " CHECKMARK_STRING;
  368. menu->addChild(createMenuItem("Bypass", bypassText, [w, bypassed]() {
  369. w->bypassSelectionAction(!bypassed);
  370. }, n == 0, true));
  371. // Duplicate
  372. menu->addChild(createMenuItem("Duplicate", RACK_MOD_CTRL_NAME "+D", [w]() {
  373. w->cloneSelectionAction(false);
  374. }, n == 0));
  375. // Duplicate with cables
  376. menu->addChild(createMenuItem("└ with cables", RACK_MOD_SHIFT_NAME "+" RACK_MOD_CTRL_NAME "+D", [w]() {
  377. w->cloneSelectionAction(true);
  378. }, n == 0));
  379. // Delete
  380. menu->addChild(createMenuItem("Delete", "Backspace/Delete", [w]() {
  381. w->deleteSelectionAction();
  382. }, n == 0, true));
  383. }
  384. }