From a50590370f56723511f5b8104c2be8a3f5470a9d Mon Sep 17 00:00:00 2001 From: Mikolaj Boc Date: Wed, 22 Jun 2022 14:33:48 +0200 Subject: Create the Qt File Filter => showOpen/SaveFilePicker options mapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/gui/CMakeLists.txt | 1 + src/gui/platform/wasm/qlocalfileapi.cpp | 197 ++++++++++++++++++++++++++++ src/gui/platform/wasm/qlocalfileapi_p.h | 87 +++++++++++++ tests/auto/wasm/CMakeLists.txt | 14 ++ tests/auto/wasm/tst_localfileapi.cpp | 221 ++++++++++++++++++++++++++++++++ 5 files changed, 520 insertions(+) create mode 100644 src/gui/platform/wasm/qlocalfileapi.cpp create mode 100644 src/gui/platform/wasm/qlocalfileapi_p.h create mode 100644 tests/auto/wasm/tst_localfileapi.cpp 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 +#include + +QT_BEGIN_NAMESPACE +namespace LocalFileApi { +namespace { +std::optional 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("push", type->asVal()); + } + + return types["length"].as() == 0 ? std::optional() : types; +} +} + +Type::Type(QStringView description, std::optional 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::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::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("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::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 +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +namespace LocalFileApi { +class Q_CORE_EXPORT Type { +public: + class Accept { + public: + class MimeType { + public: + class Extension { + public: + static std::optional 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 fromQt(QStringView type); + + ~Accept(); + + void addMimeType(MimeType mimeType); + + emscripten::val asVal() const; + + private: + Accept(); + emscripten::val m_storage; + }; + + Type(QStringView description, std::optional accept); + ~Type(); + + static std::optional 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 +#include +#include + +class tst_LocalFileApi : public QObject +{ + Q_OBJECT + +private: + emscripten::val makeAccept(std::vector 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 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 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 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("stringify", lhs); + auto rhsAsJsonString = json.call("stringify", rhs); + + return lhsAsJsonString.equals(rhsAsJsonString); +} + +void tst_LocalFileApi::fileExtensionFilterTransformation_data() +{ + QTest::addColumn("qtFileFilter"); + QTest::addColumn>("expectedWebExtensionFilter"); + + QTest::newRow("PNG extension with an asterisk") << QString("*.png") << std::make_optional(".png"); + QTest::newRow("Long extension with an asterisk") << QString("*.someotherfile") << std::make_optional(".someotherfile"); + QTest::newRow(".dat with no asterisk") << QString(".dat") << std::make_optional(".dat"); + QTest::newRow("Multiple asterisks") << QString("*ot*.abc") << std::optional(); + QTest::newRow("Filename") << QString("abcd.abc") << std::optional(); + QTest::newRow("match all") << QString("*.*") << std::optional(); +} + +void tst_LocalFileApi::fileExtensionFilterTransformation() +{ + QFETCH(QString, qtFileFilter); + QFETCH(std::optional, expectedWebExtensionFilter); + + auto result = LocalFileApi::Type::Accept::MimeType::Extension::fromQt(qtFileFilter); + if (expectedWebExtensionFilter) { + QCOMPARE_EQ(expectedWebExtensionFilter, result->asVal().as()); + } else { + QVERIFY(!result.has_value()); + } +} + +void tst_LocalFileApi::acceptTransformation_data() +{ + using namespace emscripten; + + QTest::addColumn("qtFilterList"); + QTest::addColumn("expectedWebType"); + + QTest::newRow("Multiple types") << QString("*.png *.other *.txt") + << makeAccept(std::vector { val(".png"), val(".other"), val(".txt") }); + + QTest::newRow("Single type") << QString("*.png") + << makeAccept(std::vector { 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(".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("qtFilterList"); + QTest::addColumn("expectedWebType"); + + QTest::newRow("With description") << QString("Text files (*.txt)") + << makeType("Text files", std::vector { val(".txt") }); + + QTest::newRow("No description") << QString("*.jpg") + << makeType("", std::vector { 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("qtFilterList"); + QTest::addColumn("multiple"); + QTest::addColumn("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("qtFilterList"); + QTest::addColumn("suggestedName"); + QTest::addColumn("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" -- cgit v1.2.3