Closes #51 Signed-off-by: falkTX <falktx@falktx.com>tags/22.02
| @@ -39,3 +39,23 @@ | |||||
| #endif | #endif | ||||
| #undef PRIVATE_WAS_DEFINED | #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/Label.hpp> | ||||
| #include <ui/MenuOverlay.hpp> | #include <ui/MenuOverlay.hpp> | ||||
| #include <ui/SequentialLayout.hpp> | #include <ui/SequentialLayout.hpp> | ||||
| #include <ui/TextField.hpp> | |||||
| #include <widget/OpaqueWidget.hpp> | #include <widget/OpaqueWidget.hpp> | ||||
| namespace asyncDialog | namespace asyncDialog | ||||
| @@ -77,9 +78,9 @@ struct AsyncDialog : OpaqueWidget | |||||
| struct AsyncOkButton : Button { | struct AsyncOkButton : Button { | ||||
| AsyncDialog* dialog; | AsyncDialog* dialog; | ||||
| std::function<void()> action; | |||||
| std::function<void()> action; | |||||
| void onAction(const ActionEvent& e) override { | void onAction(const ActionEvent& e) override { | ||||
| action(); | |||||
| action(); | |||||
| dialog->getParent()->requestDelete(); | dialog->getParent()->requestDelete(); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -87,7 +88,7 @@ struct AsyncDialog : OpaqueWidget | |||||
| okButton->box.size.x = buttonWidth; | okButton->box.size.x = buttonWidth; | ||||
| okButton->text = "Ok"; | okButton->text = "Ok"; | ||||
| okButton->dialog = this; | okButton->dialog = this; | ||||
| okButton->action = action; | |||||
| okButton->action = action; | |||||
| buttonLayout->addChild(okButton); | buttonLayout->addChild(okButton); | ||||
| } | } | ||||
| @@ -109,7 +110,7 @@ struct AsyncDialog : OpaqueWidget | |||||
| layout->addChild(contentLayout); | layout->addChild(contentLayout); | ||||
| buttonLayout = new SequentialLayout; | buttonLayout = new SequentialLayout; | ||||
| buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT; | |||||
| buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT; | |||||
| buttonLayout->box.size = box.size; | buttonLayout->box.size = box.size; | ||||
| buttonLayout->spacing = math::Vec(margin, margin); | buttonLayout->spacing = math::Vec(margin, margin); | ||||
| layout->addChild(buttonLayout); | layout->addChild(buttonLayout); | ||||
| @@ -158,4 +159,129 @@ void create(const char* const message, const std::function<void()> action) | |||||
| APP->scene->addChild(overlay); | 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); | ||||
| void create(const char* message, std::function<void()> action); | 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 <string.hpp> | ||||
| #include <system.hpp> | #include <system.hpp> | ||||
| #include <app/Scene.hpp> | #include <app/Scene.hpp> | ||||
| #include <window/Window.hpp> | |||||
| #ifdef NDEBUG | #ifdef NDEBUG | ||||
| # undef DEBUG | # undef DEBUG | ||||
| @@ -50,6 +51,7 @@ | |||||
| namespace patchUtils | namespace patchUtils | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| static void promptClear(const char* const message, const std::function<void()> action) | static void promptClear(const char* const message, const std::function<void()> action) | ||||
| { | { | ||||
| if (APP->history->isSaved() || APP->scene->rack->hasModules()) | 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() | static std::string homeDir() | ||||
| { | { | ||||
| #ifdef ARCH_WIN | |||||
| # ifdef ARCH_WIN | |||||
| if (const char* const userprofile = getenv("USERPROFILE")) | if (const char* const userprofile = getenv("USERPROFILE")) | ||||
| { | { | ||||
| return userprofile; | return userprofile; | ||||
| @@ -70,19 +72,21 @@ static std::string homeDir() | |||||
| if (const char* const homepath = getenv("HOMEPATH")) | if (const char* const homepath = getenv("HOMEPATH")) | ||||
| return system::join(homedrive, homepath); | return system::join(homedrive, homepath); | ||||
| } | } | ||||
| #else | |||||
| # else | |||||
| if (const char* const home = getenv("HOME")) | if (const char* const home = getenv("HOME")) | ||||
| return home; | return home; | ||||
| else if (struct passwd* const pwd = getpwuid(getuid())) | else if (struct passwd* const pwd = getpwuid(getuid())) | ||||
| return pwd->pw_dir; | return pwd->pw_dir; | ||||
| #endif | |||||
| # endif | |||||
| return {}; | return {}; | ||||
| } | } | ||||
| #endif | |||||
| using namespace rack; | using namespace rack; | ||||
| void loadDialog() | void loadDialog() | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| promptClear("The current patch is unsaved. Clear it and open a new patch?", []() { | promptClear("The current patch is unsaved. Clear it and open a new patch?", []() { | ||||
| std::string dir; | std::string dir; | ||||
| if (! APP->patch->path.empty()) | if (! APP->patch->path.empty()) | ||||
| @@ -101,33 +105,41 @@ void loadDialog() | |||||
| opts.saving = ui->saving = false; | opts.saving = ui->saving = false; | ||||
| ui->openFileBrowser(opts); | ui->openFileBrowser(opts); | ||||
| }); | }); | ||||
| #endif | |||||
| } | } | ||||
| void loadPathDialog(const std::string& path) | void loadPathDialog(const std::string& path) | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| promptClear("The current patch is unsaved. Clear it and open the new patch?", [path]() { | promptClear("The current patch is unsaved. Clear it and open the new patch?", [path]() { | ||||
| APP->patch->loadAction(path); | APP->patch->loadAction(path); | ||||
| }); | }); | ||||
| #endif | |||||
| } | } | ||||
| void loadTemplateDialog() | void loadTemplateDialog() | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| promptClear("The current patch is unsaved. Clear it and start a new patch?", []() { | promptClear("The current patch is unsaved. Clear it and start a new patch?", []() { | ||||
| APP->patch->loadTemplate(); | APP->patch->loadTemplate(); | ||||
| }); | }); | ||||
| #endif | |||||
| } | } | ||||
| void revertDialog() | void revertDialog() | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| if (APP->patch->path.empty()) | if (APP->patch->path.empty()) | ||||
| return; | return; | ||||
| promptClear("Revert patch to the last saved state?", []{ | promptClear("Revert patch to the last saved state?", []{ | ||||
| APP->patch->loadAction(APP->patch->path); | APP->patch->loadAction(APP->patch->path); | ||||
| }); | }); | ||||
| #endif | |||||
| } | } | ||||
| void saveDialog(const std::string& path) | void saveDialog(const std::string& path) | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| if (path.empty()) { | if (path.empty()) { | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -142,10 +154,12 @@ void saveDialog(const std::string& path) | |||||
| asyncDialog::create(string::f("Could not save patch: %s", e.what()).c_str()); | asyncDialog::create(string::f("Could not save patch: %s", e.what()).c_str()); | ||||
| return; | return; | ||||
| } | } | ||||
| #endif | |||||
| } | } | ||||
| void saveAsDialog() | void saveAsDialog() | ||||
| { | { | ||||
| #ifndef HEADLESS | |||||
| std::string dir; | std::string dir; | ||||
| if (! APP->patch->path.empty()) | if (! APP->patch->path.empty()) | ||||
| dir = system::getDirectory(APP->patch->path); | dir = system::getDirectory(APP->patch->path); | ||||
| @@ -162,6 +176,54 @@ void saveAsDialog() | |||||
| opts.startDir = dir.c_str(); | opts.startDir = dir.c_str(); | ||||
| opts.saving = ui->saving = true; | opts.saving = ui->saving = true; | ||||
| ui->openFileBrowser(opts); | 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 | 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(); | repaint(); | ||||
| } | } | ||||
| @@ -30,6 +30,7 @@ | |||||
| #ifndef HEADLESS | #ifndef HEADLESS | ||||
| # include "DistrhoUI.hpp" | # include "DistrhoUI.hpp" | ||||
| # include "extra/FileBrowserDialog.hpp" | |||||
| #endif | #endif | ||||
| START_NAMESPACE_DISTRHO | START_NAMESPACE_DISTRHO | ||||
| @@ -131,15 +132,25 @@ public: | |||||
| CardinalPluginContext* const context; | CardinalPluginContext* const context; | ||||
| bool saving; | bool saving; | ||||
| // for 3rd party modules | |||||
| std::function<void(char* path)> filebrowseraction; | |||||
| FileBrowserHandle filebrowserhandle; | |||||
| CardinalBaseUI(const uint width, const uint height) | CardinalBaseUI(const uint width, const uint height) | ||||
| : UI(width, height), | : UI(width, height), | ||||
| context(getRackContextFromPlugin(getPluginInstancePointer())), | context(getRackContextFromPlugin(getPluginInstancePointer())), | ||||
| saving(false) | |||||
| saving(false), | |||||
| filebrowseraction(), | |||||
| filebrowserhandle(nullptr) | |||||
| { | { | ||||
| context->ui = this; | context->ui = this; | ||||
| } | } | ||||
| ~CardinalBaseUI() override | ~CardinalBaseUI() override | ||||
| { | { | ||||
| if (filebrowserhandle != nullptr) | |||||
| fileBrowserClose(filebrowserhandle); | |||||
| context->ui = nullptr; | context->ui = nullptr; | ||||
| } | } | ||||
| }; | }; | ||||