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.

419 lines
12KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021 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 <system.hpp>
  37. namespace rack {
  38. namespace app {
  39. struct CardinalModuleWidget : ModuleWidget {
  40. CardinalModuleWidget() : ModuleWidget() {}
  41. DEPRECATED CardinalModuleWidget(engine::Module* module) : ModuleWidget() {
  42. setModule(module);
  43. }
  44. void onButton(const ButtonEvent& e) override;
  45. };
  46. struct ModuleWidget::Internal {
  47. math::Vec dragOffset;
  48. math::Vec dragRackPos;
  49. bool dragEnabled;
  50. math::Vec oldPos;
  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, 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, 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. // Note: This is not cached, so opening this menu each time might have a bit of latency.
  114. if (system::isDirectory(presetDir))
  115. {
  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")
  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, selectionDir.c_str(), "Save selection as", [w](char* pathC) {
  239. if (!pathC) {
  240. // No path selected
  241. return;
  242. }
  243. std::string path = pathC;
  244. std::free(pathC);
  245. // Automatically append .vcvs extension
  246. if (system::getExtension(path) != ".vcvs")
  247. path += ".vcvs";
  248. w->saveSelection(path);
  249. });
  250. }
  251. void CardinalModuleWidget::onButton(const ButtonEvent& e)
  252. {
  253. bool selected = APP->scene->rack->isSelected(this);
  254. if (selected) {
  255. if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) {
  256. ui::Menu* menu = createMenu();
  257. patchUtils::appendSelectionContextMenu(menu);
  258. }
  259. e.consume(this);
  260. }
  261. OpaqueWidget::onButton(e);
  262. if (e.getTarget() == this) {
  263. // Set starting drag position
  264. if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) {
  265. internal->dragOffset = e.pos;
  266. }
  267. // Toggle selection on Shift-click
  268. if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) {
  269. APP->scene->rack->select(this, !selected);
  270. }
  271. }
  272. if (!e.isConsumed() && !selected) {
  273. // Open context menu on right-click
  274. if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) {
  275. CardinalModuleWidget__createContextMenu(this, model, module);
  276. e.consume(this);
  277. }
  278. }
  279. }
  280. }
  281. }
  282. namespace patchUtils
  283. {
  284. using namespace rack;
  285. void appendSelectionContextMenu(ui::Menu* const menu)
  286. {
  287. app::RackWidget* const w = APP->scene->rack;
  288. int n = w->getSelected().size();
  289. menu->addChild(createMenuLabel(string::f("%d selected %s", n, n == 1 ? "module" : "modules")));
  290. // Enable alwaysConsume of menu items if the number of selected modules changes
  291. // Select all
  292. menu->addChild(createMenuItem("Select all", RACK_MOD_CTRL_NAME "+A", [w]() {
  293. w->selectAll();
  294. }, false, true));
  295. // Deselect
  296. menu->addChild(createMenuItem("Deselect", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+A", [w]() {
  297. w->deselectAll();
  298. }, n == 0, true));
  299. // Copy
  300. menu->addChild(createMenuItem("Copy", RACK_MOD_CTRL_NAME "+C", [w]() {
  301. w->copyClipboardSelection();
  302. }, n == 0));
  303. // Paste
  304. menu->addChild(createMenuItem("Paste", RACK_MOD_CTRL_NAME "+V", [w]() {
  305. w->pasteClipboardAction();
  306. }, false, true));
  307. // Save
  308. menu->addChild(createMenuItem("Save selection as", "", [w]() {
  309. CardinalModuleWidget__saveSelectionDialog(w);
  310. }, n == 0));
  311. // Initialize
  312. menu->addChild(createMenuItem("Initialize", RACK_MOD_CTRL_NAME "+I", [w]() {
  313. w->resetSelectionAction();
  314. }, n == 0));
  315. // Randomize
  316. menu->addChild(createMenuItem("Randomize", RACK_MOD_CTRL_NAME "+R", [w]() {
  317. w->randomizeSelectionAction();
  318. }, n == 0));
  319. // Disconnect cables
  320. menu->addChild(createMenuItem("Disconnect cables", RACK_MOD_CTRL_NAME "+U", [w]() {
  321. w->disconnectSelectionAction();
  322. }, n == 0));
  323. // Bypass
  324. std::string bypassText = RACK_MOD_CTRL_NAME "+E";
  325. bool bypassed = (n > 0) && w->isSelectionBypassed();
  326. if (bypassed)
  327. bypassText += " " CHECKMARK_STRING;
  328. menu->addChild(createMenuItem("Bypass", bypassText, [w, bypassed]() {
  329. w->bypassSelectionAction(!bypassed);
  330. }, n == 0, true));
  331. // Duplicate
  332. menu->addChild(createMenuItem("Duplicate", RACK_MOD_CTRL_NAME "+D", [w]() {
  333. w->cloneSelectionAction(false);
  334. }, n == 0));
  335. // Duplicate with cables
  336. menu->addChild(createMenuItem("└ with cables", RACK_MOD_SHIFT_NAME "+" RACK_MOD_CTRL_NAME "+D", [w]() {
  337. w->cloneSelectionAction(true);
  338. }, n == 0));
  339. // Delete
  340. menu->addChild(createMenuItem("Delete", "Backspace/Delete", [w]() {
  341. w->deleteSelectionAction();
  342. }, n == 0, true));
  343. }
  344. }