Merge commit '5461f6700dac79e9e71e3966f8a1270706c385ba'

This commit is contained in:
Gary Talent 2024-05-31 19:36:34 -05:00
commit 6ddb6b42ed
7 changed files with 567 additions and 198 deletions

View File

@ -16,21 +16,43 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Format code - name: Format code
run: find src/ test/ -iname '*.c' -or -iname '*.cpp' -or -iname '*.m' -or -iname '*.mm' -or -iname '*.h' -or -iname '*.hpp' | xargs clang-format -i -style=file # ClangFormat 14 has a bug, which seems to be fixed in ClangFormat 15. Until GitHub-hosted runners support ClangFormat 15, we will stay at ClangFormat 13.
run: find src/ test/ -iname '*.c' -or -iname '*.cpp' -or -iname '*.m' -or -iname '*.mm' -or -iname '*.h' -or -iname '*.hpp' | xargs clang-format-13 -i -style=file
- name: Check diff - name: Check diff
run: git diff --exit-code run: git diff --exit-code
build-ubuntu: build-ubuntu:
name: Ubuntu (${{ matrix.os }}, ${{ matrix.portal.name }}, ${{ matrix.compiler.c }}, C++${{ matrix.cppstd }}) name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os.label }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, ubuntu-18.04] os: [ {label: ubuntu-latest, name: latest}, {label: ubuntu-20.04, name: 20.04} ]
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) 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)
compiler: [ {c: gcc, cpp: g++}, {c: clang, cpp: clang++} ] # The default compiler is gcc/g++ autoappend: [ {flag: OFF, name: NoAppendExtn} ] # By default the NFD_PORTAL mode does not append extensions, because it breaks some features of the portal
cppstd: [23, 11] compiler: [ {c: gcc, cpp: g++, name: GCC}, {c: clang, cpp: clang++, name: Clang} ] # The default compiler is gcc/g++
cppstd: [20, 11]
shared_lib: [ {flag: OFF, name: Static} ]
include:
- os: {label: ubuntu-latest, name: latest}
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
autoappend: {flag: ON, name: AutoAppendExtn}
compiler: {c: gcc, cpp: g++, name: GCC}
cppstd: 11
shared_lib: {flag: OFF, name: Static}
- os: {label: ubuntu-latest, name: latest}
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
autoappend: {flag: ON, name: AutoAppendExtn}
compiler: {c: clang, cpp: clang++, name: Clang}
cppstd: 11
shared_lib: {flag: OFF, name: Static}
- os: {label: ubuntu-latest, name: latest}
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
autoappend: {flag: ON, name: NoAppendExtn}
compiler: {c: gcc, cpp: g++, name: GCC}
cppstd: 11
shared_lib: {flag: ON, name: Shared}
steps: steps:
- name: Checkout - name: Checkout
@ -38,60 +60,72 @@ jobs:
- name: Installing Dependencies - name: Installing Dependencies
run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }}
- name: Configure - 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_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_BUILD_TESTS=ON .. 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 ..
- name: Build - name: Build
run: cmake --build build --target install run: cmake --build build --target install
- name: Upload test binaries - name: Upload test binaries
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: $${{ matrix.os }} - $${{ matrix.compiler.c }} name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }}
path: | path: |
build/src/libnfd.a build/src/*
build/test/test_* build/test/*
build-macos-clang: build-macos-clang:
name: MacOS latest - Clang name: MacOS ${{ matrix.os.name }} - Clang, ${{ matrix.shared_lib.name }}
runs-on: macos-latest runs-on: ${{ matrix.os.label }}
strategy:
matrix:
os: [ {label: macos-latest, name: latest}, {label: macos-11, name: 11} ]
shared_lib: [ {flag: OFF, name: Static} ]
include:
- os: {label: macos-latest, name: latest}
shared_lib: {flag: ON, name: Shared}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Configure - name: Configure
run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=ON .. 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" -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON ..
- name: Build - name: Build
run: cmake --build build --target install run: cmake --build build --target install
- name: Upload test binaries - name: Upload test binaries
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: MacOS latest - Clang name: MacOS ${{ matrix.os.name }} - Clang, ${{ matrix.shared_lib.name }}
path: | path: |
build/src/libnfd.a build/src/*
build/test/test_* build/test/*
build-windows-msvc: build-windows-msvc:
name: Windows latest - MSVC name: Windows latest - MSVC, ${{ matrix.shared_lib.name }}
runs-on: windows-latest runs-on: windows-latest
strategy:
matrix:
shared_lib: [ {flag: OFF, name: Static}, {flag: ON, name: Shared} ]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Configure - name: Configure
run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DNFD_BUILD_TESTS=ON .. run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON ..
- name: Build - name: Build
run: cmake --build build --target install --config Release run: cmake --build build --target install --config Release
- name: Upload test binaries - name: Upload test binaries
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: Windows latest - MSVC name: Windows latest - MSVC, ${{ matrix.shared_lib.name }}
path: | path: |
build/src/Release/nfd.lib build/src/Release/*
build/test/Release/test_* build/test/Release/*
build-windows-clang: build-windows-clang:
name: Windows latest - Clang name: Windows latest - Clang, Static
runs-on: windows-latest runs-on: windows-latest
steps: steps:
@ -104,14 +138,14 @@ jobs:
- name: Upload test binaries - name: Upload test binaries
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: Windows latest - Clang name: Windows latest - Clang, Static
path: | path: |
build/src/Release/nfd.lib build/src/Release/*
build/test/Release/test_* build/test/Release/*
build-windows-mingw: build-windows-mingw:
name: Windows latest - MinGW name: Windows latest - MinGW, Static
runs-on: windows-latest runs-on: windows-latest
defaults: defaults:
@ -130,13 +164,13 @@ jobs:
mingw-w64-x86_64-gcc mingw-w64-x86_64-gcc
mingw-w64-x86_64-cmake mingw-w64-x86_64-cmake
- name: Configure - name: Configure
run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=ON .. run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=ON ..
- name: Build - name: Build
run: cmake --build build --target install run: cmake --build build --target install
- name: Upload test binaries - name: Upload test binaries
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: Windows latest - MinGW name: Windows latest - MinGW, Static
path: | path: |
build/src/libnfd.a build/src/*
build/test/test_* build/test/*

View File

@ -1,5 +1,14 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.5)
project(nativefiledialog-extended) project(nativefiledialog-extended VERSION 1.1.1)
set(nfd_ROOT_PROJECT OFF)
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
set(nfd_ROOT_PROJECT ON)
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_INSTALL "Generate install target for nfd" ${nfd_ROOT_PROJECT})
if(NOT MSVC) if(NOT MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w")
@ -30,12 +39,19 @@ message("nfd Compiler: ${nfd_COMPILER}")
# Use latest C++ by default (should be the best one), but let user override it # Use latest C++ by default (should be the best one), but let user override it
if(NOT DEFINED CMAKE_CXX_STANDARD) if(NOT DEFINED CMAKE_CXX_STANDARD)
set (CMAKE_CXX_STANDARD 23) if(CMAKE_VERSION VERSION_LESS "3.8")
set (CMAKE_CXX_STANDARD 14)
elseif(CMAKE_VERSION VERSION_LESS "3.12")
set (CMAKE_CXX_STANDARD 17)
elseif(CMAKE_VERSION VERSION_LESS "3.20")
set (CMAKE_CXX_STANDARD 20)
else()
set (CMAKE_CXX_STANDARD 23)
endif()
endif() endif()
add_subdirectory(src) add_subdirectory(src)
option(NFD_BUILD_TESTS "Build tests for nfd" OFF)
if(${NFD_BUILD_TESTS}) if(${NFD_BUILD_TESTS})
add_subdirectory(test) add_subdirectory(test)
endif() endif()

34
deps/nfde/README.md vendored
View File

@ -43,6 +43,8 @@ Features added in Native File Dialog Extended:
There is also significant code refractoring, especially for the Windows implementation. There is also significant code refractoring, especially for the Windows implementation.
The [wiki](https://github.com/btzy/nativefiledialog-extended/wiki) keeps track of known language bindings and known popular projects that depend on this library.
# Basic Usage # Basic Usage
```C ```C
@ -84,9 +86,12 @@ If you are using a platform abstraction framework such as SDL or GLFW, also see
# Screenshots # # Screenshots #
![Windows 10](screens/open_win10.png?raw=true) ![Windows 10](screens/open_win10.png?raw=true#gh-light-mode-only)
![MacOS 10.13](screens/open_macos_11.0.png?raw=true) ![Windows 10](screens/open_win10_dark.png?raw=true#gh-dark-mode-only)
![GTK3 on Ubuntu 20.04](screens/open_gtk3.png?raw=true) ![MacOS 10.13](screens/open_macos_11.0.png?raw=true#gh-light-mode-only)
![MacOS 10.13](screens/open_macos_11.0_dark.png?raw=true#gh-dark-mode-only)
![GTK3 on Ubuntu 20.04](screens/open_gtk3.png?raw=true#gh-light-mode-only)
![GTK3 on Ubuntu 20.04](screens/open_gtk3_dark.png?raw=true#gh-dark-mode-only)
# Building # Building
@ -99,6 +104,9 @@ target_link_libraries(MyProgram PRIVATE nfd)
``` ```
Make sure that you also have the needed [dependencies](#dependencies). Make sure that you also have the needed [dependencies](#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 ## Standalone Library
If you want to build the standalone static library, If you want to build the standalone static library,
execute the following commands (starting from the project root directory): execute the following commands (starting from the project root directory):
@ -114,8 +122,8 @@ and build the project (in release mode) there.
If you are developing NFDe, you may want to do `-DCMAKE_BUILD_TYPE=Debug` If you are developing NFDe, you may want to do `-DCMAKE_BUILD_TYPE=Debug`
to build a debug version of the library instead. to build a debug version of the library instead.
If you want to build the sample programs, When building as a standalone library, sample programs are built and the install target is enabled by default.
add `-DNFD_BUILD_TESTS=ON` (sample programs are not built 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. 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.
@ -148,10 +156,10 @@ Make sure `libgtk-3-dev` is installed on your system.
Make sure `libdbus-1-dev` is installed on your system. Make sure `libdbus-1-dev` is installed on your system.
### MacOS ### MacOS
On MacOS, add `AppKit` to the list of frameworks. On MacOS, add `AppKit` and `UniformTypeIdentifiers` to the list of frameworks.
### Windows ### Windows
On Windows (both MSVC and MinGW), ensure you are building against `ole32.lib` and `uuid.lib`. On Windows (both MSVC and MinGW), ensure you are building against `ole32.lib`, `uuid.lib`, and `shell32.lib`.
# Usage # Usage
@ -244,11 +252,11 @@ SDL_Quit(); // Then deinitialize SDL2
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. 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.
The portal implementation is much less battle-tested than the GTK implementation. There may be bugs — please report them on the issue tracker.
To use the portal implementation, add `-DNFD_PORTAL=ON` to the build command. 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.* *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? ### What is a portal?
@ -256,6 +264,12 @@ Unlike Windows and MacOS, Linux does not have a file chooser baked into the oper
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.) 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
- If the MacOS deployment target is ≥ 11.0, the [allowedContentTypes](https://developer.apple.com/documentation/appkit/nssavepanel/3566857-allowedcontenttypes?language=objc) property of NSSavePanel is used instead of the deprecated [allowedFileTypes](https://developer.apple.com/documentation/appkit/nssavepanel/1534419-allowedfiletypes?language=objc) property for file filters. Thus, if you are filtering by a custom file extension specific to your application, you will need to define the data type in your `Info.plist` file as per the [Apple documentation](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app). (It is possible to force NFDe to use allowedFileTypes by adding `-DNFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE=OFF` to your CMake build command, but this is not recommended. If you need to support older MacOS versions, you should be setting the correct deployment target instead.)
# Known Limitations # # Known Limitations #
- No support for Windows XP's legacy dialogs such as `GetOpenFileName`. (There are no plans to support this; you shouldn't be still using Windows XP anyway.) - No support for Windows XP's legacy dialogs such as `GetOpenFileName`. (There are no plans to support this; you shouldn't be still using Windows XP anyway.)

View File

@ -25,17 +25,49 @@ if(nfd_PLATFORM STREQUAL PLATFORM_UNIX)
endif() endif()
if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) if(nfd_PLATFORM STREQUAL PLATFORM_MACOS)
# For setting the filter list, macOS introduced allowedContentTypes in version 11.0 and deprecated allowedFileTypes in 12.0.
# By default (set to ON), NFDe will use allowedContentTypes when targeting macOS >= 11.0.
# Set this option to OFF to always use allowedFileTypes regardless of the target macOS version.
# This is mainly needed for applications that are built on macOS >= 11.0 but should be able to run on lower versions
# and should not be used otherwise.
option(NFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE "Use allowedContentTypes for filter lists on macOS >= 11.0" ON)
find_library(APPKIT_LIBRARY AppKit) find_library(APPKIT_LIBRARY AppKit)
if(NFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles(
"
#include <Availability.h>
#if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || !defined(__MAC_11_0) || __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0
static_assert(false);
#endif
int main() { return 0; }
"
NFD_USE_ALLOWEDCONTENTTYPES
)
if(NFD_USE_ALLOWEDCONTENTTYPES)
find_library(UNIFORMTYPEIDENTIFIERS_LIBRARY UniformTypeIdentifiers)
if(NOT UNIFORMTYPEIDENTIFIERS_LIBRARY)
message(FATAL_ERROR "UniformTypeIdentifiers framework is not available even though we are targeting macOS >= 11.0")
endif()
endif()
endif()
list(APPEND SOURCE_FILES nfd_cocoa.m) list(APPEND SOURCE_FILES nfd_cocoa.m)
endif() endif()
# Define the library # Define the library
add_library(${TARGET_NAME} add_library(${TARGET_NAME} ${SOURCE_FILES})
${SOURCE_FILES})
if (BUILD_SHARED_LIBS)
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_EXPORT INTERFACE NFD_SHARED)
endif ()
# Allow includes from include/ # Allow includes from include/
target_include_directories(${TARGET_NAME} target_include_directories(${TARGET_NAME}
PUBLIC include/) PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
if(nfd_PLATFORM STREQUAL PLATFORM_UNIX) if(nfd_PLATFORM STREQUAL PLATFORM_UNIX)
if(NOT NFD_PORTAL) if(NOT NFD_PORTAL)
@ -45,15 +77,25 @@ if(nfd_PLATFORM STREQUAL PLATFORM_UNIX)
target_include_directories(${TARGET_NAME} target_include_directories(${TARGET_NAME}
PRIVATE ${DBUS_INCLUDE_DIRS}) PRIVATE ${DBUS_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} target_link_libraries(${TARGET_NAME}
PRIVATE ${DBUS_LIBRARIES}) PRIVATE ${DBUS_LINK_LIBRARIES})
target_compile_definitions(${TARGET_NAME} target_compile_definitions(${TARGET_NAME}
PUBLIC NFD_PORTAL) PUBLIC NFD_PORTAL)
endif() endif()
option(NFD_APPEND_EXTENSION "Automatically append file extension to an extensionless selection in SaveDialog()" OFF)
if(NFD_APPEND_EXTENSION)
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_APPEND_EXTENSION)
endif()
endif() endif()
if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) if(nfd_PLATFORM STREQUAL PLATFORM_MACOS)
target_link_libraries(${TARGET_NAME} if(NFD_USE_ALLOWEDCONTENTTYPES)
PRIVATE ${APPKIT_LIBRARY}) target_link_libraries(${TARGET_NAME} PRIVATE ${APPKIT_LIBRARY} ${UNIFORMTYPEIDENTIFIERS_LIBRARY})
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_MACOS_ALLOWEDCONTENTTYPES=1)
else()
target_link_libraries(${TARGET_NAME} PRIVATE ${APPKIT_LIBRARY})
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_MACOS_ALLOWEDCONTENTTYPES=0)
endif()
endif() endif()
if(nfd_COMPILER STREQUAL COMPILER_MSVC) if(nfd_COMPILER STREQUAL COMPILER_MSVC)
@ -71,6 +113,20 @@ if(nfd_COMPILER STREQUAL COMPILER_GNU)
target_compile_options(${TARGET_NAME} PRIVATE -nostdlib -fno-exceptions -fno-rtti) target_compile_options(${TARGET_NAME} PRIVATE -nostdlib -fno-exceptions -fno-rtti)
endif() endif()
set_target_properties(${TARGET_NAME} PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADER_FILES}") set_target_properties(${TARGET_NAME} PROPERTIES
PUBLIC_HEADER "${PUBLIC_HEADER_FILES}"
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR})
install(TARGETS ${TARGET_NAME} LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include) if (NFD_INSTALL)
include(GNUInstallDirs)
install(TARGETS ${TARGET_NAME} EXPORT ${TARGET_NAME}-export
LIBRARY DESTINATION ${LIB_INSTALL_DIR} ARCHIVE DESTINATION ${LIB_INSTALL_DIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT ${TARGET_NAME}-export
DESTINATION lib/cmake/${TARGET_NAME}
NAMESPACE ${TARGET_NAME}::
FILE ${TARGET_NAME}-config.cmake
)
endif()

View File

@ -10,6 +10,23 @@
#ifndef _NFD_H #ifndef _NFD_H
#define _NFD_H #define _NFD_H
#if defined(_WIN32)
#if defined(NFD_EXPORT)
#define NFD_API __declspec(dllexport)
#elif defined(NFD_SHARED)
#define NFD_API __declspec(dllimport)
#endif
#else
#if defined(NFD_EXPORT) || defined(NFD_SHARED)
#if defined(__GNUC__) || defined(__clang__)
#define NFD_API __attribute__((visibility("default")))
#endif
#endif
#endif
#ifndef NFD_API
#define NFD_API
#endif
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif // __cplusplus #endif // __cplusplus
@ -64,60 +81,60 @@ typedef struct {
/* free a file path that was returned by the dialogs */ /* free a file path that was returned by the dialogs */
/* Note: use NFD_PathSet_FreePath to free path from pathset instead of this function */ /* Note: use NFD_PathSet_FreePath to free path from pathset instead of this function */
void NFD_FreePathN(nfdnchar_t* filePath); NFD_API void NFD_FreePathN(nfdnchar_t* filePath);
/* initialize NFD - call this for every thread that might use NFD, before calling any other NFD /* initialize NFD - call this for every thread that might use NFD, before calling any other NFD
* functions on that thread */ * functions on that thread */
nfdresult_t NFD_Init(void); NFD_API nfdresult_t NFD_Init(void);
/* call this to de-initialize NFD, if NFD_Init returned NFD_OKAY */ /* call this to de-initialize NFD, if NFD_Init returned NFD_OKAY */
void NFD_Quit(void); NFD_API void NFD_Quit(void);
/* single file open dialog */ /* single file open dialog */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns
* NFD_OKAY */ * NFD_OKAY */
/* If filterCount is zero, filterList is ignored (you can use NULL) */ /* If filterCount is zero, filterList is ignored (you can use NULL) */
/* If defaultPath is NULL, the operating system will decide */ /* If defaultPath is NULL, the operating system will decide */
nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, NFD_API nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount, nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath); const nfdnchar_t* defaultPath);
/* multiple file open dialog */ /* multiple file open dialog */
/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function /* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function
* returns NFD_OKAY */ * returns NFD_OKAY */
/* If filterCount is zero, filterList is ignored (you can use NULL) */ /* If filterCount is zero, filterList is ignored (you can use NULL) */
/* If defaultPath is NULL, the operating system will decide */ /* If defaultPath is NULL, the operating system will decide */
nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, NFD_API nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount, nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath); const nfdnchar_t* defaultPath);
/* save dialog */ /* save dialog */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns
* NFD_OKAY */ * NFD_OKAY */
/* If filterCount is zero, filterList is ignored (you can use NULL) */ /* If filterCount is zero, filterList is ignored (you can use NULL) */
/* If defaultPath is NULL, the operating system will decide */ /* If defaultPath is NULL, the operating system will decide */
nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, NFD_API nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount, nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath, const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName); const nfdnchar_t* defaultName);
/* select folder dialog */ /* select folder dialog */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns
* NFD_OKAY */ * NFD_OKAY */
/* If defaultPath is NULL, the operating system will decide */ /* If defaultPath is NULL, the operating system will decide */
nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath); NFD_API nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath);
/* Get last error -- set when nfdresult_t returns NFD_ERROR */ /* Get last error -- set when nfdresult_t returns NFD_ERROR */
/* Returns the last error that was set, or NULL if there is no error. */ /* Returns the last error that was set, or NULL if there is no error. */
/* The memory is owned by NFD and should not be freed by user code. */ /* The memory is owned by NFD and should not be freed by user code. */
/* This is *always* ASCII printable characters, so it can be interpreted as UTF-8 without any /* This is *always* ASCII printable characters, so it can be interpreted as UTF-8 without any
* conversion. */ * conversion. */
const char* NFD_GetError(void); NFD_API const char* NFD_GetError(void);
/* clear the error */ /* clear the error */
void NFD_ClearError(void); NFD_API void NFD_ClearError(void);
/* path set operations */ /* path set operations */
#ifdef _WIN32 #ifdef _WIN32
@ -131,36 +148,37 @@ typedef unsigned int nfdpathsetsize_t;
/* Gets the number of entries stored in pathSet */ /* Gets the number of entries stored in pathSet */
/* note that some paths might be invalid (NFD_ERROR will be returned by NFD_PathSet_GetPath), so we /* note that some paths might be invalid (NFD_ERROR will be returned by NFD_PathSet_GetPath), so we
* might not actually have this number of usable paths */ * might not actually have this number of usable paths */
nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count); NFD_API nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count);
/* Gets the UTF-8 path at offset index */ /* Gets the UTF-8 path at offset index */
/* It is the caller's responsibility to free `outPath` via NFD_PathSet_FreePathN() if this function /* It is the caller's responsibility to free `outPath` via NFD_PathSet_FreePathN() if this function
* returns NFD_OKAY */ * returns NFD_OKAY */
nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, NFD_API nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet,
nfdpathsetsize_t index, nfdpathsetsize_t index,
nfdnchar_t** outPath); nfdnchar_t** outPath);
/* Free the path gotten by NFD_PathSet_GetPathN */ /* Free the path gotten by NFD_PathSet_GetPathN */
#ifdef _WIN32 #ifdef _WIN32
#define NFD_PathSet_FreePathN NFD_FreePathN #define NFD_PathSet_FreePathN NFD_FreePathN
#elif __APPLE__ #elif __APPLE__
#define NFD_PathSet_FreePathN NFD_FreePathN #define NFD_PathSet_FreePathN NFD_FreePathN
#else #else
void NFD_PathSet_FreePathN(const nfdnchar_t* filePath); NFD_API void NFD_PathSet_FreePathN(const nfdnchar_t* filePath);
#endif // _WIN32, __APPLE__ #endif // _WIN32, __APPLE__
/* Gets an enumerator of the path set. */ /* Gets an enumerator of the path set. */
/* It is the caller's responsibility to free `enumerator` via NFD_PathSet_FreeEnum() if this /* It is the caller's responsibility to free `enumerator` via NFD_PathSet_FreeEnum() if this
* function returns NFD_OKAY, and it should be freed before freeing the pathset. */ * function returns NFD_OKAY, and it should be freed before freeing the pathset. */
nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator); NFD_API nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet,
nfdpathsetenum_t* outEnumerator);
/* Frees an enumerator of the path set. */ /* Frees an enumerator of the path set. */
void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator); NFD_API void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator);
/* Gets the next item from the path set enumerator. /* Gets the next item from the path set enumerator.
* If there are no more items, then *outPaths will be set to NULL. */ * If there are no more items, then *outPaths will be set to NULL. */
/* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePath() if this /* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePath() if this
* function returns NFD_OKAY and `*outPath` is not null */ * function returns NFD_OKAY and `*outPath` is not null */
nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath); NFD_API nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath);
/* Free the pathSet */ /* Free the pathSet */
void NFD_PathSet_Free(const nfdpathset_t* pathSet); NFD_API void NFD_PathSet_Free(const nfdpathset_t* pathSet);
#ifdef _WIN32 #ifdef _WIN32
@ -177,50 +195,50 @@ typedef struct {
/* UTF-8 compatibility functions */ /* UTF-8 compatibility functions */
/* free a file path that was returned */ /* free a file path that was returned */
void NFD_FreePathU8(nfdu8char_t* outPath); NFD_API void NFD_FreePathU8(nfdu8char_t* outPath);
/* single file open dialog */ /* single file open dialog */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns
* NFD_OKAY */ * NFD_OKAY */
nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, NFD_API nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t count,
const nfdu8char_t* defaultPath);
/* multiple file open dialog */
/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function
* returns NFD_OKAY */
nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths,
const nfdu8filteritem_t* filterList, const nfdu8filteritem_t* filterList,
nfdfiltersize_t count, nfdfiltersize_t count,
const nfdu8char_t* defaultPath); const nfdu8char_t* defaultPath);
/* multiple file open dialog */
/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function
* returns NFD_OKAY */
NFD_API nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths,
const nfdu8filteritem_t* filterList,
nfdfiltersize_t count,
const nfdu8char_t* defaultPath);
/* save dialog */ /* save dialog */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns
* NFD_OKAY */ * NFD_OKAY */
nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, NFD_API nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath,
const nfdu8filteritem_t* filterList, const nfdu8filteritem_t* filterList,
nfdfiltersize_t count, nfdfiltersize_t count,
const nfdu8char_t* defaultPath, const nfdu8char_t* defaultPath,
const nfdu8char_t* defaultName); const nfdu8char_t* defaultName);
/* select folder dialog */ /* select folder dialog */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns
* NFD_OKAY */ * NFD_OKAY */
nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath); NFD_API nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath);
/* Get the UTF-8 path at offset index */ /* Get the UTF-8 path at offset index */
/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns
* NFD_OKAY */ * NFD_OKAY */
nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, NFD_API nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet,
nfdpathsetsize_t index, nfdpathsetsize_t index,
nfdu8char_t** outPath); nfdu8char_t** outPath);
/* Gets the next item from the path set enumerator. /* Gets the next item from the path set enumerator.
* If there are no more items, then *outPaths will be set to NULL. */ * If there are no more items, then *outPaths will be set to NULL. */
/* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePathU8() if this /* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePathU8() if this
* function returns NFD_OKAY and `*outPath` is not null */ * function returns NFD_OKAY and `*outPath` is not null */
nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath); NFD_API nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath);
#define NFD_PathSet_FreePathU8 NFD_FreePathU8 #define NFD_PathSet_FreePathU8 NFD_FreePathU8

