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>
This commit is contained in:
Lorn Potter 2021-05-14 18:37:12 +10:00
parent 3b24713098
commit f0be152896
13 changed files with 1086 additions and 82 deletions

View File

@ -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 val getClipboardData()
{
return QWasmString::fromQString(g_clipboardText);
}
static val getClipboardFormat()
{
return QWasmString::fromQString(g_clipboardFormat);
}
static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr)
{
QString formatString = QWasmString::toQString(format);
QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>());
QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>());
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<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 void qClipboardPromiseResolve(emscripten::val something)
static void qClipboardPromiseResolve(emscripten::val clipboardItems)
{
pasteClipboardData(emscripten::val("text/plain"), something);
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 qClipboardCopyPromiseResolve(emscripten::val something)
{
qWarning() << "copy succeeeded";
}
static emscripten::val qClipboardPromiseException(emscripten::val 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)
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<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::writeToClipboardApi()
{
if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi)
return;
// 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"));
// 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::readTextFromClipboard()
void QWasmClipboard::writeToClipboard(const QMimeData *data)
{
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);
}
}
void QWasmClipboard::writeTextToClipboard()
{
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"));
}

View File

@ -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

View File

@ -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();

View File

@ -2,4 +2,5 @@ add_subdirectory(eventloop)
if(QT_FEATURE_widgets)
add_subdirectory(cursors)
add_subdirectory(localfiles)
add_subdirectory(clipboard)
endif()

View File

@ -0,0 +1,46 @@
# Generated from clipboard.pro.
#####################################################################
## clipboard Binary:
#####################################################################
qt_internal_add_manual_test(clipboard
GUI
SOURCES
main.cpp
mainwindow.cpp mainwindow.h mainwindow.ui
PUBLIC_LIBRARIES
Qt::Core
Qt::Gui
Qt::Widgets
ENABLE_AUTOGEN_TOOLS
uic
)
# Resources:
set(data_resource_files
"data/qticon64.png"
)
qt_internal_add_resource(clipboard "data"
PREFIX
"/"
FILES
${data_resource_files}
)
## Scopes:
#####################################################################
qt_internal_extend_target(clipboard CONDITION (QT_MAJOR_VERSION GREATER 4)
PUBLIC_LIBRARIES
Qt::Widgets
)
#### Keys ignored in scope 3:.:.:clipboard.pro:QNX:
# target.path = "/tmp/$${TARGET}/bin"
#### Keys ignored in scope 5:.:.:clipboard.pro:UNIX AND NOT ANDROID:
# target.path = "/opt/$${TARGET}/bin"
#### Keys ignored in scope 6:.:.:clipboard.pro:NOT target.path_ISEMPTY:
# INSTALLS = "target"

View File

@ -0,0 +1,2 @@
The Clipboard manual test app can be used both on desktop and in the browser
using WebAssembly to test clipboard use between WebAssembly app and the desktop.

View File

@ -0,0 +1,27 @@
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
RESOURCES += \
data.qrc
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>data/qticon64.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,61 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** 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.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}

View File

