// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmdom.h" #include #include #include #include #include #include #include #include #include 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("setDragImage", element, emscripten::val(hotspot.x()), emscripten::val(hotspot.y())); } void DataTransfer::setData(std::string format, std::string data) { webDataTransfer.call("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 callback) { enum class ItemKind { File, String, }; class MimeContext { public: MimeContext(int itemCount, std::function callback) :m_remainingItemCount(itemCount), m_callback(callback) { } void deref() { if (--m_remainingItemCount > 0) return; mimeData->setUrls(fileUrls); 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 fileUrls; private: int m_remainingItemCount; std::function m_callback; }; const auto items = webDataTransfer["items"]; const int itemCount = items["length"].as(); const int fileCount = webDataTransfer["files"]["length"].as(); MimeContext *mimeContext = new MimeContext(itemCount, callback); for (int i = 0; i < itemCount; ++i) { const auto item = items[i]; const auto itemKind = item["kind"].as() == "string" ? ItemKind::String : ItemKind::File; const auto itemMimeType = QString::fromStdString(item["type"].as()); switch (itemKind) { case ItemKind::File: { qstdweb::File webfile(item.call("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( "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 { // 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 uriList; for (int i = 0; i < webDataTransfer["items"]["length"].as(); ++i) { const auto item = webDataTransfer["items"][i]; if (item["kind"].as() == "file") { uriList.append(QUrl("blob://placeholder")); } else { const auto itemMimeType = QString::fromStdString(item["type"].as()); data->setData(itemMimeType, QByteArray()); } } data->setUrls(uriList); return data; } void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag) { if (flag) { element["classList"].call("add", emscripten::val(std::move(cssClassName))); return; } element["classList"].call("remove", emscripten::val(std::move(cssClassName))); } QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point) { const auto sourceBoundingRect = QRectF::fromDOMRect(source.call("getBoundingClientRect")); const auto targetBoundingRect = QRectF::fromDOMRect(target.call("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() == "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( "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("set", scanlineMemory, (sourceRect.y() + row) * sourceImage.width() * BytesPerColor + sourceRect.x() * BytesPerColor); } } } } // namespace dom QT_END_NAMESPACE