qtbase/src/plugins/platforms/wasm/qwasmclipboard.cpp
Lorn Potter 6b9270fd72 wasm: move DataTransfer to dom::
Change-Id: I069292154bafd1c08a0d0f2e8a62052f596a80f3
Done-with: Mikolaj.Boc@qt.io
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
2023-12-27 06:55:59 +00:00

296 lines
10 KiB
C++

// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "qwasmclipboard.h"
#include "qwasmdom.h"
#include "qwasmevent.h"
#include "qwasmwindow.h"
#include <private/qstdweb_p.h>
#include <QCoreApplication>
#include <qpa/qwindowsysteminterface.h>
#include <QBuffer>
#include <QString>
#include <emscripten/val.h>
QT_BEGIN_NAMESPACE
using namespace emscripten;
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"),
_mimes->text().toEcmaString());
}
if (_mimes->hasHtml()) {
event["clipboardData"].call<void>("setData", val("text/html"), _mimes->html().toEcmaString());
}
for (auto mimetype : _mimes->formats()) {
if (mimetype.contains("text/"))
continue;
QByteArray ba = _mimes->data(mimetype);
if (!ba.isEmpty())
event["clipboardData"].call<void>("setData", mimetype.toEcmaString(),
val(ba.constData()));
}
event.call<void>("preventDefault");
}
static void qClipboardCutTo(val event)
{
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_X, Qt::ControlModifier, "X");
}
commonCopyEvent(event);
}
static void qClipboardCopyTo(val event)
{
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 event)
{
event.call<void>("preventDefault"); // prevent browser from handling drop event
static std::shared_ptr<qstdweb::CancellationFlag> readDataCancellation = nullptr;
dom::DataTransfer transfer(event["clipboardData"]);
auto data = transfer.toMimeDataWithFile();
if (data->formats().isEmpty())
return;
// Persist clipboard data so that the app can read it when handling the CTRL+V
QWasmIntegration::get()->clipboard()->QPlatformClipboard::setMimeData(data,
QClipboard::Clipboard);
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_V,
Qt::ControlModifier, "V");
}
EMSCRIPTEN_BINDINGS(qtClipboardModule) {
function("qtClipboardCutTo", &qClipboardCutTo);
function("qtClipboardCopyTo", &qClipboardCopyTo);
function("qtClipboardPasteTo", &qClipboardPasteTo);
}
QWasmClipboard::QWasmClipboard()
{
val clipboard = val::global("navigator")["clipboard"];
const bool hasPermissionsApi = !val::global("navigator")["permissions"].isUndefined();
m_hasClipboardApi = !clipboard.isUndefined() && !clipboard["readText"].isUndefined();
if (m_hasClipboardApi && hasPermissionsApi)
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)
{
// handle setText/ setData programmatically
QPlatformClipboard::setMimeData(mimeData, mode);
if (m_hasClipboardApi)
writeToClipboardApi();
else
writeToClipboard();
}
QWasmClipboard::ProcessKeyboardResult QWasmClipboard::processKeyboard(const KeyEvent &event)
{
if (event.type != EventType::KeyDown || !event.modifiers.testFlag(Qt::ControlModifier))
return ProcessKeyboardResult::Ignored;
if (event.key != Qt::Key_C && event.key != Qt::Key_V && event.key != Qt::Key_X)
return ProcessKeyboardResult::Ignored;
const bool isPaste = event.key == Qt::Key_V;
return m_hasClipboardApi && !isPaste
? ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded
: ProcessKeyboardResult::NativeClipboardEventNeeded;
}
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::initClipboardPermissions()
{
val permissions = val::global("navigator")["permissions"];
qstdweb::Promise::make(permissions, "query", { .catchFunc = [](emscripten::val) {} }, ([]() {
val readPermissionsMap = val::object();
readPermissionsMap.set("name", val("clipboard-read"));
return readPermissionsMap;
})());
qstdweb::Promise::make(permissions, "query", { .catchFunc = [](emscripten::val) {} }, ([]() {
val readPermissionsMap = val::object();
readPermissionsMap.set("name", val("clipboard-write"));
return readPermissionsMap;
})());
}
void QWasmClipboard::installEventHandlers(const emscripten::val &target)
{
emscripten::val cContext = val::undefined();
emscripten::val isChromium = val::global("window")["chrome"];
if (!isChromium.isUndefined()) {
cContext = val::global("document");
} else {
cContext = target;
}
// Fallback path for browsers which do not support direct clipboard access
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);
}
bool QWasmClipboard::hasClipboardApi()
{
return m_hasClipboardApi;
}
void QWasmClipboard::writeToClipboardApi()
{
Q_ASSERT(m_hasClipboardApi);
// copy event
// browser event handler detected ctrl c if clipboard API
// or Qt call from keyboard event handler
QMimeData *_mimes = 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", mimetype.toEcmaString());
emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type);
emscripten::val clipboardItemObject = emscripten::val::object();
clipboardItemObject.set(mimetype.toEcmaString(), 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 navigator = val::global("navigator");
qstdweb::Promise::make(
navigator["clipboard"], "write",
{
.catchFunc = [](emscripten::val error) {
qWarning() << "clipboard error"
<< QString::fromStdString(error["name"].as<std::string>())
<< QString::fromStdString(error["message"].as<std::string>());
}
},
clipboardWriteArray);
}
void QWasmClipboard::writeToClipboard()
{
// 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"));
}
QT_END_NAMESPACE