Skip to content

Commit 49896fd

Browse files
committed
Add GTK4 support
1 parent 4d3d271 commit 49896fd

File tree

3 files changed

+165
-26
lines changed

3 files changed

+165
-26
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ set (CMAKE_CXX_STANDARD 17)
2626
add_subdirectory(src)
2727

2828
option(NFD_BUILD_TESTS "Build tests for nfd" OFF)
29-
if(${NFD_BUILD_TESTS})
29+
if(NFD_BUILD_TESTS)
3030
add_subdirectory(test)
3131
endif()

src/CMakeLists.txt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,27 @@ endif()
1212

1313
if(nfd_PLATFORM STREQUAL PLATFORM_LINUX)
1414
find_package(PkgConfig REQUIRED)
15-
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
16-
message("Using GTK version: ${GTK3_VERSION}")
15+
set(NFD_GTK_VERSION "" CACHE STRING "GTK version for Linux builds ('3' or '4')")
16+
set_property(CACHE NFD_GTK_VERSION PROPERTY STRINGS "" 3 4)
17+
# For Linux, we support both GTK3 and GTK4.
18+
# If NFD_GTK_VERSION is not explicitly set, then we take one that is available.
19+
# Otherwise, we find the version that the user wants.
20+
if(NFD_GTK_VERSION STREQUAL "")
21+
pkg_search_module(GTK REQUIRED gtk+-3.0 gtk4)
22+
if(DEFINED GTK_gtk+-3.0_VERSION)
23+
set(GTK_VERSION ${GTK_gtk+-3.0_VERSION})
24+
elseif(DEFINED GTK_gtk4_VERSION)
25+
set(GTK_VERSION ${GTK_gtk4_VERSION})
26+
endif()
27+
elseif(NFD_GTK_VERSION STREQUAL 3)
28+
pkg_check_modules(GTK REQUIRED gtk+-3.0)
29+
elseif(NFD_GTK_VERSION STREQUAL 4)
30+
pkg_check_modules(GTK REQUIRED gtk4)
31+
else()
32+
message(FATAL_ERROR "Unsupported GTK version: ${NFD_GTK_VERSION}")
33+
endif()
34+
35+
message("Using GTK version: ${GTK_VERSION}")
1736
list(APPEND SOURCE_FILES nfd_gtk.cpp)
1837
endif()
1938

@@ -32,9 +51,9 @@ target_include_directories(${TARGET_NAME}
3251

3352
if(nfd_PLATFORM STREQUAL PLATFORM_LINUX)
3453
target_include_directories(${TARGET_NAME}
35-
PRIVATE ${GTK3_INCLUDE_DIRS})
54+
PRIVATE ${GTK_INCLUDE_DIRS})
3655
target_link_libraries(${TARGET_NAME}
37-
PRIVATE ${GTK3_LIBRARIES})
56+
PRIVATE ${GTK_LIBRARIES})
3857
endif()
3958

4059
if(nfd_PLATFORM STREQUAL PLATFORM_MACOS)

src/nfd_gtk.cpp

Lines changed: 141 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
Authors: Bernard Teo, Michael Labbe
66
77
Note: We do not check for malloc failure on Linux - Linux overcommits memory!
8+
Note: The GTK4 implementation does not distinguish between local and network files,
9+
so it is possible for the file picker to return a network URI instead of a local filename.
810
*/
911

1012
#include <assert.h>
1113
#include <gtk/gtk.h>
1214
#if defined(GDK_WINDOWING_X11)
15+
#if GTK_MAJOR_VERSION == 3
1316
#include <gdk/gdkx.h>
17+
#elif GTK_MAJOR_VERSION == 4
18+
#include <gdk/x11/gdkx.h>
19+
#endif
1420
#endif
1521
#include <stddef.h>
1622
#include <stdio.h>
@@ -19,6 +25,8 @@
1925

2026
#include "nfd.h"
2127

28+
#define UNSUPPORTED_ERROR() static_assert(false, "Unsupported GTK version, this is an NFD bug.")
29+
2230
namespace {
2331

2432
template <typename T>
@@ -37,6 +45,14 @@ struct FreeCheck_Guard {
3745
}
3846
};
3947

48+
template <typename T>
49+
struct GUnref_Guard {
50+
T* data;
51+
GUnref_Guard(T* object) noexcept : data(object) {}
52+
~GUnref_Guard() { g_object_unref(data); }
53+
T* get() const noexcept { return data; }
54+
};
55+
4056
/* current error */
4157
const char* g_errorstr = nullptr;
4258

@@ -278,33 +294,107 @@ Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* choo
278294
return map;
279295
}
280296

