Skip to content

Commit a1a4010

Browse files
authored
Linux: Allow case-insensitive file filters and make that the default (#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.
1 parent 194eae0 commit a1a4010

File tree

5 files changed

+153
-21
lines changed

5 files changed

+153
-21
lines changed

.github/workflows/cmake.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,50 +37,68 @@ jobs:
3737

3838
build-ubuntu:
3939

40-
name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }}
40+
name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.casesensitive.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }}
4141
runs-on: ${{ matrix.os.label }}
4242

4343
strategy:
4444
matrix:
4545
os: [ {label: ubuntu-latest, name: latest}, {label: ubuntu-22.04, name: 22.04} ]
4646
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)
4747
autoappend: [ {flag: OFF, name: NoAppendExtn} ] # By default the NFD_PORTAL mode does not append extensions, because it breaks some features of the portal
48+
casesensitive: [ {flag: OFF, name: CaseInsensitive} ] # Case insensitive or case sensitive file filtering
4849
compiler: [ {c: gcc, cpp: g++, name: GCC}, {c: clang, cpp: clang++, name: Clang} ] # The default compiler is gcc/g++
4950
cppstd: [20, 11]
5051
shared_lib: [ {flag: OFF, name: Static} ]
5152
include:
5253
- os: {label: ubuntu-latest, name: latest}
5354
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
5455
autoappend: {flag: ON, name: AutoAppendExtn}
56+
casesensitive: {flag: OFF, name: CaseInsensitive}
5557
compiler: {c: gcc, cpp: g++, name: GCC}
5658
cppstd: 11
5759
shared_lib: {flag: OFF, name: Static}
5860
- os: {label: ubuntu-latest, name: latest}
5961
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
6062
autoappend: {flag: ON, name: AutoAppendExtn}
63+
casesensitive: {flag: OFF, name: CaseInsensitive}
6164
compiler: {c: clang, cpp: clang++, name: Clang}
6265
cppstd: 11
6366
shared_lib: {flag: OFF, name: Static}
6467
- os: {label: ubuntu-latest, name: latest}
6568
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
6669
autoappend: {flag: OFF, name: NoAppendExtn}
70+
casesensitive: {flag: OFF, name: CaseInsensitive}
6771
compiler: {c: gcc, cpp: g++, name: GCC}
6872
cppstd: 11
6973
shared_lib: {flag: ON, name: Shared}
74+
- os: {label: ubuntu-latest, name: latest}
75+
portal: {flag: OFF, dep: libgtk-3-dev, name: GTK}
76+
autoappend: {flag: OFF, name: NoAppendExtn}
77+
casesensitive: {flag: ON, name: CaseSensitive}
78+
compiler: {c: gcc, cpp: g++, name: GCC}
79+
cppstd: 11
80+
shared_lib: {flag: OFF, name: Static}
81+
- os: {label: ubuntu-latest, name: latest}
82+
portal: {flag: ON, dep: libdbus-1-dev, name: Portal}
83+
autoappend: {flag: OFF, name: NoAppendExtn}
84+
casesensitive: {flag: ON, name: CaseSensitive}
85+
compiler: {c: gcc, cpp: g++, name: GCC}
86+
cppstd: 11
87+
shared_lib: {flag: OFF, name: Static}
7088

