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