diff options
Diffstat (limited to 'src/plugins/platforms/wasm/qwasmdom.cpp')
-rw-r--r-- | src/plugins/platforms/wasm/qwasmdom.cpp | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/src/plugins/platforms/wasm/qwasmdom.cpp b/src/plugins/platforms/wasm/qwasmdom.cpp new file mode 100644 index 0000000000..96790ca71f --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.cpp @@ -0,0 +1,310 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmdom.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/qpoint.h> +#include <QtCore/qrect.h> +#include <QtGui/qimage.h> +#include <private/qstdweb_p.h> +#include <QtCore/qurl.h> + +#include <utility> +#include <emscripten/wire.h> + +QT_BEGIN_NAMESPACE + +namespace dom { +namespace { +std::string dropActionToDropEffect(Qt::DropAction action) +{ + switch (action) { + case Qt::DropAction::CopyAction: + return "copy"; + case Qt::DropAction::IgnoreAction: + return "none"; + case Qt::DropAction::LinkAction: + return "link"; + case Qt::DropAction::MoveAction: + case Qt::DropAction::TargetMoveAction: + return "move"; + case Qt::DropAction::ActionMask: + Q_ASSERT(false); + return ""; + } +} +} // namespace + +DataTransfer::DataTransfer(emscripten::val webDataTransfer) + : webDataTransfer(webDataTransfer) { +} + +DataTransfer::~DataTransfer() = default; + +DataTransfer::DataTransfer(const DataTransfer &other) = default; + +DataTransfer::DataTransfer(DataTransfer &&other) = default; + +DataTransfer &DataTransfer::operator=(const DataTransfer &other) = default; + +DataTransfer &DataTransfer::operator=(DataTransfer &&other) = default; + +void DataTransfer::setDragImage(emscripten::val element, const QPoint &hotspot) +{ + webDataTransfer.call<void>("setDragImage", element, emscripten::val(hotspot.x()), + emscripten::val(hotspot.y())); +} + +void DataTransfer::setData(std::string format, std::string data) +{ + webDataTransfer.call<void>("setData", emscripten::val(std::move(format)), + emscripten::val(std::move(data))); +} + +void DataTransfer::setDropAction(Qt::DropAction action) +{ + webDataTransfer.set("dropEffect", emscripten::val(dropActionToDropEffect(action))); +} + +void DataTransfer::setDataFromMimeData(const QMimeData &mimeData) +{ + for (const auto &format : mimeData.formats()) { + auto data = mimeData.data(format); + + auto encoded = format.startsWith("text/") + ? QString::fromLocal8Bit(data).toStdString() + : "QB64" + QString::fromLocal8Bit(data.toBase64()).toStdString(); + + setData(format.toStdString(), std::move(encoded)); + } +} + +// Converts a DataTransfer instance to a QMimeData instance. Invokes the +// given callback when the conversion is complete. The callback takes ownership +// of the QMimeData. +void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback) +{ + enum class ItemKind { + File, + String, + }; + + class MimeContext { + + public: + MimeContext(int itemCount, std::function<void(QMimeData *)> callback) + :m_remainingItemCount(itemCount), m_callback(callback) + { + + } + + void deref() { + if (--m_remainingItemCount > 0) + return; + + QList<QUrl> allUrls; + allUrls.append(mimeData->urls()); + allUrls.append(fileUrls); + mimeData->setUrls(allUrls); + + m_callback(mimeData); + + // Delete files; we expect that the user callback reads/copies + // file content before returning. + // Fixme: tie file lifetime to lifetime of the QMimeData? + for (QUrl fileUrl: fileUrls) + QFile(fileUrl.toLocalFile()).remove(); + + delete this; + } + + QMimeData *mimeData = new QMimeData(); + QList<QUrl> fileUrls; + + private: + int m_remainingItemCount; + std::function<void(QMimeData *)> m_callback; + }; + + const auto items = webDataTransfer["items"]; + const int itemCount = items["length"].as<int>(); + const int fileCount = webDataTransfer["files"]["length"].as<int>(); + MimeContext *mimeContext = new MimeContext(itemCount, callback); + + for (int i = 0; i < itemCount; ++i) { + const auto item = items[i]; + const auto itemKind = + item["kind"].as<std::string>() == "string" ? ItemKind::String : ItemKind::File; + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + + switch (itemKind) { + case ItemKind::File: { + qstdweb::File webfile(item.call<emscripten::val>("getAsFile")); + + if (webfile.size() > 1e+9) { // limit file size to 1 GB + qWarning() << "File is too large (> 1GB) and will be skipped. File size is" << webfile.size(); + mimeContext->deref(); + continue; + } + + QString mimeFormat = QString::fromStdString(webfile.type()); + QString fileName = QString::fromStdString(webfile.name()); + + // there's a file, now read it + QByteArray fileContent(webfile.size(), Qt::Uninitialized); + webfile.stream(fileContent.data(), [=]() { + + // If we get a single file, and that file is an image, then + // try to decode the image data. This handles the case where + // image data (i.e. not an image file) is pasted. The browsers + // will then create a fake "image.png" file which has the image + // data. As a side effect Qt will also decode the image for + // single-image-file drops, since there is no way to differentiate + // the fake "image.png" from a real one. + if (fileCount == 1 && mimeFormat.contains("image/")) { + QImage image; + if (image.loadFromData(fileContent)) + mimeContext->mimeData->setImageData(image); + } + + QDir qtTmpDir("/qt/tmp/"); // "tmp": indicate that these files won't stay around + qtTmpDir.mkpath(qtTmpDir.path()); + + QUrl fileUrl = QUrl::fromLocalFile(qtTmpDir.filePath(QString::fromStdString(webfile.name()))); + mimeContext->fileUrls.append(fileUrl); + + QFile file(fileUrl.toLocalFile()); + if (!file.open(QFile::WriteOnly)) { + qWarning() << "File was not opened"; + mimeContext->deref(); + return; + } + if (file.write(fileContent) < 0) { + qWarning() << "Write failed"; + file.close(); + } + mimeContext->deref(); + }); + break; + } + case ItemKind::String: + if (itemMimeType.contains("STRING", Qt::CaseSensitive) + || itemMimeType.contains("TEXT", Qt::CaseSensitive)) { + mimeContext->deref(); + break; + } + QString a; + QString data = QString::fromEcmaString(webDataTransfer.call<emscripten::val>( + "getData", emscripten::val(itemMimeType.toStdString()))); + + if (!data.isEmpty()) { + if (itemMimeType == "text/html") + mimeContext->mimeData->setHtml(data); + else if (itemMimeType.isEmpty() || itemMimeType == "text/plain") + mimeContext->mimeData->setText(data); // the type can be empty + else if (itemMimeType.isEmpty() || itemMimeType == "text/uri-list") { + QList<QUrl> urls; + urls.append(data); + mimeContext->mimeData->setUrls(urls); + } else { + // TODO improve encoding + if (data.startsWith("QB64")) { + data.remove(0, 4); + mimeContext->mimeData->setData(itemMimeType, + QByteArray::fromBase64(QByteArray::fromStdString( + data.toStdString()))); + } else { + mimeContext->mimeData->setData(itemMimeType, data.toLocal8Bit()); + } + } + } + mimeContext->deref(); + break; + } + } // for items +} + +QMimeData *DataTransfer::toMimeDataPreview() +{ + auto data = new QMimeData(); + + QList<QUrl> uriList; + for (int i = 0; i < webDataTransfer["items"]["length"].as<int>(); ++i) { + const auto item = webDataTransfer["items"][i]; + if (item["kind"].as<std::string>() == "file") { + uriList.append(QUrl("blob://placeholder")); + } else { + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + data->setData(itemMimeType, QByteArray()); + } + } + data->setUrls(uriList); + return data; +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag) +{ + if (flag) { + element["classList"].call<void>("add", emscripten::val(std::move(cssClassName))); + return; + } + + element["classList"].call<void>("remove", emscripten::val(std::move(cssClassName))); +} + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point) +{ + const auto sourceBoundingRect = + QRectF::fromDOMRect(source.call<emscripten::val>("getBoundingClientRect")); + const auto targetBoundingRect = + QRectF::fromDOMRect(target.call<emscripten::val>("getBoundingClientRect")); + + const auto offset = sourceBoundingRect.topLeft() - targetBoundingRect.topLeft(); + return point + offset; +} + +void drawImageToWebImageDataArray(const QImage &sourceImage, emscripten::val destinationImageData, + const QRect &sourceRect) +{ + Q_ASSERT_X(destinationImageData["constructor"]["name"].as<std::string>() == "ImageData", + Q_FUNC_INFO, "The destination should be an ImageData instance"); + + constexpr int BytesPerColor = 4; + if (sourceRect.width() == sourceImage.width()) { + // Copy a contiguous chunk of memory + // ............... + // OOOOOOOOOOOOOOO + // OOOOOOOOOOOOOOO -> image data + // OOOOOOOOOOOOOOO + // ............... + auto imageMemory = emscripten::typed_memory_view(sourceRect.width() * sourceRect.height() + * BytesPerColor, + sourceImage.constScanLine(sourceRect.y())); + destinationImageData["data"].call<void>( + "set", imageMemory, sourceRect.y() * sourceImage.width() * BytesPerColor); + } else { + // Go through the scanlines manually to set the individual lines in bulk. This is + // marginally less performant than the above. + // ............... + // ...OOOOOOOOO... r = 0 -> image data + // ...OOOOOOOOO... r = 1 -> image data + // ...OOOOOOOOO... r = 2 -> image data + // ............... + for (int row = 0; row < sourceRect.height(); ++row) { + auto scanlineMemory = + emscripten::typed_memory_view(sourceRect.width() * BytesPerColor, + sourceImage.constScanLine(row + sourceRect.y()) + + BytesPerColor * sourceRect.x()); + destinationImageData["data"].call<void>("set", scanlineMemory, + (sourceRect.y() + row) * sourceImage.width() + * BytesPerColor + + sourceRect.x() * BytesPerColor); + } + } +} + +} // namespace dom + +QT_END_NAMESPACE |