diff --git a/framework/interactive/CMakeLists.txt b/framework/interactive/CMakeLists.txt
index 17ddd55647..7ef24391f1 100644
--- a/framework/interactive/CMakeLists.txt
+++ b/framework/interactive/CMakeLists.txt
@@ -35,6 +35,8 @@ target_sources(muse_interactive PRIVATE
dev/testdialog.h
dev/testdialog.ui
+ internal/filedialogfilters.cpp
+ internal/filedialogfilters.h
internal/iinteractiveprovider.h
internal/interactive.cpp
internal/interactive.h
diff --git a/framework/interactive/internal/filedialogfilters.cpp b/framework/interactive/internal/filedialogfilters.cpp
new file mode 100644
index 0000000000..7655cd81e7
--- /dev/null
+++ b/framework/interactive/internal/filedialogfilters.cpp
@@ -0,0 +1,116 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "filedialogfilters.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "global/stringutils.h"
+
+using namespace muse::interactive;
+
+namespace {
+struct NameFilterParts {
+ std::string description;
+ std::string globs;
+};
+
+std::optional splitNameFilter(const std::string& filter)
+{
+ const bool endsWithGroup = !filter.empty() && filter.back() == ')';
+ const size_t open = endsWithGroup ? filter.rfind('(') : std::string::npos;
+
+ if (open == std::string::npos) {
+ return std::nullopt;
+ }
+
+ return NameFilterParts { filter.substr(0, open), filter.substr(open + 1, filter.size() - open - 2) };
+}
+
+std::vector splitGlobs(const std::string& globs)
+{
+ std::istringstream in(globs);
+ return { std::istream_iterator(in), std::istream_iterator() };
+}
+
+std::vector tokenizeGlob(const std::string& glob)
+{
+ std::vector tokens;
+
+ for (size_t pos = 0; pos < glob.size();) {
+ const size_t close = glob[pos] == '[' ? glob.find(']', pos) : std::string::npos;
+ const size_t length = close == std::string::npos ? 1 : close - pos + 1;
+ tokens.push_back(glob.substr(pos, length));
+ pos += length;
+ }
+
+ return tokens;
+}
+
+bool isPlainLetter(const std::string& token)
+{
+ return token.size() == 1 && std::isalpha(static_cast(token.front()));
+}
+
+std::string bothCases(const std::string& letter)
+{
+ const unsigned char c = static_cast(letter.front());
+ return { '[', static_cast(std::tolower(c)), static_cast(std::toupper(c)), ']' };
+}
+
+bool needsCaseInsensitiveRewrite(const std::string& glob)
+{
+ return std::ranges::any_of(tokenizeGlob(glob), isPlainLetter);
+}
+
+std::string caseInsensitiveGlobIfNeeded(const std::string& glob)
+{
+ return needsCaseInsensitiveRewrite(glob) ? caseInsensitiveGlob(glob) : glob;
+}
+}
+
+std::string muse::interactive::caseInsensitiveGlob(const std::string& glob)
+{
+ std::string result;
+ for (const std::string& token : tokenizeGlob(glob)) {
+ result += isPlainLetter(token) ? bothCases(token) : token;
+ }
+ return result;
+}
+
+std::string muse::interactive::caseInsensitiveNameFilter(const std::string& filter)
+{
+ const std::optional parts = splitNameFilter(filter);
+ if (!parts) {
+ return filter;
+ }
+
+ std::vector globs = splitGlobs(parts->globs);
+ std::ranges::transform(globs, globs.begin(), caseInsensitiveGlobIfNeeded);
+
+ return parts->description + '(' + muse::strings::join(globs, " ") + ')';
+}
diff --git a/framework/interactive/internal/filedialogfilters.h b/framework/interactive/internal/filedialogfilters.h
new file mode 100644
index 0000000000..12421ea418
--- /dev/null
+++ b/framework/interactive/internal/filedialogfilters.h
@@ -0,0 +1,30 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+
+namespace muse::interactive {
+std::string caseInsensitiveGlob(const std::string& glob);
+std::string caseInsensitiveNameFilter(const std::string& filter);
+}
diff --git a/framework/interactive/internal/interactive.cpp b/framework/interactive/internal/interactive.cpp
index e52b83b531..62ed8c3cc8 100644
--- a/framework/interactive/internal/interactive.cpp
+++ b/framework/interactive/internal/interactive.cpp
@@ -39,6 +39,7 @@
#include "diagnostics/diagnosticutils.h"
+#include "filedialogfilters.h"
#include "widgetdialogadapter.h"
#include "ui/view/widgetdialog.h"
@@ -318,9 +319,13 @@ static UriQuery makeSelectFileQuery(FileDialogMode mode, const std::string& titl
UriQuery q("muse://interactive/selectfile");
q.set("title", title);
+ const bool isOpenMode = mode == FileDialogMode::OpenFile || mode == FileDialogMode::OpenFiles;
+ const bool hidesFilterDetails = options & QFileDialog::HideNameFilterDetails;
+ const bool matchCaseInsensitively = isOpenMode && hidesFilterDetails;
+
ValList filterList;
for (const std::string& f : filter) {
- filterList.push_back(Val(f));
+ filterList.push_back(Val(matchCaseInsensitively ? caseInsensitiveNameFilter(f) : f));
}
q.set("nameFilters", filterList);
diff --git a/framework/interactive/tests/CMakeLists.txt b/framework/interactive/tests/CMakeLists.txt
index ac3ff16bb2..30b8492c2e 100644
--- a/framework/interactive/tests/CMakeLists.txt
+++ b/framework/interactive/tests/CMakeLists.txt
@@ -21,7 +21,12 @@
set(MODULE_TEST muse_interactive_tests)
set(MODULE_TEST_SRC
+ ${CMAKE_CURRENT_LIST_DIR}/filedialogfilters_tests.cpp
${CMAKE_CURRENT_LIST_DIR}/mocks/interactivemock.h
)
+set(MODULE_TEST_LINK
+ muse_interactive
+)
+
include(SetupGTest)
diff --git a/framework/interactive/tests/filedialogfilters_tests.cpp b/framework/interactive/tests/filedialogfilters_tests.cpp
new file mode 100644
index 0000000000..eaa5eb4458
--- /dev/null
+++ b/framework/interactive/tests/filedialogfilters_tests.cpp
@@ -0,0 +1,113 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+
+#include "interactive/internal/filedialogfilters.h"
+
+using namespace muse::interactive;
+
+class Interactive_FileDialogFiltersTests : public ::testing::Test
+{
+};
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveGlob_ExpandsLetters)
+{
+ EXPECT_EQ(caseInsensitiveGlob("*.mp3"), "*.[mM][pP]3");
+ EXPECT_EQ(caseInsensitiveGlob("*.3gp"), "*.3[gG][pP]");
+ EXPECT_EQ(caseInsensitiveGlob("*.film_cpk"), "*.[fF][iI][lL][mM]_[cC][pP][kK]");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveGlob_NormalisesUpperCaseInput)
+{
+ EXPECT_EQ(caseInsensitiveGlob("*.MTV"), "*.[mM][tT][vV]");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveGlob_LeavesNonLettersUntouched)
+{
+ EXPECT_EQ(caseInsensitiveGlob("*"), "*");
+ EXPECT_EQ(caseInsensitiveGlob("*.302"), "*.302");
+ EXPECT_EQ(caseInsensitiveGlob(""), "");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveGlob_LeavesBracketExpressionsUntouched)
+{
+ EXPECT_EQ(caseInsensitiveGlob("*.[mM][pP]3"), "*.[mM][pP]3");
+ EXPECT_EQ(caseInsensitiveGlob("*.[0-9]"), "*.[0-9]");
+ EXPECT_EQ(caseInsensitiveGlob("[a-z]*"), "[a-z]*");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveGlob_ExpandsLettersAroundBracketExpressions)
+{
+ EXPECT_EQ(caseInsensitiveGlob("*.m[34]a"), "*.[mM][34][aA]");
+ EXPECT_EQ(caseInsensitiveGlob("*.[mM]p[34]"), "*.[mM][pP][34]");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_SingleGroup)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("Audio files (*.mp3 *.wav)"),
+ "Audio files (*.[mM][pP]3 *.[wW][aA][vV])");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_GlobWithBracketExpression)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("MPEG-4 audio (*.m4a *.m[34]a)"),
+ "MPEG-4 audio (*.[mM]4[aA] *.[mM][34][aA])");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_RewritesOnlyTrailingGroup)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("All supported files (*.mp3,*.aac, ...) (*.aac *.ac3 *.mp2)"),
+ "All supported files (*.mp3,*.aac, ...) (*.[aA][aA][cC] *.[aA][cC]3 *.[mM][pP]2)");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_AllFiles)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("All files (*)"), "All files (*)");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_EmptyGroup)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("Name ()"), "Name ()");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_CollapsesWhitespaceBetweenGlobs)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("Audio (*.mp3 *.wav )"), "Audio (*.[mM][pP]3 *.[wW][aA][vV])");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_LeavesBareGlobListUntouched)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter("*.mp3 *.wav"), "*.mp3 *.wav");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_EmptyString)
+{
+ EXPECT_EQ(caseInsensitiveNameFilter(""), "");
+}
+
+TEST_F(Interactive_FileDialogFiltersTests, CaseInsensitiveNameFilter_IsIdempotent)
+{
+ const std::string once = caseInsensitiveNameFilter("Audio files (*.mp3 *.wav *.m[34]a)");
+ EXPECT_EQ(caseInsensitiveNameFilter(once), once);
+}