/* * DISTRHO Cardinal Plugin * Copyright (C) 2021-2022 Filipe Coelho * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 3 of * the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * For a full copy of the GNU General Public License see the LICENSE file. */ /** * This file is partially based on VCVRack's ModuleWidget.cpp * Copyright (C) 2016-2021 VCV. * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. */ #include "CardinalCommon.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace rack { namespace app { struct CardinalModuleWidget : ModuleWidget { CardinalModuleWidget() : ModuleWidget() {} DEPRECATED CardinalModuleWidget(engine::Module* module) : ModuleWidget() { setModule(module); } void onButton(const ButtonEvent& e) override; }; struct ModuleWidget::Internal { math::Vec dragOffset; math::Vec dragRackPos; bool dragEnabled; widget::Widget* panel; }; static void CardinalModuleWidget__loadDialog(ModuleWidget* const w) { std::string presetDir = w->model->getUserPresetDirectory(); system::createDirectories(presetDir); WeakPtr weakThis = w; async_dialog_filebrowser(false, presetDir.c_str(), "Load preset", [=](char* pathC) { // Delete directories if empty DEFER({ try { system::remove(presetDir); system::remove(system::getDirectory(presetDir)); } catch (Exception& e) { // Ignore exceptions if directory cannot be removed. } }); if (!weakThis) return; if (!pathC) return; try { weakThis->loadAction(pathC); } catch (Exception& e) { async_dialog_message(e.what()); } std::free(pathC); }); } void CardinalModuleWidget__saveDialog(ModuleWidget* const w) { const std::string presetDir = w->model->getUserPresetDirectory(); system::createDirectories(presetDir); WeakPtr weakThis = w; async_dialog_filebrowser(true, presetDir.c_str(), "Save preset", [=](char* pathC) { // Delete directories if empty DEFER({ try { system::remove(presetDir); system::remove(system::getDirectory(presetDir)); } catch (Exception& e) { // Ignore exceptions if directory cannot be removed. } }); if (!weakThis) return; if (!pathC) return; std::string path = pathC; std::free(pathC); // Automatically append .vcvm extension if (system::getExtension(path) != ".vcvm") path += ".vcvm"; weakThis->save(path); }); } // Create ModulePresetPathItems for each patch in a directory. static void appendPresetItems(ui::Menu* menu, WeakPtr moduleWidget, std::string presetDir) { bool foundPresets = false; // Note: This is not cached, so opening this menu each time might have a bit of latency. if (system::isDirectory(presetDir)) { std::vector entries = system::getEntries(presetDir); std::sort(entries.begin(), entries.end()); for (std::string path : entries) { std::string name = system::getStem(path); // Remove "1_", "42_", "001_", etc at the beginning of preset filenames std::regex r("^\\d+_"); name = std::regex_replace(name, r, ""); if (system::getExtension(path) == ".vcvm") { if (!foundPresets) menu->addChild(new ui::MenuSeparator); foundPresets = true; menu->addChild(createMenuItem(name, "", [=]() { if (!moduleWidget) return; try { moduleWidget->loadAction(path); } catch (Exception& e) { async_dialog_message(e.what()); } })); } } } }; static void CardinalModuleWidget__createContextMenu(ModuleWidget* const w, plugin::Model* const model, engine::Module* const module) { DISTRHO_SAFE_ASSERT_RETURN(model != nullptr,); ui::Menu* menu = createMenu(); WeakPtr weakThis = w; // Brand and module name menu->addChild(createMenuLabel(model->name)); menu->addChild(createMenuLabel(model->plugin->brand)); // Info menu->addChild(createSubmenuItem("Info", "", [model](ui::Menu* menu) { model->appendContextMenu(menu); })); // Preset menu->addChild(createSubmenuItem("Preset", "", [weakThis](ui::Menu* menu) { menu->addChild(createMenuItem("Copy", RACK_MOD_CTRL_NAME "+C", [weakThis]() { if (!weakThis) return; weakThis->copyClipboard(); })); menu->addChild(createMenuItem("Paste", RACK_MOD_CTRL_NAME "+V", [weakThis]() { if (!weakThis) return; weakThis->pasteClipboardAction(); })); menu->addChild(createMenuItem("Open", "", [weakThis]() { if (!weakThis) return; CardinalModuleWidget__loadDialog(weakThis); })); /* TODO requires setting up user dir menu->addChild(createMenuItem("Save as", "", [weakThis]() { if (!weakThis) return; CardinalModuleWidget__saveDialog(weakThis); })); // Scan `/presets//` for presets. menu->addChild(new ui::MenuSeparator); menu->addChild(createMenuLabel("User presets")); appendPresetItems(menu, weakThis, weakThis->model->getUserPresetDirectory()); */ // Scan `/presets/` for presets. appendPresetItems(menu, weakThis, weakThis->model->getFactoryPresetDirectory()); })); // Initialize menu->addChild(createMenuItem("Initialize", RACK_MOD_CTRL_NAME "+I", [weakThis]() { if (!weakThis) return; weakThis->resetAction(); })); // Randomize menu->addChild(createMenuItem("Randomize", RACK_MOD_CTRL_NAME "+R", [weakThis]() { if (!weakThis) return; weakThis->randomizeAction(); })); // Disconnect cables menu->addChild(createMenuItem("Disconnect cables", RACK_MOD_CTRL_NAME "+U", [weakThis]() { if (!weakThis) return; weakThis->disconnectAction(); })); // Bypass std::string bypassText = RACK_MOD_CTRL_NAME "+E"; bool bypassed = module && module->isBypassed(); if (bypassed) bypassText += " " CHECKMARK_STRING; menu->addChild(createMenuItem("Bypass", bypassText, [weakThis, bypassed]() { if (!weakThis) return; weakThis->bypassAction(!bypassed); })); // Duplicate menu->addChild(createMenuItem("Duplicate", RACK_MOD_CTRL_NAME "+D", [weakThis]() { if (!weakThis) return; weakThis->cloneAction(false); })); // Duplicate with cables menu->addChild(createMenuItem("└ with cables", RACK_MOD_SHIFT_NAME "+" RACK_MOD_CTRL_NAME "+D", [weakThis]() { if (!weakThis) return; weakThis->cloneAction(true); })); // Delete menu->addChild(createMenuItem("Delete", "Backspace/Delete", [weakThis]() { if (!weakThis) return; weakThis->removeAction(); }, false, true)); w->appendContextMenu(menu); } static void CardinalModuleWidget__saveSelectionDialog(RackWidget* const w) { std::string selectionDir = asset::user("selections"); system::createDirectories(selectionDir); async_dialog_filebrowser(true, selectionDir.c_str(), "Save selection as", [w](char* pathC) { if (!pathC) { // No path selected return; } std::string path = pathC; std::free(pathC); // Automatically append .vcvs extension if (system::getExtension(path) != ".vcvs") path += ".vcvs"; w->saveSelection(path); }); } void CardinalModuleWidget::onButton(const ButtonEvent& e) { const bool selected = APP->scene->rack->isSelected(this); if (selected) { if (e.button == GLFW_MOUSE_BUTTON_RIGHT) { if (e.action == GLFW_PRESS) { ui::Menu* menu = createMenu(); patchUtils::appendSelectionContextMenu(menu); } e.consume(this); } if (e.button == GLFW_MOUSE_BUTTON_LEFT) { if (e.action == GLFW_PRESS) { // Toggle selection on Shift-click if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) { APP->scene->rack->select(this, false); e.consume(NULL); return; } internal->dragOffset = e.pos; } e.consume(this); } return; } // Dispatch event to children Widget::onButton(e); e.stopPropagating(); if (e.isConsumed()) return; if (e.button == GLFW_MOUSE_BUTTON_LEFT) { if (e.action == GLFW_PRESS) { // Toggle selection on Shift-click if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) { APP->scene->rack->select(this, true); e.consume(NULL); return; } // If module positions are locked, don't consume left-click if (settings::lockModules) { return; } internal->dragOffset = e.pos; } e.consume(this); } // Open context menu on right-click if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) { CardinalModuleWidget__createContextMenu(this, model, module); e.consume(this); } } } } namespace patchUtils { using namespace rack; void appendSelectionContextMenu(ui::Menu* const menu) { app::RackWidget* const w = APP->scene->rack; int n = w->getSelected().size(); menu->addChild(createMenuLabel(string::f("%d selected %s", n, n == 1 ? "module" : "modules"))); // Enable alwaysConsume of menu items if the number of selected modules changes // Select all menu->addChild(createMenuItem("Select all", RACK_MOD_CTRL_NAME "+A", [w]() { w->selectAll(); }, false, true)); // Deselect menu->addChild(createMenuItem("Deselect", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+A", [w]() { w->deselectAll(); }, n == 0, true)); // Copy menu->addChild(createMenuItem("Copy", RACK_MOD_CTRL_NAME "+C", [w]() { w->copyClipboardSelection(); }, n == 0)); // Paste menu->addChild(createMenuItem("Paste", RACK_MOD_CTRL_NAME "+V", [w]() { w->pasteClipboardAction(); }, false, true)); // Save menu->addChild(createMenuItem("Save selection as", "", [w]() { CardinalModuleWidget__saveSelectionDialog(w); }, n == 0)); // Initialize menu->addChild(createMenuItem("Initialize", RACK_MOD_CTRL_NAME "+I", [w]() { w->resetSelectionAction(); }, n == 0)); // Randomize menu->addChild(createMenuItem("Randomize", RACK_MOD_CTRL_NAME "+R", [w]() { w->randomizeSelectionAction(); }, n == 0)); // Disconnect cables menu->addChild(createMenuItem("Disconnect cables", RACK_MOD_CTRL_NAME "+U", [w]() { w->disconnectSelectionAction(); }, n == 0)); // Bypass std::string bypassText = RACK_MOD_CTRL_NAME "+E"; bool bypassed = (n > 0) && w->isSelectionBypassed(); if (bypassed) bypassText += " " CHECKMARK_STRING; menu->addChild(createMenuItem("Bypass", bypassText, [w, bypassed]() { w->bypassSelectionAction(!bypassed); }, n == 0, true)); // Duplicate menu->addChild(createMenuItem("Duplicate", RACK_MOD_CTRL_NAME "+D", [w]() { w->cloneSelectionAction(false); }, n == 0)); // Duplicate with cables menu->addChild(createMenuItem("└ with cables", RACK_MOD_SHIFT_NAME "+" RACK_MOD_CTRL_NAME "+D", [w]() { w->cloneSelectionAction(true); }, n == 0)); // Delete menu->addChild(createMenuItem("Delete", "Backspace/Delete", [w]() { w->deleteSelectionAction(); }, n == 0, true)); } }