diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 1ead142..3ee2273 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -37,7 +37,7 @@ jobs: build-ubuntu: - name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} + name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.casesensitive.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} runs-on: ${{ matrix.os.label }} strategy: @@ -45,6 +45,7 @@ jobs: os: [ {label: ubuntu-latest, name: latest}, {label: ubuntu-22.04, name: 22.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) autoappend: [ {flag: OFF, name: NoAppendExtn} ] # By default the NFD_PORTAL mode does not append extensions, because it breaks some features of the portal + casesensitive: [ {flag: OFF, name: CaseInsensitive} ] # Case insensitive or case sensitive file filtering 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} ] @@ -52,21 +53,38 @@ jobs: - os: {label: ubuntu-latest, name: latest} portal: {flag: ON, dep: libdbus-1-dev, name: Portal} autoappend: {flag: ON, name: AutoAppendExtn} + casesensitive: {flag: OFF, name: CaseInsensitive} 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} + casesensitive: {flag: OFF, name: CaseInsensitive} 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: OFF, name: NoAppendExtn} + casesensitive: {flag: OFF, name: CaseInsensitive} compiler: {c: gcc, cpp: g++, name: GCC} cppstd: 11 shared_lib: {flag: ON, name: Shared} + - os: {label: ubuntu-latest, name: latest} + portal: {flag: OFF, dep: libgtk-3-dev, name: GTK} + autoappend: {flag: OFF, name: NoAppendExtn} + casesensitive: {flag: ON, name: CaseSensitive} + 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: OFF, name: NoAppendExtn} + casesensitive: {flag: ON, name: CaseSensitive} + compiler: {c: gcc, cpp: g++, name: GCC} + cppstd: 11 + shared_lib: {flag: OFF, name: Static} steps: - name: Checkout @@ -74,13 +92,13 @@ jobs: - name: Install Dependencies run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} - name: Configure - run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_C_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.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 -Wshadow -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DNFD_CASE_SENSITIVE_FILTER=${{ matrix.casesensitive.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install - name: Upload test binaries uses: actions/upload-artifact@v4 with: - name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} + name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.casesensitive.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} path: | build/src/* build/test/* diff --git a/README.md b/README.md index d1a1ffe..3b7c193 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,8 @@ A wildcard filter is always added to every dialog. *Note 4: On Windows, the default folder parameter is only used if there is no recently used folder available, unless the `NFD_OVERRIDE_RECENT_WITH_DEFAULT` build option is set to ON. Otherwise, the default folder will be the folder that was last used. Internally, the Windows implementation calls [IFileDialog::SetDefaultFolder(IShellItem)](https://docs.microsoft.com/en-us/windows/desktop/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setdefaultfolder). This is usual Windows behaviour and users expect it.* +*Note 5: Linux is designed for case-sensitive file filters, but this is perhaps not what most users expect. A simple hack is used to make filters case-insensitive. To get case-sensitive filtering, set the `NFD_CASE_SENSITIVE_FILTER` build option to ON.* + ## 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: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3dcd160..1eb7c38 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -94,6 +94,10 @@ if(nfd_PLATFORM STREQUAL PLATFORM_LINUX) if(NFD_APPEND_EXTENSION) target_compile_definitions(${TARGET_NAME} PRIVATE NFD_APPEND_EXTENSION) endif() + option(NFD_CASE_SENSITIVE_FILTER "Make filters case sensitive" OFF) + if(NFD_CASE_SENSITIVE_FILTER) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_CASE_SENSITIVE_FILTER) + endif() endif() if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index a52b6d3..bb7d3cc 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -19,6 +19,13 @@ #include "nfd.h" +/* +Define NFD_CASE_SENSITIVE_FILTER if you want file filters to be case-sensitive. The default +is case-insensitive. While Linux uses a case-sensitive filesystem and is designed for +case-sensitive file extensions, perhaps in the vast majority of cases users actually expect the file +filters to be case-insensitive. +*/ + namespace { template @@ -66,6 +73,27 @@ T* copy(const T* begin, const T* end, T* out) { return out; } +#ifndef NFD_CASE_SENSITIVE_FILTER +nfdnchar_t* emit_case_insensitive_glob(const nfdnchar_t* begin, + const nfdnchar_t* end, + nfdnchar_t* out) { + // this code will only make regular Latin characters case-insensitive; other + // characters remain case sensitive + for (; begin != end; ++begin) { + if ((*begin >= 'A' && *begin <= 'Z') || (*begin >= 'a' && *begin <= 'z')) { + *out++ = '['; + *out++ = *begin; + // invert the case of the original character + *out++ = *begin ^ static_cast(0x20); + *out++ = ']'; + } else { + *out++ = *begin; + } + } + return out; +} +#endif + // Does not own the filter and extension. struct Pair_GtkFileFilter_FileExtension { GtkFileFilter* filter; @@ -122,6 +150,7 @@ void AddFiltersToDialog(GtkFileChooser* chooser, *p_nameBuf++ = ' '; } +#ifdef NFD_CASE_SENSITIVE_FILTER // +1 for the trailing '\0' nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3)); @@ -130,10 +159,23 @@ void AddFiltersToDialog(GtkFileChooser* chooser, *p_extnBufEnd++ = '.'; p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd); *p_extnBufEnd++ = '\0'; - assert((size_t)(p_extnBufEnd - extnBuf) == - sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3)); gtk_file_filter_add_pattern(filter, extnBuf); NFDi_Free(extnBuf); +#else + // Each character in the Latin alphabet is converted into 4 characters. E.g. + // 'a' is converted into "[Aa]". Other characters are preserved. Then we +1 + // for the trailing '\0'. + nfdnchar_t* extnBuf = NFDi_Malloc( + sizeof(nfdnchar_t) * ((p_spec - p_extensionStart) * 4 + 3)); + nfdnchar_t* p_extnBufEnd = extnBuf; + *p_extnBufEnd++ = '*'; + *p_extnBufEnd++ = '.'; + p_extnBufEnd = + emit_case_insensitive_glob(p_extensionStart, p_spec, p_extnBufEnd); + *p_extnBufEnd++ = '\0'; + gtk_file_filter_add_pattern(filter, extnBuf); + NFDi_Free(extnBuf); +#endif if (*p_spec) { // update the extension start point @@ -222,6 +264,7 @@ Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* choo *p_nameBuf++ = ' '; } +#ifdef NFD_CASE_SENSITIVE_FILTER // +1 for the trailing '\0' nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3)); @@ -230,10 +273,23 @@ Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* choo *p_extnBufEnd++ = '.'; p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd); *p_extnBufEnd++ = '\0'; - assert((size_t)(p_extnBufEnd - extnBuf) == - sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3)); gtk_file_filter_add_pattern(filter, extnBuf); NFDi_Free(extnBuf); +#else + // Each character in the Latin alphabet is converted into 4 characters. E.g. + // 'a' is converted into "[Aa]". Other characters are preserved. Then we +1 + // for the trailing '\0'. + nfdnchar_t* extnBuf = NFDi_Malloc( + sizeof(nfdnchar_t) * ((p_spec - p_extensionStart) * 4 + 3)); + nfdnchar_t* p_extnBufEnd = extnBuf; + *p_extnBufEnd++ = '*'; + *p_extnBufEnd++ = '.'; + p_extnBufEnd = + emit_case_insensitive_glob(p_extensionStart, p_spec, p_extnBufEnd); + *p_extnBufEnd++ = '\0'; + gtk_file_filter_add_pattern(filter, extnBuf); + NFDi_Free(extnBuf); +#endif // store current pointer in map (if it's // the first one) diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp index 15a4a73..0ee222b 100644 --- a/src/nfd_portal.cpp +++ b/src/nfd_portal.cpp @@ -36,6 +36,13 @@ know that we appended an extension, so they will not check or whitelist the corr NFD_APPEND_EXTENSION is not recommended for portals. */ +/* +Define NFD_CASE_SENSITIVE_FILTER if you want file filters to be case-sensitive. The default +is case-insensitive. While Linux uses a case-sensitive filesystem and is designed for +case-sensitive file extensions, perhaps in the vast majority of cases users actually expect the file +filters to be case-insensitive. +*/ + namespace { template @@ -124,6 +131,27 @@ T* reverse_copy(const T* begin, const T* end, T* out) { return out; } +#ifndef NFD_CASE_SENSITIVE_FILTER +nfdnchar_t* emit_case_insensitive_glob(const nfdnchar_t* begin, + const nfdnchar_t* end, + nfdnchar_t* out) { + // this code will only make regular Latin characters case-insensitive; other + // characters remain case sensitive + for (; begin != end; ++begin) { + if ((*begin >= 'A' && *begin <= 'Z') || (*begin >= 'a' && *begin <= 'z')) { + *out++ = '['; + *out++ = *begin; + // invert the case of the original character + *out++ = *begin ^ static_cast(0x20); + *out++ = ']'; + } else { + *out++ = *begin; + } + } + return out; +} +#endif + // 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'); @@ -316,13 +344,25 @@ void AppendSingleFilter(DBusMessageIter& base_iter, const nfdnfilteritem_t& filt do { ++extn_end; } while (*extn_end != ',' && *extn_end != '\0'); - char* buf = static_cast(alloca(2 + (extn_end - extn_begin) + 1)); - char* buf_end = buf; - *buf_end++ = '*'; - *buf_end++ = '.'; - buf_end = copy(extn_begin, extn_end, buf_end); - *buf_end = '\0'; - dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); + { +#ifdef NFD_CASE_SENSITIVE_FILTER + char* buf = static_cast(alloca(2 + (extn_end - extn_begin) + 1)); + char* buf_end = buf; + *buf_end++ = '*'; + *buf_end++ = '.'; + buf_end = copy(extn_begin, extn_end, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); +#else + char* buf = static_cast(alloca(2 + (extn_end - extn_begin) * 4 + 1)); + char* buf_end = buf; + *buf_end++ = '*'; + *buf_end++ = '.'; + buf_end = emit_case_insensitive_glob(extn_begin, extn_end, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); +#endif + } dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter); if (*extn_end == '\0') { break; @@ -385,13 +425,25 @@ bool AppendSingleFilterCheckExtn(DBusMessageIter& base_iter, do { ++extn_end; } while (*extn_end != ',' && *extn_end != '\0'); - char* buf = static_cast(alloca(2 + (extn_end - extn_begin) + 1)); - char* buf_end = buf; - *buf_end++ = '*'; - *buf_end++ = '.'; - buf_end = copy(extn_begin, extn_end, buf_end); - *buf_end = '\0'; - dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); + { +#ifdef NFD_CASE_SENSITIVE_FILTER + char* buf = static_cast(alloca(2 + (extn_end - extn_begin) + 1)); + char* buf_end = buf; + *buf_end++ = '*'; + *buf_end++ = '.'; + buf_end = copy(extn_begin, extn_end, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); +#else + char* buf = static_cast(alloca(2 + (extn_end - extn_begin) * 4 + 1)); + char* buf_end = buf; + *buf_end++ = '*'; + *buf_end++ = '.'; + buf_end = emit_case_insensitive_glob(extn_begin, extn_end, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); +#endif + } dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter); if (!extn_matched) { const char* match_extn_p;