btzy / nativefiledialog-extended

Cross platform (Windows, Mac, Linux) native file dialog library with C and C++ bindings, based on mlabbe/nativefiledialog.
zlib License
598 stars 89 forks source link
c c-plus-plus cpp cross-platform file-dialog folder-picker

Native File Dialog Extended

GitHub Actions

A small C library with that portably invokes native file open, folder select and file save dialogs. Write dialog code once and have it pop up native dialogs on all supported platforms. Avoid linking large dependencies like wxWidgets and Qt.

This library is based on Michael Labbe's Native File Dialog (mlabbe/nativefiledialog).

Features:

Comparison with original Native File Dialog:

The friendly names feature is the primary reason for breaking API compatibility with Michael Labbe's library (and hence this library probably will never be merged with it). There are also a number of tweaks that cause observable differences in this library.

Features added in Native File Dialog Extended:

There is also significant code refractoring, especially for the Windows implementation.

The wiki keeps track of known language bindings and known popular projects that depend on this library.

Basic Usage

#include <nfd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

    NFD_Init();

    nfdu8char_t *outPath;
    nfdu8filteritem_t filters[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } };
    nfdopendialogu8args_t args = {0};
    args.filterList = filters;
    args.filterCount = 2;
    nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args);
    if (result == NFD_OKAY)
    {
        puts("Success!");
        puts(outPath);
        NFD_FreePathU8(outPath);
    }
    else if (result == NFD_CANCEL)
    {
        puts("User pressed cancel.");
    }
    else 
    {
        printf("Error: %s\n", NFD_GetError());
    }

    NFD_Quit();
    return 0;
}

The U8/u8 in NFDe refer to the API for UTF-8 characters (char), which most consumers probably want. An N/n version is also available, which uses the native character type (wchar_t on Windows and char on other platforms).

For the full list of arguments that you can set on the args struct, see the "All Options" section below.

If you are using a platform abstraction framework such as SDL or GLFW, also see the "Usage with a Platform Abstraction Framework" section below.

Screenshots

Windows 10 Windows 10 macOS 10.13 macOS 10.13 GTK3 on Ubuntu 20.04 GTK3 on Ubuntu 20.04

Building

CMake Projects

If your project uses CMake, simply add the following lines to your CMakeLists.txt:

add_subdirectory(path/to/nativefiledialog-extended)
target_link_libraries(MyProgram PRIVATE nfd)

Make sure that you also have the needed dependencies.

When included as a subproject, sample programs are not built and the install target is disabled by default. Add -DNFD_BUILD_TESTS=ON to build sample programs and -DNFD_INSTALL=ON to enable the install target.

Standalone Library

If you want to build the standalone static library, execute the following commands (starting from the project root directory):

For GCC and Clang:

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .

For MSVC:

mkdir build
cd build
cmake ..
cmake --build . --config Release

The above commands will make a build directory, and build the project (in release mode) there. If you are developing NFDe, you may want to do -DCMAKE_BUILD_TYPE=Debug/--config Debug to build a debug version of the library instead.

When building as a standalone library, sample programs are built and the install target is enabled by default. Add -DNFD_BUILD_TESTS=OFF to disable building sample programs and -DNFD_INSTALL=OFF to disable the install target.

On Linux, if you want to use the Flatpak desktop portal instead of GTK, add -DNFD_PORTAL=ON. (Otherwise, GTK will be used.) See the "Usage" section below for more information.

See the CI build file for some example build commands.

Visual Studio on Windows

Recent versions of Visual Studio have CMake support built into the IDE. You should be able to "Open Folder" in the project root directory, and Visual Studio will recognize and configure the project appropriately. From there, you will be able to set configurations for Debug vs Release, and for x86 vs x64. For more information, see the Microsoft Docs page. This has been tested to work on Visual Studio 2019, and it probably works on Visual Studio 2017 too.

Compiling Your Programs

  1. Add src/include to your include search path.
  2. Add nfd.lib or nfd_d.lib to the list of static libraries to link against (for release or debug, respectively).
  3. Add build/<debug|release>/<arch> to the library search path.

Dependencies

Linux

GTK (default)

Make sure libgtk-3-dev is installed on your system.

Portal

Make sure libdbus-1-dev is installed on your system.

macOS

On macOS, add AppKit and UniformTypeIdentifiers to the list of frameworks.

