summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMikolaj Boc <mikolaj.boc@qt.io>2022-06-22 14:33:48 +0200
committerMikolaj Boc <mikolaj.boc@qt.io>2022-07-05 22:22:54 +0200
commita50590370f56723511f5b8104c2be8a3f5470a9d (patch)
tree654db830612fdca3ea4dab7dd32b95fde1ba5be3
parentd48ebb02fb171d0b6fe3749a28a312fc624f8b8e (diff)
Create the Qt File Filter => showOpen/SaveFilePicker options mapper
As a preparatory measure for using showXFilePicker, the Qt file filter has to be transformed to the format used by the showXFilePicker (sXFP) options. A class structure reflecting the options was created. Based on an input in the form of a qt file filter, it will parse the filter to the sXFP options format. Unit tests were added and the code is not yet used in non-test env, next change will use it. Task-number: QTBUG-99611 Change-Id: I277286467a7b5ce6f323c19bdd31740a41b6a6be Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
-rw-r--r--src/gui/CMakeLists.txt1
-rw-r--r--src/gui/platform/wasm/qlocalfileapi.cpp197
-rw-r--r--src/gui/platform/wasm/qlocalfileapi_p.h87
-rw-r--r--tests/auto/wasm/CMakeLists.txt14
-rw-r--r--tests/auto/wasm/tst_localfileapi.cpp221
5 files changed, 520 insertions, 0 deletions
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 36dae764d7..95579b0c9e 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -969,6 +969,7 @@ add_custom_command(
qt_internal_extend_target(Gui CONDITION WASM
SOURCES
+ platform/wasm/qlocalfileapi.cpp platform/wasm/qlocalfileapi_p.h
platform/wasm/qwasmlocalfileaccess.cpp platform/wasm/qwasmlocalfileaccess_p.h
)
diff --git a/src/gui/platform/wasm/qlocalfileapi.cpp b/src/gui/platform/wasm/qlocalfileapi.cpp
new file mode 100644
index 0000000000..ec7ae40391
--- /dev/null
+++ b/src/gui/platform/wasm/qlocalfileapi.cpp
@@ -0,0 +1,197 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qlocalfileapi_p.h"
+#include <private/qstdweb_p.h>
+#include <QtCore/QRegularExpression>
+
+QT_BEGIN_NAMESPACE
+namespace LocalFileApi {
+namespace {
+std::optional<emscripten::val> qtFilterListToTypes(const QStringList &filterList)
+{
+ using namespace qstdweb;
+ using namespace emscripten;
+
+ auto types = emscripten::val::array();
+
+ for (const auto &fileFilter : filterList) {
+ auto type = Type::fromQt(fileFilter);
+ if (type)
+ types.call<void>("push", type->asVal());
+ }
+
+ return types["length"].as<int>() == 0 ? std::optional<emscripten::val>() : types;
+}
+}
+
+Type::Type(QStringView description, std::optional<Accept> accept)
+ : m_storage(emscripten::val::object())
+{
+ m_storage.set("description", description.trimmed().toString().toStdString());
+ if (accept)
+ m_storage.set("accept", accept->asVal());
+}
+
+Type::~Type() = default;
+
+std::optional<Type> Type::fromQt(QStringView type)
+{
+ using namespace emscripten;
+
+ // Accepts either a string in format:
+ // GROUP3
+ // or in this format:
+ // GROUP1 (GROUP2)
+ // Group 1 is treated as the description, whereas group 2 or 3 are treated as the filter list.
+ static QRegularExpression regex(
+ QString(QStringLiteral("(?:(?:([^(]*)\\(([^()]+)\\)[^)]*)|([^()]+))")));
+ const auto match = regex.match(type);
+
+ if (!match.hasMatch())
+ return std::nullopt;
+
+ constexpr size_t DescriptionIndex = 1;
+ constexpr size_t FilterListFromParensIndex = 2;
+ constexpr size_t PlainFilterListIndex = 3;
+
+ const auto description = match.hasCaptured(DescriptionIndex)
+ ? match.capturedView(DescriptionIndex)
+ : QStringView();
+ const auto filterList = match.capturedView(match.hasCaptured(FilterListFromParensIndex)
+ ? FilterListFromParensIndex
+ : PlainFilterListIndex);
+
+ auto accept = Type::Accept::fromQt(filterList);
+ if (!accept)
+ return std::nullopt;
+
+ return Type(description, std::move(*accept));
+}
+
+emscripten::val Type::asVal() const
+{
+ return m_storage;
+}
+
+Type::Accept::Accept() : m_storage(emscripten::val::object()) { }
+
+Type::Accept::~Accept() = default;
+
+std::optional<Type::Accept> Type::Accept::fromQt(QStringView qtRepresentation)
+{
+ Accept accept;
+
+ // Used for accepting multiple extension specifications on a filter list.
+ // The next group of non-empty characters.
+ static QRegularExpression internalRegex(QString(QStringLiteral("([^\\s]+)\\s*")));
+ int offset = 0;
+ auto internalMatch = internalRegex.match(qtRepresentation, offset);
+ MimeType mimeType;
+
+ while (internalMatch.hasMatch()) {
+ auto webExtension = MimeType::Extension::fromQt(internalMatch.capturedView(1));
+
+ if (!webExtension)
+ return std::nullopt;
+
+ mimeType.addExtension(*webExtension);
+
+ internalMatch = internalRegex.match(qtRepresentation, internalMatch.capturedEnd());
+ }
+
+ accept.addMimeType(mimeType);
+ return accept;
+}
+
+void Type::Accept::addMimeType(MimeType mimeType)
+{
+ // The mime type provided here does not seem to have any effect at the result at all.
+ m_storage.set("application/octet-stream", mimeType.asVal());
+}
+
+emscripten::val Type::Accept::asVal() const
+{
+ return m_storage;
+}
+
+Type::Accept::MimeType::MimeType() : m_storage(emscripten::val::array()) { }
+
+Type::Accept::MimeType::~MimeType() = default;
+
+void Type::Accept::MimeType::addExtension(Extension extension)
+{
+ m_storage.call<void>("push", extension.asVal());
+}
+
+emscripten::val Type::Accept::MimeType::asVal() const
+{
+ return m_storage;
+}
+
+Type::Accept::MimeType::Extension::Extension(QStringView extension)
+ : m_storage(extension.toString().toStdString())
+{
+}
+
+Type::Accept::MimeType::Extension::~Extension() = default;
+
+std::optional<Type::Accept::MimeType::Extension>
+Type::Accept::MimeType::Extension::fromQt(QStringView qtRepresentation)
+{
+ // Checks for a filter that matches everything:
+ // Any number of asterisks or any number of asterisks with a '.' between them.
+ // The web filter does not support wildcards.
+ static QRegularExpression qtAcceptAllRegex(
+ QRegularExpression::anchoredPattern(QString(QStringLiteral("[*]+|[*]+\\.[*]+"))));
+ if (qtAcceptAllRegex.match(qtRepresentation).hasMatch())
+ return std::nullopt;
+
+ // Checks for correctness. The web filter only allows filename extensions and does not filter
+ // the actual filenames, therefore we check whether the filter provided only filters for the
+ // extension.
+ static QRegularExpression qtFilenameMatcherRegex(
+ QRegularExpression::anchoredPattern(QString(QStringLiteral("(\\*?)(\\.[^*]+)"))));
+
+ auto extensionMatch = qtFilenameMatcherRegex.match(qtRepresentation);
+ if (extensionMatch.hasMatch())
+ return Extension(extensionMatch.capturedView(2));
+
+ // Mapping impossible.
+ return std::nullopt;
+}
+
+emscripten::val Type::Accept::MimeType::Extension::asVal() const
+{
+ return m_storage;
+}
+
+emscripten::val makeOpenFileOptions(const QStringList &filterList, bool acceptMultiple)
+{
+ auto options = emscripten::val::object();
+ if (auto typeList = LocalFileApi::qtFilterListToTypes(filterList)) {
+ options.set("types", std::move(*typeList));
+ options.set("excludeAcceptAllOption", true);
+ }
+
+ options.set("multiple", acceptMultiple);
+
+ return options;
+}
+
+emscripten::val makeSaveFileOptions(const QStringList &filterList, const std::string& suggestedName)
+{
+ auto options = emscripten::val::object();
+
+ if (!suggestedName.empty())
+ options.set("suggestedName", emscripten::val(suggestedName));
+
+ if (auto typeList = LocalFileApi::qtFilterListToTypes(filterList))
+ options.set("types", emscripten::val(std::move(*typeList)));
+
+ return options;
+}
+
+} // namespace LocalFileApi
+
+QT_END_NAMESPACE
diff --git a/src/gui/platform/wasm/qlocalfileapi_p.h b/src/gui/platform/wasm/qlocalfileapi_p.h
new file mode 100644
index 0000000000..a8e7f666f9
--- /dev/null
+++ b/src/gui/platform/wasm/qlocalfileapi_p.h
@@ -0,0 +1,87 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QLOCALFILEAPI_P_H
+#define QLOCALFILEAPI_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <private/qglobal_p.h>
+#include <qstringview.h>
+#include <emscripten/val.h>
+#include <cstdint>
+#include <functional>
+
+QT_BEGIN_NAMESPACE
+
+namespace LocalFileApi {
+class Q_CORE_EXPORT Type {
+public:
+ class Accept {
+ public:
+ class MimeType {
+ public:
+ class Extension {
+ public:
+ static std::optional<Extension> fromQt(QStringView extension);
+
+ ~Extension();
+
+ emscripten::val asVal() const;
+
+ private:
+ explicit Extension(QStringView extension);
+
+ emscripten::val m_storage;
+ };
+
+ MimeType();
+ ~MimeType();
+
+ void addExtension(Extension type);
+
+ emscripten::val asVal() const;
+
+ private:
+ emscripten::val m_storage;
+ };
+
+ static std::optional<Accept> fromQt(QStringView type);
+
+ ~Accept();
+
+ void addMimeType(MimeType mimeType);
+
+ emscripten::val asVal() const;
+
+ private:
+ Accept();
+ emscripten::val m_storage;
+ };
+
+ Type(QStringView description, std::optional<Accept> accept);
+ ~Type();
+
+ static std::optional<Type> fromQt(QStringView type);
+ emscripten::val asVal() const;
+
+private:
+ emscripten::val m_storage;
+};
+
+Q_CORE_EXPORT emscripten::val makeOpenFileOptions(const QStringList &filterList, bool acceptMultiple);
+Q_CORE_EXPORT emscripten::val makeSaveFileOptions(const QStringList &filterList, const std::string& suggestedName);
+
+} // namespace LocalFileApi
+QT_END_NAMESPACE
+
+#endif // QLOCALFILEAPI_P_H
diff --git a/tests/auto/wasm/CMakeLists.txt b/tests/auto/wasm/CMakeLists.txt
index 2e6c6976fb..6c10838a92 100644
--- a/tests/auto/wasm/CMakeLists.txt
+++ b/tests/auto/wasm/CMakeLists.txt
@@ -2,6 +2,20 @@
## tst_wasm Test:
#####################################################################
+qt_internal_add_test(tst_localfileapi
+ SOURCES
+ tst_localfileapi.cpp
+ DEFINES
+ QT_NO_FOREACH
+ QT_NO_KEYWORDS
+ LIBRARIES
+ Qt::GuiPrivate
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
+
qt_internal_add_test(tst_qstdweb
SOURCES
tst_qstdweb.cpp
diff --git a/tests/auto/wasm/tst_localfileapi.cpp b/tests/auto/wasm/tst_localfileapi.cpp
new file mode 100644
index 0000000000..0d17202d4b
--- /dev/null
+++ b/tests/auto/wasm/tst_localfileapi.cpp
@@ -0,0 +1,221 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// Copyright (C) 2016 Intel Corporation.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QtGui/private/qlocalfileapi_p.h>
+#include <QTest>
+#include <emscripten/val.h>
+
+class tst_LocalFileApi : public QObject
+{
+ Q_OBJECT
+
+private:
+ emscripten::val makeAccept(std::vector<emscripten::val> types) {
+ auto accept = emscripten::val::object();
+ accept.set("application/octet-stream",
+ emscripten::val::array(std::move(types)));
+ return accept;
+ }
+
+ emscripten::val makeType(QString description, std::vector<emscripten::val> acceptExtensions) {
+ using namespace emscripten;
+
+ auto type = val::object();
+ type.set("description", description.toStdString());
+
+ auto accept = val::object();
+ accept.set("application/octet-stream",
+ val::array(std::move(acceptExtensions)));
+ type.set("accept", makeAccept(std::move(acceptExtensions)));
+
+ return type;
+ }
+
+ emscripten::val makeOpenFileOptions(bool acceptMultiple, std::vector<emscripten::val> types) {
+ using namespace emscripten;
+
+ auto webFilter = val::object();
+ webFilter.set("types", val::array(std::move(types)));
+ if (!types.empty())
+ webFilter.set("excludeAcceptAllOption", val(true));
+ webFilter.set("multiple", val(acceptMultiple));
+
+ return webFilter;
+ }
+
+ emscripten::val makeSaveFileOptions(QString suggestedName, std::vector<emscripten::val> types) {
+ using namespace emscripten;
+
+ auto webFilter = val::object();
+ webFilter.set("suggestedName", val(suggestedName.toStdString()));
+ webFilter.set("types", val::array(std::move(types)));
+
+ return webFilter;
+ }
+
+private Q_SLOTS:
+ void fileExtensionFilterTransformation_data();
+ void fileExtensionFilterTransformation();
+ void acceptTransformation_data();
+ void acceptTransformation();
+ void typeTransformation_data();
+ void typeTransformation();
+ void openFileOptions_data();
+ void openFileOptions();
+ void saveFileOptions_data();
+ void saveFileOptions();
+};
+
+bool valDeepEquals(emscripten::val lhs, emscripten::val rhs)
+{
+ auto json = emscripten::val::global("JSON");
+ auto lhsAsJsonString = json.call<emscripten::val>("stringify", lhs);
+ auto rhsAsJsonString = json.call<emscripten::val>("stringify", rhs);
+
+ return lhsAsJsonString.equals(rhsAsJsonString);
+}
+
+void tst_LocalFileApi::fileExtensionFilterTransformation_data()
+{
+ QTest::addColumn<QString>("qtFileFilter");
+ QTest::addColumn<std::optional<std::string>>("expectedWebExtensionFilter");
+
+ QTest::newRow("PNG extension with an asterisk") << QString("*.png") << std::make_optional<std::string>(".png");
+ QTest::newRow("Long extension with an asterisk") << QString("*.someotherfile") << std::make_optional<std::string>(".someotherfile");
+ QTest::newRow(".dat with no asterisk") << QString(".dat") << std::make_optional<std::string>(".dat");
+ QTest::newRow("Multiple asterisks") << QString("*ot*.abc") << std::optional<std::string>();
+ QTest::newRow("Filename") << QString("abcd.abc") << std::optional<std::string>();
+ QTest::newRow("match all") << QString("*.*") << std::optional<std::string>();
+}
+
+void tst_LocalFileApi::fileExtensionFilterTransformation()
+{
+ QFETCH(QString, qtFileFilter);
+ QFETCH(std::optional<std::string>, expectedWebExtensionFilter);
+
+ auto result = LocalFileApi::Type::Accept::MimeType::Extension::fromQt(qtFileFilter);
+ if (expectedWebExtensionFilter) {
+ QCOMPARE_EQ(expectedWebExtensionFilter, result->asVal().as<std::string>());
+ } else {
+ QVERIFY(!result.has_value());
+ }
+}
+
+void tst_LocalFileApi::acceptTransformation_data()
+{
+ using namespace emscripten;
+
+ QTest::addColumn<QString>("qtFilterList");
+ QTest::addColumn<emscripten::val>("expectedWebType");
+
+ QTest::newRow("Multiple types") << QString("*.png *.other *.txt")
+ << makeAccept(std::vector<val> { val(".png"), val(".other"), val(".txt") });
+
+ QTest::newRow("Single type") << QString("*.png")
+ << makeAccept(std::vector<val> { val(".png") });
+
+ QTest::newRow("No filter when accepts all") << QString("*.*")
+ << val::undefined();
+
+ QTest::newRow("No filter when one filter accepts all") << QString("*.* *.jpg")
+ << val::undefined();
+
+ QTest::newRow("Weird spaces") << QString(" *.jpg *.png *.icon ")
+ << makeAccept(std::vector<val> { val(".jpg"), val(".png"), val(".icon") });
+}
+
+void tst_LocalFileApi::acceptTransformation()
+{
+ QFETCH(QString, qtFilterList);
+ QFETCH(emscripten::val, expectedWebType);
+
+ auto result = LocalFileApi::Type::Accept::fromQt(qtFilterList);
+ if (!expectedWebType.isUndefined()) {
+ QVERIFY(valDeepEquals(result->asVal(), expectedWebType));
+ } else {
+ QVERIFY(!result.has_value());
+ }
+}
+
+void tst_LocalFileApi::typeTransformation_data()
+{
+ using namespace emscripten;
+
+ QTest::addColumn<QString>("qtFilterList");
+ QTest::addColumn<emscripten::val>("expectedWebType");
+
+ QTest::newRow("With description") << QString("Text files (*.txt)")
+ << makeType("Text files", std::vector<val> { val(".txt") });
+
+ QTest::newRow("No description") << QString("*.jpg")
+ << makeType("", std::vector<val> { val(".jpg") });
+}
+
+void tst_LocalFileApi::typeTransformation()
+{
+ QFETCH(QString, qtFilterList);
+ QFETCH(emscripten::val, expectedWebType);
+
+ auto result = LocalFileApi::Type::fromQt(qtFilterList);
+ if (!expectedWebType.isUndefined()) {
+ QVERIFY(valDeepEquals(result->asVal(), expectedWebType));
+ } else {
+ QVERIFY(!result.has_value());
+ }
+}
+
+void tst_LocalFileApi::openFileOptions_data()
+{
+ using namespace emscripten;
+
+ QTest::addColumn<QStringList>("qtFilterList");
+ QTest::addColumn<bool>("multiple");
+ QTest::addColumn<emscripten::val>("expectedWebType");
+
+ QTest::newRow("Multiple files") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"})
+ << true
+ << makeOpenFileOptions(true, { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})});
+ QTest::newRow("Single file") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"})
+ << false
+ << makeOpenFileOptions(false, { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})});
+}
+
+void tst_LocalFileApi::openFileOptions()
+{
+ QFETCH(QStringList, qtFilterList);
+ QFETCH(bool, multiple);
+ QFETCH(emscripten::val, expectedWebType);
+
+ auto result = LocalFileApi::makeOpenFileOptions(qtFilterList, multiple);
+ QVERIFY(valDeepEquals(result, expectedWebType));
+}
+
+void tst_LocalFileApi::saveFileOptions_data()
+{
+ using namespace emscripten;
+
+ QTest::addColumn<QStringList>("qtFilterList");
+ QTest::addColumn<QString>("suggestedName");
+ QTest::addColumn<emscripten::val>("expectedWebType");
+
+ QTest::newRow("Multiple files") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"})
+ << "someName1"
+ << makeSaveFileOptions("someName1", { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})});
+ QTest::newRow("Single file") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"})
+ << "some name 2"
+ << makeSaveFileOptions("some name 2", { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})});
+}
+
+void tst_LocalFileApi::saveFileOptions()
+{
+ QFETCH(QStringList, qtFilterList);
+ QFETCH(QString, suggestedName);
+ QFETCH(emscripten::val, expectedWebType);
+
+ auto result = LocalFileApi::makeSaveFileOptions(qtFilterList, suggestedName.toStdString());
+ QVERIFY(valDeepEquals(result, expectedWebType));
+}
+
+QTEST_APPLESS_MAIN(tst_LocalFileApi)
+#include "tst_localfileapi.moc"