/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 or (at your option) any later version ** approved by the KDE Free Qt Foundation. The licenses are as published by ** the Free Software Foundation and appearing in the file LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qwasmclipboard.h" #include "qwasmwindow.h" #include "qwasmstring.h" #include #include #include #include #include #include #include #include #include using namespace emscripten; static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr) { QString formatString = QWasmString::toQString(format); QByteArray dataArray = QByteArray::fromStdString(dataPtr.as()); QMimeData *mMimeData = new QMimeData; mMimeData->setData(formatString, dataArray); QWasmClipboard::qWasmClipboardPaste(mMimeData); // QWasmIntegration::get()->getWasmClipboard()->isPaste = false; } static void qClipboardPasteResolve(emscripten::val blob) { // read Blob here auto fileReader = std::make_shared(); auto _blob = qstdweb::Blob(blob); QString formatString = QString::fromStdString(_blob.type()); fileReader->readAsArrayBuffer(_blob); char *chunkBuffer = nullptr; qstdweb::ArrayBuffer result = fileReader->result(); qstdweb::Uint8Array(result).copyTo(chunkBuffer); QMimeData *mMimeData = new QMimeData; mMimeData->setData(formatString, chunkBuffer); QWasmClipboard::qWasmClipboardPaste(mMimeData); } static void qClipboardPromiseResolve(emscripten::val clipboardItems) { int itemsCount = clipboardItems["length"].as(); for (int i = 0; i < itemsCount; i++) { int typesCount = clipboardItems[i]["types"]["length"].as(); // ClipboardItem std::string mimeFormat = clipboardItems[i]["types"][0].as(); if (mimeFormat.find(std::string("text")) != std::string::npos) { // simple val object, no further processing val navigator = val::global("navigator"); val textPromise = navigator["clipboard"].call("readText"); val readTextResolve = val::global("Module")["qtClipboardTextPromiseResolve"]; textPromise.call("then", readTextResolve); } else { // binary types require additional processing for (int j = 0; j < typesCount; j++) { val pasteResolve = emscripten::val::module_property("qtClipboardPasteResolve"); val pasteException = emscripten::val::module_property("qtClipboardPromiseException"); // get the blob clipboardItems[i] .call("getType", clipboardItems[i]["types"][j]) .call("then", pasteResolve) .call("catch", pasteException); } } } } static void qClipboardCopyPromiseResolve(emscripten::val something) { Q_UNUSED(something) qWarning() << "copy succeeeded"; } static emscripten::val qClipboardPromiseException(emscripten::val something) { qWarning() << "clipboard error" << QString::fromStdString(something["name"].as()) << QString::fromStdString(something["message"].as()); return something; } static void commonCopyEvent(val event) { QMimeData *_mimes = QWasmIntegration::get()->getWasmClipboard()->mimeData(QClipboard::Clipboard); if (!_mimes) return; // doing it this way seems to sanitize the text better that calling data() like down below if (_mimes->hasText()) { event["clipboardData"].call("setData", val("text/plain") , QWasmString::fromQString(_mimes->text())); } if (_mimes->hasHtml()) { event["clipboardData"].call("setData", val("text/html") , QWasmString::fromQString(_mimes->html())); } for (auto mimetype : _mimes->formats()) { if (mimetype.contains("text/")) continue; QByteArray ba = _mimes->data(mimetype); if (!ba.isEmpty()) event["clipboardData"].call("setData", QWasmString::fromQString(mimetype) , val(ba.constData())); } event.call("preventDefault"); QWasmIntegration::get()->getWasmClipboard()->m_isListener = false; } static void qClipboardCutTo(val event) { QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { // Send synthetic Ctrl+X to make the app cut data to Qt's clipboard QWindowSystemInterface::handleKeyEvent( 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "X"); } commonCopyEvent(event); } static void qClipboardCopyTo(val event) { QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { // Send synthetic Ctrl+C to make the app copy data to Qt's clipboard QWindowSystemInterface::handleKeyEvent( 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); } commonCopyEvent(event); } static void qClipboardPasteTo(val dataTransfer) { QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; val clipboardData = dataTransfer["clipboardData"]; val types = clipboardData["types"]; int typesCount = types["length"].as(); std::string stdMimeFormat; QMimeData *mMimeData = new QMimeData; for (int i = 0; i < typesCount; i++) { stdMimeFormat = types[i].as(); QString mimeFormat = QString::fromStdString(stdMimeFormat); if (mimeFormat.contains("STRING", Qt::CaseSensitive) || mimeFormat.contains("TEXT", Qt::CaseSensitive)) continue; if (mimeFormat.contains("text")) { // also "text/plain;charset=utf-8" // "UTF8_STRING" "MULTIPLE" val mimeData = clipboardData.call("getData", val(stdMimeFormat)); // as DataTransfer const QString qstr = QWasmString::toQString(mimeData); if (qstr.length() > 0) { if (mimeFormat.contains("text/html")) { mMimeData->setHtml(qstr); } else if (mimeFormat.isEmpty() || mimeFormat.contains("text/plain")) { mMimeData->setText(qstr); // the type can be empty } else { mMimeData->setData(mimeFormat, qstr.toLocal8Bit());} } } else { val items = clipboardData["items"]; int itemsCount = items["length"].as(); // handle data for (int i = 0; i < itemsCount; i++) { val item = items[i]; val clipboardFile = item.call("getAsFile"); // string kind is handled above if (clipboardFile.isUndefined() || item["kind"].as() == "string" ) { continue; } qstdweb::File file(clipboardFile); mimeFormat = QString::fromStdString(file.type()); QByteArray fileContent; fileContent.resize(file.size()); file.stream(fileContent.data(), [=]() { if (!fileContent.isEmpty()) { if (mimeFormat.contains("image")) { QImage image; image.loadFromData(fileContent, nullptr); mMimeData->setImageData(image); } else { mMimeData->setData(mimeFormat,fileContent.data()); } QWasmClipboard::qWasmClipboardPaste(mMimeData); } }); } // next item } } QWasmClipboard::qWasmClipboardPaste(mMimeData); QWasmIntegration::get()->getWasmClipboard()->m_isListener = false; } static void qClipboardTextPromiseResolve(emscripten::val clipdata) { pasteClipboardData(emscripten::val("text/plain"), clipdata); } EMSCRIPTEN_BINDINGS(qtClipboardModule) { function("qtPasteClipboardData", &pasteClipboardData); function("qtClipboardTextPromiseResolve", &qClipboardTextPromiseResolve); function("qtClipboardPromiseResolve", &qClipboardPromiseResolve); function("qtClipboardCopyPromiseResolve", &qClipboardCopyPromiseResolve); function("qtClipboardPromiseException", &qClipboardPromiseException); function("qtClipboardCutTo", &qClipboardCutTo); function("qtClipboardCopyTo", &qClipboardCopyTo); function("qtClipboardPasteTo", &qClipboardPasteTo); function("qtClipboardPasteResolve", &qClipboardPasteResolve); } QWasmClipboard::QWasmClipboard() : isPaste(false), m_isListener(false) { val clipboard = val::global("navigator")["clipboard"]; val permissions = val::global("navigator")["permissions"]; val hasInstallTrigger = val::global("window")["InstallTrigger"]; hasPermissionsApi = !permissions.isUndefined(); hasClipboardApi = (!clipboard.isUndefined() && !clipboard["readText"].isUndefined()); bool isFirefox = !hasInstallTrigger.isUndefined(); isSafari = !emscripten::val::global("window")["safari"].isUndefined(); // firefox has clipboard API if user sets these config tweaks: // dom.events.asyncClipboard.clipboardItem true // dom.events.asyncClipboard.read true // dom.events.testing.asyncClipboard // and permissions API, but does not currently support // the clipboardRead and clipboardWrite permissions if (hasClipboardApi && hasPermissionsApi && !isFirefox) initClipboardPermissions(); } QWasmClipboard::~QWasmClipboard() { } QMimeData *QWasmClipboard::mimeData(QClipboard::Mode mode) { if (mode != QClipboard::Clipboard) return nullptr; return QPlatformClipboard::mimeData(mode); } void QWasmClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) { QPlatformClipboard::setMimeData(mimeData, mode); // handle setText/ setData programmatically if (!isPaste) { if (hasClipboardApi) { writeToClipboardApi(); } else if (!m_isListener) { writeToClipboard(mimeData); } } isPaste = false; } bool QWasmClipboard::supportsMode(QClipboard::Mode mode) const { return mode == QClipboard::Clipboard; } bool QWasmClipboard::ownsMode(QClipboard::Mode mode) const { Q_UNUSED(mode); return false; } void QWasmClipboard::qWasmClipboardPaste(QMimeData *mData) { QWasmIntegration::get()->clipboard()->setMimeData(mData, QClipboard::Clipboard); QWindowSystemInterface::handleKeyEvent( 0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V"); } void QWasmClipboard::initClipboardPermissions() { if (!hasClipboardApi) return; val permissions = val::global("navigator")["permissions"]; val readPermissionsMap = val::object(); readPermissionsMap.set("name", val("clipboard-read")); permissions.call("query", readPermissionsMap); val writePermissionsMap = val::object(); writePermissionsMap.set("name", val("clipboard-write")); permissions.call("query", writePermissionsMap); } void QWasmClipboard::installEventHandlers(const emscripten::val &canvas) { emscripten::val cContext = val::undefined(); emscripten::val isChromium = val::global("window")["chrome"]; if (!isChromium.isUndefined()) { cContext = val::global("document"); } else { cContext = canvas; } // Fallback path for browsers which do not support direct clipboard access cContext.call("addEventListener", val("cut"), val::module_property("qtClipboardCutTo"), true); cContext.call("addEventListener", val("copy"), val::module_property("qtClipboardCopyTo"), true); cContext.call("addEventListener", val("paste"), val::module_property("qtClipboardPasteTo"), true); } void QWasmClipboard::writeToClipboardApi() { if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) return; // copy event // browser event handler detected ctrl c if clipboard API // or Qt call from keyboard event handler QMimeData *_mimes = QWasmIntegration::get()->getWasmClipboard()->mimeData(QClipboard::Clipboard); if (!_mimes) return; emscripten::val clipboardWriteArray = emscripten::val::array(); QByteArray ba; for (auto mimetype : _mimes->formats()) { // we need to treat binary and text differently, as the blob method below // fails for text mimetypes // ignore text types if (mimetype.contains("STRING", Qt::CaseSensitive) || mimetype.contains("TEXT", Qt::CaseSensitive)) continue; if (_mimes->hasHtml()) { // prefer html over text ba = _mimes->html().toLocal8Bit(); // force this mime mimetype = "text/html"; } else if (mimetype.contains("text/plain")) { ba = _mimes->text().toLocal8Bit(); } else if (mimetype.contains("image")) { QImage img = qvariant_cast( _mimes->imageData()); QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); img.save(&buffer, "PNG"); mimetype = "image/png"; // chrome only allows png // clipboard error "NotAllowedError" "Type application/x-qt-image not supported on write." // safari silently fails // so we use png internally for now } else { // DATA ba = _mimes->data(mimetype); } // Create file data Blob const char *content = ba.data(); int dataLength = ba.length(); if (dataLength < 1) { qDebug() << "no content found"; return; } emscripten::val document = emscripten::val::global("document"); emscripten::val window = emscripten::val::global("window"); emscripten::val fileContentView = emscripten::val(emscripten::typed_memory_view(dataLength, content)); emscripten::val fileContentCopy = emscripten::val::global("ArrayBuffer").new_(dataLength); emscripten::val fileContentCopyView = emscripten::val::global("Uint8Array").new_(fileContentCopy); fileContentCopyView.call("set", fileContentView); emscripten::val contentArray = emscripten::val::array(); contentArray.call("push", fileContentCopyView); // we have a blob, now create a ClipboardItem emscripten::val type = emscripten::val::array(); type.set("type", val(QWasmString::fromQString(mimetype))); emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type); emscripten::val clipboardItemObject = emscripten::val::object(); clipboardItemObject.set(val(QWasmString::fromQString(mimetype)), contentBlob); val clipboardItemData = val::global("ClipboardItem").new_(clipboardItemObject); clipboardWriteArray.call("push", clipboardItemData); // Clipboard write is only supported with one ClipboardItem at the moment // but somehow this still works? // break; } val copyResolve = emscripten::val::module_property("qtClipboardCopyPromiseResolve"); val copyException = emscripten::val::module_property("qtClipboardPromiseException"); val navigator = val::global("navigator"); navigator["clipboard"] .call("write", clipboardWriteArray) .call("then", copyResolve) .call("catch", copyException); } void QWasmClipboard::writeToClipboard(const QMimeData *data) { Q_UNUSED(data) // this works for firefox, chrome by generating // copy event, but not safari // execCommand has been deemed deprecated in the docs, but browsers do not seem // interested in removing it. There is no replacement, so we use it here. val document = val::global("document"); document.call("execCommand", val("copy")); }