Closes #51 Signed-off-by: falkTX <falktx@falktx.com>tags/22.02
| @@ -39,3 +39,23 @@ | |||
| #endif | |||
| #undef PRIVATE_WAS_DEFINED | |||
| // Cardinal specific API | |||
| #include <functional> | |||
| #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<void(char* path)> 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<void()> 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<void(char* newText)> action); | |||
| @@ -23,6 +23,7 @@ | |||
| #include <ui/Label.hpp> | |||
| #include <ui/MenuOverlay.hpp> | |||
| #include <ui/SequentialLayout.hpp> | |||
| #include <ui/TextField.hpp> | |||
| #include <widget/OpaqueWidget.hpp> | |||
| namespace asyncDialog | |||
| @@ -77,9 +78,9 @@ struct AsyncDialog : OpaqueWidget | |||
| struct AsyncOkButton : Button { | |||
| AsyncDialog* dialog; | |||
| std::function<void()> action; | |||
| std::function<void()> 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<void()> 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<void(char* newText)> 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<void(char*)> 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<void(char*)> 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<void(char* newText)> 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); | |||
| } | |||
| } | |||
| @@ -24,5 +24,6 @@ namespace asyncDialog | |||
| void create(const char* message); | |||
| void create(const char* message, std::function<void()> action); | |||
| void textInput(const char* message, const char* text, std::function<void(char* newText)> action); | |||
| } | |||
| @@ -36,6 +36,7 @@ | |||
| #include <string.hpp> | |||
| #include <system.hpp> | |||
| #include <app/Scene.hpp> | |||
| #include <window/Window.hpp> | |||
| #ifdef NDEBUG | |||
| # undef DEBUG | |||
| @@ -50,6 +51,7 @@ | |||
| namespace patchUtils | |||
| { | |||
| #ifndef HEADLESS | |||
| static void promptClear(const char* const message, const std::function<void()> action) | |||
| { | |||
| if (APP->history->isSaved() || APP->scene->rack->hasModules()) | |||
| @@ -60,7 +62,7 @@ static void promptClear(const char* const message, const std::function<void()> 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<void(char* path)> action) | |||
| { | |||
| #ifndef HEADLESS | |||
| CardinalPluginContext* const pcontext = static_cast<CardinalPluginContext*>(APP); | |||
| DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,); | |||
| CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(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<void()> action) | |||
| { | |||
| #ifndef HEADLESS | |||
| asyncDialog::create(message, action); | |||
| #endif | |||
| } | |||
| void async_dialog_text_input(const char* const message, const char* const text, | |||
| const std::function<void(char* newText)> action) | |||
| { | |||
| #ifndef HEADLESS | |||
| asyncDialog::textInput(message, text, action); | |||
| #endif | |||
| } | |||
| @@ -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(); | |||
| } | |||
| @@ -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<void(char* path)> 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; | |||
| } | |||
| }; | |||