281-
void SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) {
282-
if (!defaultPath || !*defaultPath) return;
297+
/*
298+
Note: GTK+ manual recommends not specifically setting the default path.
299+
We do it anyway in order to be consistent across platforms.
283300
284-
/* GTK+ manual recommends not specifically setting the default path.
285-
We do it anyway in order to be consistent across platforms.
301+
If consistency with the native OS is preferred,
302+
then this function should be made a no-op.
303+
*/
304+
#if GTK_MAJOR_VERSION == 3
305+
nfdresult_t SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) {
306+
if (!defaultPath || !*defaultPath) return NFD_OKAY;
286307

287-
If consistency with the native OS is preferred, this is the line
288-
to comment out. -ml */
289308
gtk_file_chooser_set_current_folder(chooser, defaultPath);
309+
return NFD_OKAY;
310+
}
311+
#elif GTK_MAJOR_VERSION == 4
312+
nfdresult_t SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) {
313+
if (!defaultPath || !*defaultPath) return NFD_OKAY;
314+
315+
GUnref_Guard<GFile> file(g_file_new_for_path(defaultPath));
316+
317+
if (!gtk_file_chooser_set_current_folder(chooser, file.get(), NULL)) {
318+
NFDi_SetError("Failed to set default path.");
319+
return NFD_ERROR;
320+
}
321+
return NFD_OKAY;
290322
}
323+
#endif
291324

292325
void SetDefaultName(GtkFileChooser* chooser, const char* defaultName) {
293326
if (!defaultName || !*defaultName) return;
294327

295328
gtk_file_chooser_set_current_name(chooser, defaultName);
296329
}
297330

331+
#if GTK_MAJOR_VERSION == 3
332+
nfdresult_t GetSingleFileNameForOpen(GtkWidget* widget, char** outPath) {
333+
char* tmp_outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
334+
if (tmp_outPath) {
335+
*outPath = tmp_outPath;
336+
return NFD_OKAY;
337+
}
338+
return NFD_ERROR;
339+
}
340+
nfdresult_t GetSingleFileNameForSave(GtkWidget* widget, char** outPath) {
341+
return GetSingleFilenameForOpen(widget, outPath);
342+
}
343+
#elif GTK_MAJOR_VERSION == 4
344+
nfdresult_t GetSingleFileNameForOpen(GtkWidget* widget, char** outPath) {
345+
GUnref_Guard<GFile> file(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(widget)));
346+
char* tmp_outPath = g_file_get_path(file.get());
347+
if (tmp_outPath) {
348+
*outPath = tmp_outPath;
349+
return NFD_OKAY;
350+
}
351+
// it's not a local file... we should copy it
352+
GFileIOStream* localFileIOStream;
353+
GFile* localFile = g_file_new_tmp(NULL, &localFileIOStream, NULL);
354+
if (!localFile) return NFD_ERROR;
355+
GUnref_Guard<GFile> localFileGuard(localFile);
356+
GUnref_Guard<GFileIOStream> localFileIOStreamGuard(localFileIOStream);
357+
g_io_stream_close(G_IO_STREAM(localFileIOStream), NULL, NULL);
358+
if (!g_file_copy(file.get(), localFile, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, NULL))
359+
return NFD_ERROR;
360+
*outPath = g_file_get_path(localFile);
361+
return NFD_OKAY;
362+
}
363+
nfdresult_t GetSingleFileNameForSave(GtkWidget* widget, char** outPath) {
364+
GUnref_Guard<GFile> file(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(widget)));
365+
char* tmp_outPath = g_file_get_path(file.get());
366+
if (tmp_outPath) {
367+
*outPath = tmp_outPath;
368+
return NFD_OKAY;
369+
}
370+
// it's not a local file... we balk and say the user cancelled the dialog
371+
return NFD_CANCEL;
372+
}
373+
#endif
374+
298375
void WaitForCleanup() {
376+
#if GTK_MAJOR_VERSION == 3
299377
while (gtk_events_pending()) gtk_main_iteration();
378+
#elif GTK_MAJOR_VERSION == 4
379+
while (g_main_context_iteration(NULL, FALSE))
380+
;
381+
#else
382+
UNSUPPORTED_ERROR();
383+
#endif
300384
}
301385

302386
struct Widget_Guard {
303387
GtkWidget* data;
304388
Widget_Guard(GtkWidget* widget) : data(widget) {}
305389
~Widget_Guard() {
306390
WaitForCleanup();
391+
#if GTK_MAJOR_VERSION == 3
307392
gtk_widget_destroy(data);
393+
#elif GTK_MAJOR_VERSION == 4
394+
gtk_window_destroy(GTK_WINDOW(data));
395+
#else
396+
UNSUPPORTED_ERROR();
397+
#endif
308398
WaitForCleanup();
309399
}
310400
};
@@ -362,12 +452,20 @@ void FileActivatedSignalHandler(GtkButton* saveButton, void* userdata) {
362452
g_free(currentFileName);
363453
}
364454

