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; | ||||
} | } | ||||
}; | }; | ||||