/* * DISTRHO Cardinal Plugin * Copyright (C) 2021-2023 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. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef DPF_RUNTIME_TESTING # include #endif #ifdef DISTRHO_OS_WASM # include # include # include # include # include #endif #ifdef NDEBUG # undef DEBUG #endif #include "Application.hpp" #include "AsyncDialog.hpp" #include "CardinalCommon.hpp" #include "PluginContext.hpp" #include "WindowParameters.hpp" #include "extra/Base64.hpp" #ifndef DISTRHO_OS_WASM # include "extra/SharedResourcePointer.hpp" #endif #ifndef HEADLESS # include "extra/ScopedValueSetter.hpp" #endif namespace rack { #ifdef DISTRHO_OS_WASM namespace asset { std::string patchesPath(); } #endif namespace engine { void Engine_setAboutToClose(Engine*); void Engine_setRemoteDetails(Engine*, remoteUtils::RemoteDetails*); } namespace window { void WindowSetPluginUI(Window* window, CardinalBaseUI* ui); void WindowSetMods(Window* window, int mods); void WindowSetInternalSize(rack::window::Window* window, math::Vec size); } } START_NAMESPACE_DISTRHO // -------------------------------------------------------------------------------------------------------------------- #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS uint32_t Plugin::getBufferSize() const noexcept { return 0; } double Plugin::getSampleRate() const noexcept { return 0.0; } const char* Plugin::getBundlePath() const noexcept { return nullptr; } bool Plugin::isSelfTestInstance() const noexcept { return false; } bool Plugin::writeMidiEvent(const MidiEvent&) noexcept { return false; } #endif // -------------------------------------------------------------------------------------------------------------------- #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI struct WasmWelcomeDialog : rack::widget::OpaqueWidget { static const constexpr float margin = 10; static const constexpr float buttonWidth = 110; WasmWelcomeDialog() { using rack::ui::Button; using rack::ui::Label; using rack::ui::MenuOverlay; using rack::ui::SequentialLayout; box.size = rack::math::Vec(550, 310); SequentialLayout* const layout = new SequentialLayout; layout->box.pos = rack::math::Vec(0, 0); layout->box.size = box.size; layout->orientation = SequentialLayout::VERTICAL_ORIENTATION; layout->margin = rack::math::Vec(margin, margin); layout->spacing = rack::math::Vec(margin, margin); layout->wrap = false; addChild(layout); SequentialLayout* const contentLayout = new SequentialLayout; contentLayout->spacing = rack::math::Vec(margin, margin); layout->addChild(contentLayout); SequentialLayout* const buttonLayout = new SequentialLayout; buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT; buttonLayout->box.size = box.size; buttonLayout->spacing = rack::math::Vec(margin, margin); layout->addChild(buttonLayout); Label* const label = new Label; label->box.size.x = box.size.x - 2*margin; label->box.size.y = box.size.y - 2*margin - 40; label->fontSize = 20; label->text = "" "Welcome to Cardinal on the Web!\n" "\n" "If using mobile/touch devices, please note:\n" " - Single quick press does simple mouse click\n" " - Press & move does click & drag action\n" " - Press & hold does right-click (and opens module browser)\n" "\n" "Still a bit experimental, so proceed with caution.\n" "Have fun!"; contentLayout->addChild(label); struct JoinDiscussionButton : Button { WasmWelcomeDialog* dialog; void onAction(const ActionEvent& e) override { patchUtils::openBrowser("https://github.com/DISTRHO/Cardinal/issues/287"); dialog->getParent()->requestDelete(); } }; JoinDiscussionButton* const discussionButton = new JoinDiscussionButton; discussionButton->box.size.x = buttonWidth; discussionButton->text = "Join discussion"; discussionButton->dialog = this; buttonLayout->addChild(discussionButton); struct DismissButton : Button { WasmWelcomeDialog* dialog; void onAction(const ActionEvent& e) override { dialog->getParent()->requestDelete(); } }; DismissButton* const dismissButton = new DismissButton; dismissButton->box.size.x = buttonWidth; dismissButton->text = "Dismiss"; dismissButton->dialog = this; buttonLayout->addChild(dismissButton); MenuOverlay* const overlay = new MenuOverlay; overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33); overlay->addChild(this); APP->scene->addChild(overlay); } void step() override { OpaqueWidget::step(); box.pos = parent->box.size.minus(box.size).div(2).round(); } void draw(const DrawArgs& args) override { bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0); Widget::draw(args); } }; struct WasmRemotePatchLoadingDialog : rack::widget::OpaqueWidget { static const constexpr float margin = 10; rack::ui::MenuOverlay* overlay; WasmRemotePatchLoadingDialog(const bool isFromPatchStorage) { using rack::ui::Label; using rack::ui::MenuOverlay; using rack::ui::SequentialLayout; box.size = rack::math::Vec(300, 40); SequentialLayout* const layout = new SequentialLayout; layout->box.pos = rack::math::Vec(0, 0); layout->box.size = box.size; layout->alignment = SequentialLayout::CENTER_ALIGNMENT; layout->margin = rack::math::Vec(margin, margin); layout->spacing = rack::math::Vec(margin, margin); layout->wrap = false; addChild(layout); Label* const label = new Label; label->box.size.x = box.size.x - 2*margin; label->box.size.y = box.size.y - 2*margin; label->fontSize = 16; label->text = isFromPatchStorage ? "Loading patch from PatchStorage...\n" : "Loading remote patch...\n"; layout->addChild(label); overlay = new MenuOverlay; overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33); overlay->addChild(this); APP->scene->addChild(overlay); } void step() override { OpaqueWidget::step(); box.pos = parent->box.size.minus(box.size).div(2).round(); } void draw(const DrawArgs& args) override { bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0); Widget::draw(args); } }; static void downloadRemotePatchFailed(const char* const filename) { d_stdout("downloadRemotePatchFailed %s", filename); CardinalPluginContext* const context = static_cast(APP); CardinalBaseUI* const ui = static_cast(context->ui); if (ui->psDialog != nullptr) { ui->psDialog->overlay->requestDelete(); ui->psDialog = nullptr; asyncDialog::create("Failed to fetch remote patch"); } using namespace rack; context->patch->templatePath = rack::system::join(asset::patchesPath(), "templates/main.vcv"); context->patch->loadTemplate(); context->scene->rackScroll->reset(); } static void downloadRemotePatchSucceeded(const char* const filename) { d_stdout("downloadRemotePatchSucceeded %s | %s", filename, APP->patch->templatePath.c_str()); CardinalPluginContext* const context = static_cast(APP); CardinalBaseUI* const ui = static_cast(context->ui); ui->psDialog->overlay->requestDelete(); ui->psDialog = nullptr; if (FILE* f = fopen(filename, "r")) { uint8_t buf[8] = {}; fread(buf, 8, 1, f); d_stdout("read patch %x %x %x %x %x %x %x %x", buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7]); fclose(f); } try { context->patch->load(filename); } catch (rack::Exception& e) { const std::string message = rack::string::f("Could not load patch: %s", e.what()); asyncDialog::create(message.c_str()); return; } context->patch->path.clear(); context->scene->rackScroll->reset(); context->history->setSaved(); } #endif // ----------------------------------------------------------------------------------------------------------- class CardinalUI : public CardinalBaseUI, public WindowParametersCallback { #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS #ifdef DISTRHO_OS_WASM ScopedPointer fInitializer; #else SharedResourcePointer fInitializer; #endif std::string fAutosavePath; #endif rack::math::Vec lastMousePos; WindowParameters windowParameters; int rateLimitStep = 0; #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI int8_t counterForFirstIdlePoint = 0; #endif #ifdef DPF_RUNTIME_TESTING bool inSelfTest = false; #endif struct ScopedContext { CardinalPluginContext* const context; ScopedContext(CardinalUI* const ui) : context(ui->context) { rack::contextSet(context); WindowParametersRestore(context->window); } ScopedContext(CardinalUI* const ui, const int mods) : context(ui->context) { rack::contextSet(context); rack::window::WindowSetMods(context->window, mods); WindowParametersRestore(context->window); } ~ScopedContext() { if (context->window != nullptr) WindowParametersSave(context->window); } }; public: CardinalUI() : CardinalBaseUI(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT), #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS #ifdef DISTRHO_OS_WASM fInitializer(new Initializer(static_cast(nullptr), this)), #else fInitializer(static_cast(nullptr), this), #endif #endif lastMousePos() { rack::contextSet(context); #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS // create unique temporary path for this instance try { char uidBuf[24]; const std::string tmp = rack::system::getTempDirectory(); for (int i=1;; ++i) { std::snprintf(uidBuf, sizeof(uidBuf), "Cardinal.%04d", i); const std::string trypath = rack::system::join(tmp, uidBuf); if (! rack::system::exists(trypath)) { if (rack::system::createDirectories(trypath)) fAutosavePath = trypath; break; } } } DISTRHO_SAFE_EXCEPTION("create unique temporary path"); const float sampleRate = 60; // fake audio running at 60 fps rack::settings::sampleRate = sampleRate; context->dataIns = new const float*[DISTRHO_PLUGIN_NUM_INPUTS]; context->dataOuts = new float*[DISTRHO_PLUGIN_NUM_OUTPUTS]; for (uint32_t i=0; i(&context->dataIns[i]); *bufferptr = new float[1]; (*bufferptr)[0] = 0.f; } for (uint32_t i=0; idataOuts[i] = new float[1]; context->bufferSize = 1; context->sampleRate = sampleRate; context->engine = new rack::engine::Engine; context->engine->setSampleRate(sampleRate); context->history = new rack::history::State; context->patch = new rack::patch::Manager; context->patch->autosavePath = fAutosavePath; context->patch->templatePath = context->patch->factoryTemplatePath = fInitializer->factoryTemplatePath; context->event = new rack::widget::EventState; context->scene = new rack::app::Scene; context->event->rootWidget = context->scene; context->window = new rack::window::Window; context->patch->loadTemplate(); context->scene->rackScroll->reset(); DISTRHO_SAFE_ASSERT(remoteUtils::connectToRemote(CARDINAL_DEFAULT_REMOTE_URL)); Engine_setRemoteDetails(context->engine, remoteDetails); #endif Window& window(getWindow()); window.setIgnoringKeyRepeat(true); context->nativeWindowId = window.getNativeWindowHandle(); const double scaleFactor = getScaleFactor(); setGeometryConstraints(648 * scaleFactor, 538 * scaleFactor); if (rack::isStandalone() && rack::system::exists(rack::settings::settingsPath)) { const double width = std::max(648.f, rack::settings::windowSize.x) * scaleFactor; const double height = std::max(538.f, rack::settings::windowSize.y) * scaleFactor; setSize(width, height); } else if (scaleFactor != 1.0) { setSize(DISTRHO_UI_DEFAULT_WIDTH * scaleFactor, DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor); } #if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS const DGL_NAMESPACE::Window::ScopedGraphicsContext sgc(window); #endif rack::window::WindowSetPluginUI(context->window, this); // hide "Browse VCV Library" button rack::widget::Widget* const browser = context->scene->browser->children.back(); rack::widget::Widget* const headerLayout = browser->children.front(); rack::widget::Widget* const libraryButton = headerLayout->children.back(); libraryButton->hide(); // Report to user if something is wrong with the installation std::string errorMessage; if (rack::asset::systemDir.empty()) { errorMessage = "Failed to locate Cardinal plugin bundle.\n" "Install Cardinal with its plugin bundle folder intact and try again."; } else if (! rack::system::exists(rack::asset::systemDir)) { errorMessage = rack::string::f("System directory \"%s\" does not exist. " "Make sure Cardinal was downloaded and installed correctly.", rack::asset::systemDir.c_str()); } if (! errorMessage.empty()) { static bool shown = false; if (! shown) { shown = true; asyncDialog::create(errorMessage.c_str()); } } #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI if (rack::patchStorageSlug != nullptr) { psDialog = new WasmRemotePatchLoadingDialog(true); } else if (rack::patchRemoteURL != nullptr) { psDialog = new WasmRemotePatchLoadingDialog(false); } else if (rack::patchFromURL != nullptr) { static_cast(context->plugin)->setState("patch", rack::patchFromURL); rack::contextSet(context); } else { new WasmWelcomeDialog(); } #endif context->window->step(); rack::contextSet(nullptr); WindowParametersSetCallback(context->window, this); } ~CardinalUI() override { rack::contextSet(context); context->nativeWindowId = 0; rack::window::WindowSetPluginUI(context->window, nullptr); context->tlw = nullptr; context->ui = nullptr; #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS { const ScopedContext sc(this); context->patch->clear(); // do a little dance to prevent context scene deletion from saving to temp dir const ScopedValueSetter svs(rack::settings::headless, true); Engine_setAboutToClose(context->engine); delete context; } if (! fAutosavePath.empty()) rack::system::removeRecursively(fAutosavePath); #endif #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS if (fInitializer->shouldSaveSettings) { INFO("Save settings"); rack::settings::save(); } #endif rack::contextSet(nullptr); } void onNanoDisplay() override { const ScopedContext sc(this); context->window->step(); } void uiIdle() override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) { context->window->step(); return; } if (context->plugin->isSelfTestInstance()) { inSelfTest = true; Application& app(getApp()); const ScopedContext sc(this); context->patch->clear(); app.idle(); const rack::math::Vec mousePos(getWidth()/2,getHeight()/2); context->event->handleButton(mousePos, GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0x0); context->event->handleHover(mousePos, rack::math::Vec(0,0)); app.idle(); for (rack::plugin::Plugin* p : rack::plugin::plugins) { for (rack::plugin::Model* m : p->models) { d_stdout(">>>>>>>>>>>>>>>>> LOADING module %s : %s", p->slug.c_str(), m->slug.c_str()); rack::engine::Module* const module = m->createModule(); DISTRHO_SAFE_ASSERT_CONTINUE(module != nullptr); rack::CardinalPluginModelHelper* const helper = dynamic_cast(m); DISTRHO_SAFE_ASSERT_CONTINUE(helper != nullptr); d_stdout(">>>>>>>>>>>>>>>>> LOADING moduleWidget %s : %s", p->slug.c_str(), m->slug.c_str()); rack::app::ModuleWidget* const moduleWidget = helper->createModuleWidget(module); DISTRHO_SAFE_ASSERT_CONTINUE(moduleWidget != nullptr); d_stdout(">>>>>>>>>>>>>>>>> ADDING TO ENGINE %s : %s", p->slug.c_str(), m->slug.c_str()); context->engine->addModule(module); d_stdout(">>>>>>>>>>>>>>>>> ADDING TO RACK VIEW %s : %s", p->slug.c_str(), m->slug.c_str()); context->scene->rack->addModuleAtMouse(moduleWidget); for (int i=5; --i>=0;) app.idle(); d_stdout(">>>>>>>>>>>>>>>>> REMOVING FROM RACK VIEW %s : %s", p->slug.c_str(), m->slug.c_str()); context->scene->rack->removeModule(moduleWidget); app.idle(); d_stdout(">>>>>>>>>>>>>>>>> DELETING module + moduleWidget %s : %s", p->slug.c_str(), m->slug.c_str()); delete moduleWidget; app.idle(); } } inSelfTest = false; } #endif #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI if (counterForFirstIdlePoint >= 0 && ++counterForFirstIdlePoint == 30) { counterForFirstIdlePoint = -1; if (rack::patchStorageSlug != nullptr) { std::string url("/patchstorage.php?slug="); url += rack::patchStorageSlug; std::free(rack::patchStorageSlug); rack::patchStorageSlug = nullptr; emscripten_async_wget(url.c_str(), context->patch->templatePath.c_str(), downloadRemotePatchSucceeded, downloadRemotePatchFailed); } else if (rack::patchRemoteURL != nullptr) { std::string url("/patchurl.php?url="); url += rack::patchRemoteURL; std::free(rack::patchRemoteURL); rack::patchRemoteURL = nullptr; emscripten_async_wget(url.c_str(), context->patch->templatePath.c_str(), downloadRemotePatchSucceeded, downloadRemotePatchFailed); } } #endif if (filebrowserhandle != nullptr && fileBrowserIdle(filebrowserhandle)) { { const char* const path = fileBrowserGetPath(filebrowserhandle); const ScopedContext sc(this); filebrowseraction(path != nullptr ? strdup(path) : nullptr); } fileBrowserClose(filebrowserhandle); filebrowseraction = nullptr; filebrowserhandle = nullptr; } #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS { const ScopedContext sc(this); for (uint32_t i=0; idataOuts[i][0] = 0.f; ++context->processCounter; context->engine->stepBlock(1); } #endif if (windowParameters.rateLimit != 0 && ++rateLimitStep % (windowParameters.rateLimit * 2)) return; rateLimitStep = 0; repaint(); } void WindowParametersChanged(const WindowParameterList param, float value) override { float mult = 1.0f; switch (param) { case kWindowParameterShowTooltips: windowParameters.tooltips = value > 0.5f; break; case kWindowParameterCableOpacity: mult = 100.0f; windowParameters.cableOpacity = value; break; case kWindowParameterCableTension: mult = 100.0f; windowParameters.cableTension = value; break; case kWindowParameterRackBrightness: mult = 100.0f; windowParameters.rackBrightness = value; break; case kWindowParameterHaloBrightness: mult = 100.0f; windowParameters.haloBrightness = value; break; case kWindowParameterKnobMode: switch (static_cast(value + 0.5f)) { case rack::settings::KNOB_MODE_LINEAR: value = 0; windowParameters.knobMode = rack::settings::KNOB_MODE_LINEAR; break; case rack::settings::KNOB_MODE_ROTARY_ABSOLUTE: value = 1; windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_ABSOLUTE; break; case rack::settings::KNOB_MODE_ROTARY_RELATIVE: value = 2; windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_RELATIVE; break; } break; case kWindowParameterWheelKnobControl: windowParameters.knobScroll = value > 0.5f; break; case kWindowParameterWheelSensitivity: mult = 1000.0f; windowParameters.knobScrollSensitivity = value; break; case kWindowParameterLockModulePositions: windowParameters.lockModules = value > 0.5f; break; case kWindowParameterUpdateRateLimit: windowParameters.rateLimit = static_cast(value + 0.5f); rateLimitStep = 0; break; case kWindowParameterBrowserSort: windowParameters.browserSort = static_cast(value + 0.5f); break; case kWindowParameterBrowserZoom: windowParameters.browserZoom = value; value = std::pow(2.f, value) * 100.0f; break; case kWindowParameterInvertZoom: windowParameters.invertZoom = value > 0.5f; break; case kWindowParameterSqueezeModulePositions: windowParameters.squeezeModules = value > 0.5f; break; default: return; } setParameterValue(kCardinalParameterStartWindow + param, value * mult); } protected: /* -------------------------------------------------------------------------------------------------------- * DSP/Plugin Callbacks */ /** A parameter has changed on the plugin side. This is called by the host to inform the UI about parameter changes. */ void parameterChanged(const uint32_t index, const float value) override { // host mapped parameters if (index < kCardinalParameterCountAtModules) { #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS context->parameters[index] = value; #endif return; } // bypass if (index == kCardinalParameterBypass) { #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS context->bypassed = value > 0.5f; #endif return; } if (index < kCardinalParameterCountAtWindow) { switch (index - kCardinalParameterStartWindow) { case kWindowParameterShowTooltips: windowParameters.tooltips = value > 0.5f; break; case kWindowParameterCableOpacity: windowParameters.cableOpacity = value / 100.0f; break; case kWindowParameterCableTension: windowParameters.cableTension = value / 100.0f; break; case kWindowParameterRackBrightness: windowParameters.rackBrightness = value / 100.0f; break; case kWindowParameterHaloBrightness: windowParameters.haloBrightness = value / 100.0f; break; case kWindowParameterKnobMode: switch (static_cast(value + 0.5f)) { case 0: windowParameters.knobMode = rack::settings::KNOB_MODE_LINEAR; break; case 1: windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_ABSOLUTE; break; case 2: windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_RELATIVE; break; } break; case kWindowParameterWheelKnobControl: windowParameters.knobScroll = value > 0.5f; break; case kWindowParameterWheelSensitivity: windowParameters.knobScrollSensitivity = value / 1000.0f; break; case kWindowParameterLockModulePositions: windowParameters.lockModules = value > 0.5f; break; case kWindowParameterUpdateRateLimit: windowParameters.rateLimit = static_cast(value + 0.5f); rateLimitStep = 0; break; case kWindowParameterBrowserSort: windowParameters.browserSort = static_cast(value + 0.5f); break; case kWindowParameterBrowserZoom: // round up to nearest valid value { float rvalue = value - 1.0f; if (rvalue <= 25.0f) rvalue = -2.0f; else if (rvalue <= 35.0f) rvalue = -1.5f; else if (rvalue <= 50.0f) rvalue = -1.0f; else if (rvalue <= 71.0f) rvalue = -0.5f; else if (rvalue <= 100.0f) rvalue = 0.0f; else if (rvalue <= 141.0f) rvalue = 0.5f; else if (rvalue <= 200.0f) rvalue = 1.0f; else rvalue = 0.0f; windowParameters.browserZoom = rvalue; } break; case kWindowParameterInvertZoom: windowParameters.invertZoom = value > 0.5f; break; case kWindowParameterSqueezeModulePositions: windowParameters.squeezeModules = value > 0.5f; break; default: return; } WindowParametersSetValues(context->window, windowParameters); return; } #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS if (index < kCardinalParameterCountAtMiniBuffers) { float* const buffer = *const_cast(&context->dataIns[index - kCardinalParameterStartMiniBuffers]); buffer[0] = value; return; } switch (index) { case kCardinalParameterMiniTimeFlags: { const int32_t flags = static_cast(value + 0.5f); context->playing = flags & 0x1; context->bbtValid = flags & 0x2; context->reset = flags & 0x4; return; } case kCardinalParameterMiniTimeBar: context->bar = static_cast(value + 0.5f); return; case kCardinalParameterMiniTimeBeat: context->beat = static_cast(value + 0.5f); return; case kCardinalParameterMiniTimeBeatsPerBar: context->beatsPerBar = static_cast(value + 0.5f); return; case kCardinalParameterMiniTimeBeatType: context->beatType = static_cast(value + 0.5f); context->ticksPerClock = context->ticksPerBeat / context->beatType; context->tickClock = std::fmod(context->tick, context->ticksPerClock); return; case kCardinalParameterMiniTimeFrame: context->frame = static_cast(value * context->sampleRate + 0.5f); return; case kCardinalParameterMiniTimeBarStartTick: context->barStartTick = value; return; case kCardinalParameterMiniTimeBeatsPerMinute: context->beatsPerMinute = value; context->ticksPerFrame = 1.0 / (60.0 * context->sampleRate / context->beatsPerMinute / context->ticksPerBeat); return; case kCardinalParameterMiniTimeTick: context->tick = value; context->tickClock = std::fmod(context->tick, context->ticksPerClock); return; case kCardinalParameterMiniTimeTicksPerBeat: context->ticksPerBeat = value; context->ticksPerClock = context->ticksPerBeat / context->beatType; context->ticksPerFrame = 1.0 / (60.0 * context->sampleRate / context->beatsPerMinute / context->ticksPerBeat); context->tickClock = std::fmod(context->tick, context->ticksPerClock); return; } #endif } void stateChanged(const char* const key, const char* const value) override { #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS if (std::strcmp(key, "patch") == 0) { if (fAutosavePath.empty()) return; rack::system::removeRecursively(fAutosavePath); rack::system::createDirectories(fAutosavePath); FILE* const f = std::fopen(rack::system::join(fAutosavePath, "patch.json").c_str(), "w"); DISTRHO_SAFE_ASSERT_RETURN(f != nullptr,); std::fwrite(value, std::strlen(value), 1, f); std::fclose(f); const ScopedContext sc(this); try { context->patch->loadAutosave(); } catch(const rack::Exception& e) { d_stderr(e.what()); } DISTRHO_SAFE_EXCEPTION_RETURN("setState loadAutosave",); return; } #endif if (std::strcmp(key, "windowSize") == 0) { int width = 0; int height = 0; std::sscanf(value, "%d:%d", &width, &height); if (width > 0 && height > 0) { const double scaleFactor = getScaleFactor(); setSize(width * scaleFactor, height * scaleFactor); } return; } } // ------------------------------------------------------------------------------------------------------- static int glfwMods(const uint mod) noexcept { int mods = 0; if (mod & kModifierControl) mods |= GLFW_MOD_CONTROL; if (mod & kModifierShift) mods |= GLFW_MOD_SHIFT; if (mod & kModifierAlt) mods |= GLFW_MOD_ALT; if (mod & kModifierSuper) mods |= GLFW_MOD_SUPER; /* if (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) mods |= GLFW_MOD_SHIFT; if (glfwGetKey(win, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) mods |= GLFW_MOD_CONTROL; if (glfwGetKey(win, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS) mods |= GLFW_MOD_ALT; if (glfwGetKey(win, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS) mods |= GLFW_MOD_SUPER; */ return mods; } bool onMouse(const MouseEvent& ev) override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) return false; #endif if (ev.press) getWindow().focus(); const int action = ev.press ? GLFW_PRESS : GLFW_RELEASE; int mods = glfwMods(ev.mod); int button; switch (ev.button) { case kMouseButtonLeft: button = GLFW_MOUSE_BUTTON_LEFT; break; case kMouseButtonRight: button = GLFW_MOUSE_BUTTON_RIGHT; break; case kMouseButtonMiddle: button = GLFW_MOUSE_BUTTON_MIDDLE; break; default: button = ev.button; break; } #ifdef DISTRHO_OS_MAC // Remap Ctrl-left click to right click on macOS if (button == GLFW_MOUSE_BUTTON_LEFT && (mods & RACK_MOD_MASK) == GLFW_MOD_CONTROL) { button = GLFW_MOUSE_BUTTON_RIGHT; mods &= ~GLFW_MOD_CONTROL; } // Remap Ctrl-shift-left click to middle click on macOS if (button == GLFW_MOUSE_BUTTON_LEFT && (mods & RACK_MOD_MASK) == (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT)) { button = GLFW_MOUSE_BUTTON_MIDDLE; mods &= ~(GLFW_MOD_CONTROL | GLFW_MOD_SHIFT); } #endif const ScopedContext sc(this, mods); return context->event->handleButton(lastMousePos, button, action, mods); } bool onMotion(const MotionEvent& ev) override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) return false; #endif const rack::math::Vec mousePos = rack::math::Vec(ev.pos.getX(), ev.pos.getY()).div(getScaleFactor()).round(); const rack::math::Vec mouseDelta = mousePos.minus(lastMousePos); lastMousePos = mousePos; const ScopedContext sc(this, glfwMods(ev.mod)); return context->event->handleHover(mousePos, mouseDelta); } bool onScroll(const ScrollEvent& ev) override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) return false; #endif rack::math::Vec scrollDelta = rack::math::Vec(-ev.delta.getX(), ev.delta.getY()); #ifndef DISTRHO_OS_MAC scrollDelta = scrollDelta.mult(50.0); #endif const int mods = glfwMods(ev.mod); const ScopedContext sc(this, mods); return context->event->handleScroll(lastMousePos, scrollDelta); } bool onCharacterInput(const CharacterInputEvent& ev) override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) return false; #endif if (ev.character < ' ' || ev.character >= kKeyDelete) return false; const int mods = glfwMods(ev.mod); const ScopedContext sc(this, mods); return context->event->handleText(lastMousePos, ev.character); } bool onKeyboard(const KeyboardEvent& ev) override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) return false; #endif const int action = ev.press ? GLFW_PRESS : GLFW_RELEASE; const int mods = glfwMods(ev.mod); int key; switch (ev.key) { case kKeyTab: key = GLFW_KEY_TAB; break; case kKeyBackspace: key = GLFW_KEY_BACKSPACE; break; case kKeyEnter: key = GLFW_KEY_ENTER; break; case kKeyEscape: key = GLFW_KEY_ESCAPE; break; case kKeyDelete: key = GLFW_KEY_DELETE; break; case kKeySpace: key = GLFW_KEY_SPACE; break; case kKeyF1: key = GLFW_KEY_F1; break; case kKeyF2: key = GLFW_KEY_F2; break; case kKeyF3: key = GLFW_KEY_F3; break; case kKeyF4: key = GLFW_KEY_F4; break; case kKeyF5: key = GLFW_KEY_F5; break; case kKeyF6: key = GLFW_KEY_F6; break; case kKeyF7: key = GLFW_KEY_F7; break; case kKeyF8: key = GLFW_KEY_F8; break; case kKeyF9: key = GLFW_KEY_F9; break; case kKeyF10: key = GLFW_KEY_F10; break; case kKeyF11: key = GLFW_KEY_F11; break; case kKeyF12: key = GLFW_KEY_F12; break; case kKeyPageUp: key = GLFW_KEY_PAGE_UP; break; case kKeyPageDown: key = GLFW_KEY_PAGE_DOWN; break; case kKeyEnd: key = GLFW_KEY_END; break; case kKeyHome: key = GLFW_KEY_HOME; break; case kKeyLeft: key = GLFW_KEY_LEFT; break; case kKeyUp: key = GLFW_KEY_UP; break; case kKeyRight: key = GLFW_KEY_RIGHT; break; case kKeyDown: key = GLFW_KEY_DOWN; break; case kKeyPrintScreen: key = GLFW_KEY_PRINT_SCREEN; break; case kKeyInsert: key = GLFW_KEY_INSERT; break; case kKeyPause: key = GLFW_KEY_PAUSE; break; case kKeyMenu: key = GLFW_KEY_MENU; break; case kKeyNumLock: key = GLFW_KEY_NUM_LOCK; break; case kKeyScrollLock: key = GLFW_KEY_SCROLL_LOCK; break; case kKeyCapsLock: key = GLFW_KEY_CAPS_LOCK; break; case kKeyShiftL: key = GLFW_KEY_LEFT_SHIFT; break; case kKeyShiftR: key = GLFW_KEY_RIGHT_SHIFT; break; case kKeyControlL: key = GLFW_KEY_LEFT_CONTROL; break; case kKeyControlR: key = GLFW_KEY_RIGHT_CONTROL; break; case kKeyAltL: key = GLFW_KEY_LEFT_ALT; break; case kKeyAltR: key = GLFW_KEY_RIGHT_ALT; break; case kKeySuperL: key = GLFW_KEY_LEFT_SUPER; break; case kKeySuperR: key = GLFW_KEY_RIGHT_SUPER; break; case kKeyPad0: key = GLFW_KEY_KP_0; break; case kKeyPad1: key = GLFW_KEY_KP_1; break; case kKeyPad2: key = GLFW_KEY_KP_2; break; case kKeyPad3: key = GLFW_KEY_KP_3; break; case kKeyPad4: key = GLFW_KEY_KP_4; break; case kKeyPad5: key = GLFW_KEY_KP_5; break; case kKeyPad6: key = GLFW_KEY_KP_6; break; case kKeyPad7: key = GLFW_KEY_KP_7; break; case kKeyPad8: key = GLFW_KEY_KP_8; break; case kKeyPad9: key = GLFW_KEY_KP_9; break; case kKeyPadEnter: key = GLFW_KEY_KP_ENTER; break; /* undefined in glfw case kKeyPadPageUp: case kKeyPadPageDown: case kKeyPadEnd: case kKeyPadHome: case kKeyPadLeft: case kKeyPadUp: case kKeyPadRight: case kKeyPadDown: case kKeyPadClear: case kKeyPadInsert: case kKeyPadDelete: */ case kKeyPadEqual: key = GLFW_KEY_KP_EQUAL; break; case kKeyPadMultiply: key = GLFW_KEY_KP_MULTIPLY; break; case kKeyPadAdd: key = GLFW_KEY_KP_ADD; break; /* undefined in glfw case kKeyPadSeparator: */ case kKeyPadSubtract: key = GLFW_KEY_KP_SUBTRACT; break; case kKeyPadDecimal: key = GLFW_KEY_KP_DECIMAL; break; case kKeyPadDivide: key = GLFW_KEY_KP_DIVIDE; break; default: // glfw expects uppercase if (ev.key >= 'a' && ev.key <= 'z') key = ev.key - ('a' - 'A'); else key = ev.key; break; } const ScopedContext sc(this, mods); return context->event->handleKey(lastMousePos, key, ev.keycode, action, mods); } void onResize(const ResizeEvent& ev) override { UI::onResize(ev); if (context->window != nullptr) WindowSetInternalSize(context->window, rack::math::Vec(ev.size.getWidth(), ev.size.getHeight())); const double scaleFactor = getScaleFactor(); const int width = static_cast(ev.size.getWidth() / scaleFactor + 0.5); const int height = static_cast(ev.size.getHeight() / scaleFactor + 0.5); char sizeString[64] = {}; std::snprintf(sizeString, sizeof(sizeString), "%d:%d", width, height); setState("windowSize", sizeString); if (rack::isStandalone()) rack::settings::windowSize = rack::math::Vec(width, height); } void uiFocus(const bool focus, CrossingMode) override { #ifdef DPF_RUNTIME_TESTING if (inSelfTest) return; #endif if (!focus) { const ScopedContext sc(this, 0); context->event->handleLeave(); } } void uiFileBrowserSelected(const char* const filename) override { if (filename == nullptr) return; rack::contextSet(context); WindowParametersRestore(context->window); std::string sfilename = filename; if (saving) { const bool uncompressed = savingUncompressed; savingUncompressed = false; if (rack::system::getExtension(sfilename) != ".vcv") sfilename += ".vcv"; try { if (uncompressed) { context->engine->prepareSave(); if (json_t* const rootJ = context->patch->toJson()) { if (FILE* const file = std::fopen(sfilename.c_str(), "w")) { json_dumpf(rootJ, file, JSON_INDENT(2)); std::fclose(file); } json_decref(rootJ); } } else { context->patch->save(sfilename); } } catch (rack::Exception& e) { std::string message = rack::string::f("Could not save patch: %s", e.what()); asyncDialog::create(message.c_str()); return; } } else { try { context->patch->load(sfilename); } catch (rack::Exception& e) { std::string message = rack::string::f("Could not load patch: %s", e.what()); asyncDialog::create(message.c_str()); return; } } context->patch->path = sfilename; context->patch->pushRecentPath(sfilename); context->history->setSaved(); #ifdef DISTRHO_OS_WASM rack::syncfs(); #else rack::settings::save(); #endif } #if 0 void uiReshape(const uint width, const uint height) override { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0.0, width, 0.0, height, -1.0, 1.0); glViewport(0, 0, width, height); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } #endif private: /** Set our UI class as non-copyable and add a leak detector just in case. */ DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CardinalUI) }; /* ------------------------------------------------------------------------------------------------------------ * UI entry point, called by DPF to create a new UI instance. */ UI* createUI() { return new CardinalUI(); } // ----------------------------------------------------------------------------------------------------------- END_NAMESPACE_DISTRHO