2022-05-10 10:06:48 +00:00
|
|
|
// Copyright (C) 2018 The Qt Company Ltd.
|
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
2018-02-21 18:35:25 +00:00
|
|
|
|
|
|
|
#include "qwasmclipboard.h"
|
|
|
|
#include "qwasmwindow.h"
|
2020-01-17 09:32:27 +00:00
|
|
|
#include "qwasmstring.h"
|
2021-05-14 08:37:12 +00:00
|
|
|
#include <private/qstdweb_p.h>
|
2018-02-21 18:35:25 +00:00
|
|
|
|
|
|
|
#include <emscripten.h>
|
|
|
|
#include <emscripten/html5.h>
|
|
|
|
#include <emscripten/bind.h>
|
2021-05-14 08:37:12 +00:00
|
|
|
#include <emscripten/val.h>
|
2018-02-21 18:35:25 +00:00
|
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
#include <qpa/qwindowsysteminterface.h>
|
2021-05-14 08:37:12 +00:00
|
|
|
#include <QBuffer>
|
|
|
|
#include <QString>
|
2018-02-21 18:35:25 +00:00
|
|
|
|
2022-06-15 03:56:42 +00:00
|
|
|
QT_BEGIN_NAMESPACE
|
2018-02-21 18:35:25 +00:00
|
|
|
using namespace emscripten;
|
|
|
|
|
2021-05-14 08:37:12 +00:00
|
|
|
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");
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
2019-02-23 08:40:30 +00:00
|
|
|
static void qClipboardCutTo(val event)
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2022-09-20 07:47:19 +00:00
|
|
|
if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) {
|
2019-02-23 08:40:30 +00:00
|
|
|
// Send synthetic Ctrl+X to make the app cut data to Qt's clipboard
|
2021-12-06 12:43:34 +00:00
|
|
|
QWindowSystemInterface::handleKeyEvent(
|
2021-05-14 08:37:12 +00:00
|
|
|
0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "X");
|
|
|
|
}
|
|
|
|
|
|
|
|
commonCopyEvent(event);
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
2019-02-23 08:40:30 +00:00
|
|
|
static void qClipboardCopyTo(val event)
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2022-09-20 07:47:19 +00:00
|
|
|
if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) {
|
2019-02-23 08:40:30 +00:00
|
|
|
// Send synthetic Ctrl+C to make the app copy data to Qt's clipboard
|
2021-12-06 12:43:34 +00:00
|
|
|
QWindowSystemInterface::handleKeyEvent(
|
2021-05-14 08:37:12 +00:00
|
|
|
0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C");
|
2019-02-23 08:40:30 +00:00
|
|
|
}
|
2021-05-14 08:37:12 +00:00
|
|
|
commonCopyEvent(event);
|
2019-02-23 08:40:30 +00:00
|
|
|
}
|
|
|
|
|
2022-09-20 07:47:19 +00:00
|
|
|
static void qWasmClipboardPaste(QMimeData *mData)
|
|
|
|
{
|
|
|
|
// Persist clipboard data so that the app can read it when handling the CTRL+V
|
|
|
|
QWasmIntegration::get()->clipboard()->
|
|
|
|
QPlatformClipboard::setMimeData(mData, QClipboard::Clipboard);
|
|
|
|
|
2021-12-06 12:43:34 +00:00
|
|
|
QWindowSystemInterface::handleKeyEvent(
|
2022-09-20 07:47:19 +00:00
|
|
|
0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V");
|
|
|
|
}
|
|
|
|
|
2021-05-14 08:37:12 +00:00
|
|
|
static void qClipboardPasteTo(val dataTransfer)
|
2019-02-23 08:40:30 +00:00
|
|
|
{
|
2022-11-25 12:17:16 +00:00
|
|
|
enum class ItemKind {
|
|
|
|
File,
|
|
|
|
String,
|
|
|
|
};
|
|
|
|
|
|
|
|
struct Data
|
|
|
|
{
|
|
|
|
std::unique_ptr<QMimeData> data;
|
|
|
|
int fileCount = 0;
|
|
|
|
int doneCount = 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
auto sharedData = std::make_shared<Data>();
|
|
|
|
sharedData->data = std::make_unique<QMimeData>();
|
|
|
|
|
|
|
|
auto continuation = [sharedData]() {
|
|
|
|
Q_ASSERT(sharedData->doneCount <= sharedData->fileCount);
|
|
|
|
if (sharedData->doneCount < sharedData->fileCount)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!sharedData->data->formats().isEmpty())
|
|
|
|
qWasmClipboardPaste(sharedData->data.release());
|
|
|
|
};
|
|
|
|
|
|
|
|
const val clipboardData = dataTransfer["clipboardData"];
|
|
|
|
const val items = clipboardData["items"];
|
|
|
|
for (int i = 0; i < items["length"].as<int>(); ++i) {
|
|
|
|
const val 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: {
|
|
|
|
++sharedData->fileCount;
|
|
|
|
|
|
|
|
qstdweb::File file(item.call<emscripten::val>("getAsFile"));
|
|
|
|
|
|
|
|
QByteArray fileContent(file.size(), Qt::Uninitialized);
|
|
|
|
file.stream(fileContent.data(),
|
|
|
|
[continuation, itemMimeType, fileContent, sharedData]() {
|
|
|
|
if (!fileContent.isEmpty()) {
|
|
|
|
if (itemMimeType.startsWith("image/")) {
|
|
|
|
QImage image;
|
|
|
|
image.loadFromData(fileContent, nullptr);
|
|
|
|
sharedData->data->setImageData(image);
|
|
|
|
} else {
|
|
|
|
sharedData->data->setData(itemMimeType, fileContent.data());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
++sharedData->doneCount;
|
|
|
|
continuation();
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case ItemKind::String:
|
|
|
|
if (itemMimeType.contains("STRING", Qt::CaseSensitive)
|
|
|
|
|| itemMimeType.contains("TEXT", Qt::CaseSensitive)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const QString data = QWasmString::toQString(
|
|
|
|
clipboardData.call<val>("getData", val(itemMimeType.toStdString())));
|
|
|
|
|
|
|
|
if (!data.isEmpty()) {
|
|
|
|
if (itemMimeType == "text/html")
|
|
|
|
sharedData->data->setHtml(data);
|
|
|
|
else if (itemMimeType.isEmpty() || itemMimeType == "text/plain")
|
|
|
|
sharedData->data->setText(data); // the type can be empty
|
|
|
|
else
|
|
|
|
sharedData->data->setData(itemMimeType, data.toLocal8Bit());
|
2021-05-14 08:37:12 +00:00
|
|
|
}
|
2022-11-25 12:17:16 +00:00
|
|
|
break;
|
2021-05-14 08:37:12 +00:00
|
|
|
}
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
2022-11-25 12:17:16 +00:00
|
|
|
continuation();
|
2021-05-14 08:37:12 +00:00
|
|
|
}
|
|
|
|
|
2019-03-15 14:04:59 +00:00
|
|
|
EMSCRIPTEN_BINDINGS(qtClipboardModule) {
|
|
|
|
function("qtClipboardCutTo", &qClipboardCutTo);
|
|
|
|
function("qtClipboardCopyTo", &qClipboardCopyTo);
|
|
|
|
function("qtClipboardPasteTo", &qClipboardPasteTo);
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-20 07:17:50 +00:00
|
|
|
QWasmClipboard::QWasmClipboard()
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2019-02-15 21:04:09 +00:00
|
|
|
val clipboard = val::global("navigator")["clipboard"];
|
2022-09-08 11:11:32 +00:00
|
|
|
|
|
|
|
const bool hasPermissionsApi = !val::global("navigator")["permissions"].isUndefined();
|
2022-09-20 07:47:19 +00:00
|
|
|
m_hasClipboardApi = !clipboard.isUndefined() && !clipboard["readText"].isUndefined();
|
2022-09-08 11:11:32 +00:00
|
|
|
|
2022-09-20 07:47:19 +00:00
|
|
|
if (m_hasClipboardApi && hasPermissionsApi)
|
2021-05-14 08:37:12 +00:00
|
|
|
initClipboardPermissions();
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QWasmClipboard::~QWasmClipboard()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-05-14 08:37:12 +00:00
|
|
|
QMimeData *QWasmClipboard::mimeData(QClipboard::Mode mode)
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
|
|
|
if (mode != QClipboard::Clipboard)
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
return QPlatformClipboard::mimeData(mode);
|
|
|
|
}
|
|
|
|
|
2021-05-14 08:37:12 +00:00
|
|
|
void QWasmClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode)
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2021-05-14 08:37:12 +00:00
|
|
|
// handle setText/ setData programmatically
|
2022-09-20 07:09:43 +00:00
|
|
|
QPlatformClipboard::setMimeData(mimeData, mode);
|
2022-09-20 07:47:19 +00:00
|
|
|
if (m_hasClipboardApi)
|
2022-09-20 07:09:43 +00:00
|
|
|
writeToClipboardApi();
|
2022-09-20 07:17:50 +00:00
|
|
|
else
|
2022-09-20 07:47:19 +00:00
|
|
|
writeToClipboard();
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-16 13:34:03 +00:00
|
|
|
QWasmClipboard::ProcessKeyboardResult
|
|
|
|
QWasmClipboard::processKeyboard(const QWasmEventTranslator::TranslatedEvent &event,
|
|
|
|
const QFlags<Qt::KeyboardModifier> &modifiers)
|
2022-09-06 10:56:42 +00:00
|
|
|
{
|
2022-09-16 13:34:03 +00:00
|
|
|
if (event.type != QEvent::KeyPress || !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;
|
|
|
|
|
2022-09-20 07:09:43 +00:00
|
|
|
const bool isPaste = event.key == Qt::Key_V;
|
2022-09-16 13:34:03 +00:00
|
|
|
|
2022-09-20 07:47:19 +00:00
|
|
|
return m_hasClipboardApi && !isPaste
|
2022-09-16 13:34:03 +00:00
|
|
|
? ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded
|
|
|
|
: ProcessKeyboardResult::NativeClipboardEventNeeded;
|
2022-09-06 10:56:42 +00:00
|
|
|
}
|
|
|
|
|
2018-02-21 18:35:25 +00:00
|
|
|
bool QWasmClipboard::supportsMode(QClipboard::Mode mode) const
|
|
|
|
{
|
|
|
|
return mode == QClipboard::Clipboard;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool QWasmClipboard::ownsMode(QClipboard::Mode mode) const
|
|
|
|
{
|
|
|
|
Q_UNUSED(mode);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-05-14 08:37:12 +00:00
|
|
|
void QWasmClipboard::initClipboardPermissions()
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2019-02-15 21:04:09 +00:00
|
|
|
val permissions = val::global("navigator")["permissions"];
|
|
|
|
|
2022-09-08 11:11:32 +00:00
|
|
|
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;
|
|
|
|
})());
|
2019-02-15 21:04:09 +00:00
|
|
|
}
|
|
|
|
|
2023-02-03 11:59:42 +00:00
|
|
|
void QWasmClipboard::installEventHandlers(const emscripten::val &target)
|
2019-02-15 21:04:09 +00:00
|
|
|
{
|
2021-05-14 08:37:12 +00:00
|
|
|
emscripten::val cContext = val::undefined();
|
|
|
|
emscripten::val isChromium = val::global("window")["chrome"];
|
2023-02-03 11:59:42 +00:00
|
|
|
if (!isChromium.isUndefined()) {
|
2021-05-14 08:37:12 +00:00
|
|
|
cContext = val::global("document");
|
2023-02-03 11:59:42 +00:00
|
|
|
} else {
|
|
|
|
cContext = target;
|
|
|
|
}
|
2019-02-15 21:04:09 +00:00
|
|
|
// Fallback path for browsers which do not support direct clipboard access
|
2021-05-14 08:37:12 +00:00
|
|
|
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);
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-20 07:47:19 +00:00
|
|
|
bool QWasmClipboard::hasClipboardApi()
|
|
|
|
{
|
|
|
|
return m_hasClipboardApi;
|
|
|
|
}
|
|
|
|
|
2021-05-14 08:37:12 +00:00
|
|
|
void QWasmClipboard::writeToClipboardApi()
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2022-11-24 16:21:05 +00:00
|
|
|
Q_ASSERT(m_hasClipboardApi);
|
2021-05-14 08:37:12 +00:00
|
|
|
|
|
|
|
// copy event
|
|
|
|
// browser event handler detected ctrl c if clipboard API
|
|
|
|
// or Qt call from keyboard event handler
|
|
|
|
|
2022-09-20 07:47:19 +00:00
|
|
|
QMimeData *_mimes = mimeData(QClipboard::Clipboard);
|
2021-05-14 08:37:12 +00:00
|
|
|
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;
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
2021-05-14 08:37:12 +00:00
|
|
|
|
|
|
|
val navigator = val::global("navigator");
|
2022-06-21 15:50:04 +00:00
|
|
|
|
|
|
|
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);
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-20 07:47:19 +00:00
|
|
|
void QWasmClipboard::writeToClipboard()
|
2018-02-21 18:35:25 +00:00
|
|
|
{
|
2021-05-14 08:37:12 +00:00
|
|
|
// 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"));
|
2018-02-21 18:35:25 +00:00
|
|
|
}
|
2022-06-15 03:56:42 +00:00
|
|
|
QT_END_NAMESPACE
|