Windows

On Windows (both MSVC and MinGW), ensure you are building against ole32.lib, uuid.lib, and shell32.lib.

Usage

All Options

To open a dialog, you set options on a struct and then pass that struct to an NFDe function, e.g.:

nfdopendialogu8args_t args = {0};
args.filterList = filters;
args.filterCount = 2;
nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args);

All options are optional and may be set individually (zero initialization sets all options to reasonable defaults), except for filterList and filterCount which must be either both set or both left unset.

Future versions of NFDe may add additional options to the end of the arguments struct without bumping the major version number, so to ensure backward API compatibility, you should not assume that the struct has a specific length or number of fields. You may assume that zero-initialization of the struct will continue to set all options to reasonable defaults, so assigning {0} to the struct is acceptable. For those building shared libraries of NFDe, backward ABI compatibility is ensured by an internal version index (NFD_INTERFACE_VERSION), which is expected to be transparent to consumers.

OpenDialog/OpenDialogMultiple:

typedef struct {
    const nfdu8filteritem_t* filterList;
    nfdfiltersize_t filterCount;
    const nfdu8char_t* defaultPath;
    nfdwindowhandle_t parentWindow;
} nfdopendialogu8args_t;

SaveDialog:

typedef struct {
    const nfdu8filteritem_t* filterList;
    nfdfiltersize_t filterCount;
    const nfdu8char_t* defaultPath;
    const nfdu8char_t* defaultName;
    nfdwindowhandle_t parentWindow;
} nfdsavedialogu8args_t;

PickFolder/PickFolderMultiple:

typedef struct {
    const nfdu8char_t* defaultPath;
    nfdwindowhandle_t parentWindow;
} nfdpickfolderu8args_t;

Examples

See the test directory for example code (both C and C++).

If you turned on the option to build the test directory (-DNFD_BUILD_TESTS=ON), then build/bin will contain the compiled test programs.

There is also an SDL2 example, which needs to be enabled separately with -DNFD_BUILD_SDL2_TESTS=ON. It requires SDL2 to be installed on your machine.

Compiled examples (including the SDL2 example) are also uploaded as artefacts to GitHub Actions, and may be downloaded from there.

File Filter Syntax

Files can be filtered by file extension groups:

nfdu8filteritem_t filters[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } };

A file filter is a pair of strings comprising the friendly name and the specification (multiple file extensions are comma-separated).

A list of file filters can be passed as an argument when invoking the library.

A wildcard filter is always added to every dialog.

Note: On macOS, the file dialogs do not have friendly names and there is no way to switch between filters, so the filter specifications are combined (e.g. "c,cpp,cc,h,hpp"). The filter specification is also never explicitly shown to the user. This is usual macOS behaviour and users expect it.

Note 2: You must ensure that the specification string is non-empty and that every file extension has at least one character. Otherwise, bad things might ensue (i.e. undefined behaviour).

Note 3: On Linux, the file extension is appended (if missing) when the user presses down the "Save" button. The appended file extension will remain visible to the user, even if an overwrite prompt is shown and the user then presses "Cancel".

Note 4: On Windows, the default folder parameter is only used if there is no recently used folder available. Otherwise, the default folder will be the folder that was last used. Internally, the Windows implementation calls IFileDialog::SetDefaultFolder(IShellItem). This is usual Windows behaviour and users expect it.

Iterating Over PathSets

A file open dialog that supports multiple selection produces a PathSet, which is a thin abstraction over the platform-specific collection. There are two ways to iterate over a PathSet:

Accessing by index

This method does array-like access on the PathSet, and is the easiest to use. However, on certain platforms (Linux, and possibly Windows), it takes O(N2) time in total to iterate the entire PathSet, because the underlying platform-specific implementation uses a linked list.

See test_opendialogmultiple.c.

Using an enumerator (experimental)

This method uses an enumerator object to iterate the paths in the PathSet. It is guaranteed to take O(N) time in total to iterate the entire PathSet.

See test_opendialogmultiple_enum.c.

This API is experimental, and subject to change.

Customization Macros

You can define the following macros before including nfd.h/nfd.hpp:

Macros that might be defined by nfd.h:

Usage with a Platform Abstraction Framework

NFDe is known to work with SDL2 and GLFW, and should also work with other platform abstraction framworks. This section explains how to use NFDe properly with such frameworks.