7189
steps:
7290
- name: Checkout
7391
uses: actions/checkout@v4
7492
- name: Install Dependencies
7593
run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }}
7694
- name: Configure
77-
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 ..
95+
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 ..
7896
- name: Build
7997
run: cmake --build build --target install
8098
- name: Upload test binaries
8199
uses: actions/upload-artifact@v4
82100
with:
83-
name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }}
101+
name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.casesensitive.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }}
84102
path: |
85103
build/src/*
86104
build/test/*

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ A wildcard filter is always added to every dialog.
259259
260260
*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.*
261261
262+
*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.*
263+
262264
## Iterating Over PathSets
263265
264266
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:

src/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ if(nfd_PLATFORM STREQUAL PLATFORM_LINUX)
9494
if(NFD_APPEND_EXTENSION)
9595
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_APPEND_EXTENSION)
9696
endif()
97+
option(NFD_CASE_SENSITIVE_FILTER "Make filters case sensitive" OFF)
98+
if(NFD_CASE_SENSITIVE_FILTER)
99+
target_compile_definitions(${TARGET_NAME} PRIVATE NFD_CASE_SENSITIVE_FILTER)
100+
endif()
97101
endif()
98102

99103
if(nfd_PLATFORM STREQUAL PLATFORM_MACOS)

src/nfd_gtk.cpp

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919

2020
#include "nfd.h"
2121

22+
/*
23+
Define NFD_CASE_SENSITIVE_FILTER if you want file filters to be case-sensitive. The default
24+
is case-insensitive. While Linux uses a case-sensitive filesystem and is designed for
25+
case-sensitive file extensions, perhaps in the vast majority of cases users actually expect the file
26+
filters to be case-insensitive.
27+
*/
28+
2229
namespace {
2330

2431
template <typename T>
@@ -66,6 +73,27 @@ T* copy(const T* begin, const T* end, T* out) {
6673
return out;
6774
}
6875

76+
#ifndef NFD_CASE_SENSITIVE_FILTER
77+
nfdnchar_t* emit_case_insensitive_glob(const nfdnchar_t* begin,
78+
const nfdnchar_t* end,
79+
nfdnchar_t* out) {
80+
// this code will only make regular Latin characters case-insensitive; other
81+
// characters remain case sensitive
82+
for (; begin != end; ++begin) {
83+
if ((*begin >= 'A' && *begin <= 'Z') || (*begin >= 'a' && *begin <= 'z')) {
84+
*out++ = '[';
85+
*out++ = *begin;
86+
// invert the case of the original character
87+
*out++ = *begin ^ static_cast<nfdnchar_t>(0x20);
88+
*out++ = ']';
89+
} else {
90+
*out++ = *begin;
91+
}
92+
}
93+
return out;
94+
}
95+
#endif
96+
6997
// Does not own the filter and extension.
7098
struct Pair_GtkFileFilter_FileExtension {
7199
GtkFileFilter* filter;
@@ -122,6 +150,7 @@ void AddFiltersToDialog(GtkFileChooser* chooser,
122150
*p_nameBuf++ = ' ';
123151
}
124152

153+
#ifdef NFD_CASE_SENSITIVE_FILTER
125154
// +1 for the trailing '\0'
126155
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(sizeof(nfdnchar_t) *
127156
(p_spec - p_extensionStart + 3));
@@ -130,10 +159,23 @@ void AddFiltersToDialog(GtkFileChooser* chooser,
130159
*p_extnBufEnd++ = '.';
131160
p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd);
132161
*p_extnBufEnd++ = '\0';
133-
assert((size_t)(p_extnBufEnd - extnBuf) ==
134-
sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3));
135162
gtk_file_filter_add_pattern(filter, extnBuf);
136163
NFDi_Free(extnBuf);
164+
#else
165+
// Each character in the Latin alphabet is converted into 4 characters. E.g.
166+
// 'a' is converted into "[Aa]". Other characters are preserved. Then we +1
167+
// for the trailing '\0'.
168+
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(
169+
sizeof(nfdnchar_t) * ((p_spec - p_extensionStart) * 4 + 3));
170+
nfdnchar_t* p_extnBufEnd = extnBuf;
171+
*p_extnBufEnd++ = '*';
172+
*p_extnBufEnd++ = '.';
173+
p_extnBufEnd =
174+
emit_case_insensitive_glob(p_extensionStart, p_spec, p_extnBufEnd);
175+
*p_extnBufEnd++ = '\0';
176+
gtk_file_filter_add_pattern(filter, extnBuf);
177+
NFDi_Free(extnBuf);
178+
#endif
137179

138180
if (*p_spec) {
139181
// update the extension start point
@@ -222,6 +264,7 @@ Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* choo
222264
*p_nameBuf++ = ' ';
223265
}
224266

267+
#ifdef NFD_CASE_SENSITIVE_FILTER
225268
// +1 for the trailing '\0'
226269
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(sizeof(nfdnchar_t) *
227270
(p_spec - p_extensionStart + 3));
@@ -230,10 +273,23 @@ Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* choo
230273
*p_extnBufEnd++ = '.';
231274
p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd);
232275
*p_extnBufEnd++ = '\0';
233-
assert((size_t)(p_extnBufEnd - extnBuf) ==
234-
sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3));
235276
gtk_file_filter_add_pattern(filter, extnBuf);
236277
NFDi_Free(extnBuf);
278+
#else
279+
// Each character in the Latin alphabet is converted into 4 characters. E.g.
280+
// 'a' is converted into "[Aa]". Other characters are preserved. Then we +1
281+
// for the trailing '\0'.
282+
nfdnchar_t* extnBuf = NFDi_Malloc<nfdnchar_t>(
283+
sizeof(nfdnchar_t) * ((p_spec - p_extensionStart) * 4 + 3));
284+
nfdnchar_t* p_extnBufEnd = extnBuf;
285+
*p_extnBufEnd++ = '*';
286+
*p_extnBufEnd++ = '.';
287+
p_extnBufEnd =
288+
emit_case_insensitive_glob(p_extensionStart, p_spec, p_extnBufEnd);
289+
*p_extnBufEnd++ = '\0';
290+
gtk_file_filter_add_pattern(filter, extnBuf);
291+
NFDi_Free(extnBuf);
292+
#endif
237293