455+
#if GTK_MAJOR_VERSION == 4
456+
void DialogResponseHandler(GtkDialog*, gint resp, gpointer out_resp_gp) {
457+
gint* out_resp = static_cast<gint*>(out_resp_gp);
458+
*out_resp = resp;
459+
}
460+
#endif
461+
365462
// wrapper for gtk_dialog_run() that brings the dialog to the front
366463
// see issues at:
367464
// https://github.com/btzy/nativefiledialog-extended/issues/31
368465
// https://github.com/mlabbe/nativefiledialog/pull/92
369466
// https://github.com/guillaumechereau/noc/pull/11
370467
gint RunDialogWithFocus(GtkDialog* dialog) {
468+
#if GTK_MAJOR_VERSION == 3
371469
#if defined(GDK_WINDOWING_X11)
372470
gtk_widget_show_all(GTK_WIDGET(dialog)); // show the dialog so that it gets a display
373471
if (GDK_IS_X11_DISPLAY(gtk_widget_get_display(GTK_WIDGET(dialog)))) {
@@ -379,6 +477,20 @@ gint RunDialogWithFocus(GtkDialog* dialog) {
379477
}
380478
#endif
381479
return gtk_dialog_run(dialog);
480+
#elif GTK_MAJOR_VERSION == 4
481+
// TODO: the X11 popup issues
482+
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
483+
gtk_widget_show(GTK_WIDGET(dialog));
484+
gint resp = 0;
485+
g_signal_connect(G_OBJECT(dialog),
486+
"response",
487+
G_CALLBACK(DialogResponseHandler),
488+
static_cast<gpointer>(&resp));
489+
while (resp == 0) g_main_context_iteration(NULL, TRUE);
490+
return resp;
491+
#else
492+
UNSUPPORTED_ERROR();
493+
#endif
382494
}
383495

384496
} // namespace
@@ -395,7 +507,14 @@ void NFD_ClearError(void) {
395507

396508
nfdresult_t NFD_Init(void) {
397509
// Init GTK
398-
if (!gtk_init_check(NULL, NULL)) {
510+
#if GTK_MAJOR_VERSION == 3
511+
if (!gtk_init_check(NULL, NULL))
512+
#elif GTK_MAJOR_VERSION == 4
513+
if (!gtk_init_check())
514+
#else
515+
UNSUPPORTED_ERROR();
516+
#endif
517+
{
399518
NFDi_SetError("Failed to initialize GTK+ with gtk_init_check.");
400519
return NFD_ERROR;
401520
}
@@ -430,13 +549,12 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
430549
AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount);
431550

432551
/* Set the default path */
433-
SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
552+
nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
553+
if (res != NFD_OKAY) return res;
434554

435555
if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) {
436556
// write out the file name
437-
*outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
438-
439-
return NFD_OKAY;
557+
return GetSingleFileNameForOpen(widget, outPath);
440558
} else {
441559
return NFD_CANCEL;
442560
}
@@ -465,7 +583,8 @@ nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
465583
AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount);
466584

467585
/* Set the default path */
468-
SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
586+
nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
587+
if (res != NFD_OKAY) return res;
469588

470589
if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) {
471590
// write out the file name
@@ -495,8 +614,10 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
495614

496615
GtkWidget* saveButton = gtk_dialog_add_button(GTK_DIALOG(widget), "_Save", GTK_RESPONSE_ACCEPT);
497616

498-
// Prompt on overwrite
617+
// Prompt on overwrite (GTK3 only, because GTK4 automatically prompts)
618+
#if GTK_MAJOR_VERSION == 3
499619
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(widget), TRUE);
620+
#endif
500621

501622
/* Build the filter list */
502623
ButtonClickedArgs buttonClickedArgs;
@@ -505,7 +626,8 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
505626
AddFiltersToDialogWithMap(GTK_FILE_CHOOSER(widget), filterList, filterCount);
506627

507628
/* Set the default path */
508-
SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
629+
nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
630+
if (res != NFD_OKAY) return res;
509631

510632
/* Set the default file name */
511633
SetDefaultName(GTK_FILE_CHOOSER(widget), defaultName);
@@ -526,9 +648,7 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
526648

527649
if (result == GTK_RESPONSE_ACCEPT) {
528650
// write out the file name
529-
*outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
530-
531-
return NFD_OKAY;
651+
return GetSingleFileNameForSave(widget, outPath);
532652
} else {
533653
return NFD_CANCEL;
534654
}
@@ -548,13 +668,13 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath)
548668
Widget_Guard widgetGuard(widget);
549669

550670
/* Set the default path */
551-
SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
671+
nfdresult_t res = SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath);
672+
if (res != NFD_OKAY) return res;
552673

553674
if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) {
554675
// write out the file name
555-
*outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
556-
557-
return NFD_OKAY;
676+
// we don't support non-local files, so the behaviour is the same as the save dialog
677+
return GetSingleFileNameForSave(widget, outPath);
558678
} else {
559679
return NFD_CANCEL;
560680
}

0 commit comments

Comments
 (0)