From ce64476fa4c087d27563d2a90772057dccde2d2e Mon Sep 17 00:00:00 2001 From: falkTX Date: Sat, 11 Dec 2021 21:53:50 +0000 Subject: [PATCH] Define custom Cardinal API for async dialogs Closes #51 Signed-off-by: falkTX --- include/common.hpp | 20 ++++++ src/AsyncDialog.cpp | 134 +++++++++++++++++++++++++++++++++++++++-- src/AsyncDialog.hpp | 1 + src/CardinalCommon.cpp | 68 ++++++++++++++++++++- src/CardinalUI.cpp | 14 +++++ src/PluginContext.hpp | 13 +++- 6 files changed, 242 insertions(+), 8 deletions(-) diff --git a/include/common.hpp b/include/common.hpp index 3d2d9d3..23b0808 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -39,3 +39,23 @@ #endif #undef PRIVATE_WAS_DEFINED + +// Cardinal specific API +#include +#define USING_CARDINAL_NOT_RACK + +// opens a file browser, startDir and title can be null +// action is always triggered on close (path can be null), must be freed if not null +void async_dialog_filebrowser(bool saving, const char* startDir, const char* title, + std::function action); + +// opens a message dialog with only an "ok" button +void async_dialog_message(const char* message); + +// opens a message dialog with "ok" and "cancel" buttons +// action is triggered if user presses "ok" +void async_dialog_message(const char* message, std::function action); + +// opens a text input dialog, message and text can be null +// action is always triggered on close (newText can be null), must be freed if not null +void async_dialog_text_input(const char* message, const char* text, std::function action); diff --git a/src/AsyncDialog.cpp b/src/AsyncDialog.cpp index ed985d5..3c046e5 100644 --- a/src/AsyncDialog.cpp +++ b/src/AsyncDialog.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace asyncDialog @@ -77,9 +78,9 @@ struct AsyncDialog : OpaqueWidget struct AsyncOkButton : Button { AsyncDialog* dialog; - std::function action; + std::function action; void onAction(const ActionEvent& e) override { - action(); + action(); dialog->getParent()->requestDelete(); } }; @@ -87,7 +88,7 @@ struct AsyncDialog : OpaqueWidget okButton->box.size.x = buttonWidth; okButton->text = "Ok"; okButton->dialog = this; - okButton->action = action; + okButton->action = action; buttonLayout->addChild(okButton); } @@ -109,7 +110,7 @@ struct AsyncDialog : OpaqueWidget layout->addChild(contentLayout); buttonLayout = new SequentialLayout; - buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT; + buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT; buttonLayout->box.size = box.size; buttonLayout->spacing = math::Vec(margin, margin); layout->addChild(buttonLayout); @@ -158,4 +159,129 @@ void create(const char* const message, const std::function action) APP->scene->addChild(overlay); } +struct AsyncTextInput : OpaqueWidget +{ + static const constexpr float margin = 10; + static const constexpr float buttonWidth = 100; + + AsyncTextInput(const char* const message, const char* const text, const std::function action) + { + box.size = math::Vec(400, 80); + + SequentialLayout* const layout = new SequentialLayout; + layout->box.pos = math::Vec(0, 0); + layout->box.size = box.size; + layout->orientation = SequentialLayout::VERTICAL_ORIENTATION; + layout->margin = math::Vec(margin, margin); + layout->spacing = math::Vec(margin, margin); + layout->wrap = false; + addChild(layout); + + SequentialLayout* const contentLayout = new SequentialLayout; + contentLayout->box.size.x = box.size.x - 2*margin; + contentLayout->box.size.y = box.size.y / 2 - margin; + contentLayout->spacing = math::Vec(margin, margin); + layout->addChild(contentLayout); + + SequentialLayout* const buttonLayout = new SequentialLayout; + buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT; + buttonLayout->box.size.x = box.size.x - 2*margin; + buttonLayout->box.size.y = box.size.y / 2 - margin; + buttonLayout->spacing = math::Vec(margin, margin); + layout->addChild(buttonLayout); + + Label* label; + if (message != nullptr) + { + label = new Label; + nvgFontSize(APP->window->vg, 14); + label->box.size.x = std::min(bndLabelWidth(APP->window->vg, -1, message) + margin, + box.size.x / 2 - margin); + label->box.size.y = contentLayout->box.size.y; + label->fontSize = 14; + label->text = message; + contentLayout->addChild(label); + } + else + { + label = nullptr; + } + + struct AsyncTextField : TextField { + AsyncTextInput* dialog; + std::function action; + void onSelectKey(const SelectKeyEvent& e) override { + if (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER) + { + e.consume(this); + action(strdup(text.c_str())); + dialog->getParent()->requestDelete(); + return; + } + TextField::onSelectKey(e); + } + }; + AsyncTextField* const textField = new AsyncTextField; + textField->box.size.x = contentLayout->box.size.x - (label != nullptr ? label->box.size.x + margin : 0); + textField->box.size.y = 24; + textField->dialog = this; + textField->action = action; + if (text != nullptr) + textField->text = text; + contentLayout->addChild(textField); + + struct AsyncCancelButton : Button { + AsyncTextInput* dialog; + void onAction(const ActionEvent& e) override { + dialog->getParent()->requestDelete(); + } + }; + AsyncCancelButton* const cancelButton = new AsyncCancelButton; + cancelButton->box.size.x = buttonWidth; + cancelButton->text = "Cancel"; + cancelButton->dialog = this; + buttonLayout->addChild(cancelButton); + + struct AsyncOkButton : Button { + AsyncTextInput* dialog; + TextField* textField; + std::function action; + void onAction(const ActionEvent& e) override { + action(strdup(textField->text.c_str())); + dialog->getParent()->requestDelete(); + } + }; + AsyncOkButton* const okButton = new AsyncOkButton; + okButton->box.size.x = buttonWidth; + okButton->text = "Ok"; + okButton->dialog = this; + okButton->textField = textField; + okButton->action = action; + buttonLayout->addChild(okButton); + } + + 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); + } +}; + +void textInput(const char* const message, const char* const text, std::function action) +{ + MenuOverlay* const overlay = new MenuOverlay; + overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33); + + AsyncTextInput* const dialog = new AsyncTextInput(message, text, action); + overlay->addChild(dialog); + + APP->scene->addChild(overlay); +} + } diff --git a/src/AsyncDialog.hpp b/src/AsyncDialog.hpp index 671abe5..ce77c7c 100644 --- a/src/AsyncDialog.hpp +++ b/src/AsyncDialog.hpp @@ -24,5 +24,6 @@ namespace asyncDialog void create(const char* message); void create(const char* message, std::function action); +void textInput(const char* message, const char* text, std::function action); } diff --git a/src/CardinalCommon.cpp b/src/CardinalCommon.cpp index 514015a..b9c0c87 100644 --- a/src/CardinalCommon.cpp +++ b/src/CardinalCommon.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #ifdef NDEBUG # undef DEBUG @@ -50,6 +51,7 @@ namespace patchUtils { +#ifndef HEADLESS static void promptClear(const char* const message, const std::function action) { if (APP->history->isSaved() || APP->scene->rack->hasModules()) @@ -60,7 +62,7 @@ static void promptClear(const char* const message, const std::function a static std::string homeDir() { -#ifdef ARCH_WIN +# ifdef ARCH_WIN if (const char* const userprofile = getenv("USERPROFILE")) { return userprofile; @@ -70,19 +72,21 @@ static std::string homeDir() if (const char* const homepath = getenv("HOMEPATH")) return system::join(homedrive, homepath); } -#else +# else if (const char* const home = getenv("HOME")) return home; else if (struct passwd* const pwd = getpwuid(getuid())) return pwd->pw_dir; -#endif +# endif return {}; } +#endif using namespace rack; void loadDialog() { +#ifndef HEADLESS promptClear("The current patch is unsaved. Clear it and open a new patch?", []() { std::string dir; if (! APP->patch->path.empty()) @@ -101,33 +105,41 @@ void loadDialog() opts.saving = ui->saving = false; ui->openFileBrowser(opts); }); +#endif } void loadPathDialog(const std::string& path) { +#ifndef HEADLESS promptClear("The current patch is unsaved. Clear it and open the new patch?", [path]() { APP->patch->loadAction(path); }); +#endif } void loadTemplateDialog() { +#ifndef HEADLESS promptClear("The current patch is unsaved. Clear it and start a new patch?", []() { APP->patch->loadTemplate(); }); +#endif } void revertDialog() { +#ifndef HEADLESS if (APP->patch->path.empty()) return; promptClear("Revert patch to the last saved state?", []{ APP->patch->loadAction(APP->patch->path); }); +#endif } void saveDialog(const std::string& path) { +#ifndef HEADLESS if (path.empty()) { return; } @@ -142,10 +154,12 @@ void saveDialog(const std::string& path) asyncDialog::create(string::f("Could not save patch: %s", e.what()).c_str()); return; } +#endif } void saveAsDialog() { +#ifndef HEADLESS std::string dir; if (! APP->patch->path.empty()) dir = system::getDirectory(APP->patch->path); @@ -162,6 +176,54 @@ void saveAsDialog() opts.startDir = dir.c_str(); opts.saving = ui->saving = true; ui->openFileBrowser(opts); +#endif +} + +} + +void async_dialog_filebrowser(const bool saving, + const char* const startDir, + const char* const title, + const std::function action) +{ +#ifndef HEADLESS + CardinalPluginContext* const pcontext = static_cast(APP); + DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,); + + CardinalBaseUI* const ui = static_cast(pcontext->ui); + DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); + + // only 1 dialog possible at a time + DISTRHO_SAFE_ASSERT_RETURN(ui->filebrowserhandle == nullptr,); + + FileBrowserOptions opts; + opts.saving = saving; + opts.startDir = startDir; + opts.title = title; + + ui->filebrowseraction = action; + ui->filebrowserhandle = fileBrowserCreate(true, pcontext->nativeWindowId, pcontext->window->pixelRatio, opts); +#endif } +void async_dialog_message(const char* const message) +{ +#ifndef HEADLESS + asyncDialog::create(message); +#endif +} + +void async_dialog_message(const char* const message, const std::function action) +{ +#ifndef HEADLESS + asyncDialog::create(message, action); +#endif +} + +void async_dialog_text_input(const char* const message, const char* const text, + const std::function action) +{ +#ifndef HEADLESS + asyncDialog::textInput(message, text, action); +#endif } diff --git a/src/CardinalUI.cpp b/src/CardinalUI.cpp index b2ced7d..f7ad248 100644 --- a/src/CardinalUI.cpp +++ b/src/CardinalUI.cpp @@ -322,6 +322,20 @@ public: void uiIdle() override { + 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; + } + repaint(); } diff --git a/src/PluginContext.hpp b/src/PluginContext.hpp index f7cc916..916bf3c 100644 --- a/src/PluginContext.hpp +++ b/src/PluginContext.hpp @@ -30,6 +30,7 @@ #ifndef HEADLESS # include "DistrhoUI.hpp" +# include "extra/FileBrowserDialog.hpp" #endif START_NAMESPACE_DISTRHO @@ -131,15 +132,25 @@ public: CardinalPluginContext* const context; bool saving; + // for 3rd party modules + std::function filebrowseraction; + FileBrowserHandle filebrowserhandle; + CardinalBaseUI(const uint width, const uint height) : UI(width, height), context(getRackContextFromPlugin(getPluginInstancePointer())), - saving(false) + saving(false), + filebrowseraction(), + filebrowserhandle(nullptr) { context->ui = this; } + ~CardinalBaseUI() override { + if (filebrowserhandle != nullptr) + fileBrowserClose(filebrowserhandle); + context->ui = nullptr; } };