diff --git a/CMakeLists.txt b/CMakeLists.txt index da9f07e..b61d0d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,6 @@ set (CMAKE_CXX_STANDARD 17) add_subdirectory(src) option(NFD_BUILD_TESTS "Build tests for nfd" OFF) -if(${NFD_BUILD_TESTS}) +if(NFD_BUILD_TESTS) add_subdirectory(test) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac776e9..e67647d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,8 +12,27 @@ endif() if(nfd_PLATFORM STREQUAL PLATFORM_LINUX) find_package(PkgConfig REQUIRED) - pkg_check_modules(GTK3 REQUIRED gtk+-3.0) - message("Using GTK version: ${GTK3_VERSION}") + set(NFD_GTK_VERSION "" CACHE STRING "GTK version for Linux builds ('3' or '4')") + set_property(CACHE NFD_GTK_VERSION PROPERTY STRINGS "" 3 4) + # For Linux, we support both GTK3 and GTK4. + # If NFD_GTK_VERSION is not explicitly set, then we take one that is available. + # Otherwise, we find the version that the user wants. + if(NFD_GTK_VERSION STREQUAL "") + pkg_search_module(GTK REQUIRED gtk+-3.0 gtk4) + if(DEFINED GTK_gtk+-3.0_VERSION) + set(GTK_VERSION ${GTK_gtk+-3.0_VERSION}) + elseif(DEFINED GTK_gtk4_VERSION) + set(GTK_VERSION ${GTK_gtk4_VERSION}) + endif() + elseif(NFD_GTK_VERSION STREQUAL 3) + pkg_check_modules(GTK REQUIRED gtk+-3.0) + elseif(NFD_GTK_VERSION STREQUAL 4) + pkg_check_modules(GTK REQUIRED gtk4) + else() + message(FATAL_ERROR "Unsupported GTK version: ${NFD_GTK_VERSION}") + endif() + + message("Using GTK version: ${GTK_VERSION}") list(APPEND SOURCE_FILES nfd_gtk.cpp) endif() @@ -32,9 +51,9 @@ target_include_directories(${TARGET_NAME} if(nfd_PLATFORM STREQUAL PLATFORM_LINUX) target_include_directories(${TARGET_NAME} - PRIVATE ${GTK3_INCLUDE_DIRS}) + PRIVATE ${GTK_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} - PRIVATE ${GTK3_LIBRARIES}) + PRIVATE ${GTK_LIBRARIES}) endif() if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index 29d7dc0..d4a37a7 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -5,12 +5,18 @@ Authors: Bernard Teo, Michael Labbe Note: We do not check for malloc failure on Linux - Linux overcommits memory! + Note: The GTK4 implementation does not distinguish between local and network files, + so it is possible for the file picker to return a network URI instead of a local filename. */ #include #include #if defined(GDK_WINDOWING_X11) +#if GTK_MAJOR_VERSION == 3 #include +#elif GTK_MAJOR_VERSION == 4 +#include +#endif #endif #include #include @@ -19,6 +25,8 @@ #include "nfd.h" +#define UNSUPPORTED_ERROR() static_assert(false, "Unsupported GTK version, this is an NFD bug.") + namespace { template @@ -37,6 +45,14 @@ struct FreeCheck_Guard { } }; +template +struct GUnref_Guard { + T* data; + GUnref_Guard(T* object) noexcept : data(object) {} + ~GUnref_Guard() { g_object_unref(data); } + T* get() const noexcept { return data; } +}; + /* current error */ const char* g_errorstr = nullptr; @@ -278,16 +294,33 @@ Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* choo return map; } -void SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) { - if (!defaultPath || !*defaultPath) return; +/* +Note: GTK+ manual recommends not specifically setting the default path. +We do it anyway in order to be consistent across platforms. - /* GTK+ manual recommends not specifically setting the default path. - We do it anyway in order to be consistent across platforms. +If consistency with the native OS is preferred, +then this function should be made a no-op. +*/ +#if GTK_MAJOR_VERSION == 3 +nfdresult_t SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) { + if (!defaultPath || !*defaultPath) return NFD_OKAY; - If consistency with the native OS is preferred, this is the line - to comment out. -ml */ gtk_file_chooser_set_current_folder(chooser, defaultPath); + return NFD_OKAY; +} +#elif GTK_MAJOR_VERSION == 4 +nfdresult_t SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) { + if (!defaultPath || !*defaultPath) return NFD_OKAY; + + GUnref_Guard file(g_file_new_for_path(defaultPath)); + + if (!gtk_file_chooser_set_current_folder(chooser, file.get(), nullptr)) { + NFDi_SetError("Failed to set default path."); + return NFD_ERROR; + } + return NFD_OKAY; } +#endif void SetDefaultName(GtkFileChooser* chooser, const char* defaultName) { if (!defaultName || !*defaultName) return; @@ -295,8 +328,26 @@ void SetDefaultName(GtkFileChooser* chooser, const char* defaultName) { gtk_file_chooser_set_current_name(chooser, defaultName); } +char* GetSingleFileName(GtkFileChooser* chooser) { +#if GTK_MAJOR_VERSION == 3 + return gtk_file_chooser_get_filename(chooser); +#elif GTK_MAJOR_VERSION == 4 + GUnref_Guard file(gtk_file_chooser_get_file(chooser)); + return g_file_get_path(file.get()); +#else + UNSUPPORTED_ERROR(); +#endif +} + void WaitForCleanup() { +#if GTK_MAJOR_VERSION == 3 while (gtk_events_pending()) gtk_main_iteration(); +#elif GTK_MAJOR_VERSION == 4 + while (g_main_context_iteration(NULL, FALSE)) + ; +#else + UNSUPPORTED_ERROR(); +#endif } struct Widget_Guard { @@ -304,7 +355,13 @@ struct Widget_Guard { Widget_Guard(GtkWidget* widget) : data(widget) {} ~Widget_Guard() { WaitForCleanup(); +#if GTK_MAJOR_VERSION == 3 gtk_widget_destroy(data); +#elif GTK_MAJOR_VERSION == 4 + gtk_window_destroy(GTK_WINDOW(data)); +#else + UNSUPPORTED_ERROR(); +#endif WaitForCleanup(); } }; @@ -362,12 +419,20 @@ void FileActivatedSignalHandler(GtkButton* saveButton, void* userdata) { g_free(currentFileName); } +#if GTK_MAJOR_VERSION == 4 +void DialogResponseHandler(GtkDialog*, gint resp, gpointer out_resp_gp) { + gint* out_resp = static_cast(out_resp_gp); + *out_resp = resp; +} +#endif + // wrapper for gtk_dialog_run() that brings the dialog to the front // see issues at: // https://github.com/btzy/nativefiledialog-extended/issues/31 // https://github.com/mlabbe/nativefiledialog/pull/92 // https://github.com/guillaumechereau/noc/pull/11 gint RunDialogWithFocus(GtkDialog* dialog) { +#if GTK_MAJOR_VERSION == 3 #if defined(GDK_WINDOWING_X11) gtk_widget_show_all(GTK_WIDGET(dialog)); // show the dialog so that it gets a display if (GDK_IS_X11_DISPLAY(gtk_widget_get_display(GTK_WIDGET(dialog)))) { @@ -379,6 +444,20 @@ gint RunDialogWithFocus(GtkDialog* dialog) { } #endif return gtk_dialog_run(dialog); +#elif GTK_MAJOR_VERSION == 4 + // TODO: the X11 popup issues + gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + gtk_widget_show(GTK_WIDGET(dialog)); + gint resp = 0; + g_signal_connect(G_OBJECT(dialog), + "response", + G_CALLBACK(DialogResponseHandler), + static_cast(&resp)); + while (resp == 0) g_main_context_iteration(NULL, TRUE); + return resp; +#else + UNSUPPORTED_ERROR(); +#endif } } // namespace @@ -395,7 +474,14 @@ void NFD_ClearError(void) { nfdresult_t NFD_Init(void) { // Init GTK - if (!gtk_init_check(NULL, NULL)) { +#if GTK_MAJOR_VERSION == 3 + if (!gtk_init_check(NULL, NULL)) +#elif GTK_MAJOR_VERSION == 4 + if (!gtk_init_check()) +#else + UNSUPPORTED_ERROR(); +#endif + { NFDi_SetError("gtk_init_check failed to initilaize GTK+"); return NFD_ERROR; } @@ -430,13 +516,14 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + if (res != NFD_OKAY) return res; if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { // write out the file name - *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + *outPath = GetSingleFileName(GTK_FILE_CHOOSER(widget)); - return NFD_OKAY; + return *outPath ? NFD_OKAY : NFD_CANCEL; } else { return NFD_CANCEL; } @@ -465,7 +552,8 @@ nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + if (res != NFD_OKAY) return res; if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { // write out the file name @@ -495,8 +583,10 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, GtkWidget* saveButton = gtk_dialog_add_button(GTK_DIALOG(widget), "_Save", GTK_RESPONSE_ACCEPT); - // Prompt on overwrite + // Prompt on overwrite (GTK3 only, because GTK4 automatically prompts) +#if GTK_MAJOR_VERSION == 3 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(widget), TRUE); +#endif /* Build the filter list */ ButtonClickedArgs buttonClickedArgs; @@ -505,7 +595,8 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, AddFiltersToDialogWithMap(GTK_FILE_CHOOSER(widget), filterList, filterCount); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + if (res != NFD_OKAY) return res; /* Set the default file name */ SetDefaultName(GTK_FILE_CHOOSER(widget), defaultName); @@ -526,9 +617,9 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, if (result == GTK_RESPONSE_ACCEPT) { // write out the file name - *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + *outPath = GetSingleFileName(GTK_FILE_CHOOSER(widget)); - return NFD_OKAY; + return *outPath ? NFD_OKAY : NFD_CANCEL; } else { return NFD_CANCEL; } @@ -548,13 +639,14 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) Widget_Guard widgetGuard(widget); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + if (res != NFD_OKAY) return res; if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { // write out the file name - *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + *outPath = GetSingleFileName(GTK_FILE_CHOOSER(widget)); - return NFD_OKAY; + return *outPath ? NFD_OKAY : NFD_CANCEL; } else { return NFD_CANCEL; }