From 4f88f4b70a99ce78050faa40b66b3e75d3165e68 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Tue, 23 Apr 2024 19:24:33 +0200 Subject: [PATCH] New feature: Accept native parent window handle This is necessary for platforms to present the dialog properly, e.g. ensuring that the dialog never goes behind the parent window. --- .github/workflows/cmake.yml | 76 ++++++- CMakeLists.txt | 3 +- src/include/nfd.h | 24 +++ src/include/nfd_glfw.h | 72 +++++++ src/include/nfd_sdl.h | 70 +++++++ src/nfd_cocoa.m | 42 +++- src/nfd_gtk.cpp | 162 ++++++++++++++- src/nfd_portal.cpp | 115 +++++++--- src/nfd_win.cpp | 32 ++- test/CMakeLists.txt | 78 ++++--- test/test_sdl.c | 405 ++++++++++++++++++++++++++++++++++++ test/test_sdl.manifest | 29 +++ 12 files changed, 1022 insertions(+), 86 deletions(-) create mode 100644 src/include/nfd_glfw.h create mode 100644 src/include/nfd_sdl.h create mode 100644 test/test_sdl.c create mode 100644 test/test_sdl.manifest diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 089fc847..e42daa2a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Installing Dependencies + - name: Install Dependencies run: sudo apt-get update && sudo apt-get install dos2unix - name: Convert to Unix line endings run: dos2unix */* @@ -72,7 +72,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Installing Dependencies + - name: Install Dependencies run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} - name: Configure run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. @@ -189,3 +189,75 @@ jobs: path: | build/src/* build/test/* + + build-ubuntu-sdl2: + + name: Ubuntu latest - GCC, ${{ matrix.portal.name }}, Static, SDL2 + runs-on: ubuntu-latest + + strategy: + matrix: + portal: [ {flag: OFF, dep: libgtk-3-dev, name: GTK}, {flag: ON, dep: libdbus-1-dev, name: Portal} ] # The NFD_PORTAL setting defaults to OFF (i.e. uses GTK) + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Dependencies + run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} libsdl2-dev libsdl2-ttf-dev + - name: Configure + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=OFF -DNFD_BUILD_TESTS=OFF -DNFD_BUILD_SDL2_TESTS=ON .. + - name: Build + run: cmake --build build --target install + - name: Upload test binaries + uses: actions/upload-artifact@v2 + with: + name: Ubuntu latest - GCC, ${{ matrix.portal.name }}, Static, SDL2 + path: | + build/src/* + build/test/* + + build-macos-sdl2: + + name: MacOS latest - Clang, Static, SDL2 + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Dependencies + run: brew install sdl2 sdl2_ttf + - name: Configure + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=OFF -DNFD_BUILD_SDL2_TESTS=ON .. + - name: Build + run: cmake --build build --target install + - name: Upload test binaries + uses: actions/upload-artifact@v2 + with: + name: MacOS latest - Clang, Static, SDL2 + path: | + build/src/* + build/test/* + + build-windows-sdl2: + + name: Windows latest - MSVC, Static, SDL2 + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install pkgconfiglite + run: choco install pkgconfiglite + - name: Install Dependencies + run: vcpkg integrate install && vcpkg install sdl2 sdl2-ttf --triplet=x64-windows-release + - name: Configure + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_TOOLCHAIN_FILE="C:/vcpkg/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET="x64-windows-release" -DNFD_BUILD_TESTS=OFF -DNFD_BUILD_SDL2_TESTS=ON .. + - name: Build + run: cmake --build build --target install --config Release + - name: Upload test binaries + uses: actions/upload-artifact@v2 + with: + name: Windows latest - MSVC, Static, SDL2 + path: | + build/src/Release/* + build/test/Release/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 86873e83..fedef3aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ endif () option(BUILD_SHARED_LIBS "Build a shared library instead of static" OFF) option(NFD_BUILD_TESTS "Build tests for nfd" ${nfd_ROOT_PROJECT}) +option(NFD_BUILD_SDL2_TESTS "Build SDL2 tests for nfd" OFF) option(NFD_INSTALL "Generate install target for nfd" ${nfd_ROOT_PROJECT}) set(nfd_PLATFORM Undefined) @@ -48,6 +49,6 @@ endif() add_subdirectory(src) -if(${NFD_BUILD_TESTS}) +if(${NFD_BUILD_TESTS} OR ${NFD_BUILD_SDL2_TESTS}) add_subdirectory(test) endif() diff --git a/src/include/nfd.h b/src/include/nfd.h index 9aaf6fc6..450a8ce8 100644 --- a/src/include/nfd.h +++ b/src/include/nfd.h @@ -93,12 +93,31 @@ typedef struct { typedef nfdu8filteritem_t nfdnfilteritem_t; #endif // _WIN32 +// The native window handle type. +enum { + NFD_WINDOW_HANDLE_TYPE_UNSET = 0, + // Windows: handle is HWND (the Windows API typedefs this to void*) + NFD_WINDOW_HANDLE_TYPE_WINDOWS = 1, + // Cocoa: handle is NSWindow* + NFD_WINDOW_HANDLE_TYPE_COCOA = 2, + // X11: handle is Window + NFD_WINDOW_HANDLE_TYPE_X11 = 3, + // Wayland support will be implemented separately in the future +}; +// The native window handle. If using a platform abstraction framework (e.g. SDL), this should be +// obtained using the corresponding NFD glue header (e.g. nfd_sdl.h). +typedef struct { + size_t type; // this is one of the values of the enum above + void* handle; +} nfdwindowhandle_t; + typedef size_t nfdversion_t; typedef struct { const nfdu8filteritem_t* filterList; nfdfiltersize_t filterCount; const nfdu8char_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdopendialogu8args_t; #ifdef _WIN32 @@ -106,6 +125,7 @@ typedef struct { const nfdnfilteritem_t* filterList; nfdfiltersize_t filterCount; const nfdnchar_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdopendialognargs_t; #else typedef nfdopendialogu8args_t nfdopendialognargs_t; @@ -116,6 +136,7 @@ typedef struct { nfdfiltersize_t filterCount; const nfdu8char_t* defaultPath; const nfdu8char_t* defaultName; + nfdwindowhandle_t parentWindow; } nfdsavedialogu8args_t; #ifdef _WIN32 @@ -124,6 +145,7 @@ typedef struct { nfdfiltersize_t filterCount; const nfdnchar_t* defaultPath; const nfdnchar_t* defaultName; + nfdwindowhandle_t parentWindow; } nfdsavedialognargs_t; #else typedef nfdsavedialogu8args_t nfdsavedialognargs_t; @@ -131,11 +153,13 @@ typedef nfdsavedialogu8args_t nfdsavedialognargs_t; typedef struct { const nfdu8char_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdpickfolderu8args_t; #ifdef _WIN32 typedef struct { const nfdnchar_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdpickfoldernargs_t; #else typedef nfdpickfolderu8args_t nfdpickfoldernargs_t; diff --git a/src/include/nfd_glfw.h b/src/include/nfd_glfw.h new file mode 100644 index 00000000..0cc15597 --- /dev/null +++ b/src/include/nfd_glfw.h @@ -0,0 +1,72 @@ +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo + + This header contains a function to convert a GLFW window handle to a native window handle for + passing to NFDe. + */ + +#ifndef _NFD_GLFW_H +#define _NFD_GLFW_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Converts a GLFW window handle to a native window handle that can be passed to NFDe. + * @param sdlWindow The GLFW window handle. + * @param[out] nativeWindow The output native window handle, populated if and only if this function + * returns true. + * @return Either true to indicate success, or false to indicate failure. It is intended that + * users ignore the error and simply pass a value-initialized nfdwindowhandle_t to NFDe if this + * function fails. */ +inline bool NFD_GetNativeWindowFromGLFWWindow(GLFWwindow* glfwWindow, + nfdwindowhandle_t* nativeWindow) { + GLFWerrorfun* oldCallback = glfwSetErrorCallback(NULL); + bool success = false; +#if defined(GLFW_EXPOSE_NATIVE_WIN32) + if (!success) { + const HWND hwnd = glfwGetWin32Window(glfwWindow); + if (hwnd) { + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WINDOWS; + nativeWindow->handle = (void*)hwnd; + success = true; + } + } +#endif +#if defined(GLFW_EXPOSE_NATIVE_COCOA) + if (!success) { + NSWindow* const cocoa_window = glfwGetCocoaWindow(glfwWindow); + if (cocoa_window) { + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_COCOA; + nativeWindow->handle = (void*)cocoa_window; + success = true; + } + } +#endif +#if defined(NFD_WINDOW_HANDLE_TYPE_X11) + if (!success) { + const Window x11_window = glfwGetX11Window(glfwWindow); + if (x11_window != None) { + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11; + nativeWindow->handle = (void*)x11_window; + success = true; + } + } +#endif + glfwSetErrorCallback(oldCallback); + return success; +} + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // _NFD_GLFW_H diff --git a/src/include/nfd_sdl.h b/src/include/nfd_sdl.h new file mode 100644 index 00000000..ca87c54c --- /dev/null +++ b/src/include/nfd_sdl.h @@ -0,0 +1,70 @@ +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo + + This header contains a function to convert an SDL window handle to a native window handle for + passing to NFDe. + + This is meant to be used with SDL2, but if there are incompatibilities with future SDL versions, + we can conditionally compile based on SDL_MAJOR_VERSION. + */ + +#ifndef _NFD_SDL_H +#define _NFD_SDL_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Converts an SDL window handle to a native window handle that can be passed to NFDe. + * @param sdlWindow The SDL window handle. + * @param[out] nativeWindow The output native window handle, populated if and only if this function + * returns true. + * @return Either true to indicate success, or false to indicate failure. If false is returned, + * you can call SDL_GetError() for more information. However, it is intended that users ignore the + * error and simply pass a value-initialized nfdwindowhandle_t to NFDe if this function fails. */ +inline bool NFD_GetNativeWindowFromSDLWindow(SDL_Window* sdlWindow, + nfdwindowhandle_t* nativeWindow) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(sdlWindow, &info)) { + return false; + } + switch (info.subsystem) { +#if defined(SDL_VIDEO_DRIVER_WINDOWS) + case SDL_SYSWM_WINDOWS: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WINDOWS; + nativeWindow->handle = (void*)info.info.win.window; + return true; +#endif +#if defined(SDL_VIDEO_DRIVER_COCOA) + case SDL_SYSWM_COCOA: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_COCOA; + nativeWindow->handle = (void*)info.info.cocoa.window; + return true; +#endif +#if defined(SDL_VIDEO_DRIVER_X11) + case SDL_SYSWM_X11: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11; + nativeWindow->handle = (void*)info.info.x11.window; + return true; +#endif + default: + SDL_SetError("Unsupported native window type."); + return false; + } +} + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // _NFD_SDL_H diff --git a/src/nfd_cocoa.m b/src/nfd_cocoa.m index 9f1fa3b5..4bcea220 100644 --- a/src/nfd_cocoa.m +++ b/src/nfd_cocoa.m @@ -174,6 +174,13 @@ static nfdresult_t CopyUtf8String(const char* utf8Str, nfdnchar_t** out) { return NFD_ERROR; } +static NSWindow* GetNativeWindowHandle(const nfdwindowhandle_t* parentWindow) { + if (parentWindow->type != NFD_WINDOW_HANDLE_TYPE_COCOA) { + return NULL; + } + return (NSWindow*)parentWindow->handle; +} + /* public */ const char* NFD_GetError(void) { @@ -230,7 +237,12 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:NO]; @@ -285,7 +297,12 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:YES]; @@ -347,7 +364,12 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSSavePanel* dialog = [NSSavePanel savePanel]; [dialog setExtensionHidden:NO]; @@ -404,7 +426,12 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:NO]; @@ -451,7 +478,12 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:YES]; diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index 6f519558..c70d7dd4 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -381,6 +381,113 @@ gint RunDialogWithFocus(GtkDialog* dialog) { return gtk_dialog_run(dialog); } +// Gets the GdkWindow from the given window handle. This function might fail even if parentWindow +// is set correctly, since it calls some failable GDK functions. If it fails, it will return +// nullptr. The caller is responsible for freeing ths returned GdkWindow, if not nullptr. +GdkWindow* GetAllocNativeWindowHandle(const nfdwindowhandle_t& parentWindow, + GdkDisplayManager*& outDisplayManager, + GdkDisplay*& outDisplay) { + switch (parentWindow.type) { +#if defined(GDK_WINDOWING_X11) + case NFD_WINDOW_HANDLE_TYPE_X11: { + const Window x11_handle = reinterpret_cast(parentWindow.handle); + // AFAIK, _any_ X11 display will do, because Windows are not associated to a specific + // Display. Supposedly, a Display is just a connection to the X server. + + // This will contain the X11 display we want to use. + GdkDisplay* x11_display = nullptr; + GdkDisplayManager* display_manager = gdk_display_manager_get(); + + // If we can find an existing X11 display, use it. + GSList* gdk_display_list = gdk_display_manager_list_displays(display_manager); + while (gdk_display_list) { + GSList* node = gdk_display_list; + GdkDisplay* display = GDK_DISPLAY(node->data); + if (GDK_IS_X11_DISPLAY(display)) { + g_slist_free(node); + x11_display = display; + break; + } else { + gdk_display_list = node->next; + g_slist_free_1(node); + } + } + + // Otherwise, we have to create our own X11 display. + if (!x11_display) { + // This is not very nice, because we are always resetting the allowed backends + // setting to NULL (which means all backends are allowed), even though we can't be + // sure that the user didn't call gdk_set_allowed_backends() earlier to force a + // specific backend. But well if the user doesn't have an X11 display already open + // and yet is telling us with have an X11 window as parent, they probably don't use + // GTK in their application at all so they probably won't notice this. + // + // There is no way, AFAIK, to get the allowed backends first so we can restore it + // later, and gdk_x11_display_open() is GTK4-only (the GTK3 version is a private + // implementation detail). + // + // Also, we don't close the display we specially opened, since GTK will need it to + // show the dialog. Though it probably doesn't matter very much if we want to free + // up resources and clean it up. + gdk_set_allowed_backends("x11"); + x11_display = gdk_display_manager_open_display(display_manager, NULL); + gdk_set_allowed_backends(NULL); + } + if (!x11_display) return nullptr; + outDisplayManager = display_manager; + outDisplay = x11_display; + GdkWindow* gdk_window = gdk_x11_window_foreign_new_for_display(x11_display, x11_handle); + return gdk_window; + } +#endif + default: + return nullptr; + } +} + +void RealizedSignalHandler(GtkWidget* window, void* userdata) { + GdkWindow* const parentWindow = static_cast(userdata); + gdk_window_set_transient_for(gtk_widget_get_window(window), parentWindow); +} + +struct NativeWindowParenter { + NativeWindowParenter(GtkWidget* widget, const nfdwindowhandle_t& parentWindow) noexcept + : widget(widget), displayManager(nullptr) { + GdkDisplay* new_display = nullptr; + parent = GetAllocNativeWindowHandle(parentWindow, displayManager, new_display); + + if (parent) { + /* set the handler to the realize signal to set the transient GDK parent */ + handlerID = g_signal_connect(G_OBJECT(widget), + "realize", + G_CALLBACK(RealizedSignalHandler), + static_cast(parent)); + + /* Set the default display to a display that we know is X11 (so that realizing the file + * dialog will use it) */ + /* Note: displayManager here must be non-null since parent is non-null */ + originalDisplay = gdk_display_manager_get_default_display(displayManager); + gdk_display_manager_set_default_display(displayManager, new_display); + } + } + ~NativeWindowParenter() { + if (parent) { + /* Set the default display back to whatever it was, to be nice */ + /* Note: displayManager here must be non-null since parent is non-null */ + gdk_display_manager_set_default_display(displayManager, originalDisplay); + + /* unset the handler and delete the parent GdkWindow */ + g_signal_handler_disconnect(G_OBJECT(widget), handlerID); + g_object_unref(parent); + } + } + GtkWidget* const widget; + GdkWindow* parent; + GdkDisplayManager* displayManager; + GdkDisplay* originalDisplay; + gulong handlerID; +}; + } // namespace const char* NFD_GetError(void) { @@ -447,7 +554,16 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); @@ -506,7 +622,16 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget)); @@ -580,8 +705,15 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, G_CALLBACK(FileActivatedSignalHandler), static_cast(&buttonClickedArgs)); - /* invoke the dialog (blocks until dialog is closed) */ - gint result = RunDialogWithFocus(GTK_DIALOG(widget)); + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + /* unset the handler */ g_signal_handler_disconnect(G_OBJECT(saveButton), handlerID); @@ -637,7 +769,16 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); @@ -682,7 +823,16 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget)); diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp index a91b71b2..2a6cc20f 100644 --- a/src/nfd_portal.cpp +++ b/src/nfd_portal.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,41 @@ T* transform(const T* begin, const T* end, T* out, Callback callback) { return out; } +template +T* reverse_copy(const T* begin, const T* end, T* out) { + while (begin != end) { + *out++ = *--end; + } + return out; +} + +// Returns true if ch is in [0-9A-Za-z], false otherwise. +bool IsHex(char ch) { + return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); +} + +// Returns the hexadecimal value contained in the char. Precondition: IsHex(ch) +char ParseHexUnchecked(char ch) { + if ('0' <= ch && ch <= '9') return ch - '0'; + if ('A' <= ch && ch <= 'F') return ch - ('A' - 10); + if ('a' <= ch && ch <= 'f') return ch - ('a' - 10); +#if defined(__GNUC__) + __builtin_unreachable(); +#endif +} + +// Writes val as a hex string to out +char* FormatUIntToHexString(char* out, uintptr_t val) { + char tmp[sizeof(uintptr_t) * 2]; + char* tmp_end = tmp; + do { + const uintptr_t digit = val & 15u; + *tmp_end++ = digit < 10 ? '0' + digit : 'A' - 10 + digit; + val >>= 4; + } while (val != 0); + return reverse_copy(tmp, tmp_end, out); +} + constexpr const char* STR_EMPTY = ""; constexpr const char* STR_OPEN_FILE = "Open File"; constexpr const char* STR_OPEN_FILES = "Open Files"; @@ -136,6 +172,30 @@ constexpr const char* DBUS_PATH = "/org/freedesktop/portal/desktop"; constexpr const char* DBUS_FILECHOOSER_IFACE = "org.freedesktop.portal.FileChooser"; constexpr const char* DBUS_REQUEST_IFACE = "org.freedesktop.portal.Request"; +void AppendOpenFileQueryParentWindow(DBusMessageIter& iter, const nfdwindowhandle_t& parentWindow) { + switch (parentWindow.type) { + case NFD_WINDOW_HANDLE_TYPE_X11: { + constexpr size_t maxX11WindowStrLen = + 4 + sizeof(uintptr_t) * 2 + 1; // "x11:" + "" + "\0" + char serializedWindowBuf[maxX11WindowStrLen]; + const uintptr_t handle = reinterpret_cast(parentWindow.handle); + char* out = serializedWindowBuf; + *out++ = 'x'; + *out++ = '1'; + *out++ = '1'; + *out++ = ':'; + out = FormatUIntToHexString(out, handle); + *out = '\0'; + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &serializedWindowBuf); + return; + } + default: { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); + return; + } + } +} + template void AppendOpenFileQueryTitle(DBusMessageIter&); template <> @@ -557,12 +617,12 @@ void AppendOpenFileQueryParams(DBusMessage* query, const char* handle_token, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath) { + const nfdnchar_t* defaultPath, + const nfdwindowhandle_t& parentWindow) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); - dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); - + AppendOpenFileQueryParentWindow(iter, parentWindow); AppendOpenFileQueryTitle(iter); DBusMessageIter sub_iter; @@ -581,12 +641,12 @@ void AppendSaveFileQueryParams(DBusMessage* query, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, - const nfdnchar_t* defaultName) { + const nfdnchar_t* defaultName, + const nfdwindowhandle_t& parentWindow) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); - dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); - + AppendOpenFileQueryParentWindow(iter, parentWindow); AppendSaveFileQueryTitle(iter); DBusMessageIter sub_iter; @@ -963,21 +1023,6 @@ class DBusSignalSubscriptionHandler { } }; -// Returns true if ch is in [0-9A-Za-z], false otherwise. -bool IsHex(char ch) { - return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); -} - -// Returns the hexadecimal value contained in the char. Precondition: IsHex(ch) -char ParseHexUnchecked(char ch) { - if ('0' <= ch && ch <= '9') return ch - '0'; - if ('A' <= ch && ch <= 'F') return ch - ('A' - 10); - if ('a' <= ch && ch <= 'f') return ch - ('a' - 10); -#if defined(__GNUC__) - __builtin_unreachable(); -#endif -} - // Returns true if the given file URI is decodable (i.e. not malformed), and false otherwise. // If this function returns true, then `out` will be populated with the length of the decoded URI // and `fileUriEnd` will point to the trailing null byte of `fileUri`. Otherwise, `out` and @@ -1128,7 +1173,8 @@ template nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath) { + const nfdnchar_t* defaultPath, + const nfdwindowhandle_t& parentWindow) { const char* handle_token_ptr; char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); Free_Guard handle_obj_path_guard(handle_obj_path); @@ -1149,7 +1195,7 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "OpenFile"); DBusMessage_Guard query_guard(query); AppendOpenFileQueryParams( - query, handle_token_ptr, filterList, filterCount, defaultPath); + query, handle_token_ptr, filterList, filterCount, defaultPath, parentWindow); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1210,7 +1256,8 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, - const nfdnchar_t* defaultName) { + const nfdnchar_t* defaultName, + const nfdwindowhandle_t& parentWindow) { const char* handle_token_ptr; char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); Free_Guard handle_obj_path_guard(handle_obj_path); @@ -1231,7 +1278,7 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "SaveFile"); DBusMessage_Guard query_guard(query); AppendSaveFileQueryParams( - query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName); + query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName, parentWindow); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1398,7 +1445,7 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, DBusMessage* msg; { const nfdresult_t res = NFD_DBus_OpenFile( - msg, args->filterList, args->filterCount, args->defaultPath); + msg, args->filterList, args->filterCount, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1447,7 +1494,7 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, DBusMessage* msg; { const nfdresult_t res = NFD_DBus_OpenFile( - msg, args->filterList, args->filterCount, args->defaultPath); + msg, args->filterList, args->filterCount, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1496,8 +1543,12 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_SaveFile( - msg, args->filterList, args->filterCount, args->defaultPath, args->defaultName); + const nfdresult_t res = NFD_DBus_SaveFile(msg, + args->filterList, + args->filterCount, + args->defaultPath, + args->defaultName, + args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1570,7 +1621,8 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath); + const nfdresult_t res = + NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1626,7 +1678,8 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath); + const nfdresult_t res = + NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } diff --git a/src/nfd_win.cpp b/src/nfd_win.cpp index 2095b3fd..4bf07e21 100644 --- a/src/nfd_win.cpp +++ b/src/nfd_win.cpp @@ -284,6 +284,13 @@ nfdresult_t AddOptions(IFileDialog* dialog, FILEOPENDIALOGOPTIONS options) { } return NFD_OKAY; } + +HWND GetNativeWindowHandle(const nfdwindowhandle_t& parentWindow) { + if (parentWindow.type != NFD_WINDOW_HANDLE_TYPE_WINDOWS) { + return nullptr; + } + return static_cast(parentWindow.handle); +} } // namespace const char* NFD_GetError(void) { @@ -385,7 +392,7 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, } // Show the dialog. - result = fileOpenDialog->Show(nullptr); + result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { // Get the file name ::IShellItem* psiResult; @@ -469,7 +476,7 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, } // Show the dialog. - result = fileOpenDialog->Show(nullptr); + result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { ::IShellItemArray* shellItems; result = fileOpenDialog->GetResults(&shellItems); @@ -552,7 +559,7 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, } // Show the dialog. - result = fileSaveDialog->Show(nullptr); + result = fileSaveDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { // Get the file name ::IShellItem* psiResult; @@ -618,7 +625,7 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, } // Show the dialog to the user - const HRESULT result = fileOpenDialog->Show(nullptr); + const HRESULT result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return NFD_CANCEL; } else if (!SUCCEEDED(result)) { @@ -685,7 +692,7 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, } // Show the dialog. - const HRESULT result = fileOpenDialog->Show(nullptr); + const HRESULT result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { ::IShellItemArray* shellItems; if (!SUCCEEDED(fileOpenDialog->GetResults(&shellItems))) { @@ -948,7 +955,7 @@ nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, // call the native function nfdnchar_t* outPathN; const nfdopendialognargs_t argsN{ - filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data}; + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, args->parentWindow}; nfdresult_t res = NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { @@ -996,7 +1003,7 @@ nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, // call the native function const nfdopendialognargs_t argsN{ - filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data}; + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, args->parentWindow}; return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &argsN); } @@ -1039,8 +1046,11 @@ nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, // call the native function nfdnchar_t* outPathN; - const nfdsavedialognargs_t argsN{ - filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, defaultNameNGuard.data}; + const nfdsavedialognargs_t argsN{filterItemsNGuard.data, + args->filterCount, + defaultPathNGuard.data, + defaultNameNGuard.data, + args->parentWindow}; nfdresult_t res = NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { @@ -1077,7 +1087,7 @@ nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, // call the native function nfdnchar_t* outPathN; - const nfdpickfoldernargs_t argsN{defaultPathNGuard.data}; + const nfdpickfoldernargs_t argsN{defaultPathNGuard.data, args->parentWindow}; nfdresult_t res = NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { @@ -1114,7 +1124,7 @@ nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, NormalizePathSeparator(defaultPathNGuard.data); // call the native function - const nfdpickfoldernargs_t argsN{defaultPathNGuard.data}; + const nfdpickfoldernargs_t argsN{defaultPathNGuard.data, args->parentWindow}; return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &argsN); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 95f42a87..5bd5c596 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,31 +1,49 @@ +if(${NFD_BUILD_TESTS}) + set(TEST_LIST + test_opendialog.c + test_opendialog_cpp.cpp + test_opendialog_native.c + test_opendialog_with.c + test_opendialog_native_with.c + test_opendialogmultiple.c + test_opendialogmultiple_cpp.cpp + test_opendialogmultiple_native.c + test_opendialogmultiple_enum.c + test_opendialogmultiple_enum_native.c + test_pickfolder.c + test_pickfolder_cpp.cpp + test_pickfolder_native.c + test_pickfolder_with.c + test_pickfolder_native_with.c + test_pickfoldermultiple.c + test_pickfoldermultiple_native.c + test_savedialog.c + test_savedialog_native.c + test_savedialog_with.c + test_savedialog_native_with.c) -set(TEST_LIST - test_opendialog.c - test_opendialog_cpp.cpp - test_opendialog_native.c - test_opendialog_with.c - test_opendialog_native_with.c - test_opendialogmultiple.c - test_opendialogmultiple_cpp.cpp - test_opendialogmultiple_native.c - test_opendialogmultiple_enum.c - test_opendialogmultiple_enum_native.c - test_pickfolder.c - test_pickfolder_cpp.cpp - test_pickfolder_native.c - test_pickfolder_with.c - test_pickfolder_native_with.c - test_pickfoldermultiple.c - test_pickfoldermultiple_native.c - test_savedialog.c - test_savedialog_native.c - test_savedialog_with.c - test_savedialog_native_with.c) - -foreach (TEST ${TEST_LIST}) - string(REPLACE "." "_" CLEAN_TEST_NAME ${TEST}) - add_executable(${CLEAN_TEST_NAME} - ${TEST}) - target_link_libraries(${CLEAN_TEST_NAME} - PUBLIC nfd) -endforeach() + foreach (TEST ${TEST_LIST}) + string(REPLACE "." "_" CLEAN_TEST_NAME ${TEST}) + add_executable(${CLEAN_TEST_NAME} + ${TEST}) + target_link_libraries(${CLEAN_TEST_NAME} + PRIVATE nfd) + endforeach() +endif() + +if(${NFD_BUILD_SDL2_TESTS}) + find_package(PkgConfig REQUIRED) + pkg_check_modules(SDL2 REQUIRED sdl2 SDL2_ttf) + if(WIN32) + add_executable(test_sdl2 WIN32 test_sdl.c test_sdl.manifest) + else() + add_executable(test_sdl2 test_sdl.c) + endif() + target_link_libraries(test_sdl2 PRIVATE nfd) + target_include_directories(test_sdl2 PRIVATE ${SDL2_INCLUDE_DIRS}) + target_link_libraries(test_sdl2 PRIVATE ${SDL2_LINK_LIBRARIES}) + target_compile_options(test_sdl2 PUBLIC ${SDL2_CFLAGS_OTHER}) + # if(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + # set_target_properties(test_sdl2 PROPERTIES VS_DPI_AWARE "PerMonitorV2") + # endif() +endif() diff --git a/test/test_sdl.c b/test/test_sdl.c new file mode 100644 index 00000000..bb9d28cc --- /dev/null +++ b/test/test_sdl.c @@ -0,0 +1,405 @@ +#define SDL_MAIN_HANDLED +#include +#include +#include +#include +#include +#include +#include + +// Small program meant to demonstrate and test nfd_sdl.h with SDL2. Note that it quits immediately +// when it encounters an error, without calling the opposite destroy/quit function. A real-world +// application should call destroy/quit appropriately. + +void show_error(const char* message, SDL_Window* window) { + if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window) != 0) { + printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError()); + return; + } +} + +void show_path(const char* path, SDL_Window* window) { + if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Success", path, window) != 0) { + printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError()); + return; + } +} + +void show_paths(const nfdpathset_t* paths, SDL_Window* window) { + size_t num_chars = 0; + + nfdpathsetsize_t num_paths; + if (NFD_PathSet_GetCount(paths, &num_paths) != NFD_OKAY) { + printf("NFD_PathSet_GetCount failed: %s\n", NFD_GetError()); + return; + } + + nfdpathsetsize_t i; + for (i = 0; i != num_paths; ++i) { + char* path; + if (NFD_PathSet_GetPathU8(paths, i, &path) != NFD_OKAY) { + printf("NFD_PathSet_GetPathU8 failed: %s\n", NFD_GetError()); + return; + } + num_chars += strlen(path) + 1; + NFD_PathSet_FreePathU8(path); + } + + char* message = malloc(num_chars); + message[0] = '\0'; + + for (i = 0; i != num_paths; ++i) { + if (i != 0) { + strcat(message, "\n"); + } + char* path; + if (NFD_PathSet_GetPathU8(paths, i, &path) != NFD_OKAY) { + printf("NFD_PathSet_GetPathU8 failed: %s\n", NFD_GetError()); + return; + } + strcat(message, path); + NFD_PathSet_FreePathU8(path); + } + + if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Success", message, window) != 0) { + printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError()); + return; + } + + free(message); +} + +void opendialog_handler(SDL_Window* window) { + char* path; + nfdopendialogu8args_t args = {0}; + NFD_GetNativeWindowFromSDLWindow(window, &args.parentWindow); + const nfdresult_t res = NFD_OpenDialogU8_With(&path, args); + switch (res) { + case NFD_OKAY: + show_path(path, window); + NFD_FreePathU8(path); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void opendialogmultiple_handler(SDL_Window* window) { + const nfdpathset_t* paths; + nfdopendialogu8args_t args = {0}; + NFD_GetNativeWindowFromSDLWindow(window, &args.parentWindow); + const nfdresult_t res = NFD_OpenDialogMultipleU8_With(&paths, args); + switch (res) { + case NFD_OKAY: + show_paths(paths, window); + NFD_PathSet_Free(paths); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void savedialog_handler(SDL_Window* window) { + char* path; + nfdsavedialogu8args_t args = {0}; + NFD_GetNativeWindowFromSDLWindow(window, &args.parentWindow); + const nfdresult_t res = NFD_SaveDialogU8_With(&path, args); + switch (res) { + case NFD_OKAY: + show_path(path, window); + NFD_FreePathU8(path); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void pickfolder_handler(SDL_Window* window) { + char* path; + nfdpickfolderu8args_t args = {0}; + NFD_GetNativeWindowFromSDLWindow(window, &args.parentWindow); + const nfdresult_t res = NFD_PickFolderU8_With(&path, args); + switch (res) { + case NFD_OKAY: + show_path(path, window); + NFD_FreePathU8(path); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void pickfoldermultiple_handler(SDL_Window* window) { + const nfdpathset_t* paths; + nfdpickfolderu8args_t args = {0}; + NFD_GetNativeWindowFromSDLWindow(window, &args.parentWindow); + const nfdresult_t res = NFD_PickFolderMultipleU8_With(&paths, args); + switch (res) { + case NFD_OKAY: + show_paths(paths, window); + NFD_PathSet_Free(paths); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +#if defined(_WIN32) +const char font_file[] = "C:\\Windows\\Fonts\\calibri.ttf"; +#elif defined(__APPLE__) +const char font_file[] = "/Library/Fonts/font.ttf"; +#else +const char font_file[] = "/usr/share/fonts/font.ttf"; +#endif + +#define NUM_STATES 3 +#define NUM_BUTTONS 5 +const char* button_text[NUM_BUTTONS] = {"Open File", + "Open Files", + "Save File", + "Select Folder", + "Select Folders"}; +const int BUTTON_WIDTH = 400; +const int BUTTON_HEIGHT = 40; + +void (*button_handler[NUM_BUTTONS])(SDL_Window*) = {&opendialog_handler, + &opendialogmultiple_handler, + &savedialog_handler, + &pickfolder_handler, + &pickfoldermultiple_handler}; + +#ifdef _WIN32 +int WINAPI WinMain(void) +#else +int main(void) +#endif +{ +#ifdef _WIN32 + // Enable DPI awareness on Windows + SDL_SetHint("SDL_HINT_WINDOWS_DPI_AWARENESS", "permonitorv2"); + SDL_SetHint("SDL_HINT_WINDOWS_DPI_SCALING", "1"); +#endif + + // initialize SDL + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + printf("SDL_Init failed: %s\n", SDL_GetError()); + return 0; + } + + // initialize SDL_ttf + if (TTF_Init() != 0) { + printf("TTF_Init failed: %s\n", TTF_GetError()); + return 0; + } + + // initialize NFD + if (NFD_Init() != NFD_OKAY) { + printf("NFD_Init failed: %s\n", NFD_GetError()); + return 0; + } + + // create window + SDL_Window* const window = SDL_CreateWindow("Welcome", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + BUTTON_WIDTH, + BUTTON_HEIGHT * NUM_BUTTONS, + SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE); + if (!window) { + printf("SDL_CreateWindow failed: %s\n", SDL_GetError()); + return 0; + } + + // huh does this actually do anything? + // SDL_SetWindowSize(window, BUTTON_WIDTH, BUTTON_HEIGHT * NUM_BUTTONS); + + // create renderer + SDL_Renderer* const renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer) { + printf("SDL_CreateRenderer failed: %s\n", SDL_GetError()); + return 0; + } + + // prepare the buttons and handlers + SDL_Texture* textures_normal[NUM_BUTTONS][NUM_STATES]; + + TTF_Font* const font = TTF_OpenFont(font_file, 20); + if (!font) { + printf("TTF_OpenFont failed: %s\n", TTF_GetError()); + return 0; + } + + const SDL_Color back_color[NUM_STATES] = {{0, 0, 0, SDL_ALPHA_OPAQUE}, + {51, 51, 51, SDL_ALPHA_OPAQUE}, + {102, 102, 102, SDL_ALPHA_OPAQUE}}; + const SDL_Color text_color = {255, 255, 255, SDL_ALPHA_OPAQUE}; + const uint8_t text_alpha[NUM_STATES] = {153, 204, 255}; + + for (size_t i = 0; i != NUM_BUTTONS; ++i) { + SDL_Surface* const text_surface = TTF_RenderUTF8_Blended(font, button_text[i], text_color); + if (!text_surface) { + printf("TTF_RenderUTF8_Blended failed: %s\n", TTF_GetError()); + return 0; + } + + if (SDL_SetSurfaceBlendMode(text_surface, SDL_BLENDMODE_BLEND) != 0) { + printf("SDL_SetSurfaceBlendMode failed: %s\n", SDL_GetError()); + return 0; + } + + for (size_t j = 0; j != NUM_STATES; ++j) { + SDL_Surface* button_surface = + SDL_CreateRGBSurface(0, BUTTON_WIDTH, BUTTON_HEIGHT, 32, 0, 0, 0, 0); + if (!button_surface) { + printf("SDL_CreateRGBSurface failed: %s\n", SDL_GetError()); + return 0; + } + + if (SDL_FillRect(button_surface, + NULL, + SDL_MapRGBA(button_surface->format, + back_color[j].r, + back_color[j].g, + back_color[j].b, + back_color[j].a)) != 0) { + printf("SDL_FillRect failed: %s\n", SDL_GetError()); + return 0; + } + + SDL_SetSurfaceAlphaMod(text_surface, text_alpha[j]); + + SDL_Rect dstrect = {(BUTTON_WIDTH - text_surface->w) / 2, + (BUTTON_HEIGHT - text_surface->h) / 2, + text_surface->w, + text_surface->h}; + if (SDL_BlitSurface(text_surface, NULL, button_surface, &dstrect) != 0) { + printf("SDL_BlitSurface failed: %s\n", SDL_GetError()); + return 0; + } + + SDL_Texture* const texture = SDL_CreateTextureFromSurface(renderer, button_surface); + if (!texture) { + printf("SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError()); + return 0; + } + + SDL_FreeSurface(button_surface); + + textures_normal[i][j] = texture; + } + + SDL_FreeSurface(text_surface); + } + + TTF_CloseFont(font); + + // event loop + bool quit = false; + size_t button_index = (size_t)-1; + bool pressed = false; + do { + // render + for (size_t i = 0; i != NUM_BUTTONS; ++i) { + const SDL_Rect rect = {0, (int)i * BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT}; + SDL_RenderCopy( + renderer, textures_normal[i][button_index == i ? pressed ? 2 : 1 : 0], NULL, &rect); + } + SDL_RenderPresent(renderer); + + // process events + SDL_Event event; + if (SDL_WaitEvent(&event) == 0) { + printf("SDL_WaitEvent failed: %s\n", SDL_GetError()); + return 0; + } + do { + switch (event.type) { + case SDL_QUIT: { + quit = true; + break; + } + case SDL_WINDOWEVENT: { + switch (event.window.event) { + case SDL_WINDOWEVENT_CLOSE: + quit = true; + break; + case SDL_WINDOWEVENT_LEAVE: + button_index = (size_t)-1; + break; + } + break; + } + case SDL_MOUSEMOTION: { + if (event.motion.x < 0 || event.motion.x >= BUTTON_WIDTH || + event.motion.y < 0) { + button_index = (size_t)-1; + break; + } + const int index = event.motion.y / BUTTON_HEIGHT; + if (index < 0 || index >= NUM_BUTTONS) { + button_index = (size_t)-1; + break; + } + button_index = index; + pressed = event.motion.state & SDL_BUTTON(1); + break; + } + case SDL_MOUSEBUTTONDOWN: { + if (event.button.button == 1) { + pressed = true; + } + break; + } + case SDL_MOUSEBUTTONUP: { + if (event.button.button == 1) { + pressed = false; + if (button_index != (size_t)-1) { + (*button_handler[button_index])(window); + } + } + break; + } + } + } while (SDL_PollEvent(&event) != 0); + } while (!quit); + + // destroy textures + for (size_t i = 0; i != NUM_BUTTONS; ++i) { + for (size_t j = 0; j != NUM_STATES; ++j) { + SDL_DestroyTexture(textures_normal[i][j]); + } + } + + // destroy renderer + SDL_DestroyRenderer(renderer); + + // destroy window + SDL_DestroyWindow(window); + + // quit NFD + NFD_Quit(); + + // quit SDL_ttf + TTF_Quit(); + + // quit SDL + SDL_Quit(); + + return 0; +} diff --git a/test/test_sdl.manifest b/test/test_sdl.manifest new file mode 100644 index 00000000..a2d0852b --- /dev/null +++ b/test/test_sdl.manifest @@ -0,0 +1,29 @@ + + + + + + true + PerMonitorV2 + + + Your application description here. + + + + + +