From 29709cbe4e1f5d97b7630fd1664e2eb1210bb82f Mon Sep 17 00:00:00 2001 From: Filipe Coelho Date: Fri, 12 Nov 2021 15:11:48 +0000 Subject: [PATCH] UI filebrowser saving mode, separate from pugl/DGL/Window (#349) * Add UI::openFileBrowser that matches Window::openFileBrowser * Add empty implementation so it builds * Move file browser dialog implementation into its own file Signed-off-by: falkTX * Fix warnings Signed-off-by: falkTX * Fix tests; Add non-implemented saving flag Signed-off-by: falkTX * Initial DBus/freedesktop file browser implementation Signed-off-by: falkTX * Build fixes Signed-off-by: falkTX * Fix window id Signed-off-by: falkTX * More build fixes Signed-off-by: falkTX * More file dialog tweaks Signed-off-by: falkTX * Attempted fixes Signed-off-by: falkTX * Fix C++98 build Signed-off-by: falkTX * Fix windows build Signed-off-by: falkTX * Really fix windows builds Signed-off-by: falkTX * Fix for MSVC Signed-off-by: falkTX * Yet another fix attempt Signed-off-by: falkTX * Also fix macOS side Signed-off-by: falkTX * More attempted fixes, this is getting annoying... Signed-off-by: falkTX * FileBrowserDialog: Implement saving in Windows Signed-off-by: falkTX * FileBrowserDialog: Implement saving on macOS Signed-off-by: falkTX * Rework last commit Signed-off-by: falkTX * One more macOS fix needed Signed-off-by: falkTX * unref dbus connection on close Signed-off-by: falkTX * More build fixes Signed-off-by: falkTX * Hopefully final macOS fix Signed-off-by: falkTX * Add libdbus-1-dev to CI Signed-off-by: falkTX * Check that org.freedesktop.portal.Desktop exists before connecting Signed-off-by: falkTX * Less indentation Signed-off-by: falkTX * Fix macOS build --- .github/workflows/cmake.yml | 1 + .github/workflows/example-plugins.yml | 10 +- .github/workflows/makefile.yml | 2 +- Makefile.base.mk | 6 + dgl/Window.hpp | 57 +-- dgl/src/WindowPrivateData.cpp | 341 ++----------- dgl/src/WindowPrivateData.hpp | 28 +- dgl/src/pugl.cpp | 150 +----- dgl/src/pugl.hpp | 20 - distrho/DistrhoUI.hpp | 26 +- distrho/DistrhoUI_macOS.mm | 35 +- distrho/extra/FileBrowserDialog.cpp | 569 ++++++++++++++++++++++ distrho/extra/FileBrowserDialog.hpp | 134 +++++ {dgl/src => distrho/extra}/sofd/libsofd.c | 0 {dgl/src => distrho/extra}/sofd/libsofd.h | 0 distrho/src/DistrhoDefines.h | 1 + distrho/src/DistrhoUI.cpp | 42 +- distrho/src/DistrhoUIPrivateData.hpp | 2 +- tests/FileBrowserDialog.cpp | 1 + 19 files changed, 856 insertions(+), 569 deletions(-) create mode 100644 distrho/extra/FileBrowserDialog.cpp create mode 100644 distrho/extra/FileBrowserDialog.hpp rename {dgl/src => distrho/extra}/sofd/libsofd.c (100%) rename {dgl/src => distrho/extra}/sofd/libsofd.h (100%) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ef9de80c7..5932f468d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -30,6 +30,7 @@ jobs: liblo-dev \ libgl-dev \ libcairo2-dev \ + libdbus-1-dev \ libx11-dev - name: Create Build Environment shell: bash diff --git a/.github/workflows/example-plugins.yml b/.github/workflows/example-plugins.yml index 48975cc28..c8bf4505a 100644 --- a/.github/workflows/example-plugins.yml +++ b/.github/workflows/example-plugins.yml @@ -26,7 +26,7 @@ jobs: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports bionic-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ports-arm64.list echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ports-arm64.list sudo apt-get update -qq - sudo apt-get install -yq g++-aarch64-linux-gnu libasound2-dev:arm64 libcairo2-dev:arm64 libgl1-mesa-dev:arm64 liblo-dev:arm64 libpulse-dev:arm64 libx11-dev:arm64 libxcursor-dev:arm64 libxext-dev:arm64 libxrandr-dev:arm64 qemu-user-static + sudo apt-get install -yq g++-aarch64-linux-gnu libasound2-dev:arm64 libcairo2-dev:arm64 libdbus-1-dev:arm64 libgl1-mesa-dev:arm64 liblo-dev:arm64 libpulse-dev:arm64 libx11-dev:arm64 libxcursor-dev:arm64 libxext-dev:arm64 libxrandr-dev:arm64 qemu-user-static # fix broken Ubuntu packages missing pkg-config file in multi-arch package sudo apt-get install -yq libasound2-dev libgl1-mesa-dev liblo-dev libpulse-dev libxcursor-dev libxrandr-dev sudo ln -s /usr/lib/aarch64-linux-gnu/liblo.so.7 /usr/lib/aarch64-linux-gnu/liblo.so @@ -63,7 +63,7 @@ jobs: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ports-armhf.list echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ports-armhf.list sudo apt-get update -qq - sudo apt-get install -yq g++-arm-linux-gnueabihf libasound2-dev:armhf libcairo2-dev:armhf libgl1-mesa-dev:armhf liblo-dev:armhf libpulse-dev:armhf libx11-dev:armhf libxcursor-dev:armhf libxext-dev:armhf libxrandr-dev:armhf qemu-user-static + sudo apt-get install -yq g++-arm-linux-gnueabihf libasound2-dev:armhf libcairo2-dev:armhf libdbus-1-dev:armhf libgl1-mesa-dev:armhf liblo-dev:armhf libpulse-dev:armhf libx11-dev:armhf libxcursor-dev:armhf libxext-dev:armhf libxrandr-dev:armhf qemu-user-static # fix broken Ubuntu packages missing pkg-config file in multi-arch package sudo apt-get install -yq libasound2-dev libgl1-mesa-dev liblo-dev libpulse-dev libxcursor-dev libxrandr-dev sudo ln -s /usr/lib/arm-linux-gnueabihf/liblo.so.7 /usr/lib/arm-linux-gnueabihf/liblo.so @@ -96,7 +96,7 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt-get update -qq - sudo apt-get install -yq g++-multilib libasound2-dev:i386 libcairo2-dev:i386 libgl1-mesa-dev:i386 liblo-dev:i386 libpulse-dev:i386 libx11-dev:i386 libxcursor-dev:i386 libxext-dev:i386 libxrandr-dev:i386 + sudo apt-get install -yq g++-multilib libasound2-dev:i386 libcairo2-dev:i386 libdbus-1-dev:i386 libgl1-mesa-dev:i386 liblo-dev:i386 libpulse-dev:i386 libx11-dev:i386 libxcursor-dev:i386 libxext-dev:i386 libxrandr-dev:i386 - name: Build linux x86 env: CFLAGS: -m32 @@ -124,7 +124,7 @@ jobs: - name: Set up dependencies run: | sudo apt-get update -qq - sudo apt-get install -yq libasound2-dev libcairo2-dev libgl1-mesa-dev liblo-dev libpulse-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev + sudo apt-get install -yq libasound2-dev libcairo2-dev libdbus-1-dev libgl1-mesa-dev liblo-dev libpulse-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev - name: Build linux x86_64 env: LDFLAGS: -static-libgcc -static-libstdc++ @@ -250,7 +250,7 @@ jobs: sudo dpkg -i kxstudio-repos_10.0.3_all.deb sudo apt-get update -qq # build-deps - sudo apt-get install -yq libasound2-dev libcairo2-dev libgl1-mesa-dev liblo-dev libpulse-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev + sudo apt-get install -yq libasound2-dev libcairo2-dev libdbus-1-dev libgl1-mesa-dev liblo-dev libpulse-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev # runtime testing sudo apt-get install -yq carla-git lilv-utils lv2-dev lv2lint valgrind - name: Build plugins diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index f453d10d5..ea456232b 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -20,7 +20,7 @@ jobs: - name: Set up dependencies run: | sudo apt-get update -qq - sudo apt-get install -yq libasound2-dev libcairo2-dev libgl1-mesa-dev liblo-dev libpulse-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev xvfb + sudo apt-get install -yq libasound2-dev libcairo2-dev libdbus-1-dev libgl1-mesa-dev liblo-dev libpulse-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev xvfb - name: Without any warnings env: CFLAGS: -Werror diff --git a/Makefile.base.mk b/Makefile.base.mk index d5c9ce03c..b5e95ead3 100644 --- a/Makefile.base.mk +++ b/Makefile.base.mk @@ -238,6 +238,7 @@ HAVE_OPENGL = true else HAVE_OPENGL = $(shell $(PKG_CONFIG) --exists gl && echo true) ifneq ($(HAIKU),true) +HAVE_DBUS = $(shell $(PKG_CONFIG) --exists dbus-1 && echo true) HAVE_X11 = $(shell $(PKG_CONFIG) --exists x11 && echo true) HAVE_XCURSOR = $(shell $(PKG_CONFIG) --exists xcursor && echo true) HAVE_XEXT = $(shell $(PKG_CONFIG) --exists xext && echo true) @@ -284,6 +285,10 @@ DGL_SYSTEM_LIBS += -lgdi32 -lcomdlg32 endif ifneq ($(HAIKU_OR_MACOS_OR_WINDOWS),true) +ifeq ($(HAVE_DBUS),true) +DGL_FLAGS += $(shell $(PKG_CONFIG) --cflags dbus-1) -DHAVE_DBUS +DGL_SYSTEM_LIBS += $(shell $(PKG_CONFIG) --libs dbus-1) +endif ifeq ($(HAVE_X11),true) DGL_FLAGS += $(shell $(PKG_CONFIG) --cflags x11) -DHAVE_X11 DGL_SYSTEM_LIBS += $(shell $(PKG_CONFIG) --libs x11) @@ -476,6 +481,7 @@ features: $(call print_available,UNIX) @echo === Detected features $(call print_available,HAVE_ALSA) + $(call print_available,HAVE_DBUS) $(call print_available,HAVE_CAIRO) $(call print_available,HAVE_DGL) $(call print_available,HAVE_LIBLO) diff --git a/dgl/Window.hpp b/dgl/Window.hpp index 181f0122f..7aaa17df3 100644 --- a/dgl/Window.hpp +++ b/dgl/Window.hpp @@ -19,9 +19,14 @@ #include "Geometry.hpp" +#ifndef DGL_FILE_BROWSER_DISABLED +# include "../distrho/extra/FileBrowserDialog.hpp" +#endif + START_NAMESPACE_DGL class Application; +class PluginWindow; class TopLevelWidget; // ----------------------------------------------------------------------- @@ -53,53 +58,9 @@ class Window public: #ifndef DGL_FILE_BROWSER_DISABLED - /** - File browser options. - @see Window::openFileBrowser - */ - struct FileBrowserOptions { - /** - File browser button state. - This allows to customize the behaviour of the file browse dialog buttons. - Note these are merely hints, not all systems support them. - */ - enum ButtonState { - kButtonInvisible, - kButtonVisibleUnchecked, - kButtonVisibleChecked, - }; - - /** Start directory, uses current working directory if null */ - const char* startDir; - /** File browser dialog window title, uses "FileBrowser" if null */ - const char* title; - // TODO file filter - - /** - File browser buttons. - */ - struct Buttons { - /** Whether to list all files vs only those with matching file extension */ - ButtonState listAllFiles; - /** Whether to show hidden files */ - ButtonState showHidden; - /** Whether to show list of places (bookmarks) */ - ButtonState showPlaces; - - /** Constructor for default values */ - Buttons() - : listAllFiles(kButtonVisibleChecked), - showHidden(kButtonVisibleUnchecked), - showPlaces(kButtonVisibleChecked) {} - } buttons; - - /** Constructor for default values */ - FileBrowserOptions() - : startDir(nullptr), - title(nullptr), - buttons() {} - }; -#endif // DGL_FILE_BROWSER_DISABLED + typedef DISTRHO_NAMESPACE::FileBrowserHandle FileBrowserHandle; + typedef DISTRHO_NAMESPACE::FileBrowserOptions FileBrowserOptions; +#endif /** Window graphics context as a scoped struct. @@ -361,7 +322,7 @@ class Window #ifndef DGL_FILE_BROWSER_DISABLED /** - Open a file browser dialog with this window as parent. + Open a file browser dialog with this window as transient parent. A few options can be specified to setup the dialog. If a path is selected, onFileSelected() will be called with the user chosen path. diff --git a/dgl/src/WindowPrivateData.cpp b/dgl/src/WindowPrivateData.cpp index 4fa226a35..097299b24 100644 --- a/dgl/src/WindowPrivateData.cpp +++ b/dgl/src/WindowPrivateData.cpp @@ -19,18 +19,6 @@ #include "pugl.hpp" -#include "../../distrho/extra/String.hpp" - -#ifdef DISTRHO_OS_WINDOWS -# include -# include -# include -# include -# include -#else -# include -#endif - // #define DGL_DEBUG_EVENTS #if defined(DEBUG) && defined(DGL_DEBUG_EVENTS) @@ -64,11 +52,6 @@ START_NAMESPACE_DGL // ----------------------------------------------------------------------- -#ifdef DISTRHO_OS_WINDOWS -// static pointer used for direct comparisons -static const char* const kWin32SelectedFileCancelled = "__dpf_cancelled__"; -#endif - static double getDesktopScaleFactor(const PuglView* const view) { // allow custom scale for testing @@ -83,154 +66,6 @@ static double getDesktopScaleFactor(const PuglView* const view) // ----------------------------------------------------------------------- -#ifdef DISTRHO_OS_WINDOWS -struct FileBrowserThread::PrivateData { - OPENFILENAMEW ofn; - volatile bool threadCancelled; - uintptr_t threadHandle; - std::vector fileNameW; - std::vector startDirW; - std::vector titleW; - const bool isEmbed; - const char*& win32SelectedFile; - - PrivateData(const bool embed, const char*& file) - : threadCancelled(false), - threadHandle(0), - fileNameW(32768), - isEmbed(embed), - win32SelectedFile(file) - { - std::memset(&ofn, 0, sizeof(ofn)); - ofn.lStructSize = sizeof(ofn); - ofn.lpstrFile = fileNameW.data(); - ofn.nMaxFile = (DWORD)fileNameW.size(); - } - - void setup(const char* const startDir, - const char* const title, - const uintptr_t winId, - const Window::FileBrowserOptions options) - { - ofn.hwndOwner = (HWND)winId; - - ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; - if (options.buttons.showHidden == Window::FileBrowserOptions::kButtonVisibleChecked) - ofn.Flags |= OFN_FORCESHOWHIDDEN; - - ofn.FlagsEx = 0x0; - if (options.buttons.showPlaces == Window::FileBrowserOptions::kButtonInvisible) - ofn.FlagsEx |= OFN_EX_NOPLACESBAR; - - startDirW.resize(std::strlen(startDir) + 1); - if (MultiByteToWideChar(CP_UTF8, 0, startDir, -1, startDirW.data(), static_cast(startDirW.size()))) - ofn.lpstrInitialDir = startDirW.data(); - - titleW.resize(std::strlen(title) + 1); - if (MultiByteToWideChar(CP_UTF8, 0, title, -1, titleW.data(), static_cast(titleW.size()))) - ofn.lpstrTitle = titleW.data(); - } - - void run() - { - const char* nextFile = nullptr; - - if (GetOpenFileNameW(&ofn)) - { - if (threadCancelled) - { - threadHandle = 0; - return; - } - - // back to UTF-8 - std::vector fileNameA(4 * 32768); - if (WideCharToMultiByte(CP_UTF8, 0, fileNameW.data(), -1, - fileNameA.data(), (int)fileNameA.size(), - nullptr, nullptr)) - { - nextFile = strdup(fileNameA.data()); - } - } - - if (threadCancelled) - { - threadHandle = 0; - return; - } - - if (nextFile == nullptr) - nextFile = kWin32SelectedFileCancelled; - - win32SelectedFile = nextFile; - threadHandle = 0; - } -}; - -FileBrowserThread::FileBrowserThread(const bool isEmbed, const char*& file) - : pData(new PrivateData(isEmbed, file)) {} - -FileBrowserThread::~FileBrowserThread() -{ - stop(); - delete pData; -} - -unsigned __stdcall FileBrowserThread__run(void* const arg) -{ - // CoInitializeEx(nullptr, COINIT_MULTITHREADED); - static_cast(arg)->pData->run(); - // CoUninitialize(); - _endthreadex(0); - return 0; -} - -void FileBrowserThread::start(const char* const startDir, - const char* const title, - const uintptr_t winId, - const Window::FileBrowserOptions options) -{ - pData->setup(startDir, title, winId, options); - - uint threadId; - pData->threadCancelled = false; - pData->threadHandle = _beginthreadex(nullptr, 0, FileBrowserThread__run, this, 0, &threadId); -} - -void FileBrowserThread::stop() -{ - pData->threadCancelled = true; - - if (pData->threadHandle == 0) - return; - - // if previous dialog running, carefully close its window - const HWND owner = pData->isEmbed ? GetParent(pData->ofn.hwndOwner) : pData->ofn.hwndOwner; - - if (owner != nullptr && owner != INVALID_HANDLE_VALUE) - { - const HWND window = GetWindow(owner, GW_HWNDFIRST); - - if (window != nullptr && window != INVALID_HANDLE_VALUE) - { - SendMessage(window, WM_SYSCOMMAND, SC_CLOSE, 0); - SendMessage(window, WM_CLOSE, 0, 0); - WaitForSingleObject((HANDLE)pData->threadHandle, 5000); - } - } - - // not good if thread still running, but let's close the handle anyway - if (pData->threadHandle != 0) - { - CloseHandle((HANDLE)pData->threadHandle); - pData->threadHandle = 0; - } -} - -#endif // DISTRHO_OS_WINDOWS && !_MSC_VER - -// ----------------------------------------------------------------------- - Window::PrivateData::PrivateData(Application& a, Window* const s) : app(a), appData(a.pData), @@ -249,9 +84,8 @@ Window::PrivateData::PrivateData(Application& a, Window* const s) keepAspectRatio(false), ignoreIdleCallbacks(false), filenameToRenderInto(nullptr), -#ifdef DISTRHO_OS_WINDOWS - win32SelectedFile(nullptr), - win32FileThread(false, win32SelectedFile), +#ifndef DGL_FILE_BROWSER_DISABLED + fileBrowserHandle(nullptr), #endif modal() { @@ -276,9 +110,8 @@ Window::PrivateData::PrivateData(Application& a, Window* const s, PrivateData* c keepAspectRatio(false), ignoreIdleCallbacks(false), filenameToRenderInto(nullptr), -#ifdef DISTRHO_OS_WINDOWS - win32SelectedFile(nullptr), - win32FileThread(false, win32SelectedFile), +#ifndef DGL_FILE_BROWSER_DISABLED + fileBrowserHandle(nullptr), #endif modal(ppData) { @@ -307,9 +140,8 @@ Window::PrivateData::PrivateData(Application& a, Window* const s, keepAspectRatio(false), ignoreIdleCallbacks(false), filenameToRenderInto(nullptr), -#ifdef DISTRHO_OS_WINDOWS - win32SelectedFile(nullptr), - win32FileThread(isEmbed, win32SelectedFile), +#ifndef DGL_FILE_BROWSER_DISABLED + fileBrowserHandle(nullptr), #endif modal() { @@ -340,9 +172,8 @@ Window::PrivateData::PrivateData(Application& a, Window* const s, keepAspectRatio(false), ignoreIdleCallbacks(false), filenameToRenderInto(nullptr), -#ifdef DISTRHO_OS_WINDOWS - win32SelectedFile(nullptr), - win32FileThread(isEmbed, win32SelectedFile), +#ifndef DGL_FILE_BROWSER_DISABLED + fileBrowserHandle(nullptr), #endif modal() { @@ -363,8 +194,9 @@ Window::PrivateData::~PrivateData() if (isEmbed) { -#ifdef HAVE_X11 - sofdFileDialogClose(); +#ifndef DGL_FILE_BROWSER_DISABLED + if (fileBrowserHandle != nullptr) + fileBrowserClose(fileBrowserHandle); #endif puglHide(view); appData->oneWindowClosed(); @@ -372,13 +204,6 @@ Window::PrivateData::~PrivateData() isVisible = false; } -#ifdef DISTRHO_OS_WINDOWS - win32FileThread.stop(); - - if (win32SelectedFile != nullptr && win32SelectedFile != kWin32SelectedFileCancelled) - std::free(const_cast(win32SelectedFile)); -#endif - puglFreeView(view); } @@ -522,9 +347,14 @@ void Window::PrivateData::hide() if (modal.enabled) stopModal(); -#ifdef HAVE_X11 - sofdFileDialogClose(); +#ifndef DGL_FILE_BROWSER_DISABLED + if (fileBrowserHandle != nullptr) + { + fileBrowserClose(fileBrowserHandle); + fileBrowserHandle = nullptr; + } #endif + puglHide(view); isVisible = false; @@ -566,20 +396,12 @@ void Window::PrivateData::setResizable(const bool resizable) void Window::PrivateData::idleCallback() { #ifndef DGL_FILE_BROWSER_DISABLED -# ifdef DISTRHO_OS_WINDOWS - if (const char* path = win32SelectedFile) + if (fileBrowserHandle != nullptr && fileBrowserIdle(fileBrowserHandle)) { - win32SelectedFile = nullptr; - if (path == kWin32SelectedFileCancelled) - path = nullptr; - self->onFileSelected(path); - std::free(const_cast(path)); + self->onFileSelected(fileBrowserGetPath(fileBrowserHandle)); + fileBrowserClose(fileBrowserHandle); + fileBrowserHandle = nullptr; } -# endif -# ifdef HAVE_X11 - if (sofdFileDialogIdle(view)) - self->onFileSelected(sofdFileDialogGetPath()); -# endif #endif } @@ -619,120 +441,23 @@ bool Window::PrivateData::removeIdleCallback(IdleCallback* const callback) // ----------------------------------------------------------------------- // file handling -bool Window::PrivateData::openFileBrowser(const Window::FileBrowserOptions& options) +bool Window::PrivateData::openFileBrowser(const FileBrowserOptions& options) { - using DISTRHO_NAMESPACE::String; - - // -------------------------------------------------------------------------- - // configure start dir - - // TODO: get abspath if needed - // TODO: cross-platform - - String startDir(options.startDir); - - if (startDir.isEmpty()) - { - // TESTING verify this whole thing... -# ifdef DISTRHO_OS_WINDOWS - if (char* const cwd = _getcwd(nullptr, 0)) - { - startDir = cwd; - std::free(cwd); - } -# else - if (char* const cwd = getcwd(nullptr, 0)) - { - startDir = cwd; - std::free(cwd); - } -# endif - } - - DISTRHO_SAFE_ASSERT_RETURN(startDir.isNotEmpty(), false); - - if (! startDir.endsWith(DISTRHO_OS_SEP)) - startDir += DISTRHO_OS_SEP_STR; - - // -------------------------------------------------------------------------- - // configure title - - String title(options.title); - - if (title.isEmpty()) - { - title = puglGetWindowTitle(view); - - if (title.isEmpty()) - title = "FileBrowser"; - } - - // -------------------------------------------------------------------------- - // show - -# ifdef DISTRHO_OS_MAC - uint flags = 0x0; - - if (options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleChecked) - flags |= 0x001; - else if (options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleUnchecked) - flags |= 0x002; - - if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked) - flags |= 0x010; - else if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleUnchecked) - flags |= 0x020; + if (fileBrowserHandle != nullptr) + fileBrowserClose(fileBrowserHandle); - if (options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleChecked) - flags |= 0x100; - else if (options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleUnchecked) - flags |= 0x200; + FileBrowserOptions options2 = options; - return puglMacOSFilePanelOpen(view, startDir, title, flags, openPanelCallback); -# endif - -# ifdef DISTRHO_OS_WINDOWS - // only one possible at a time - DISTRHO_SAFE_ASSERT_RETURN(win32FileThread.pData->threadHandle == 0, false); - - if (win32SelectedFile != nullptr && win32SelectedFile != kWin32SelectedFileCancelled) - std::free(const_cast(win32SelectedFile)); - win32SelectedFile = nullptr; - - win32FileThread.start(startDir, title, puglGetNativeWindow(view), options); - return true; -# endif - -# ifdef HAVE_X11 - uint flags = 0x0; - - if (options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleChecked) - flags |= 0x001; - else if (options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleUnchecked) - flags |= 0x002; + if (options2.title == nullptr) + options2.title = puglGetWindowTitle(view); - if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked) - flags |= 0x010; - else if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleUnchecked) - flags |= 0x020; + fileBrowserHandle = fileBrowserCreate(isEmbed, + puglGetNativeWindow(view), + autoScaling ? autoScaleFactor : scaleFactor, + options2); - if (options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleChecked) - flags |= 0x100; - else if (options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleUnchecked) - flags |= 0x200; - - return sofdFileDialogShow(view, startDir, title, flags, autoScaling ? autoScaleFactor : scaleFactor); -# endif - - return false; + return fileBrowserHandle != nullptr; } - -# ifdef DISTRHO_OS_MAC -void Window::PrivateData::openPanelCallback(PuglView* const view, const char* const path) -{ - ((Window::PrivateData*)puglGetHandle(view))->self->onFileSelected(path); -} -# endif #endif // ! DGL_FILE_BROWSER_DISABLED // ----------------------------------------------------------------------- diff --git a/dgl/src/WindowPrivateData.hpp b/dgl/src/WindowPrivateData.hpp index 6db527471..68b2e3756 100644 --- a/dgl/src/WindowPrivateData.hpp +++ b/dgl/src/WindowPrivateData.hpp @@ -31,21 +31,6 @@ class TopLevelWidget; // ----------------------------------------------------------------------- -#ifdef DISTRHO_OS_WINDOWS -struct FileBrowserThread -{ - struct PrivateData; - PrivateData* const pData; - - FileBrowserThread(bool isEmbed, const char*& win32SelectedFile); - ~FileBrowserThread(); - void start(const char* startDir, const char* title, uintptr_t winId, Window::FileBrowserOptions options); - void stop(); -}; -#endif - -// ----------------------------------------------------------------------- - struct Window::PrivateData : IdleCallback { /** Reference to the DGL Application class this (private data) window associates with. */ Application& app; @@ -95,11 +80,9 @@ struct Window::PrivateData : IdleCallback { /** Render to a picture file when non-null, automatically free+unset after saving. */ char* filenameToRenderInto; -#ifdef DISTRHO_OS_WINDOWS - /** Selected file for openFileBrowser on windows, stored for fake async operation. */ - const char* win32SelectedFile; - /** Thread where the openFileBrowser runs. */ - FileBrowserThread win32FileThread; +#ifndef DGL_FILE_BROWSER_DISABLED + /** Handle for file browser dialog operations. */ + FileBrowserHandle fileBrowserHandle; #endif /** Modal window setup. */ @@ -176,10 +159,7 @@ struct Window::PrivateData : IdleCallback { #ifndef DGL_FILE_BROWSER_DISABLED // file handling - bool openFileBrowser(const Window::FileBrowserOptions& options); -# ifdef DISTRHO_OS_MAC - static void openPanelCallback(PuglView* view, const char* path); -# endif + bool openFileBrowser(const FileBrowserOptions& options); #endif static void renderToPicture(const char* filename, const GraphicsContext& context, uint width, uint height); diff --git a/dgl/src/pugl.cpp b/dgl/src/pugl.cpp index c4e31699e..4d28af089 100644 --- a/dgl/src/pugl.cpp +++ b/dgl/src/pugl.cpp @@ -42,6 +42,7 @@ # endif #elif defined(DISTRHO_OS_WINDOWS) # include +# include # include # include # ifdef DGL_CAIRO @@ -57,6 +58,7 @@ # endif #else # include +# include # include # include # include @@ -90,10 +92,12 @@ # endif #endif -#ifdef HAVE_X11 -# define DBLCLKTME 400 -# include "sofd/libsofd.h" -# include "sofd/libsofd.c" +#ifndef DGL_FILE_BROWSER_DISABLED +# ifdef DISTRHO_OS_MAC +# import "../../distrho/extra/FileBrowserDialog.cpp" +# else +# include "../../distrho/extra/FileBrowserDialog.cpp" +# endif #endif #ifndef DISTRHO_OS_MAC @@ -528,53 +532,6 @@ void puglMacOSShowCentered(PuglView* const view) } // -------------------------------------------------------------------------------------------------------------------- -// macOS specific, setup file browser dialog - -bool puglMacOSFilePanelOpen(PuglView* const view, - const char* const startDir, const char* const title, const uint flags, - openPanelCallback callback) -{ - PuglInternals* impl = view->impl; - - NSOpenPanel* const panel = [NSOpenPanel openPanel]; - - [panel setAllowsMultipleSelection:NO]; - [panel setCanChooseDirectories:NO]; - [panel setCanChooseFiles:YES]; - [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startDir]]]; - - // TODO file filter using allowedContentTypes: [UTType] - - if (flags & 0x001) - [panel setAllowsOtherFileTypes:YES]; - if (flags & 0x010) - [panel setShowsHiddenFiles:YES]; - - NSString* titleString = [[NSString alloc] - initWithBytes:title - length:strlen(title) - encoding:NSUTF8StringEncoding]; - [panel setTitle:titleString]; - - dispatch_async(dispatch_get_main_queue(), ^ - { - [panel beginSheetModalForWindow:(impl->window ? impl->window : [view->impl->wrapperView window]) - completionHandler:^(NSModalResponse result) - { - if (result == NSModalResponseOK && [[panel URL] isFileURL]) - { - NSString* const path = [[panel URL] path]; - callback(view, [path UTF8String]); - } - else - { - callback(view, nullptr); - } - }]; - }); - - return true; -} #endif #ifdef DISTRHO_OS_WINDOWS @@ -680,96 +637,7 @@ void puglX11SetWindowTypeAndPID(const PuglView* const view) } // -------------------------------------------------------------------------------------------------------------------- -// X11 specific stuff for sofd - -static Display* sofd_display; -static char* sofd_filename; - -// -------------------------------------------------------------------------------------------------------------------- -// X11 specific, show file dialog via sofd - -bool sofdFileDialogShow(PuglView* const view, - const char* const startDir, const char* const title, - const uint flags, const double scaleFactor) -{ - // only one possible at a time - DISTRHO_SAFE_ASSERT_RETURN(sofd_display == nullptr, false); - - sofd_display = XOpenDisplay(nullptr); - DISTRHO_SAFE_ASSERT_RETURN(sofd_display != nullptr, false); - - DISTRHO_SAFE_ASSERT_RETURN(x_fib_configure(0, startDir) == 0, false); - DISTRHO_SAFE_ASSERT_RETURN(x_fib_configure(1, title) == 0, false); - - x_fib_cfg_buttons(3, flags & 0x001 ? 1 : flags & 0x002 ? 0 : -1); - x_fib_cfg_buttons(1, flags & 0x010 ? 1 : flags & 0x020 ? 0 : -1); - x_fib_cfg_buttons(2, flags & 0x100 ? 1 : flags & 0x200 ? 0 : -1); - - return (x_fib_show(sofd_display, view->impl->win, 0, 0, scaleFactor + 0.5) == 0); -} - -// -------------------------------------------------------------------------------------------------------------------- -// X11 specific, idle sofd file dialog, returns true if dialog was closed (with or without a file selection) - -bool sofdFileDialogIdle(PuglView* const view) -{ - if (sofd_display == nullptr) - return false; - - XEvent event; - while (XPending(sofd_display) > 0) - { - XNextEvent(sofd_display, &event); - - if (x_fib_handle_events(sofd_display, &event) == 0) - continue; - - if (sofd_filename != nullptr) - std::free(sofd_filename); - - if (x_fib_status() > 0) - sofd_filename = x_fib_filename(); - else - sofd_filename = nullptr; - - x_fib_close(sofd_display); - XCloseDisplay(sofd_display); - sofd_display = nullptr; - return true; - } - - return false; -} - -// -------------------------------------------------------------------------------------------------------------------- -// X11 specific, close sofd file dialog - -void sofdFileDialogClose() -{ - if (sofd_display != nullptr) - { - x_fib_close(sofd_display); - XCloseDisplay(sofd_display); - sofd_display = nullptr; - } - - if (sofd_filename != nullptr) - { - std::free(sofd_filename); - sofd_filename = nullptr; - } -} - -// -------------------------------------------------------------------------------------------------------------------- -// X11 specific, get path chosen via sofd file dialog - -const char* sofdFileDialogGetPath() -{ - return sofd_filename; -} -#endif - -// -------------------------------------------------------------------------------------------------------------------- +#endif // HAVE_X11 #ifndef DISTRHO_OS_MAC END_NAMESPACE_DGL diff --git a/dgl/src/pugl.hpp b/dgl/src/pugl.hpp index 63b034a53..c19c82eb2 100644 --- a/dgl/src/pugl.hpp +++ b/dgl/src/pugl.hpp @@ -111,10 +111,6 @@ puglMacOSRemoveChildWindow(PuglView* view, PuglView* child); // macOS specific, center view based on parent coordinates (if there is one) PUGL_API void puglMacOSShowCentered(PuglView* view); - -// macOS specific, setup file browser dialog -typedef void (*openPanelCallback)(PuglView* view, const char* path); -bool puglMacOSFilePanelOpen(PuglView* view, const char* startDir, const char* title, uint flags, openPanelCallback callback); #endif #ifdef DISTRHO_OS_WINDOWS @@ -139,22 +135,6 @@ puglX11GrabFocus(const PuglView* view); // X11 specific, set dialog window type and pid hints PUGL_API void puglX11SetWindowTypeAndPID(const PuglView* view); - -// X11 specific, show file dialog via sofd -PUGL_API bool -sofdFileDialogShow(PuglView* view, const char* startDir, const char* title, uint flags, double scaleFactor); - -// X11 specific, idle sofd file dialog, returns true if dialog was closed (with or without a file selection) -PUGL_API bool -sofdFileDialogIdle(PuglView* const view); - -// X11 specific, close sofd file dialog -PUGL_API void -sofdFileDialogClose(); - -// X11 specific, get path chosen via sofd file dialog -PUGL_API const char* -sofdFileDialogGetPath(); #endif PUGL_END_DECLS diff --git a/distrho/DistrhoUI.hpp b/distrho/DistrhoUI.hpp index 2c121788e..e7140828e 100644 --- a/distrho/DistrhoUI.hpp +++ b/distrho/DistrhoUI.hpp @@ -48,6 +48,10 @@ typedef DGL_NAMESPACE::NanoTopLevelWidget UIWidget; typedef DGL_NAMESPACE::TopLevelWidget UIWidget; #endif +#ifndef DGL_FILE_BROWSER_DISABLED +# include "extra/FileBrowserDialog.hpp" +#endif + START_NAMESPACE_DGL class PluginWindow; END_NAMESPACE_DGL @@ -183,6 +187,22 @@ class UI : public UIWidget void sendNote(uint8_t channel, uint8_t note, uint8_t velocity); #endif +#ifndef DGL_FILE_BROWSER_DISABLED + /** + Open a file browser dialog with this window as transient parent.@n + A few options can be specified to setup the dialog. + + If a path is selected, onFileSelected() will be called with the user chosen path. + If the user cancels or does not pick a file, onFileSelected() will be called with nullptr as filename. + + This function does not block the event loop. + + @note This is exactly the same API as provided by the Window class, + but redeclared here so that non-embed/DGL based UIs can still use file browser related functions. + */ + bool openFileBrowser(const FileBrowserOptions& options = FileBrowserOptions()); +#endif + #if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS /* -------------------------------------------------------------------------------------------------------- * Direct DSP access - DO NOT USE THIS UNLESS STRICTLY NECESSARY!! */ @@ -297,8 +317,9 @@ class UI : public UIWidget The most common exception is custom OpenGL setup, but only really needed for custom OpenGL drawing code. */ virtual void uiReshape(uint width, uint height); +#endif // !DISTRHO_PLUGIN_HAS_EXTERNAL_UI -# ifndef DGL_FILE_BROWSER_DISABLED +#ifndef DGL_FILE_BROWSER_DISABLED /** Window file selected function, called when a path is selected by the user, as triggered by openFileBrowser(). This function is for plugin UIs to be able to override Window::onFileSelected(const char*). @@ -309,8 +330,7 @@ class UI : public UIWidget If you need to use files as plugin state, please setup and use DISTRHO_PLUGIN_WANT_STATEFILES instead. */ virtual void uiFileBrowserSelected(const char* filename); -# endif -#endif // !DISTRHO_PLUGIN_HAS_EXTERNAL_UI +#endif /* -------------------------------------------------------------------------------------------------------- * UI Resize Handling, internal */ diff --git a/distrho/DistrhoUI_macOS.mm b/distrho/DistrhoUI_macOS.mm index 5127762dd..24f24f377 100644 --- a/distrho/DistrhoUI_macOS.mm +++ b/distrho/DistrhoUI_macOS.mm @@ -22,10 +22,15 @@ #include "src/DistrhoDefines.h" #if DISTRHO_PLUGIN_HAS_EXTERNAL_UI -#import -#include -#include +# import +# include +# include +# ifndef DGL_FILE_BROWSER_DISABLED +# import "extra/FileBrowserDialog.cpp" +# endif + +// Declared in DistrhoUI.cpp but defined here because it uses Obj-C START_NAMESPACE_DISTRHO double getDesktopScaleFactor(const uintptr_t parentWindowHandle) { @@ -40,19 +45,19 @@ return [NSScreen mainScreen].backingScaleFactor; } END_NAMESPACE_DISTRHO -#else // DISTRHO_PLUGIN_HAS_EXTERNAL_UI -#include "../dgl/Base.hpp" -#define DISTRHO_MACOS_NAMESPACE_MACRO_HELPER(DGL_NS, SEP, PUGL_NS, INTERFACE) DGL_NS ## SEP ## PUGL_NS ## SEP ## INTERFACE -#define DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NS, PUGL_NS, INTERFACE) DISTRHO_MACOS_NAMESPACE_MACRO_HELPER(DGL_NS, _, PUGL_NS, INTERFACE) +#else // DISTRHO_PLUGIN_HAS_EXTERNAL_UI -#define PuglCairoView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, CairoView) -#define PuglOpenGLView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, OpenGLView) -#define PuglStubView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, StubView) -#define PuglVulkanView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, VulkanView) -#define PuglWindow DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, Window) -#define PuglWindowDelegate DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, WindowDelegate) -#define PuglWrapperView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, WrapperView) +# include "../dgl/Base.hpp" +# define DISTRHO_MACOS_NAMESPACE_MACRO_HELPER(DGL_NS, SEP, PUGL_NS, INTERFACE) DGL_NS ## SEP ## PUGL_NS ## SEP ## INTERFACE +# define DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NS, PUGL_NS, INTERFACE) DISTRHO_MACOS_NAMESPACE_MACRO_HELPER(DGL_NS, _, PUGL_NS, INTERFACE) +# define PuglCairoView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, CairoView) +# define PuglOpenGLView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, OpenGLView) +# define PuglStubView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, StubView) +# define PuglVulkanView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, VulkanView) +# define PuglWindow DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, Window) +# define PuglWindowDelegate DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, WindowDelegate) +# define PuglWrapperView DISTRHO_MACOS_NAMESPACE_MACRO(DGL_NAMESPACE, PUGL_NAMESPACE, WrapperView) +# import "src/pugl.mm" -#import "src/pugl.mm" #endif // DISTRHO_PLUGIN_HAS_EXTERNAL_UI diff --git a/distrho/extra/FileBrowserDialog.cpp b/distrho/extra/FileBrowserDialog.cpp new file mode 100644 index 000000000..01e57b9c7 --- /dev/null +++ b/distrho/extra/FileBrowserDialog.cpp @@ -0,0 +1,569 @@ +/* + * DISTRHO Plugin Framework (DPF) + * Copyright (C) 2012-2021 Filipe Coelho + * + * Permission to use, copy, modify, and/or distribute this software for any purpose with + * or without fee is hereby granted, provided that the above copyright notice and this + * permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "FileBrowserDialog.hpp" +#include "ScopedPointer.hpp" +#include "String.hpp" + +#ifdef DISTRHO_OS_MAC +# import +#endif +#ifdef DISTRHO_OS_WINDOWS +# include +# include +# include +# include +# include +#else +# include +#endif +#ifdef HAVE_DBUS +# include +#endif +#ifdef HAVE_X11 +# define DBLCLKTME 400 +# include "sofd/libsofd.h" +# include "sofd/libsofd.c" +#endif + +START_NAMESPACE_DISTRHO + +// -------------------------------------------------------------------------------------------------------------------- + +// static pointer used for signal null/none action taken +static const char* const kSelectedFileCancelled = "__dpf_cancelled__"; + +struct FileBrowserData { + const char* selectedFile; + +#ifdef DISTRHO_OS_MAC + NSSavePanel* nsBasePanel; + NSOpenPanel* nsOpenPanel; +#endif +#ifdef HAVE_DBUS + DBusConnection* dbuscon; +#endif +#ifdef HAVE_X11 + Display* x11display; +#endif + +#ifdef DISTRHO_OS_WINDOWS + OPENFILENAMEW ofn; + volatile bool threadCancelled; + uintptr_t threadHandle; + std::vector fileNameW; + std::vector startDirW; + std::vector titleW; + const bool saving; + bool isEmbed; + + FileBrowserData(const bool save) + : selectedFile(nullptr), + threadCancelled(false), + threadHandle(0), + fileNameW(32768), + saving(save), + isEmbed(false) + { + std::memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = fileNameW.data(); + ofn.nMaxFile = (DWORD)fileNameW.size(); + } + + ~FileBrowserData() + { + if (cancelAndStop() && selectedFile != nullptr && selectedFile != kSelectedFileCancelled) + std::free(const_cast(selectedFile)); + } + + void setupAndStart(const bool embed, + const char* const startDir, + const char* const windowTitle, + const uintptr_t winId, + const FileBrowserOptions options) + { + isEmbed = embed; + + ofn.hwndOwner = (HWND)winId; + + ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; + if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked) + ofn.Flags |= OFN_FORCESHOWHIDDEN; + + ofn.FlagsEx = 0x0; + if (options.buttons.showPlaces == FileBrowserOptions::kButtonInvisible) + ofn.FlagsEx |= OFN_EX_NOPLACESBAR; + + startDirW.resize(std::strlen(startDir) + 1); + if (MultiByteToWideChar(CP_UTF8, 0, startDir, -1, startDirW.data(), static_cast(startDirW.size()))) + ofn.lpstrInitialDir = startDirW.data(); + + titleW.resize(std::strlen(windowTitle) + 1); + if (MultiByteToWideChar(CP_UTF8, 0, windowTitle, -1, titleW.data(), static_cast(titleW.size()))) + ofn.lpstrTitle = titleW.data(); + + uint threadId; + threadCancelled = false; + threadHandle = _beginthreadex(nullptr, 0, _run, this, 0, &threadId); + } + + bool cancelAndStop() + { + threadCancelled = true; + + if (threadHandle == 0) + return true; + + // if previous dialog running, carefully close its window + const HWND owner = isEmbed ? GetParent(ofn.hwndOwner) : ofn.hwndOwner; + + if (owner != nullptr && owner != INVALID_HANDLE_VALUE) + { + const HWND window = GetWindow(owner, GW_HWNDFIRST); + + if (window != nullptr && window != INVALID_HANDLE_VALUE) + { + SendMessage(window, WM_SYSCOMMAND, SC_CLOSE, 0); + SendMessage(window, WM_CLOSE, 0, 0); + WaitForSingleObject((HANDLE)threadHandle, 5000); + } + } + + if (threadHandle == 0) + return true; + + // not good if thread still running, but let's close the handle anyway + CloseHandle((HANDLE)threadHandle); + threadHandle = 0; + return false; + } + + void run() + { + const char* nextFile = nullptr; + + if (saving ? GetSaveFileNameW(&ofn) : GetOpenFileNameW(&ofn)) + { + if (threadCancelled) + { + threadHandle = 0; + return; + } + + // back to UTF-8 + std::vector fileNameA(4 * 32768); + if (WideCharToMultiByte(CP_UTF8, 0, fileNameW.data(), -1, + fileNameA.data(), (int)fileNameA.size(), + nullptr, nullptr)) + { + nextFile = strdup(fileNameA.data()); + } + } + + if (threadCancelled) + { + threadHandle = 0; + return; + } + + if (nextFile == nullptr) + nextFile = kSelectedFileCancelled; + + selectedFile = nextFile; + threadHandle = 0; + } + + static unsigned __stdcall _run(void* const arg) + { + // CoInitializeEx(nullptr, COINIT_MULTITHREADED); + static_cast(arg)->run(); + // CoUninitialize(); + _endthreadex(0); + return 0; + } +#else // DISTRHO_OS_WINDOWS + FileBrowserData(const bool saving) + : selectedFile(nullptr) + { +#ifdef DISTRHO_OS_MAC + if (saving) + { + nsOpenPanel = nullptr; + nsBasePanel = [[NSSavePanel savePanel]retain]; + } + else + { + nsOpenPanel = [[NSOpenPanel openPanel]retain]; + nsBasePanel = nsOpenPanel; + } +#endif +#ifdef HAVE_DBUS + if ((dbuscon = dbus_bus_get(DBUS_BUS_SESSION, nullptr)) != nullptr) + dbus_connection_set_exit_on_disconnect(dbuscon, false); +#endif +#ifdef HAVE_X11 + x11display = XOpenDisplay(nullptr); +#endif + + // maybe unused + return; (void)saving; + } + + ~FileBrowserData() + { +#ifdef DISTRHO_OS_MAC + [nsBasePanel release]; +#endif +#ifdef HAVE_DBUS + if (dbuscon != nullptr) + dbus_connection_unref(dbuscon); +#endif +#ifdef HAVE_X11 + if (x11display != nullptr) + XCloseDisplay(x11display); +#endif + + if (selectedFile != nullptr && selectedFile != kSelectedFileCancelled) + std::free(const_cast(selectedFile)); + } +#endif +}; + +// -------------------------------------------------------------------------------------------------------------------- + +#ifdef DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE +namespace DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE { +#endif + +// -------------------------------------------------------------------------------------------------------------------- + +FileBrowserHandle fileBrowserCreate(const bool isEmbed, + const uintptr_t windowId, + const double scaleFactor, + const FileBrowserOptions& options) +{ + String startDir(options.startDir); + + if (startDir.isEmpty()) + { +#ifdef DISTRHO_OS_WINDOWS + if (char* const cwd = _getcwd(nullptr, 0)) + { + startDir = cwd; + std::free(cwd); + } +#else + if (char* const cwd = getcwd(nullptr, 0)) + { + startDir = cwd; + std::free(cwd); + } +#endif + } + + DISTRHO_SAFE_ASSERT_RETURN(startDir.isNotEmpty(), nullptr); + + if (! startDir.endsWith(DISTRHO_OS_SEP)) + startDir += DISTRHO_OS_SEP_STR; + + String windowTitle(options.title); + + if (windowTitle.isEmpty()) + windowTitle = "FileBrowser"; + + ScopedPointer handle(new FileBrowserData(options.saving)); + +#ifdef DISTRHO_OS_MAC + NSSavePanel* const nsBasePanel = handle->nsBasePanel; + DISTRHO_SAFE_ASSERT_RETURN(nsBasePanel != nullptr, nullptr); + + if (! options.saving) + { + NSOpenPanel* const nsOpenPanel = handle->nsOpenPanel; + DISTRHO_SAFE_ASSERT_RETURN(nsOpenPanel != nullptr, nullptr); + + [nsOpenPanel setAllowsMultipleSelection:NO]; + [nsOpenPanel setCanChooseDirectories:NO]; + [nsOpenPanel setCanChooseFiles:YES]; + } + + [nsBasePanel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startDir]]]; + + // TODO file filter using allowedContentTypes: [UTType] + + if (options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleChecked) + [nsBasePanel setAllowsOtherFileTypes:YES]; + if (options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked) + [nsBasePanel setShowsHiddenFiles:YES]; + + NSString* const titleString = [[NSString alloc] + initWithBytes:windowTitle + length:strlen(windowTitle) + encoding:NSUTF8StringEncoding]; + [nsBasePanel setTitle:titleString]; + + FileBrowserData* const handleptr = handle.get(); + + dispatch_async(dispatch_get_main_queue(), ^ + { + [nsBasePanel beginSheetModalForWindow:[(NSView*)windowId window] + completionHandler:^(NSModalResponse result) + { + if (result == NSModalResponseOK && [[nsBasePanel URL] isFileURL]) + { + NSString* const path = [[nsBasePanel URL] path]; + handleptr->selectedFile = strdup([path UTF8String]); + } + else + { + handleptr->selectedFile = kSelectedFileCancelled; + } + }]; + }); +#endif + +#ifdef DISTRHO_OS_WINDOWS + handle->setupAndStart(isEmbed, startDir, windowTitle, windowId, options); +#endif + +#ifdef HAVE_DBUS + // optional, can be null + DBusConnection* const dbuscon = handle->dbuscon; + + if (dbuscon != nullptr && dbus_bus_name_has_owner(dbuscon, "org.freedesktop.portal.Desktop", nullptr)) + { + // https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.FileChooser + if (DBusMessage* const message = dbus_message_new_method_call("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.FileChooser", + options.saving ? "SaveFile" : "OpenFile")) + { + char windowIdStr[32]; + memset(windowIdStr, 0, sizeof(windowIdStr)); +# ifdef HAVE_X11 + snprintf(windowIdStr, sizeof(windowIdStr)-1, "x11:%llx", (ulonglong)windowId); +# endif + const char* windowIdStrPtr = windowIdStr; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &windowIdStrPtr, + DBUS_TYPE_STRING, &windowTitle, + DBUS_TYPE_INVALID); + + DBusMessageIter iter, array; + dbus_message_iter_init_append(message, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array); + + /* here merely as example, in case we need to configure it + { + DBusMessageIter dict, variant; + const char* const property = "property"; + const char* const value = "value"; + + dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, nullptr, &dict); + dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &property); + dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "s", &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value); + dbus_message_iter_close_container(&dict, &variant); + dbus_message_iter_close_container(&array, &dict); + } + */ + + dbus_message_iter_close_container(&iter, &array); + + dbus_connection_send(dbuscon, message, nullptr); + + dbus_message_unref(message); + return handle.release(); + } + } +#endif + +#ifdef HAVE_X11 + Display* const x11display = handle->x11display; + DISTRHO_SAFE_ASSERT_RETURN(x11display != nullptr, nullptr); + + // unsupported at the moment + if (options.saving) + return nullptr; + + DISTRHO_SAFE_ASSERT_RETURN(x_fib_configure(0, startDir) == 0, nullptr); + DISTRHO_SAFE_ASSERT_RETURN(x_fib_configure(1, windowTitle) == 0, nullptr); + + const int button1 = options.buttons.showHidden == FileBrowserOptions::kButtonVisibleChecked ? 1 + : options.buttons.showHidden == FileBrowserOptions::kButtonVisibleUnchecked ? 0 : -1; + const int button2 = options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleChecked ? 1 + : options.buttons.showPlaces == FileBrowserOptions::kButtonVisibleUnchecked ? 0 : -1; + const int button3 = options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleChecked ? 1 + : options.buttons.listAllFiles == FileBrowserOptions::kButtonVisibleUnchecked ? 0 : -1; + + x_fib_cfg_buttons(1, button1); + x_fib_cfg_buttons(2, button2); + x_fib_cfg_buttons(3, button3); + + if (x_fib_show(x11display, windowId, 0, 0, scaleFactor + 0.5) != 0) + return nullptr; +#endif + + return handle.release(); + + // might be unused + (void)isEmbed; + (void)windowId; + (void)scaleFactor; +} + +// -------------------------------------------------------------------------------------------------------------------- +// returns true if dialog was closed (with or without a file selection) + +bool fileBrowserIdle(const FileBrowserHandle handle) +{ +#ifdef HAVE_DBUS + if (DBusConnection* dbuscon = handle->dbuscon) + { + while (dbus_connection_dispatch(dbuscon) == DBUS_DISPATCH_DATA_REMAINS) {} + dbus_connection_read_write_dispatch(dbuscon, 0); + + if (DBusMessage* const message = dbus_connection_pop_message(dbuscon)) + { + const char* const interface = dbus_message_get_interface(message); + const char* const member = dbus_message_get_member(message); + + if (interface != nullptr && std::strcmp(interface, "org.freedesktop.portal.Request") == 0 + && member != nullptr && std::strcmp(member, "Response") == 0) + { + do { + DBusMessageIter iter; + dbus_message_iter_init(message, &iter); + + // starts with uint32 for return/exit code + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_UINT32); + + uint32_t ret = 1; + dbus_message_iter_get_basic(&iter, &ret); + + if (ret != 0) + break; + + // next must be array + dbus_message_iter_next(&iter); + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY); + + // open dict array + DBusMessageIter dictArray; + dbus_message_iter_recurse(&iter, &dictArray); + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dictArray) == DBUS_TYPE_DICT_ENTRY); + + // open containing dict + DBusMessageIter dict; + dbus_message_iter_recurse(&dictArray, &dict); + + // start with the string "uris" + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRING); + + const char* key = nullptr; + dbus_message_iter_get_basic(&dict, &key); + DISTRHO_SAFE_ASSERT_BREAK(key != nullptr && std::strcmp(key, "uris") == 0); + + // then comes variant + dbus_message_iter_next(&dict); + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_VARIANT); + + DBusMessageIter variant; + dbus_message_iter_recurse(&dict, &variant); + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&variant) == DBUS_TYPE_ARRAY); + + // open variant array (variant type is string) + DBusMessageIter variantArray; + dbus_message_iter_recurse(&variant, &variantArray); + DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&variantArray) == DBUS_TYPE_STRING); + + const char* value = nullptr; + dbus_message_iter_get_basic(&variantArray, &value); + + // and finally we have our dear value, just make sure it is local + DISTRHO_SAFE_ASSERT_BREAK(value != nullptr); + + if (const char* const localvalue = std::strstr(value, "file:///")) + handle->selectedFile = strdup(localvalue + 7); + + } while(false); + + if (handle->selectedFile == nullptr) + handle->selectedFile = kSelectedFileCancelled; + } + } + } +#endif + +#ifdef HAVE_X11 + Display* const x11display = handle->x11display; + + if (x11display == nullptr) + return false; + + XEvent event; + while (XPending(x11display) > 0) + { + XNextEvent(x11display, &event); + + if (x_fib_handle_events(x11display, &event) == 0) + continue; + + if (x_fib_status() > 0) + handle->selectedFile = x_fib_filename(); + else + handle->selectedFile = kSelectedFileCancelled; + + x_fib_close(x11display); + XCloseDisplay(x11display); + handle->x11display = nullptr; + break; + } +#endif + + return handle->selectedFile != nullptr; +} + +// -------------------------------------------------------------------------------------------------------------------- +// close sofd file dialog + +void fileBrowserClose(const FileBrowserHandle handle) +{ +#ifdef HAVE_X11 + if (Display* const x11display = handle->x11display) + x_fib_close(x11display); +#endif + + delete handle; +} + +// -------------------------------------------------------------------------------------------------------------------- +// get path chosen via sofd file dialog + +const char* fileBrowserGetPath(const FileBrowserHandle handle) +{ + return handle->selectedFile != kSelectedFileCancelled ? handle->selectedFile : nullptr; +} + +// -------------------------------------------------------------------------------------------------------------------- + +#ifdef DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE +} +#endif + +END_NAMESPACE_DISTRHO diff --git a/distrho/extra/FileBrowserDialog.hpp b/distrho/extra/FileBrowserDialog.hpp new file mode 100644 index 000000000..2074e4cbe --- /dev/null +++ b/distrho/extra/FileBrowserDialog.hpp @@ -0,0 +1,134 @@ +/* + * DISTRHO Plugin Framework (DPF) + * Copyright (C) 2012-2021 Filipe Coelho + * + * Permission to use, copy, modify, and/or distribute this software for any purpose with + * or without fee is hereby granted, provided that the above copyright notice and this + * permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef DISTRHO_FILE_BROWSER_DIALOG_HPP_INCLUDED +#define DISTRHO_FILE_BROWSER_DIALOG_HPP_INCLUDED + +#include "../DistrhoUtils.hpp" + +START_NAMESPACE_DISTRHO + +// -------------------------------------------------------------------------------------------------------------------- +// File Browser Dialog stuff + +struct FileBrowserData; +typedef FileBrowserData* FileBrowserHandle; + +// -------------------------------------------------------------------------------------------------------------------- + +/** + File browser options, for customizing the file browser dialog.@n + By default the file browser dialog will be work as "open file" in the current working directory. +*/ +struct FileBrowserOptions { + /** Whether we are saving, opening files otherwise (default) */ + bool saving; + + /** Start directory, uses current working directory if null */ + const char* startDir; + + /** File browser dialog window title, uses "FileBrowser" if null */ + const char* title; + + // TODO file filter + + /** + File browser button state. + This allows to customize the behaviour of the file browse dialog buttons. + Note these are merely hints, not all systems support them. + */ + enum ButtonState { + kButtonInvisible, + kButtonVisibleUnchecked, + kButtonVisibleChecked, + }; + + /** + File browser buttons. + */ + struct Buttons { + /** Whether to list all files vs only those with matching file extension */ + ButtonState listAllFiles; + /** Whether to show hidden files */ + ButtonState showHidden; + /** Whether to show list of places (bookmarks) */ + ButtonState showPlaces; + + /** Constructor for default values */ + Buttons() + : listAllFiles(kButtonVisibleChecked), + showHidden(kButtonVisibleUnchecked), + showPlaces(kButtonVisibleChecked) {} + } buttons; + + /** Constructor for default values */ + FileBrowserOptions() + : saving(false), + startDir(nullptr), + title(nullptr), + buttons() {} +}; + +// -------------------------------------------------------------------------------------------------------------------- + +#ifdef DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE +namespace DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE { +#endif + +/** + Create a new file browser dialog. + + @p isEmbed: Whether the window this dialog belongs to is an embed/child window (needed to close dialog on Windows) + @p windowId: The native window id to attach this dialog to as transient parent (X11 Window, HWND or NSView*) + @p scaleFactor: Scale factor to use (only used on X11) + @p options: Extra options, optional + By default the file browser dialog will be work as "open file" in the current working directory. +*/ +FileBrowserHandle fileBrowserCreate(bool isEmbed, + uintptr_t windowId, + double scaleFactor, + const FileBrowserOptions& options = FileBrowserOptions()); + +/** + Idle the file browser dialog handle.@n + Returns true if dialog was closed (with or without a file selection), + in which case the handle must not be used afterwards. + You can then call fileBrowserGetPath to know the selected file (or null if cancelled). +*/ +bool fileBrowserIdle(const FileBrowserHandle handle); + +/** + Close the file browser dialog, handle must not be used afterwards. +*/ +void fileBrowserClose(const FileBrowserHandle handle); + +/** + Get the path chosen by the user or null.@n + Should only be called after fileBrowserIdle returns true. +*/ +const char* fileBrowserGetPath(const FileBrowserHandle handle); + +// -------------------------------------------------------------------------------------------------------------------- + +#ifdef DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE +} +#endif + +// -------------------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO + +#endif // DISTRHO_FILE_BROWSER_DIALOG_HPP_INCLUDED diff --git a/dgl/src/sofd/libsofd.c b/distrho/extra/sofd/libsofd.c similarity index 100% rename from dgl/src/sofd/libsofd.c rename to distrho/extra/sofd/libsofd.c diff --git a/dgl/src/sofd/libsofd.h b/distrho/extra/sofd/libsofd.h similarity index 100% rename from dgl/src/sofd/libsofd.h rename to distrho/extra/sofd/libsofd.h diff --git a/distrho/src/DistrhoDefines.h b/distrho/src/DistrhoDefines.h index 281031c2a..b3d2de6b1 100644 --- a/distrho/src/DistrhoDefines.h +++ b/distrho/src/DistrhoDefines.h @@ -207,6 +207,7 @@ typedef unsigned char uchar; typedef unsigned short int ushort; typedef unsigned int uint; typedef unsigned long int ulong; +typedef unsigned long long int ulonglong; /* Deprecated macros */ #define DISTRHO_DECLARE_NON_COPY_CLASS(ClassName) DISTRHO_DECLARE_NON_COPYABLE(ClassName) diff --git a/distrho/src/DistrhoUI.cpp b/distrho/src/DistrhoUI.cpp index c4797e537..99e322e40 100644 --- a/distrho/src/DistrhoUI.cpp +++ b/distrho/src/DistrhoUI.cpp @@ -15,6 +15,29 @@ */ #include "src/DistrhoPluginChecks.h" +#include "src/DistrhoDefines.h" + +#if !defined(DGL_FILE_BROWSER_DISABLED) && !defined(DISTRHO_OS_MAC) +# define DISTRHO_PUGL_NAMESPACE_MACRO_HELPER(NS, SEP, FUNCTION) NS ## SEP ## FUNCTION +# define DISTRHO_PUGL_NAMESPACE_MACRO(NS, FUNCTION) DISTRHO_PUGL_NAMESPACE_MACRO_HELPER(NS, _, FUNCTION) +# define DISTRHO_FILE_BROWSER_DIALOG_EXTRA_NAMESPACE Plugin +# define x_fib_add_recent DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_add_recent) +# define x_fib_cfg_buttons DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_cfg_buttons) +# define x_fib_cfg_filter_callback DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_cfg_filter_callback) +# define x_fib_close DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_close) +# define x_fib_configure DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_configure) +# define x_fib_filename DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_filename) +# define x_fib_free_recent DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_free_recent) +# define x_fib_handle_events DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_handle_events) +# define x_fib_load_recent DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_load_recent) +# define x_fib_recent_at DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_recent_at) +# define x_fib_recent_count DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_recent_count) +# define x_fib_recent_file DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_recent_file) +# define x_fib_save_recent DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_save_recent) +# define x_fib_show DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_show) +# define x_fib_status DISTRHO_PUGL_NAMESPACE_MACRO(Plugin, x_fib_status) +# include "../extra/FileBrowserDialog.cpp" +#endif #if DISTRHO_PLUGIN_HAS_EXTERNAL_UI # if defined(DISTRHO_OS_WINDOWS) @@ -255,6 +278,19 @@ void UI::sendNote(uint8_t channel, uint8_t note, uint8_t velocity) } #endif +#ifndef DGL_FILE_BROWSER_DISABLED +bool UI::openFileBrowser(const FileBrowserOptions& options) +{ +# if DISTRHO_PLUGIN_HAS_EXTERNAL_UI + // TODO + return false; + (void)options; +# else + return getWindow().openFileBrowser(options); +# endif +} +#endif + #if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS /* ------------------------------------------------------------------------------------------------------------ * Direct DSP access */ @@ -311,13 +347,13 @@ void UI::uiReshape(uint, uint) // NOTE this must be the same as Window::onReshape pData->fallbackOnResize(); } +#endif // !DISTRHO_PLUGIN_HAS_EXTERNAL_UI -# ifndef DGL_FILE_BROWSER_DISABLED +#ifndef DGL_FILE_BROWSER_DISABLED void UI::uiFileBrowserSelected(const char*) { } -# endif -#endif // !DISTRHO_PLUGIN_HAS_EXTERNAL_UI +#endif /* ------------------------------------------------------------------------------------------------------------ * UI Resize Handling, internal */ diff --git a/distrho/src/DistrhoUIPrivateData.hpp b/distrho/src/DistrhoUIPrivateData.hpp index 0b03160aa..6d837cb1f 100644 --- a/distrho/src/DistrhoUIPrivateData.hpp +++ b/distrho/src/DistrhoUIPrivateData.hpp @@ -433,7 +433,7 @@ inline bool UI::PrivateData::fileRequestCallback(const char* const key) snprintf(title, sizeof(title)-1u, DISTRHO_PLUGIN_NAME ": %s", key); title[sizeof(title)-1u] = '\0'; - DGL_NAMESPACE::Window::FileBrowserOptions opts; + FileBrowserOptions opts; opts.title = title; return window->openFileBrowser(opts); #endif diff --git a/tests/FileBrowserDialog.cpp b/tests/FileBrowserDialog.cpp index 3733c385d..bc4ece4c1 100644 --- a/tests/FileBrowserDialog.cpp +++ b/tests/FileBrowserDialog.cpp @@ -125,6 +125,7 @@ class NanoFilePicker : public NanoStandaloneWindow repaint(); FileBrowserOptions opts; + // opts.saving = true; opts.title = "Look at me"; if (! openFileBrowser(opts)) {