@ -0,0 +1,287 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** 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.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QClipboard>
#include <QMimeData>
#include <QImageReader>
#include <QBuffer>
#include <QRandomGenerator>
#include <QPainter>
#include <QKeyEvent>
#ifdef Q_OS_WASM
#include <emscripten.h>
#include <emscripten/html5.h>
#include <emscripten/val.h>
#include <emscripten/bind.h>
using namespace emscripten;
#endif
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->imageLabel->installEventFilter(this);
ui->imageLabel->setBackgroundRole(QPalette::Base);
ui->imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
ui->imageLabel->setScaledContents(true);
clipboard = QGuiApplication::clipboard();
connect(
clipboard, &QClipboard::dataChanged,
[=]() {
ui->textEdit_2->insertHtml("<b>Clipboard data changed:</b><br>");
const QMimeData *mimeData = clipboard->mimeData();
QByteArray ba;
for (auto mimetype : mimeData->formats()) {
qDebug() << Q_FUNC_INFO << mimetype;
ba = mimeData->data(mimetype);
}
QString sizeStr;
if (mimeData->hasImage()) {
qsizetype imageSize = qvariant_cast<QImage>(mimeData->imageData()).sizeInBytes();
sizeStr.setNum(imageSize);
ui->textEdit_2->insertHtml("has Image data: " + sizeStr + "<br>");
}
if (mimeData->hasHtml()) {
int size = mimeData->html().length();
sizeStr.setNum(size);
ui->textEdit_2->insertHtml("has html data: " + sizeStr + "<br>");
}
if (mimeData->hasText()) {
int size = mimeData->text().length();
sizeStr.setNum(size);
ui->textEdit_2->insertHtml("has text data: " + sizeStr + "<br>");
}
ui->textEdit_2->insertHtml(mimeData->formats().join(" | ")+ "<br>");
ui->textEdit_2->ensureCursorVisible();
const QString message = tr("Clipboard changed, %1 ")
.arg(mimeData->formats().join(' '));
statusBar()->showMessage(message + sizeStr);
}
);
#ifdef Q_OS_WASM
val clipboard = val::global("navigator")["clipboard"];
bool hasClipboardApi = (!clipboard.isUndefined() && !clipboard["readText"].isUndefined());
QString messageApi;
if (hasClipboardApi)
messageApi = QStringLiteral("Using Clipboard API");
else
messageApi = QStringLiteral("Using Clipboard events");
ui->label->setText(messageApi);
#else
ui->label->setText("desktop clipboard");
#endif
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_setTextButton_clicked()
{
QGuiApplication::clipboard()->setText(ui->textEdit->textCursor().selectedText());
}
static QImage clipboardImage()
{
if (const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData()) {
if (mimeData->hasImage()) {
const QImage image = qvariant_cast<QImage>(mimeData->imageData());
if (!image.isNull())
return image;
}
}
return QImage();
}
static QByteArray clipboardBinary()
{
if (const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData()) {
if (mimeData->formats().contains("application/octet-stream")) {
const QByteArray ba = qvariant_cast<QByteArray>(mimeData->data("application/octet-stream"));
qDebug() << Q_FUNC_INFO << ba;
if (!ba.isNull())
return ba;
}
}
return QByteArray();
}
void MainWindow::on_pasteImageButton_clicked()
{
const QImage newImage = clipboardImage();
if (newImage.isNull()) {
qDebug() << "No image in clipboard";
const QString message = tr("No image in clipboard")
.arg(newImage.width()).arg(newImage.height()).arg(newImage.depth());
statusBar()->showMessage(message);
} else {
setImage(newImage);
setWindowFilePath(QString());
const QString message = tr("Obtained image from clipboard, %1x%2, Depth: %3")
.arg(newImage.width()).arg(newImage.height()).arg(newImage.depth());
statusBar()->showMessage(message);
}
}
void MainWindow::setImage(const QImage &newImage)
{
image = newImage;
ui->imageLabel->setPixmap(QPixmap::fromImage(image));
}
void MainWindow::on_pasteTextButton_clicked()
{
ui->textEdit->insertPlainText(QGuiApplication::clipboard()->text());
}
void MainWindow::on_copyBinaryButton_clicked()
{
QByteArray ba;
ba.resize(10);
ba[0] = 0x3c;
ba[1] = 0xb8;
ba[2] = 0x64;
ba[3] = 0x18;
ba[4] = 0xca;
ba[5] = 0xca;
ba[6] = 0x18;
ba[7] = 0x64;
ba[8] = 0xb8;
ba[9] = 0x3c;
QMimeData *mimeData = new QMimeData();
mimeData->setData("application/octet-stream", ba);
QGuiApplication::clipboard()->setMimeData(mimeData);
const QString message = tr("Copied binary to clipboard: " + ba + " 10 bytes");
statusBar()->showMessage(message);
}
void MainWindow::on_pasteBinaryButton_clicked()
{
const QByteArray ba = clipboardBinary();
if (ba.isNull()) {
qDebug() << "No binary in clipboard";
const QString message = tr("No binary in clipboard");
statusBar()->showMessage(message);
} else {
setWindowFilePath(QString());
const QString message = tr("Obtained binary from clipboard: " + ba);
statusBar()->showMessage(message);
}
}
void MainWindow::on_comboBox_textActivated(const QString &arg1)
{
QImage image(QSize(150,100), QImage::Format_RGB32);
QPainter painter(&image);
painter.fillRect(QRectF(0,0,150,100),generateRandomColor());
painter.fillRect(QRectF(20,30,130,40),generateRandomColor());
painter.setPen(QPen(generateRandomColor()));
painter.drawText(QRect(25,30,130,40),"Qt WebAssembly");
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, arg1.toLocal8Bit());
qDebug() << ba.mid(0,10) << ba.length();
qDebug() << Q_FUNC_INFO << image.sizeInBytes();
QGuiApplication::clipboard()->setImage(image);
}
QColor MainWindow::generateRandomColor()
{
return QColor::fromRgb(QRandomGenerator::global()->generate());
}
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
QKeyEvent *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_V && ke->modifiers().testFlag(Qt::ControlModifier)) {
if (obj == ui->imageLabel) {
setImage(clipboardImage());
return true;
}
}
}
// standard event processing
return QObject::eventFilter(obj, event);
}
void MainWindow::on_pasteHtmlButton_clicked()
{
ui->textEdit->insertHtml(QGuiApplication::clipboard()->mimeData()->html());
}
void MainWindow::on_clearButton_clicked()
{
ui->textEdit_2->clear();
}

