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>
This commit is contained in:
parent
d48ebb02fb
commit
a50590370f
@ -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
|
||||
)
|
||||
|
||||
|
197
src/gui/platform/wasm/qlocalfileapi.cpp
Normal file
197
src/gui/platform/wasm/qlocalfileapi.cpp
Normal file
@ -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
|
87
src/gui/platform/wasm/qlocalfileapi_p.h
Normal file
87
src/gui/platform/wasm/qlocalfileapi_p.h
Normal file
@ -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
|
@ -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
|
||||
|
221
tests/auto/wasm/tst_localfileapi.cpp
Normal file
221
tests/auto/wasm/tst_localfileapi.cpp
Normal file
@ -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"
|
Loading…
Reference in New Issue
Block a user