diff options
author | Lorn Potter <lorn.potter@gmail.com> | 2021-05-14 18:37:12 +1000 |
---|---|---|
committer | Lorn Potter <lorn.potter@gmail.com> | 2021-12-08 13:39:58 +1000 |
commit | f0be152896471aa392bb1b2b649b66feb31480cc (patch) | |
tree | aa8a3d1c6776416c45578d75177c8eba05ee0f35 /src | |
parent | 3b24713098abd34cf8652da815f4dcf3a22110d3 (diff) |
wasm: improve clipboard support
Add support for Clipboard API
Add clipboard manual test
Also includes these fixes:
- improve clipboard use for chrome browser
- make QClipboard::setText work
- html copy and paste
- image copy/paste
Chrome browser supports text, html and png
To use the Clipboard API, apps need to be served from
a secure context (https). There is a fallback in the
case of non secure context (http)
- Firefox requires dom.events.asyncClipboard.read,
dom.events.asyncClipboard.clipboardItem and
dom.events.asyncClipboard.dataTransfer to be
set from about:config, in order to support the
Clipboard API.
Change-Id: Ie4cb1bbb1dfc77e9655090a30967632780d15dd9
Fixes: QTBUG-74504
Fixes: QTBUG-93619
Fixes: QTBUG-79365
Fixes: QTBUG-86169
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/plugins/platforms/wasm/qwasmclipboard.cpp | 392 | ||||
-rw-r--r-- | src/plugins/platforms/wasm/qwasmclipboard.h | 10 | ||||
-rw-r--r-- | src/plugins/platforms/wasm/qwasmeventtranslator.cpp | 11 |
3 files changed, 336 insertions, 77 deletions
diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 222dcff7fa..52471f7b41 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -30,103 +30,263 @@ #include "qwasmclipboard.h" #include "qwasmwindow.h" #include "qwasmstring.h" +#include <private/qstdweb_p.h> #include <emscripten.h> #include <emscripten/html5.h> #include <emscripten/bind.h> +#include <emscripten/val.h> #include <QCoreApplication> #include <qpa/qwindowsysteminterface.h> +#include <QBuffer> +#include <QString> using namespace emscripten; -// there has got to be a better way... -static QString g_clipboardText; -static QString g_clipboardFormat; +static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr) +{ + QString formatString = QWasmString::toQString(format); + QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>()); + + QMimeData *mMimeData = new QMimeData; + mMimeData->setData(formatString, dataArray); -static val getClipboardData() + QWasmClipboard::qWasmClipboardPaste(mMimeData); +// QWasmIntegration::get()->getWasmClipboard()->isPaste = false; +} + +static void qClipboardPasteResolve(emscripten::val blob) { - return QWasmString::fromQString(g_clipboardText); + // read Blob here + + auto fileReader = std::make_shared<qstdweb::FileReader>(); + 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 val getClipboardFormat() +static void qClipboardPromiseResolve(emscripten::val clipboardItems) { - return QWasmString::fromQString(g_clipboardFormat); + int itemsCount = clipboardItems["length"].as<int>(); + + for (int i = 0; i < itemsCount; i++) { + int typesCount = clipboardItems[i]["types"]["length"].as<int>(); // ClipboardItem + + std::string mimeFormat = clipboardItems[i]["types"][0].as<std::string>(); + + 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<val>("readText"); + val readTextResolve = val::global("Module")["qtClipboardTextPromiseResolve"]; + textPromise.call<val>("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<val>("getType", clipboardItems[i]["types"][j]) + .call<val>("then", pasteResolve) + .call<val>("catch", pasteException); + } + } + } } -static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr) +static void qClipboardCopyPromiseResolve(emscripten::val something) { - QString formatString = QWasmString::toQString(format); - QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>()); - QMimeData *mMimeData = new QMimeData; - mMimeData->setData(formatString, dataArray); - QWasmClipboard::qWasmClipboardPaste(mMimeData); + qWarning() << "copy succeeeded"; } -static void qClipboardPromiseResolve(emscripten::val something) + +static emscripten::val qClipboardPromiseException(emscripten::val something) { - pasteClipboardData(emscripten::val("text/plain"), something); + qWarning() << "clipboard error" + << QString::fromStdString(something["name"].as<std::string>()) + << QString::fromStdString(something["message"].as<std::string>()); + 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<void>("setData", val("text/plain") + , QWasmString::fromQString(_mimes->text())); + } + if (_mimes->hasHtml()) { + event["clipboardData"].call<void>("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<void>("setData", QWasmString::fromQString(mimetype) + , val(ba.constData())); + } + + event.call<void>("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<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_X, Qt::ControlModifier, "X"); - } - event["clipboardData"].call<void>("setData", getClipboardFormat(), getClipboardData()); - event.call<void>("preventDefault"); + QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( + 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<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); + QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( + 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); } - event["clipboardData"].call<void>("setData", getClipboardFormat(), getClipboardData()); - event.call<void>("preventDefault"); + commonCopyEvent(event); } -static void qClipboardPasteTo(val event) +static void qClipboardPasteTo(val dataTransfer) { - bool hasClipboardApi = QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi; - val clipdata = hasClipboardApi ? getClipboardData() : - event["clipboardData"].call<val>("getData", val("text")); + QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; + val clipboardData = dataTransfer["clipboardData"]; + val types = clipboardData["types"]; + int typesCount = types["length"].as<int>(); + std::string stdMimeFormat; + QMimeData *mMimeData = new QMimeData; + for (int i = 0; i < typesCount; i++) { + stdMimeFormat = types[i].as<std::string>(); + QString mimeFormat = QString::fromStdString(stdMimeFormat); + if (mimeFormat.contains("STRING", Qt::CaseSensitive) || mimeFormat.contains("TEXT", Qt::CaseSensitive)) + continue; - const QString qstr = QWasmString::toQString(clipdata); - if (qstr.length() > 0) { - QMimeData *mMimeData = new QMimeData; - mMimeData->setText(qstr); - QWasmClipboard::qWasmClipboardPaste(mMimeData); + if (mimeFormat.contains("text")) { +// also "text/plain;charset=utf-8" +// "UTF8_STRING" "MULTIPLE" + val mimeData = clipboardData.call<val>("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<int>(); + // handle data + for (int i = 0; i < itemsCount; i++) { + val item = items[i]; + val clipboardFile = item.call<emscripten::val>("getAsFile"); // string kind is handled above + if (clipboardFile.isUndefined() || item["kind"].as<std::string>() == "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() +QWasmClipboard::QWasmClipboard() : + isPaste(false), + m_isListener(false) { val clipboard = val::global("navigator")["clipboard"]; val permissions = val::global("navigator")["permissions"]; - hasClipboardApi = (!clipboard.isUndefined() && !permissions.isUndefined() && !clipboard["readText"].isUndefined()); - if (hasClipboardApi) - initClipboardEvents(); + 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() { - g_clipboardText.clear(); - g_clipboardFormat.clear(); } -QMimeData* QWasmClipboard::mimeData(QClipboard::Mode mode) +QMimeData *QWasmClipboard::mimeData(QClipboard::Mode mode) { if (mode != QClipboard::Clipboard) return nullptr; @@ -134,17 +294,18 @@ QMimeData* QWasmClipboard::mimeData(QClipboard::Mode mode) return QPlatformClipboard::mimeData(mode); } -void QWasmClipboard::setMimeData(QMimeData* mimeData, QClipboard::Mode mode) +void QWasmClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) { - if (mimeData->hasText()) { - g_clipboardFormat = mimeData->formats().at(0); - g_clipboardText = mimeData->text(); - } else if (mimeData->hasHtml()) { - g_clipboardFormat = mimeData->formats().at(0); - g_clipboardText = mimeData->html(); - } - 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 @@ -163,10 +324,10 @@ void QWasmClipboard::qWasmClipboardPaste(QMimeData *mData) QWasmIntegration::get()->clipboard()->setMimeData(mData, QClipboard::Clipboard); QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V"); + 0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V"); } -void QWasmClipboard::initClipboardEvents() +void QWasmClipboard::initClipboardPermissions() { if (!hasClipboardApi) return; @@ -183,32 +344,121 @@ void QWasmClipboard::initClipboardEvents() void QWasmClipboard::installEventHandlers(const emscripten::val &canvas) { - if (hasClipboardApi) - return; - + 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 - canvas.call<void>("addEventListener", val("cut"), - val::module_property("qtClipboardCutTo")); - canvas.call<void>("addEventListener", val("copy"), - val::module_property("qtClipboardCopyTo")); - canvas.call<void>("addEventListener", val("paste"), - val::module_property("qtClipboardPasteTo")); + cContext.call<void>("addEventListener", val("cut"), + val::module_property("qtClipboardCutTo"), true); + cContext.call<void>("addEventListener", val("copy"), + val::module_property("qtClipboardCopyTo"), true); + cContext.call<void>("addEventListener", val("paste"), + val::module_property("qtClipboardPasteTo"), true); } -void QWasmClipboard::readTextFromClipboard() +void QWasmClipboard::writeToClipboardApi() { - if (QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { - val navigator = val::global("navigator"); - val textPromise = navigator["clipboard"].call<val>("readText"); - val readTextResolve = val::module_property("qtClipboardPromiseResolve"); - textPromise.call<val>("then", readTextResolve); + 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<QImage>( _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<void>("set", fileContentView); + + emscripten::val contentArray = emscripten::val::array(); + contentArray.call<void>("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<void>("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<val>("write", clipboardWriteArray) + .call<val>("then", copyResolve) + .call<val>("catch", copyException); } -void QWasmClipboard::writeTextToClipboard() +void QWasmClipboard::writeToClipboard(const QMimeData *data) { - if (QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { - val navigator = val::global("navigator"); - navigator["clipboard"].call<void>("writeText", getClipboardData()); - } + // 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<val>("execCommand", val("copy")); } diff --git a/src/plugins/platforms/wasm/qwasmclipboard.h b/src/plugins/platforms/wasm/qwasmclipboard.h index 3b28e2c381..9a33b79667 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.h +++ b/src/plugins/platforms/wasm/qwasmclipboard.h @@ -51,11 +51,15 @@ public: bool ownsMode(QClipboard::Mode mode) const override; static void qWasmClipboardPaste(QMimeData *mData); - void initClipboardEvents(); + void initClipboardPermissions(); void installEventHandlers(const emscripten::val &canvas); bool hasClipboardApi; - void readTextFromClipboard(); - void writeTextToClipboard(); + bool hasPermissionsApi; + void writeToClipboardApi(); + void writeToClipboard(const QMimeData *data); + bool isPaste; + bool m_isListener; + bool isSafari; }; #endif // QWASMCLIPBOARD_H diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp b/src/plugins/platforms/wasm/qwasmeventtranslator.cpp index 5f809140f5..6d4ead60d5 100644 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp +++ b/src/plugins/platforms/wasm/qwasmeventtranslator.cpp @@ -845,7 +845,10 @@ bool QWasmEventTranslator::processKeyboard(int eventType, const EmscriptenKeyboa // handlers if direct clipboard access is not available. if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi && modifiers & Qt::ControlModifier && (qtKey == Qt::Key_X || qtKey == Qt::Key_C || qtKey == Qt::Key_V)) { - return 0; + if (qtKey == Qt::Key_V) { + QWasmIntegration::get()->getWasmClipboard()->isPaste = true; + } + return false; } bool accepted = false; @@ -853,7 +856,8 @@ bool QWasmEventTranslator::processKeyboard(int eventType, const EmscriptenKeyboa if (keyType == QEvent::KeyPress && mods.testFlag(Qt::ControlModifier) && qtKey == Qt::Key_V) { - QWasmIntegration::get()->getWasmClipboard()->readTextFromClipboard(); + QWasmIntegration::get()->getWasmClipboard()->isPaste = true; + accepted = false; // continue on to event } else { if (keyText.isEmpty()) keyText = QString(keyEvent->key); @@ -865,7 +869,8 @@ bool QWasmEventTranslator::processKeyboard(int eventType, const EmscriptenKeyboa if (keyType == QEvent::KeyPress && mods.testFlag(Qt::ControlModifier) && qtKey == Qt::Key_C) { - QWasmIntegration::get()->getWasmClipboard()->writeTextToClipboard(); + QWasmIntegration::get()->getWasmClipboard()->isPaste = false; + accepted = false; // continue on to event } QWasmEventDispatcher::maintainTimers(); |