Skip to content

Commit

Permalink
Linux: Allow case-insensitive file filters and make that the default (#…
Browse files Browse the repository at this point in the history
…158)

By default, file filters are case-sensitive on Linux, but most users probably would prefer filters to be case-insensitive.  This also aligns the behaviour on Linux to Windows and macOS.
  • Loading branch information
btzy authored Mar 4, 2025
1 parent 194eae0 commit a1a4010
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 21 deletions.
24 changes: 21 additions & 3 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,50 +37,68 @@ 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:
matrix:
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} ]
include:
- 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
uses: actions/checkout@v4
- 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/*
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 60 additions & 4 deletions src/nfd_gtk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>
Expand Down Expand Up @@ -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<nfdnchar_t>(0x20);
*out++ = ']';
} else {
*out++ = *begin;
}
}
return out;
}
#endif

// Does not own the filter and extension.
struct Pair_GtkFileFilter_FileExtension {
GtkFileFilter* filter;
Expand Down Expand Up @@ -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<nfdnchar_t>(sizeof(nfdnchar_t) *
(p_spec - p_extensionStart + 3));
Expand All @@ -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<nfdnchar_t>(
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
Expand Down Expand Up @@ -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<nfdnchar_t>(sizeof(nfdnchar_t) *
(p_spec - p_extensionStart + 3));
Expand All @@ -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<nfdnchar_t>(
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)
Expand Down
80 changes: 66 additions & 14 deletions src/nfd_portal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T = void>
Expand Down Expand Up @@ -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<nfdnchar_t>(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');
Expand Down Expand Up @@ -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<char*>(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<char*>(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<char*>(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;
Expand Down Expand Up @@ -385,13 +425,25 @@ bool AppendSingleFilterCheckExtn(DBusMessageIter& base_iter,
do {
++extn_end;
} while (*extn_end != ',' && *extn_end != '\0');
char* buf = static_cast<char*>(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<char*>(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<char*>(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;
Expand Down

0 comments on commit a1a4010

Please sign in to comment.