diff DPF-Prymula-audioplugins/dpf/distrho/extra/FileBrowserDialogImpl.cpp @ 3:84e66ea83026

DPF-Prymula-audioplugins-0.231015-2
author prymula <prymula76@outlook.com>
date Mon, 16 Oct 2023 21:53:34 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/DPF-Prymula-audioplugins/dpf/distrho/extra/FileBrowserDialogImpl.cpp	Mon Oct 16 21:53:34 2023 +0200
@@ -0,0 +1,844 @@
+/*
+ * DISTRHO Plugin Framework (DPF)
+ * Copyright (C) 2012-2022 Filipe Coelho <falktx@falktx.com>
+ *
+ * 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.
+ */
+
+#if !defined(DISTRHO_FILE_BROWSER_DIALOG_HPP_INCLUDED) && !defined(DGL_FILE_BROWSER_DIALOG_HPP_INCLUDED)
+# error bad include
+#endif
+#if !defined(FILE_BROWSER_DIALOG_DISTRHO_NAMESPACE) && !defined(FILE_BROWSER_DIALOG_DGL_NAMESPACE)
+# error bad usage
+#endif
+
+#include "ScopedPointer.hpp"
+#include "String.hpp"
+
+#ifdef DISTRHO_OS_MAC
+# import <Cocoa/Cocoa.h>
+#endif
+#ifdef DISTRHO_OS_WASM
+# include <emscripten/emscripten.h>
+#endif
+#ifdef DISTRHO_OS_WINDOWS
+# include <direct.h>
+# include <process.h>
+# include <winsock2.h>
+# include <windows.h>
+# include <commdlg.h>
+# include <vector>
+#else
+# include <unistd.h>
+#endif
+#ifdef HAVE_DBUS
+# include <dbus/dbus.h>
+#endif
+#ifdef HAVE_X11
+# define DBLCLKTME 400
+# include "sofd/libsofd.h"
+# include "sofd/libsofd.c"
+#endif
+
+#ifdef FILE_BROWSER_DIALOG_DGL_NAMESPACE
+START_NAMESPACE_DGL
+using DISTRHO_NAMESPACE::ScopedPointer;
+using DISTRHO_NAMESPACE::String;
+#else
+START_NAMESPACE_DISTRHO
+#endif
+
+// --------------------------------------------------------------------------------------------------------------------
+
+// static pointer used for signal null/none action taken
+static const char* const kSelectedFileCancelled = "__dpf_cancelled__";
+
+#ifdef HAVE_DBUS
+static constexpr bool isHexChar(const char c) noexcept
+{
+    return c >= '0' && c <= 'f' && (c <= '9' || (c >= 'A' && c <= 'F') || c >= 'a');
+}
+
+static constexpr int toHexChar(const char c) noexcept
+{
+    return c >= '0' && c <= '9' ? c - '0' : (c >= 'A' && c <= 'F' ? c - 'A' : c - 'a') + 10;
+}
+#endif
+
+// --------------------------------------------------------------------------------------------------------------------
+
+#ifdef DISTRHO_OS_WASM
+# define DISTRHO_WASM_NAMESPACE_MACRO_HELPER(NS, SEP, FUNCTION) NS ## SEP ## FUNCTION
+# define DISTRHO_WASM_NAMESPACE_MACRO(NS, FUNCTION) DISTRHO_WASM_NAMESPACE_MACRO_HELPER(NS, _, FUNCTION)
+# define DISTRHO_WASM_NAMESPACE_HELPER(NS) #NS
+# define DISTRHO_WASM_NAMESPACE(NS) DISTRHO_WASM_NAMESPACE_HELPER(NS)
+# define fileBrowserSetPathNamespaced DISTRHO_WASM_NAMESPACE_MACRO(FILE_BROWSER_DIALOG_NAMESPACE, fileBrowserSetPath)
+# define fileBrowserSetPathFuncName DISTRHO_WASM_NAMESPACE(FILE_BROWSER_DIALOG_NAMESPACE) "_fileBrowserSetPath"
+
+// FIXME use world class name as prefix
+static bool openWebBrowserFileDialog(const char* const funcname, void* const handle)
+{
+    const char* const nameprefix = DISTRHO_WASM_NAMESPACE(FILE_BROWSER_DIALOG_NAMESPACE);
+
+    return EM_ASM_INT({
+        var canvasFileObjName = UTF8ToString($0) + "_file_open";
+        var canvasFileOpenElem = document.getElementById(canvasFileObjName);
+
+        var jsfuncname = UTF8ToString($1);
+        var jsfunc = Module.cwrap(jsfuncname, 'null', ['number', 'string']);
+
+        if (canvasFileOpenElem) {
+            document.body.removeChild(canvasFileOpenElem);
+        }
+
+        canvasFileOpenElem = document.createElement('input');
+        canvasFileOpenElem.type = 'file';
+        canvasFileOpenElem.id = canvasFileObjName;
+        canvasFileOpenElem.style.display = 'none';
+        document.body.appendChild(canvasFileOpenElem);
+
+        canvasFileOpenElem.onchange = function(e) {
+            if (!canvasFileOpenElem.files) {
+                jsfunc($2, "");
+                return;
+            }
+
+            var file = canvasFileOpenElem.files[0];
+            var filename = '/' + file.name;
+            var reader = new FileReader();
+
+            reader.onloadend = function(e) {
+                var content = new Uint8Array(reader.result);
+                Module.FS.writeFile(filename, content);
+                jsfunc($2, filename);
+            };
+
+            reader.readAsArrayBuffer(file);
+        };
+
+        canvasFileOpenElem.click();
+        return 1;
+    }, nameprefix, funcname, handle) != 0;
+}
+
+static bool downloadWebBrowserFile(const char* const filename)
+{
+    const char* const nameprefix = DISTRHO_WASM_NAMESPACE(FILE_BROWSER_DIALOG_NAMESPACE);
+
+    return EM_ASM_INT({
+        var canvasFileObjName = UTF8ToString($0) + "_file_save";
+        var jsfilename = UTF8ToString($1);
+
+        var canvasFileSaveElem = document.getElementById(canvasFileObjName);
+        if (canvasFileSaveElem) {
+            // only 1 file save allowed at once
+            console.warn("One file save operation already in progress, refusing to open another");
+            return 0;
+        }
+
+        canvasFileSaveElem = document.createElement('a');
+        canvasFileSaveElem.download = jsfilename;
+        canvasFileSaveElem.id = canvasFileObjName;
+        canvasFileSaveElem.style.display = 'none';
+        document.body.appendChild(canvasFileSaveElem);
+
+        var content = Module.FS.readFile('/' + jsfilename);
+        canvasFileSaveElem.href = URL.createObjectURL(new Blob([content]));
+        canvasFileSaveElem.click();
+
+        setTimeout(function() {
+            URL.revokeObjectURL(canvasFileSaveElem.href);
+            document.body.removeChild(canvasFileSaveElem);
+        }, 2000);
+        return 1;
+    }, nameprefix, filename) != 0;
+}
+#endif
+
+// --------------------------------------------------------------------------------------------------------------------
+
+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_WASM
+    char* defaultName;
+    bool saving;
+#endif
+
+#ifdef DISTRHO_OS_WINDOWS
+    OPENFILENAMEW ofn;
+    volatile bool threadCancelled;
+    uintptr_t threadHandle;
+    std::vector<WCHAR> fileNameW;
+    std::vector<WCHAR> startDirW;
+    std::vector<WCHAR> 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())
+            free();
+    }
+
+    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<int>(startDirW.size())))
+            ofn.lpstrInitialDir = startDirW.data();
+
+        titleW.resize(std::strlen(windowTitle) + 1);
+        if (MultiByteToWideChar(CP_UTF8, 0, windowTitle, -1, titleW.data(), static_cast<int>(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<char> 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<FileBrowserData*>(arg)->run();
+        // CoUninitialize();
+        _endthreadex(0);
+        return 0;
+    }
+#else // DISTRHO_OS_WINDOWS
+    FileBrowserData(const bool save)
+        : selectedFile(nullptr)
+    {
+#ifdef DISTRHO_OS_MAC
+        if (save)
+        {
+            nsOpenPanel = nullptr;
+            nsBasePanel = [[NSSavePanel savePanel]retain];
+        }
+        else
+        {
+            nsOpenPanel = [[NSOpenPanel openPanel]retain];
+            nsBasePanel = nsOpenPanel;
+        }
+#endif
+#ifdef DISTRHO_OS_WASM
+        defaultName = nullptr;
+        saving = save;
+#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)save;
+    }
+
+    ~FileBrowserData()
+    {
+#ifdef DISTRHO_OS_MAC
+        [nsBasePanel release];
+#endif
+#ifdef DISTRHO_OS_WASM
+        std::free(defaultName);
+#endif
+#ifdef HAVE_DBUS
+        if (dbuscon != nullptr)
+            dbus_connection_unref(dbuscon);
+#endif
+#ifdef HAVE_X11
+        if (x11display != nullptr)
+            XCloseDisplay(x11display);
+#endif
+
+        free();
+    }
+#endif
+
+    void free()
+    {
+        if (selectedFile == nullptr)
+            return;
+
+        if (selectedFile == kSelectedFileCancelled || std::strcmp(selectedFile, kSelectedFileCancelled) == 0)
+        {
+            selectedFile = nullptr;
+            return;
+        }
+
+        std::free(const_cast<char*>(selectedFile));
+        selectedFile = nullptr;
+    }
+};
+
+// --------------------------------------------------------------------------------------------------------------------
+
+#ifdef DISTRHO_OS_WASM
+extern "C" {
+EMSCRIPTEN_KEEPALIVE
+void fileBrowserSetPathNamespaced(FileBrowserHandle handle, const char* filename)
+{
+    handle->free();
+
+    if (filename != nullptr && filename[0] != '\0')
+        handle->selectedFile = strdup(filename);
+    else
+        handle->selectedFile = kSelectedFileCancelled;
+}
+}
+#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))
+#else
+        if (char* const cwd = getcwd(nullptr, 0))
+#endif
+        {
+            startDir = cwd;
+            std::free(cwd);
+        }
+    }
+
+    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<FileBrowserData> handle(new FileBrowserData(options.saving));
+
+#ifdef DISTRHO_OS_MAC
+# if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8
+    // unsupported
+    d_stderr2("fileBrowserCreate is unsupported on macos < 10.8");
+    return nullptr;
+# else
+    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
+#endif
+
+#ifdef DISTRHO_OS_WASM
+    if (options.saving)
+    {
+        const size_t len = options.defaultName != nullptr ? strlen(options.defaultName) : 0;
+        DISTRHO_SAFE_ASSERT_RETURN(len != 0, nullptr);
+
+        char* const filename = static_cast<char*>(malloc(len + 2));
+        filename[0] = '/';
+        std::memcpy(filename + 1, options.defaultName, len + 1);
+
+        handle->defaultName = strdup(options.defaultName);
+        handle->selectedFile = filename;
+        return handle.release();
+    }
+
+    const char* const funcname = fileBrowserSetPathFuncName;
+    if (openWebBrowserFileDialog(funcname, handle.get()))
+        return handle.release();
+
+    return nullptr;
+#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;
+
+    // https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.FileChooser
+    if (dbuscon != nullptr)
+    {
+        // if this is the first time we are calling into DBus, check if things are working
+        static bool checkAvailable = !dbus_bus_name_has_owner(dbuscon, "org.freedesktop.portal.Desktop", nullptr);
+
+        if (checkAvailable)
+        {
+            checkAvailable = false;
+
+            if (DBusMessage* const msg = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
+                                                                      "/org/freedesktop/portal/desktop",
+                                                                      "org.freedesktop.portal.FileChooser",
+                                                                      "version"))
+            {
+                if (DBusMessage* const reply = dbus_connection_send_with_reply_and_block(dbuscon, msg, 250, nullptr))
+                    dbus_message_unref(reply);
+
+                dbus_message_unref(msg);
+            }
+        }
+
+        // Any subsquent calls should have this DBus service active
+        if (dbus_bus_name_has_owner(dbuscon, "org.freedesktop.portal.Desktop", nullptr))
+        {
+            if (DBusMessage* const msg = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
+                                                                      "/org/freedesktop/portal/desktop",
+                                                                      "org.freedesktop.portal.FileChooser",
+                                                                      options.saving ? "SaveFile" : "OpenFile"))
+            {
+               #ifdef HAVE_X11
+                char windowIdStr[32];
+                memset(windowIdStr, 0, sizeof(windowIdStr));
+                snprintf(windowIdStr, sizeof(windowIdStr)-1, "x11:%llx", (ulonglong)windowId);
+                const char* windowIdStrPtr = windowIdStr;
+               #endif
+
+                dbus_message_append_args(msg,
+                                        #ifdef HAVE_X11
+                                         DBUS_TYPE_STRING, &windowIdStrPtr,
+                                        #endif
+                                         DBUS_TYPE_STRING, &windowTitle,
+                                         DBUS_TYPE_INVALID);
+
+                DBusMessageIter iter, array;
+                dbus_message_iter_init_append(msg, &iter);
+                dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array);
+
+                {
+                    DBusMessageIter dict, variant, variantArray;
+                    const char* const current_folder_key = "current_folder";
+                    const char* const current_folder_val = startDir.buffer();
+
+                    dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, nullptr, &dict);
+                    dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &current_folder_key);
+                    dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "ay", &variant);
+                    dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, "y", &variantArray);
+                    dbus_message_iter_append_fixed_array(&variantArray, DBUS_TYPE_BYTE,
+                                                         &current_folder_val, startDir.length()+1);
+                    dbus_message_iter_close_container(&variant, &variantArray);
+                    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, msg, nullptr);
+
+                dbus_message_unref(msg);
+                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);
+
+                    // look for dict with 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);
+
+                    // keep going until we find it
+                    while (std::strcmp(key, "uris") != 0)
+                    {
+                        key = nullptr;
+                        dbus_message_iter_next(&dictArray);
+                        DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dictArray) == DBUS_TYPE_DICT_ENTRY);
+
+                        dbus_message_iter_recurse(&dictArray, &dict);
+                        DISTRHO_SAFE_ASSERT_BREAK(dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRING);
+
+                        dbus_message_iter_get_basic(&dict, &key);
+                        DISTRHO_SAFE_ASSERT_BREAK(key != nullptr);
+                    }
+
+                    if (key == nullptr)
+                        break;
+
+                    // 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:///"))
+                    {
+                        if (char* const decodedvalue = strdup(localvalue + 7))
+                        {
+                            for (char* s = decodedvalue; (s = std::strchr(s, '%')) != nullptr; ++s)
+                            {
+                                if (! isHexChar(s[1]) || ! isHexChar(s[2]))
+                                    continue;
+
+                                const int decodedNum = toHexChar(s[1]) * 0x10 + toHexChar(s[2]);
+
+                                char replacementChar;
+                                switch (decodedNum)
+                                {
+                                case 0x20: replacementChar = ' '; break;
+                                case 0x22: replacementChar = '\"'; break;
+                                case 0x23: replacementChar = '#'; break;
+                                case 0x25: replacementChar = '%'; break;
+                                case 0x3c: replacementChar = '<'; break;
+                                case 0x3e: replacementChar = '>'; break;
+                                case 0x5b: replacementChar = '['; break;
+                                case 0x5c: replacementChar = '\\'; break;
+                                case 0x5d: replacementChar = ']'; break;
+                                case 0x5e: replacementChar = '^'; break;
+                                case 0x60: replacementChar = '`'; break;
+                                case 0x7b: replacementChar = '{'; break;
+                                case 0x7c: replacementChar = '|'; break;
+                                case 0x7d: replacementChar = '}'; break;
+                                case 0x7e: replacementChar = '~'; break;
+                                default: continue;
+                                }
+
+                                s[0] = replacementChar;
+                                std::memmove(s + 1, s + 3, std::strlen(s) - 2);
+                            }
+
+                            handle->selectedFile = decodedvalue;
+                        }
+                    }
+
+                } 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 DISTRHO_OS_WASM
+    if (handle->saving && fileBrowserGetPath(handle) != nullptr)
+        downloadWebBrowserFile(handle->defaultName);
+#endif
+
+#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)
+{
+    if (const char* const selectedFile = handle->selectedFile)
+        if (selectedFile != kSelectedFileCancelled && std::strcmp(selectedFile, kSelectedFileCancelled) != 0)
+            return selectedFile;
+
+    return nullptr;
+}
+
+// --------------------------------------------------------------------------------------------------------------------
+
+#ifdef FILE_BROWSER_DIALOG_DGL_NAMESPACE
+END_NAMESPACE_DGL
+#else
+END_NAMESPACE_DISTRHO
+#endif
+
+#undef FILE_BROWSER_DIALOG_DISTRHO_NAMESPACE
+#undef FILE_BROWSER_DIALOG_DGL_NAMESPACE
+#undef FILE_BROWSER_DIALOG_NAMESPACE
+
+#undef fileBrowserSetPathNamespaced
+#undef fileBrowserSetPathFuncName