View File

@ -0,0 +1,94 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** 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.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_setTextButton_clicked();
void on_pasteImageButton_clicked();
void setImage(const QImage &newImage);
void on_pasteTextButton_clicked();
void on_copyBinaryButton_clicked();
void on_pasteBinaryButton_clicked();
void on_comboBox_textActivated(const QString &arg1);
void on_pasteHtmlButton_clicked();
void on_clearButton_clicked();
private:
Ui::MainWindow *ui;
QImage image;
QClipboard *clipboard;
bool eventFilter(QObject *obj, QEvent *event) override;
QColor generateRandomColor();
};
#endif // MAINWINDOW_H

View File

@ -0,0 +1,222 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1222</width>
<height>1011</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="setTextButton">
<property name="text">
<string>setText()</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pasteTextButton">
<property name="text">
<string>paste text</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pasteHtmlButton">
<property name="text">
<string>paste html</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="comboBox">
<property name="currentText">
<string>PNG</string>
</property>
<item>
<property name="text">
<string>PNG</string>
</property>
</item>
<item>
<property name="text">
<string>JPG</string>
</property>
</item>
<item>
<property name="text">
<string>BMP</string>
</property>
</item>
<item>
<property name="text">
<string>NAN</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="pasteImageButton">
<property name="text">
<string>paste image</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="copyBinaryButton">
<property name="text">
<string>setData</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pasteBinaryButton">
<property name="text">
<string>paste data</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="clearButton">
<property name="text">
<string>clear</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="textEdit_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QTextBrowser" name="textEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>500</width>
<height>400</height>
</size>
</property>
<property name="readOnly">
<bool>false</bool>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;img src=&quot;:/data/qticon64.png&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;a name=&quot;tw-target&quot;&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'monospace'; font-weight:600;&quot;&gt;L&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-weight:600;&quot;&gt;orem&lt;/span&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-style:italic;&quot;&gt;ipsum&lt;/span&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; text-decoration: underline;&quot;&gt;dolor&lt;/span&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; vertical-align:super;&quot;&gt;sit&lt;/span&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; vertical-align:sub;&quot;&gt;amet&lt;/span&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt;, &lt;/span&gt;&lt;a href=&quot;http://localhost&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;consectetur&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; color:#7320a4;&quot;&gt;adipiscing&lt;/span&gt;&lt;span style=&quot; font-family:'monospace';&quot;&gt; elit. Som medlemmer av byrået ønsker imidlertid en eiendomsmegler. Ullamcorper største lekseforfatter. Dolor et consectetuer litt ernæring. Maecenas smile jord sitter Vulputate medlemmer og, basketball ethvert problem. Reservert lever nå propaganda. På makroen investere laoreet kan, av enhver latter. Jasmine som en TV -tegneserie.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'monospace';&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="imageLabel">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="text">
<string>Paste image here</string>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1222</width>
<height>29</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>