238294
// store current pointer in map (if it's
239295
// the first one)

src/nfd_portal.cpp

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ know that we appended an extension, so they will not check or whitelist the corr
3636
NFD_APPEND_EXTENSION is not recommended for portals.
3737
*/
3838

39+
/*
40+
Define NFD_CASE_SENSITIVE_FILTER if you want file filters to be case-sensitive. The default
41+
is case-insensitive. While Linux uses a case-sensitive filesystem and is designed for
42+
case-sensitive file extensions, perhaps in the vast majority of cases users actually expect the file
43+
filters to be case-insensitive.
44+
*/
45+
3946
namespace {
4047

4148
template <typename T = void>
@@ -124,6 +131,27 @@ T* reverse_copy(const T* begin, const T* end, T* out) {
124131
return out;
125132
}
126133

134+
#ifndef NFD_CASE_SENSITIVE_FILTER
135+
nfdnchar_t* emit_case_insensitive_glob(const nfdnchar_t* begin,
136+
const nfdnchar_t* end,
137+
nfdnchar_t* out) {
138+
// this code will only make regular Latin characters case-insensitive; other
139+
// characters remain case sensitive
140+
for (; begin != end; ++begin) {
141+
if ((*begin >= 'A' && *begin <= 'Z') || (*begin >= 'a' && *begin <= 'z')) {
142+
*out++ = '[';
143+
*out++ = *begin;
144+
// invert the case of the original character
145+
*out++ = *begin ^ static_cast<nfdnchar_t>(0x20);
146+
*out++ = ']';
147+
} else {
148+
*out++ = *begin;
149+
}
150+
}
151+
return out;
152+
}
153+
#endif
154+
127155
// Returns true if ch is in [0-9A-Za-z], false otherwise.
128156
bool IsHex(char ch) {
129157
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
316344
do {
317345
++extn_end;
318346
} while (*extn_end != ',' && *extn_end != '\0');
319-
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) + 1));
320-
char* buf_end = buf;
321-
*buf_end++ = '*';
322-
*buf_end++ = '.';
323-
buf_end = copy(extn_begin, extn_end, buf_end);
324-
*buf_end = '\0';
325-
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
347+
{
348+
#ifdef NFD_CASE_SENSITIVE_FILTER
349+
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) + 1));
350+
char* buf_end = buf;
351+
*buf_end++ = '*';
352+
*buf_end++ = '.';
353+
buf_end = copy(extn_begin, extn_end, buf_end);
354+
*buf_end = '\0';
355+
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
356+
#else
357+
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) * 4 + 1));
358+
char* buf_end = buf;
359+
*buf_end++ = '*';
360+
*buf_end++ = '.';
361+
buf_end = emit_case_insensitive_glob(extn_begin, extn_end, buf_end);
362+
*buf_end = '\0';
363+
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
364+
#endif
365+
}
326366
dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter);
327367
if (*extn_end == '\0') {
328368
break;
@@ -385,13 +425,25 @@ bool AppendSingleFilterCheckExtn(DBusMessageIter& base_iter,
385425
do {
386426
++extn_end;
387427
} while (*extn_end != ',' && *extn_end != '\0');
388-
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) + 1));
389-
char* buf_end = buf;
390-
*buf_end++ = '*';
391-
*buf_end++ = '.';
392-
buf_end = copy(extn_begin, extn_end, buf_end);
393-
*buf_end = '\0';
394-
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
428+
{
429+
#ifdef NFD_CASE_SENSITIVE_FILTER
430+
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) + 1));
431+
char* buf_end = buf;
432+
*buf_end++ = '*';
433+
*buf_end++ = '.';
434+
buf_end = copy(extn_begin, extn_end, buf_end);
435+
*buf_end = '\0';
436+
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
437+
#else
438+
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) * 4 + 1));
439+
char* buf_end = buf;
440+
*buf_end++ = '*';
441+
*buf_end++ = '.';
442+
buf_end = emit_case_insensitive_glob(extn_begin, extn_end, buf_end);
443+
*buf_end = '\0';
444+
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
445+
#endif
446+
}
395447
dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter);
396448
if (!extn_matched) {
397449
const char* match_extn_p;

0 commit comments

Comments
 (0)