Parent window handle

The parentWindow argument allows the user to give the dialog a parent.

If using SDL2, include <nfd_sdl2.h> and call the following function to set the parent window handle:

NFD_GetNativeWindowFromSDLWindow(sdlWindow /* SDL_Window* */, &args.parentWindow);

If using GLFW3, define the appropriate GLFW_EXPOSE_NATIVE_* macros described on the GLFW native access page, and then include <nfd_glfw3.h> and call the following function to set the parent window handle:

NFD_GetNativeWindowFromGLFWWindow(glfwWindow /* GLFWwindow* */, &args.parentWindow);

If you are using another platform abstraction framework, or not using any such framework, you can set args.parentWindow manually.

Win32 (Windows), Cocoa (macOS), and X11 (Linux) windows are supported. Passing a Wayland (Linux) window currently does nothing (i.e. the dialog acts as if it has no parent), but support is likely to be added in the future.

Why pass a parent window handle?

To make a window (in this case the file dialog) stay above another window, we need to declare the bottom window as the parent of the top window. This keeps the dialog window from disappearing behind the parent window if the user clicks on the parent window while the dialog is open. Keeping the dialog above the window that invoked it is the expected behaviour on all supported operating systems, and so passing the parent window handle is recommended if possible.

Initialization order

You should initialize NFDe after initializing the framework, and probably should deinitialize NFDe before deinitializing the framework. This is because some frameworks expect to be initialized on a "clean slate", and they may configure the system in a different way from NFDe. NFD_Init is generally very careful not to disrupt the existing configuration unless necessary, and NFD_Quit restores the configuration back exactly to what it was before initialization.

An example with SDL2:

// Initialize SDL2 first
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0) {
    // display some error here
}

// Then initialize NFDe
if (NFD_Init() != NFD_OKAY) {
    // display some error here
}

/*
Your main program goes here
*/

NFD_Quit(); // deinitialize NFDe first

SDL_Quit(); // Then deinitialize SDL2

Using xdg-desktop-portal on Linux

On Linux, you can use the portal implementation instead of GTK, which will open the "native" file chooser selected by the OS or customized by the user. The user must have xdg-desktop-portal and a suitable backend installed (this comes pre-installed with most common desktop distros), otherwise NFD_ERROR will be returned.

To use the portal implementation, add -DNFD_PORTAL=ON to the build command.

Note: Setting a default path is not supported by the portal implementation, and any default path passed to NFDe will be ignored. This is a limitation of the portal API, so there is no way NFDe can work around it. If this feature is something you desire, please show your interest on https://github.com/flatpak/xdg-desktop-portal/pull/874.

*Note 2: The folder picker is only supported on org.freedesktop.portal.FileChooser interface version >= 3, which corresponds to xdg-desktop-portal version >= 1.7.1. NFD_PickFolder() will query the interface version at runtime, and return NFD_ERROR if the version is too low.

What is a portal?

Unlike Windows and macOS, Linux does not have a file chooser baked into the operating system. Linux applications that want a file chooser usually link with a library that provides one (such as GTK, as in the Linux screenshot above). This is a mostly acceptable solution that many applications use, but may make the file chooser look foreign on non-GTK distros.

Flatpak was introduced in 2015, and with it came a standardized interface to open a file chooser. Applications using this interface did not need to come with a file chooser, and could use the one provided by Flatpak. This interface became known as the desktop portal, and its use expanded to non-Flatpak applications. Now, most major desktop Linux distros come with the desktop portal installed, with file choosers that fit the theme of the distro. Users can also install a different portal backend if desired. There are currently two known backends: GTK and KDE. (XFCE does not currently seem to have a portal backend.)

Platform-specific Quirks

macOS

Known Limitations

Reporting Bugs

Please use the GitHub issue tracker to report bugs or to contribute to this repository. Feel free to submit bug reports of any kind.

Credit

Bernard Teo (me) and other contributors for everything that wasn't from Michael Labbe's Native File Dialog.

Michael Labbe for his awesome Native File Dialog library, and the other contributors to that library.

Much of this README has also been copied from the README of original Native File Dialog repository.

License

Everything in this repository is distributed under the ZLib license, as is the original Native File Dialog library.

Support

I don't provide any paid support. Michael Labbe appears to provide paid support for his library at the time of writing.