/*
* Copyright (C) 2014-2015 Canonical, Ltd.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License version 3, as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
* SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
#include "clipboard.h"
#include "logging.h"
// C++ std lib
#include
#include
#include
#include
Q_LOGGING_CATEGORY(QTMIR_CLIPBOARD, "qtmir.clipboard")
// FIXME(loicm) The clipboard data format is not defined by Ubuntu Platform API
// which makes it impossible to have non-Qt applications communicate with Qt
// applications through the clipboard API. The solution would be to have
// Ubuntu Platform define the data format or propose an API that supports
// embedding different mime types in the clipboard.
// Data format:
// number of mime types (sizeof(int))
// data layout ((4 * sizeof(int)) * number of mime types)
// mime type string offset (sizeof(int))
// mime type string size (sizeof(int))
// data offset (sizeof(int))
// data size (sizeof(int))
// data (n bytes)
namespace {
const int maxFormatsCount = 16;
}
namespace qtmir {
QByteArray serializeMimeData(QMimeData *mimeData)
{
const QStringList formats = mimeData->formats();
const int formatCount = qMin(formats.size(), maxFormatsCount);
const int headerSize = sizeof(int) + (formatCount * 4 * sizeof(int));
int bufferSize = headerSize;
for (int i = 0; i < formatCount; i++)
bufferSize += formats[i].size() + mimeData->data(formats[i]).size();
// Serialize data.
QByteArray serializedMimeData(bufferSize, 0 /* char to fill with */);
{
char *buffer = serializedMimeData.data();
int* header = reinterpret_cast(serializedMimeData.data());
int offset = headerSize;
header[0] = formatCount;
for (int i = 0; i < formatCount; i++) {
const int formatOffset = offset;
const int formatSize = formats[i].size();
const int dataOffset = offset + formatSize;
const int dataSize = mimeData->data(formats[i]).size();
memcpy(&buffer[formatOffset], formats[i].toLatin1().data(), formatSize);
memcpy(&buffer[dataOffset], mimeData->data(formats[i]).data(), dataSize);
header[i*4+1] = formatOffset;
header[i*4+2] = formatSize;
header[i*4+3] = dataOffset;
header[i*4+4] = dataSize;
offset += formatSize + dataSize;
}
}
return serializedMimeData;
}
QMimeData *deserializeMimeData(const QByteArray &serializedMimeData)
{
if (static_cast(serializedMimeData.size()) < sizeof(int)) {
// Data is invalid
return nullptr;
}
QMimeData *mimeData = new QMimeData;
const char* const buffer = serializedMimeData.constData();
const int* const header = reinterpret_cast(serializedMimeData.constData());
const int count = qMin(header[0], maxFormatsCount);
for (int i = 0; i < count; i++) {
const int formatOffset = header[i*4+1];
const int formatSize = header[i*4+2];
const int dataOffset = header[i*4+3];
const int dataSize = header[i*4+4];
if (formatOffset + formatSize <= serializedMimeData.size()
&& dataOffset + dataSize <= serializedMimeData.size()) {
QString mimeType = QString::fromLatin1(&buffer[formatOffset], formatSize);
QByteArray mimeDataBytes(&buffer[dataOffset], dataSize);
mimeData->setData(mimeType, mimeDataBytes);
}
}
return mimeData;
}
/************************************ DBusClipboard *****************************************/
bool DBusClipboard::skipDBusRegistration = false;
DBusClipboard::DBusClipboard(QObject *parent)
: QObject(parent)
{
if (!skipDBusRegistration) {
performDBusRegistration();
}
}
void DBusClipboard::setContents(QByteArray newContents)
{
setContentsHelper(std::move(newContents));
}
void DBusClipboard::SetContents(QByteArray newContents)
{
qCDebug(QTMIR_CLIPBOARD, "D-Bus SetContents - %d bytes", newContents.size());
if (setContentsHelper(std::move(newContents))) {
Q_EMIT contentsChangedRemotely();
}
}
bool DBusClipboard::setContentsHelper(QByteArray newContents)
{
if (newContents.size() > maxContentsSize) {
qCWarning(QTMIR_CLIPBOARD, "D-Bus clipboard refused the new contents (%d bytes) as they're"
" bigger than the maximum allowed size of %d bytes.",
newContents.size(), maxContentsSize);
return false;
}
if (newContents != m_contents) {
m_contents = std::move(newContents);
Q_EMIT ContentsChanged(m_contents);
return true;
} else {
return false;
}
}
QByteArray DBusClipboard::GetContents() const
{
qCDebug(QTMIR_CLIPBOARD, "D-Bus GetContents - returning %d bytes", m_contents.size());
return m_contents;
}
void DBusClipboard::performDBusRegistration()
{
QDBusConnection connection = QDBusConnection::sessionBus();
const char *serviceName = "com.canonical.QtMir";
const char *objectName = "/com/canonical/QtMir/Clipboard";
bool serviceOk = connection.registerService(serviceName);
if (!serviceOk) {
QDBusError error = connection.lastError();
QString errorMessage;
if (error.isValid()) {
errorMessage = error.message();
}
qCCritical(QTMIR_CLIPBOARD, "Failed to register service %s. %s", serviceName, qPrintable(errorMessage));
}
bool objectOk = connection.registerObject(objectName, this,
QDBusConnection::ExportScriptableSignals
| QDBusConnection::ExportScriptableSlots);
if (!objectOk) {
QDBusError error = connection.lastError();
QString errorMessage;
if (error.isValid()) {
errorMessage = error.message();
}
qCCritical(QTMIR_CLIPBOARD, "Failed to register object %s. %s", objectName, qPrintable(errorMessage));
}
if (serviceOk && objectOk) {
qCDebug(QTMIR_CLIPBOARD, "D-Bus registration successful.");
}
}
/************************************ Clipboard *****************************************/
Clipboard::Clipboard(QObject *parent)
: QObject(parent)
, m_dbusClipboard(nullptr)
{
}
QMimeData *Clipboard::mimeData(QClipboard::Mode mode)
{
if (mode == QClipboard::Clipboard) {
return QPlatformClipboard::mimeData(mode);
} else {
return nullptr;
}
}
void Clipboard::setMimeData(QMimeData *data, QClipboard::Mode mode)
{
if (mode != QClipboard::Clipboard)
return;
if (m_dbusClipboard) {
QByteArray serializedMimeData = serializeMimeData(data);
m_dbusClipboard->setContents(std::move(serializedMimeData));
}
QPlatformClipboard::setMimeData(data, mode);
}
void Clipboard::setupDBusService()
{
Q_ASSERT(!m_dbusClipboard);
m_dbusClipboard = new DBusClipboard(this);
connect(m_dbusClipboard, &DBusClipboard::contentsChangedRemotely,
this, &Clipboard::setMimeDataWithDBusClibpboardContents);
}
void Clipboard::setMimeDataWithDBusClibpboardContents()
{
Q_ASSERT(m_dbusClipboard);
QMimeData *newMimeData = deserializeMimeData(m_dbusClipboard->contents());
if (newMimeData) {
// Don't call Clipboard::setMimeData as it will also propagate the change
// to the D-Bus clipboard, which doesn't make sense here as we're doing
// the other way round (propagating the D-Bus clipboard change to the local
// clipboard).
QPlatformClipboard::setMimeData(newMimeData, QClipboard::Clipboard);
} else {
qCWarning(QTMIR_CLIPBOARD, "Failed to deserialize D-Bus clipboard contents (%d bytes)",
m_dbusClipboard->contents().size());
}
}
} // namespace qtmir