View File

@ -6,8 +6,28 @@
*/ */
#include <AppKit/AppKit.h> #include <AppKit/AppKit.h>
#include <Availability.h>
#include "nfd.h" #include "nfd.h"
// MacOS is deprecating the allowedFileTypes property in favour of allowedContentTypes, so we have
// to introduce this breaking change. Define NFD_MACOS_ALLOWEDCONTENTTYPES to 1 to have it set the
// allowedContentTypes property of the SavePanel or OpenPanel. Define
// NFD_MACOS_ALLOWEDCONTENTTYPES to 0 to have it set the allowedFileTypes property of the SavePanel
// or OpenPanel. If NFD_MACOS_ALLOWEDCONTENTTYPES is undefined, then it will set it to 1 if
// __MAC_OS_X_VERSION_MIN_REQUIRED >= 11.0, and 0 otherwise.
#if !defined(NFD_MACOS_ALLOWEDCONTENTTYPES)
#if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || !defined(__MAC_11_0) || \
__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0
#define NFD_MACOS_ALLOWEDCONTENTTYPES 0
#else
#define NFD_MACOS_ALLOWEDCONTENTTYPES 1
#endif
#endif
#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#endif
static const char* g_errorstr = NULL; static const char* g_errorstr = NULL;
static void NFDi_SetError(const char* msg) { static void NFDi_SetError(const char* msg) {
@ -26,10 +46,51 @@ static void NFDi_Free(void* ptr) {
free(ptr); free(ptr);
} }
#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1
// Returns an NSArray of UTType representing the content types.
static NSArray* BuildAllowedContentTypes(const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
NSMutableArray* buildFilterList = [[NSMutableArray alloc] init];
for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) {
// this is the spec to parse (we don't use the friendly name on OS X)
const nfdnchar_t* filterSpec = filterList[filterIndex].spec;
const nfdnchar_t* p_currentFilterBegin = filterSpec;
for (const nfdnchar_t* p_filterSpec = filterSpec; *p_filterSpec; ++p_filterSpec) {
if (*p_filterSpec == ',') {
// add the extension to the array
NSString* filterStr = [[NSString alloc]
initWithBytes:(const void*)p_currentFilterBegin
length:(sizeof(nfdnchar_t) * (p_filterSpec - p_currentFilterBegin))
encoding:NSUTF8StringEncoding];
UTType* filterType = [UTType typeWithFilenameExtension:filterStr
conformingToType:UTTypeData];
[filterStr release];
if (filterType) [buildFilterList addObject:filterType];
p_currentFilterBegin = p_filterSpec + 1;
}
}
// add the extension to the array
NSString* filterStr = [[NSString alloc] initWithUTF8String:p_currentFilterBegin];
UTType* filterType = [UTType typeWithFilenameExtension:filterStr
conformingToType:UTTypeData];
[filterStr release];
if (filterType) [buildFilterList addObject:filterType];
}
NSArray* returnArray = [NSArray arrayWithArray:buildFilterList];
[buildFilterList release];
assert([returnArray count] != 0);
return returnArray;
}
#else
// Returns an NSArray of NSString representing the file types.
static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList, static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) { nfdfiltersize_t filterCount) {
// Commas and semicolons are the same thing on this platform
NSMutableArray* buildFilterList = [[NSMutableArray alloc] init]; NSMutableArray* buildFilterList = [[NSMutableArray alloc] init];
for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) { for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) {
@ -61,6 +122,7 @@ static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList,
return returnArray; return returnArray;
} }
#endif
static void AddFilterListToDialog(NSSavePanel* dialog, static void AddFilterListToDialog(NSSavePanel* dialog,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
@ -71,11 +133,15 @@ static void AddFilterListToDialog(NSSavePanel* dialog,
assert(filterList); assert(filterList);
// make NSArray of file types // Make NSArray of file types and set it on the dialog
// We use setAllowedFileTypes or setAllowedContentTypes depending on the deployment target
#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1
NSArray* allowedContentTypes = BuildAllowedContentTypes(filterList, filterCount);
[dialog setAllowedContentTypes:allowedContentTypes];
#else
NSArray* allowedFileTypes = BuildAllowedFileTypes(filterList, filterCount); NSArray* allowedFileTypes = BuildAllowedFileTypes(filterList, filterCount);
// set it on the dialog
[dialog setAllowedFileTypes:allowedFileTypes]; [dialog setAllowedFileTypes:allowedFileTypes];
#endif
} }
static void SetDefaultPath(NSSavePanel* dialog, const nfdnchar_t* defaultPath) { static void SetDefaultPath(NSSavePanel* dialog, const nfdnchar_t* defaultPath) {
@ -114,6 +180,10 @@ const char* NFD_GetError(void) {
return g_errorstr; return g_errorstr;
} }
void NFD_ClearError(void) {
NFDi_SetError(NULL);
}
void NFD_FreePathN(nfdnchar_t* filePath) { void NFD_FreePathN(nfdnchar_t* filePath) {
NFDi_Free((void*)filePath); NFDi_Free((void*)filePath);
} }

View File

@ -14,19 +14,26 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/random.h> // for the random token string #include <unistd.h> // for access()
#include <unistd.h> // for access()
#if !defined(__has_include) || !defined(__linux__)
#include <sys/random.h> // for getrandom() - the random token string
#elif __has_include(<sys/random.h>)
#include <sys/random.h>
#else // for GLIBC < 2.25
#include <sys/syscall.h>
#define getrandom(buf, sz, flags) syscall(SYS_getrandom, buf, sz, flags)
#endif
#include "nfd.h" #include "nfd.h"
/* /*
Define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION to 0 if you don't want the file extension to be Define NFD_APPEND_EXTENSION if you want the file extension to be appended when missing. Linux
appended when missing. Linux programs usually doesn't append the file extension, but for consistency programs usually don't append the file extension, but for consistency with other OSes you might want
with other OSes we append it by default. to append it. However, when using portals, the file overwrite prompt and the Flatpak sandbox won't
know that we appended an extension, so they will not check or whitelist the correct file. Enabling
NFD_APPEND_EXTENSION is not recommended for portals.
*/ */
#ifndef NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION
#define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION 1
#endif
namespace { namespace {
@ -70,7 +77,10 @@ struct DBusMessage_Guard {
DBusConnection* dbus_conn; DBusConnection* dbus_conn;
/* current D-Bus error */ /* current D-Bus error */
DBusError dbus_err; DBusError dbus_err;
/* current error (may be a pointer to the D-Bus error message above, or a pointer to some string /* current non D-Bus error */
constexpr size_t OWNED_ERR_LEN = 1024;
char owned_err[OWNED_ERR_LEN]{};
/* current error (may be a pointer to dbus_err.message, owned_err, or a pointer to some string
* literal) */ * literal) */
const char* err_ptr = nullptr; const char* err_ptr = nullptr;
/* the unique name of our connection, used for the Request handle; owned by D-Bus so we don't free /* the unique name of our connection, used for the Request handle; owned by D-Bus so we don't free
@ -81,6 +91,14 @@ void NFDi_SetError(const char* msg) {
err_ptr = msg; err_ptr = msg;
} }
void NFDi_SetFormattedError(const char* format, ...) {
va_list args;
va_start(args, format);
vsnprintf(owned_err, OWNED_ERR_LEN, format, args);
va_end(args);
err_ptr = owned_err;
}
template <typename T> template <typename T>
T* copy(const T* begin, const T* end, T* out) { T* copy(const T* begin, const T* end, T* out) {
for (; begin != end; ++begin) { for (; begin != end; ++begin) {
@ -112,6 +130,10 @@ constexpr const char* STR_CURRENT_FOLDER = "current_folder";
constexpr const char* STR_CURRENT_FILE = "current_file"; constexpr const char* STR_CURRENT_FILE = "current_file";
constexpr const char* STR_ALL_FILES = "All files"; constexpr const char* STR_ALL_FILES = "All files";
constexpr const char* STR_ASTERISK = "*"; constexpr const char* STR_ASTERISK = "*";
constexpr const char* DBUS_DESTINATION = "org.freedesktop.portal.Desktop";
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";
template <bool Multiple, bool Directory> template <bool Multiple, bool Directory>
void AppendOpenFileQueryTitle(DBusMessageIter&); void AppendOpenFileQueryTitle(DBusMessageIter&);
@ -462,7 +484,7 @@ void AppendSaveFileQueryDictEntryCurrentName(DBusMessageIter& sub_iter, const ch
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
} }
void AppendSaveFileQueryDictEntryCurrentFolder(DBusMessageIter& sub_iter, const char* path) { void AppendOpenFileQueryDictEntryCurrentFolder(DBusMessageIter& sub_iter, const char* path) {
if (!path) return; if (!path) return;
DBusMessageIter sub_sub_iter; DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter; DBusMessageIter variant_iter;
@ -529,7 +551,8 @@ template <bool Multiple, bool Directory>
void AppendOpenFileQueryParams(DBusMessage* query, void AppendOpenFileQueryParams(DBusMessage* query,
const char* handle_token, const char* handle_token,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) { nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
DBusMessageIter iter; DBusMessageIter iter;
dbus_message_iter_init_append(query, &iter); dbus_message_iter_init_append(query, &iter);
@ -543,6 +566,7 @@ void AppendOpenFileQueryParams(DBusMessage* query,
AppendOpenFileQueryDictEntryMultiple<Multiple>(sub_iter); AppendOpenFileQueryDictEntryMultiple<Multiple>(sub_iter);
AppendOpenFileQueryDictEntryDirectory<Directory>(sub_iter); AppendOpenFileQueryDictEntryDirectory<Directory>(sub_iter);
AppendOpenFileQueryDictEntryFilters<!Directory>(sub_iter, filterList, filterCount); AppendOpenFileQueryDictEntryFilters<!Directory>(sub_iter, filterList, filterCount);
AppendOpenFileQueryDictEntryCurrentFolder(sub_iter, defaultPath);
dbus_message_iter_close_container(&iter, &sub_iter); dbus_message_iter_close_container(&iter, &sub_iter);
} }
@ -565,7 +589,7 @@ void AppendSaveFileQueryParams(DBusMessage* query,
AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token); AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token);
AppendSaveFileQueryDictEntryFilters(sub_iter, filterList, filterCount, defaultName); AppendSaveFileQueryDictEntryFilters(sub_iter, filterList, filterCount, defaultName);
AppendSaveFileQueryDictEntryCurrentName(sub_iter, defaultName); AppendSaveFileQueryDictEntryCurrentName(sub_iter, defaultName);
AppendSaveFileQueryDictEntryCurrentFolder(sub_iter, defaultPath); AppendOpenFileQueryDictEntryCurrentFolder(sub_iter, defaultPath);
AppendSaveFileQueryDictEntryCurrentFile(sub_iter, defaultPath, defaultName); AppendSaveFileQueryDictEntryCurrentFile(sub_iter, defaultPath, defaultName);
dbus_message_iter_close_container(&iter, &sub_iter); dbus_message_iter_close_container(&iter, &sub_iter);
} }
@ -649,7 +673,9 @@ nfdresult_t ReadResponseResults(DBusMessage* msg, DBusMessageIter& resultsIter)
return NFD_CANCEL; return NFD_CANCEL;
} else { } else {
// Some error occurred // Some error occurred
NFDi_SetError("D-Bus file dialog interaction was ended abruptly."); NFDi_SetFormattedError(
"D-Bus file dialog interaction was ended abruptly with response code %u.",
resp_code);
return NFD_ERROR; return NFD_ERROR;
} }
} }
@ -720,14 +746,14 @@ nfdresult_t ReadResponseUrisSingle(DBusMessage* msg, const char*& file) {
const nfdresult_t res = ReadResponseUris(msg, uri_iter); const nfdresult_t res = ReadResponseUris(msg, uri_iter);
if (res != NFD_OKAY) return res; // can be NFD_CANCEL or NFD_ERROR if (res != NFD_OKAY) return res; // can be NFD_CANCEL or NFD_ERROR
if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) { if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not an string."); NFDi_SetError("D-Bus response signal URI sub iter is not a string.");
return NFD_ERROR; return NFD_ERROR;
} }
dbus_message_iter_get_basic(&uri_iter, &file); dbus_message_iter_get_basic(&uri_iter, &file);
return NFD_OKAY; return NFD_OKAY;
} }
#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 #ifdef NFD_APPEND_EXTENSION
// Read the response URI and selected extension (in the form "*.abc" or "*") (if any). If response // Read the response URI and selected extension (in the form "*.abc" or "*") (if any). If response
// was okay, then returns NFD_OKAY and set file and extn to them (the pointer is set to some string // was okay, then returns NFD_OKAY and set file and extn to them (the pointer is set to some string
// owned by msg, so you should not manually free it). `file` is the user-entered file name, and // owned by msg, so you should not manually free it). `file` is the user-entered file name, and
@ -763,47 +789,37 @@ nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg,
[&tmp_extn](DBusMessageIter& current_filter_iter) { [&tmp_extn](DBusMessageIter& current_filter_iter) {
// current_filter is best_effort, so if we fail, we still return NFD_OKAY. // current_filter is best_effort, so if we fail, we still return NFD_OKAY.
if (dbus_message_iter_get_arg_type(&current_filter_iter) != DBUS_TYPE_STRUCT) { if (dbus_message_iter_get_arg_type(&current_filter_iter) != DBUS_TYPE_STRUCT) {
// NFDi_SetError("D-Bus response signal current_filter iter is not a struct.");
return NFD_OKAY; return NFD_OKAY;
} }
DBusMessageIter current_filter_struct_iter; DBusMessageIter current_filter_struct_iter;
dbus_message_iter_recurse(&current_filter_iter, &current_filter_struct_iter); dbus_message_iter_recurse(&current_filter_iter, &current_filter_struct_iter);
if (!dbus_message_iter_next(&current_filter_struct_iter)) { if (!dbus_message_iter_next(&current_filter_struct_iter)) {
// NFDi_SetError("D-Bus response signal current_filter struct iter ended
// prematurely.");
return NFD_OKAY; return NFD_OKAY;
} }
if (dbus_message_iter_get_arg_type(&current_filter_struct_iter) != if (dbus_message_iter_get_arg_type(&current_filter_struct_iter) !=
DBUS_TYPE_ARRAY) { DBUS_TYPE_ARRAY) {
// NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_OKAY; return NFD_OKAY;
} }
DBusMessageIter current_filter_array_iter; DBusMessageIter current_filter_array_iter;
dbus_message_iter_recurse(&current_filter_struct_iter, &current_filter_array_iter); dbus_message_iter_recurse(&current_filter_struct_iter, &current_filter_array_iter);
if (dbus_message_iter_get_arg_type(&current_filter_array_iter) != if (dbus_message_iter_get_arg_type(&current_filter_array_iter) !=
DBUS_TYPE_STRUCT) { DBUS_TYPE_STRUCT) {
// NFDi_SetError("D-Bus response signal current_filter iter is not a struct.");
return NFD_OKAY; return NFD_OKAY;
} }
DBusMessageIter current_filter_extn_iter; DBusMessageIter current_filter_extn_iter;
dbus_message_iter_recurse(&current_filter_array_iter, &current_filter_extn_iter); dbus_message_iter_recurse(&current_filter_array_iter, &current_filter_extn_iter);
if (dbus_message_iter_get_arg_type(&current_filter_extn_iter) != DBUS_TYPE_UINT32) { if (dbus_message_iter_get_arg_type(&current_filter_extn_iter) != DBUS_TYPE_UINT32) {
// NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_OKAY; return NFD_OKAY;
} }
dbus_uint32_t type; dbus_uint32_t type;
dbus_message_iter_get_basic(&current_filter_extn_iter, &type); dbus_message_iter_get_basic(&current_filter_extn_iter, &type);
if (type != 0) { if (type != 0) {
// NFDi_SetError("Wrong filter type.");
return NFD_OKAY; return NFD_OKAY;
} }
if (!dbus_message_iter_next(&current_filter_extn_iter)) { if (!dbus_message_iter_next(&current_filter_extn_iter)) {
// NFDi_SetError("D-Bus response signal current_filter struct iter ended
// prematurely.");
return NFD_OKAY; return NFD_OKAY;
} }
if (dbus_message_iter_get_arg_type(&current_filter_extn_iter) != DBUS_TYPE_STRING) { if (dbus_message_iter_get_arg_type(&current_filter_extn_iter) != DBUS_TYPE_STRING) {
// NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_OKAY; return NFD_OKAY;
} }
dbus_message_iter_get_basic(&current_filter_extn_iter, &tmp_extn); dbus_message_iter_get_basic(&current_filter_extn_iter, &tmp_extn);
@ -942,29 +958,95 @@ 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
// `fileUriEnd` will be unmodified.
bool TryUriDecodeLen(const char* fileUri, size_t& out, const char*& fileUriEnd) {
size_t len = 0;
while (*fileUri) {
if (*fileUri != '%') {
++fileUri;
} else {
if (*(fileUri + 1) == '\0' || *(fileUri + 2) == '\0') {
return false;
}
if (!IsHex(*(fileUri + 1)) || !IsHex(*(fileUri + 2))) {
return false;
}
fileUri += 3;
}
++len;
}
out = len;
fileUriEnd = fileUri;
return true;
}
// Decodes the given URI and writes it to `outPath`. The caller must ensure that the given URI is
// not malformed (typically with a prior call to `TryUriDecodeLen`). This function does not write
// any trailing null character.
char* UriDecodeUnchecked(const char* fileUri, const char* fileUriEnd, char* outPath) {
while (fileUri != fileUriEnd) {
if (*fileUri != '%') {
*outPath++ = *fileUri++;
} else {
++fileUri;
const char high_nibble = ParseHexUnchecked(*fileUri++);
const char low_nibble = ParseHexUnchecked(*fileUri++);
*outPath++ = (high_nibble << 4) | low_nibble;
}
}
return outPath;
}
constexpr const char FILE_URI_PREFIX[] = "file://"; constexpr const char FILE_URI_PREFIX[] = "file://";
constexpr size_t FILE_URI_PREFIX_LEN = sizeof(FILE_URI_PREFIX) - 1; constexpr size_t FILE_URI_PREFIX_LEN = sizeof(FILE_URI_PREFIX) - 1;
// If fileUri starts with "file://", strips that prefix and copies it to a new buffer, and make // If fileUri starts with "file://", strips that prefix and URI-decodes the remaining part to a new
// outPath point to it, and returns NFD_OKAY. Otherwise, does not modify outPath and returns // buffer, and make outPath point to it, and returns NFD_OKAY. Otherwise, does not modify outPath
// NFD_ERROR (with the correct error set) // and returns NFD_ERROR (with the correct error set)
nfdresult_t AllocAndCopyFilePath(const char* fileUri, char*& outPath) { nfdresult_t AllocAndCopyFilePath(const char* fileUri, char*& outPath) {
const char* file_uri_iter = fileUri;
const char* prefix_begin = FILE_URI_PREFIX; const char* prefix_begin = FILE_URI_PREFIX;
const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN; const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN;
for (; prefix_begin != prefix_end; ++prefix_begin, ++fileUri) { for (; prefix_begin != prefix_end; ++prefix_begin, ++file_uri_iter) {
if (*prefix_begin != *fileUri) { if (*prefix_begin != *file_uri_iter) {
NFDi_SetError("D-Bus freedesktop portal returned a URI that is not a file URI."); NFDi_SetFormattedError(
"D-Bus freedesktop portal returned \"%s\", which is not a file URI.", fileUri);
return NFD_ERROR; return NFD_ERROR;
} }
} }
size_t len = strlen(fileUri); size_t decoded_len;
char* path_without_prefix = NFDi_Malloc<char>(len + 1); const char* file_uri_end;
copy(fileUri, fileUri + (len + 1), path_without_prefix); if (!TryUriDecodeLen(file_uri_iter, decoded_len, file_uri_end)) {
NFDi_SetFormattedError("D-Bus freedesktop portal returned a malformed URI \"%s\".",
fileUri);
return NFD_ERROR;
}
char* const path_without_prefix = NFDi_Malloc<char>(decoded_len + 1);
char* const out_end = UriDecodeUnchecked(file_uri_iter, file_uri_end, path_without_prefix);
*out_end = '\0';
outPath = path_without_prefix; outPath = path_without_prefix;
return NFD_OKAY; return NFD_OKAY;
} }
#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 #ifdef NFD_APPEND_EXTENSION
bool TryGetValidExtension(const char* extn, bool TryGetValidExtension(const char* extn,
const char*& trimmed_extn, const char*& trimmed_extn,
const char*& trimmed_extn_end) { const char*& trimmed_extn_end) {
@ -985,19 +1067,30 @@ bool TryGetValidExtension(const char* extn,
// expected to be either in the form "*.abc" or "*", but this function will check for it, and ignore // expected to be either in the form "*.abc" or "*", but this function will check for it, and ignore
// the extension if it is not in the correct form. // the extension if it is not in the correct form.
nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, char*& outPath) { nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, char*& outPath) {
const char* file_uri_iter = fileUri;
const char* prefix_begin = FILE_URI_PREFIX; const char* prefix_begin = FILE_URI_PREFIX;
const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN; const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN;
for (; prefix_begin != prefix_end; ++prefix_begin, ++fileUri) { for (; prefix_begin != prefix_end; ++prefix_begin, ++file_uri_iter) {
if (*prefix_begin != *fileUri) { if (*prefix_begin != *file_uri_iter) {
NFDi_SetError("D-Bus freedesktop portal returned a URI that is not a file URI."); NFDi_SetFormattedError(
"D-Bus freedesktop portal returned \"%s\", which is not a file URI.", fileUri);
return NFD_ERROR; return NFD_ERROR;
} }
} }
const char* file_end = fileUri; size_t decoded_len;
for (; *file_end != '\0'; ++file_end) const char* file_uri_end;
; if (!TryUriDecodeLen(file_uri_iter, decoded_len, file_uri_end)) {
const char* file_it = file_end; NFDi_SetFormattedError("D-Bus freedesktop portal returned a malformed URI \"%s\".",
fileUri);
return NFD_ERROR;
}
const char* file_it = file_uri_end;
// The following loop condition is safe because `FILE_URI_PREFIX` ends with '/',
// so we won't iterate past the beginning of the URI.
// Also in UTF-8 all non-ASCII code points are encoded using bytes 128-255 so every '.' or '/'
// is also '.' or '/' in UTF-8.
do { do {
--file_it; --file_it;
} while (*file_it != '/' && *file_it != '.'); } while (*file_it != '/' && *file_it != '.');
@ -1005,16 +1098,17 @@ nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn,
const char* trimmed_extn_end; // includes the '\0' const char* trimmed_extn_end; // includes the '\0'
if (*file_it == '.' || !TryGetValidExtension(extn, trimmed_extn, trimmed_extn_end)) { if (*file_it == '.' || !TryGetValidExtension(extn, trimmed_extn, trimmed_extn_end)) {
// has file extension already or no valid extension in `extn` // has file extension already or no valid extension in `extn`
++file_end; // includes the '\0' char* const path_without_prefix = NFDi_Malloc<char>(decoded_len + 1);
char* path_without_prefix = NFDi_Malloc<char>(file_end - fileUri); char* const out_end = UriDecodeUnchecked(file_uri_iter, file_uri_end, path_without_prefix);
copy(fileUri, file_end, path_without_prefix); *out_end = '\0';
outPath = path_without_prefix; outPath = path_without_prefix;
} else { } else {
// no file extension and we have a valid extension // no file extension and we have a valid extension
char* path_without_prefix = char* const path_without_prefix =
NFDi_Malloc<char>((file_end - fileUri) + (trimmed_extn_end - trimmed_extn)); NFDi_Malloc<char>(decoded_len + (trimmed_extn_end - trimmed_extn));
char* out = copy(fileUri, file_end, path_without_prefix); char* const out_mid = UriDecodeUnchecked(file_uri_iter, file_uri_end, path_without_prefix);
copy(trimmed_extn, trimmed_extn_end, out); char* const out_end = copy(trimmed_extn, trimmed_extn_end, out_mid);
*out_end = '\0';
outPath = path_without_prefix; outPath = path_without_prefix;
} }
return NFD_OKAY; return NFD_OKAY;
@ -1028,7 +1122,8 @@ nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn,
template <bool Multiple, bool Directory> template <bool Multiple, bool Directory>
nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) { nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
const char* handle_token_ptr; const char* handle_token_ptr;
char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr);
Free_Guard<char> handle_obj_path_guard(handle_obj_path); Free_Guard<char> handle_obj_path_guard(handle_obj_path);
@ -1045,13 +1140,11 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg,
// TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on // TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on
// Wayland? // Wayland?
DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop", DBusMessage* query = dbus_message_new_method_call(
"/org/freedesktop/portal/desktop", DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "OpenFile");
"org.freedesktop.portal.FileChooser",
"OpenFile");
DBusMessage_Guard query_guard(query); DBusMessage_Guard query_guard(query);
AppendOpenFileQueryParams<Multiple, Directory>( AppendOpenFileQueryParams<Multiple, Directory>(
query, handle_token_ptr, filterList, filterCount); query, handle_token_ptr, filterList, filterCount, defaultPath);
DBusMessage* reply = DBusMessage* reply =
dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err);
@ -1090,7 +1183,7 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg,
DBusMessage* msg = dbus_connection_pop_message(dbus_conn); DBusMessage* msg = dbus_connection_pop_message(dbus_conn);
if (!msg) break; if (!msg) break;
if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { if (dbus_message_is_signal(msg, DBUS_REQUEST_IFACE, "Response")) {
// this is the response we're looking for // this is the response we're looking for
outMsg = msg; outMsg = msg;
return NFD_OKAY; return NFD_OKAY;
@ -1129,10 +1222,8 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg,
// TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on // TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on
// Wayland? // Wayland?
DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop", DBusMessage* query = dbus_message_new_method_call(
"/org/freedesktop/portal/desktop", DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "SaveFile");
"org.freedesktop.portal.FileChooser",
"SaveFile");
DBusMessage_Guard query_guard(query); DBusMessage_Guard query_guard(query);
AppendSaveFileQueryParams( AppendSaveFileQueryParams(
query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName); query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName);
@ -1174,7 +1265,7 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg,
DBusMessage* msg = dbus_connection_pop_message(dbus_conn); DBusMessage* msg = dbus_connection_pop_message(dbus_conn);
if (!msg) break; if (!msg) break;
if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { if (dbus_message_is_signal(msg, DBUS_REQUEST_IFACE, "Response")) {
// this is the response we're looking for // this is the response we're looking for
outMsg = msg; outMsg = msg;
return NFD_OKAY; return NFD_OKAY;
@ -1188,6 +1279,57 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg,
return NFD_ERROR; return NFD_ERROR;
} }
nfdresult_t NFD_DBus_GetVersion(dbus_uint32_t& outVersion) {
DBusError err; // need a separate error object because we don't want to mess with the old one
// if it's stil set
dbus_error_init(&err);
DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.DBus.Properties",
"Get");
DBusMessage_Guard query_guard(query);
{
DBusMessageIter iter;
dbus_message_iter_init_append(query, &iter);
constexpr const char* STR_INTERFACE = "org.freedesktop.portal.FileChooser";
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_INTERFACE);
constexpr const char* STR_VERSION = "version";
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_VERSION);
}
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err);
if (!reply) {
dbus_error_free(&dbus_err);
dbus_move_error(&err, &dbus_err);
NFDi_SetError(dbus_err.message);
return NFD_ERROR;
}
DBusMessage_Guard reply_guard(reply);
{
DBusMessageIter iter;
if (!dbus_message_iter_init(reply, &iter)) {
NFDi_SetError("D-Bus reply for version query is missing an argument.");
return NFD_ERROR;
}
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) {
NFDi_SetError("D-Bus reply for version query is not a variant.");
return NFD_ERROR;
}
DBusMessageIter variant_iter;
dbus_message_iter_recurse(&iter, &variant_iter);
if (dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_UINT32) {
NFDi_SetError("D-Bus reply for version query is not a uint32.");
return NFD_ERROR;
}
dbus_message_iter_get_basic(&variant_iter, &outVersion);
}
return NFD_OKAY;
}
} // namespace } // namespace
/* public */ /* public */
@ -1202,7 +1344,7 @@ void NFD_ClearError(void) {
} }
nfdresult_t NFD_Init(void) { nfdresult_t NFD_Init(void) {
// Initialize dbus_error // Initialize dbus_err
dbus_error_init(&dbus_err); dbus_error_init(&dbus_err);
// Get DBus connection // Get DBus connection
dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_err); dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_err);
@ -1233,37 +1375,35 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount, nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) { const nfdnchar_t* defaultPath) {
(void)defaultPath; // Default path not supported for portal backend
DBusMessage* msg; DBusMessage* msg;
{ {
const nfdresult_t res = NFD_DBus_OpenFile<false, false>(msg, filterList, filterCount); const nfdresult_t res =
NFD_DBus_OpenFile<false, false>(msg, filterList, filterCount, defaultPath);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
} }
DBusMessage_Guard msg_guard(msg); DBusMessage_Guard msg_guard(msg);
const char* file; const char* uri;
{ {
const nfdresult_t res = ReadResponseUrisSingle(msg, file); const nfdresult_t res = ReadResponseUrisSingle(msg, uri);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
} }
return AllocAndCopyFilePath(file, *outPath); return AllocAndCopyFilePath(uri, *outPath);
} }
nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
const nfdnfilteritem_t* filterList, const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount, nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) { const nfdnchar_t* defaultPath) {
(void)defaultPath; // Default path not supported for portal backend
DBusMessage* msg; DBusMessage* msg;
{ {
const nfdresult_t res = NFD_DBus_OpenFile<true, false>(msg, filterList, filterCount); const nfdresult_t res =
NFD_DBus_OpenFile<true, false>(msg, filterList, filterCount, defaultPath);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
@ -1295,51 +1435,67 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
} }
DBusMessage_Guard msg_guard(msg); DBusMessage_Guard msg_guard(msg);
#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 #ifdef NFD_APPEND_EXTENSION
const char* file; const char* uri;
const char* extn; const char* extn;
{ {
const nfdresult_t res = ReadResponseUrisSingleAndCurrentExtension(msg, file, extn); const nfdresult_t res = ReadResponseUrisSingleAndCurrentExtension(msg, uri, extn);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
} }
return AllocAndCopyFilePathWithExtn(file, extn, *outPath); return AllocAndCopyFilePathWithExtn(uri, extn, *outPath);
#else #else
const char* file; const char* uri;
{ {
const nfdresult_t res = ReadResponseUrisSingle(msg, file); const nfdresult_t res = ReadResponseUrisSingle(msg, uri);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
} }
return AllocAndCopyFilePath(file, *outPath); return AllocAndCopyFilePath(uri, *outPath);
#endif #endif
} }
nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) {
(void)defaultPath; // Default path not supported for portal backend (void)defaultPath; // Default path not supported for portal backend
{
dbus_uint32_t version;
const nfdresult_t res = NFD_DBus_GetVersion(version);
if (res != NFD_OKAY) {
return res;
}
if (version < 3) {
NFDi_SetFormattedError(
"The xdg-desktop-portal installed on this system does not support a folder picker; "
"at least version 3 of the org.freedesktop.portal.FileChooser interface is "
"required but the installed interface version is %u.",
version);
return NFD_ERROR;
}
}
DBusMessage* msg; DBusMessage* msg;
{ {
const nfdresult_t res = NFD_DBus_OpenFile<false, true>(msg, nullptr, 0); const nfdresult_t res = NFD_DBus_OpenFile<false, true>(msg, nullptr, 0, defaultPath);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
} }
DBusMessage_Guard msg_guard(msg); DBusMessage_Guard msg_guard(msg);
const char* file; const char* uri;
{ {
const nfdresult_t res = ReadResponseUrisSingle(msg, file); const nfdresult_t res = ReadResponseUrisSingle(msg, uri);
if (res != NFD_OKAY) { if (res != NFD_OKAY) {
return res; return res;
} }
} }
return AllocAndCopyFilePath(file, *outPath); return AllocAndCopyFilePath(uri, *outPath);
} }
nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) {
@ -1356,20 +1512,25 @@ nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet,
DBusMessage* msg = const_cast<DBusMessage*>(static_cast<const DBusMessage*>(pathSet)); DBusMessage* msg = const_cast<DBusMessage*>(static_cast<const DBusMessage*>(pathSet));
DBusMessageIter uri_iter; DBusMessageIter uri_iter;
ReadResponseUrisUnchecked(msg, uri_iter); ReadResponseUrisUnchecked(msg, uri_iter);
while (index > 0) { nfdpathsetsize_t rem_index = index;
--index; while (rem_index > 0) {
--rem_index;
if (!dbus_message_iter_next(&uri_iter)) { if (!dbus_message_iter_next(&uri_iter)) {
NFDi_SetError("Index out of bounds."); NFDi_SetFormattedError(
"Index out of bounds; you asked for index %u but there are only %u file paths "
"available.",
index,
index - rem_index);
return NFD_ERROR; return NFD_ERROR;
} }
} }
if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) { if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not an string."); NFDi_SetError("D-Bus response signal URI sub iter is not a string.");
return NFD_ERROR; return NFD_ERROR;
} }
const char* file; const char* uri;
dbus_message_iter_get_basic(&uri_iter, &file); dbus_message_iter_get_basic(&uri_iter, &uri);
return AllocAndCopyFilePath(file, *outPath); return AllocAndCopyFilePath(uri, *outPath);
} }
void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) { void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) {
@ -1402,12 +1563,12 @@ nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** out
return NFD_OKAY; return NFD_OKAY;
} }
if (arg_type != DBUS_TYPE_STRING) { if (arg_type != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not an string."); NFDi_SetError("D-Bus response signal URI sub iter is not a string.");
return NFD_ERROR; return NFD_ERROR;
} }
const char* file; const char* uri;
dbus_message_iter_get_basic(&uri_iter, &file); dbus_message_iter_get_basic(&uri_iter, &uri);
const nfdresult_t res = AllocAndCopyFilePath(file, *outPath); const nfdresult_t res = AllocAndCopyFilePath(uri, *outPath);
if (res != NFD_OKAY) return res; if (res != NFD_OKAY) return res;
dbus_message_iter_next(&uri_iter); dbus_message_iter_next(&uri_iter);
return NFD_OKAY; return NFD_OKAY;