Port to the new org.kde.KWin.ScreenShots2 interface

Fixes https://invent.kde.org/plasma/plasma-phone-components/-/issues/85
This commit is contained in:
Aleix Pol 2021-07-23 03:30:16 +02:00 committed by Aleix Pol Gonzalez
parent 393047e905
commit 989d6fdc1d
4 changed files with 282 additions and 77 deletions

View file

@ -1,4 +1,4 @@
qt_add_dbus_interfaces(DBUS_SRCS dbus/org.kde.KWin.Screenshot.xml
qt_add_dbus_interfaces(DBUS_SRCS dbus/org.kde.KWin.ScreenShot2.xml
dbus/org.kde.KScreen.xml)
set(phonepanel_SRCS

View file

@ -0,0 +1,184 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!--
SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
-->
<node name="/org/kde/KWin/ScreenShot2">
<!--
org.kde.KWin.ScreenShot2:
@short_description: Screen shot interface
This interface provides a way to request a screenshot of a rectangular area,
a screen, or a window.
-->
<interface name="org.kde.KWin.ScreenShot2">
<!--
CaptureWindow:
@handle: The unique handle that identified the window
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of the specified window. The application that
requests the screenshot must have the org.kde.KWin.ScreenShot2
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop
file entry.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "include-decoration" (b): Whether the decoration should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureWindow">
<arg name="handle" type="s" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
<!--
CaptureArea:
@x: The x coordinate of the upper left corner of the area
@y: The y coordinate of the upper left corner of the area
@width: The width of the screenshot area
@height: The height of the screenshot area
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of the specified rectangular area. The application
that requests the screenshot must have the org.kde.KWin.ScreenShot2
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop file
entry.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureArea">
<arg name="x" type="i" direction="in" />
<arg name="y" type="i" direction="in" />
<arg name="width" type="u" direction="in" />
<arg name="height" type="u" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In4" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
<!--
CaptureScreen:
@name: The name of the screen assigned by the compositor
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of the specified monitor. The application that
requests the screenshot must have the org.kde.KWin.ScreenShot2
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop file
entry.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureScreen">
<arg name="name" type="s" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
<!--
CaptureInteractive:
@kind: 0 - window, 1 - screen
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of a screen or a window as selected by the user.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "include-decoration" (b): Whether the decoration should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureInteractive">
<arg name="kind" type="u" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
</interface>
</node>

View file

@ -20,6 +20,7 @@
#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QGuiApplication>
#include <QProcess>
#include <QScreen>
#include <QStandardPaths>
@ -31,44 +32,45 @@ constexpr int SCREENSHOT_DELAY = 200;
/* -- Static Helpers --------------------------------------------------------------------------- */
static int readData(int theFile, QByteArray &theDataOut)
static QImage allocateImage(const QVariantMap &metadata)
{
// implementation based on QtWayland file qwaylanddataoffer.cpp
char lBuffer[4096];
int lRetryCount = 0;
ssize_t lBytesRead = 0;
bool ok;
do {
// give user 30 sec to click a window, afterwards considered as error
while (true) {
lBytesRead = QT_READ(theFile, lBuffer, sizeof lBuffer);
if (lBytesRead == -1 && (errno == EAGAIN) && ++lRetryCount < 30000) {
usleep(1000);
} else {
break;
}
}
if (lBytesRead > 0) {
theDataOut.append(lBuffer, lBytesRead);
}
} while (lBytesRead > 0);
return lBytesRead;
}
static QImage readImage(int thePipeFd)
{
QByteArray lContent;
if (readData(thePipeFd, lContent) != 0) {
close(thePipeFd);
const uint width = metadata.value(QStringLiteral("width")).toUInt(&ok);
if (!ok) {
return QImage();
}
close(thePipeFd);
QDataStream lDataStream(lContent);
QImage lImage;
lDataStream >> lImage;
return lImage;
const uint height = metadata.value(QStringLiteral("height")).toUInt(&ok);
if (!ok) {
return QImage();
}
const uint format = metadata.value(QStringLiteral("format")).toUInt(&ok);
if (!ok) {
return QImage();
}
return QImage(width, height, QImage::Format(format));
}
static QImage readImage(int fileDescriptor, const QVariantMap &metadata)
{
QFile file;
if (!file.open(fileDescriptor, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
close(fileDescriptor);
return QImage();
}
QImage result = allocateImage(metadata);
if (result.isNull()) {
return QImage();
}
QDataStream stream(&file);
stream.readRawData(reinterpret_cast<char *>(result.bits()), result.sizeInBytes());
return result;
}
PhonePanel::PhonePanel(QObject *parent, const QVariantList &args)
@ -76,7 +78,10 @@ PhonePanel::PhonePanel(QObject *parent, const QVariantList &args)
{
// setHasConfigurationInterface(true);
m_kscreenInterface = new org::kde::KScreen(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/kscreen"), QDBusConnection::sessionBus(), this);
m_screenshotInterface = new org::kde::kwin::Screenshot(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QDBusConnection::sessionBus(), this);
m_screenshotInterface = new OrgKdeKWinScreenShot2Interface(QStringLiteral("org.kde.KWin.ScreenShot2"),
QStringLiteral("/org/kde/KWin/ScreenShot2"),
QDBusConnection::sessionBus(),
this);
m_localeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig);
m_localeConfigWatcher = KConfigWatcher::create(m_localeConfig);
@ -143,55 +148,70 @@ void PhonePanel::setAutoRotate(bool value)
}
}
void PhonePanel::handleMetaDataReceived(const QVariantMap &metadata, int fd)
{
const QString type = metadata.value(QStringLiteral("type")).toString();
if (type != QLatin1String("raw")) {
qWarning() << "Unsupported metadata type:" << type;
return;
}
auto watcher = new QFutureWatcher<QImage>(this);
connect(watcher, &QFutureWatcher<QImage>::finished, this, [watcher]() {
watcher->deleteLater();
QString filePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
if (filePath.isEmpty()) {
qWarning() << "Couldn't find a writable location for the screenshot!";
return;
}
QDir picturesDir(filePath);
if (!picturesDir.mkpath(QStringLiteral("Screenshots"))) {
qWarning() << "Couldn't create folder at" << picturesDir.path() + QStringLiteral("/Screenshots") << "to take screenshot.";
return;
}
filePath += QStringLiteral("/Screenshots/Screenshot_%1.png").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss")));
const auto m_result = watcher->result();
if (m_result.isNull() || !m_result.save(filePath)) {
qWarning() << "Screenshot failed";
} else {
KNotification *notif = new KNotification("captured");
notif->setComponentName(QStringLiteral("plasma_phone_components"));
notif->setTitle(i18n("New Screenshot"));
notif->setUrls({filePath});
notif->setText(i18n("New screenshot saved to %1", filePath));
notif->sendEvent();
}
});
watcher->setFuture(QtConcurrent::run(readImage, fd, metadata));
}
void PhonePanel::takeScreenshot()
{
QString filePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
if (filePath.isEmpty()) {
qWarning() << "Couldn't find a writable location for the screenshot!";
return;
}
QDir picturesDir(filePath);
if (!picturesDir.mkpath(QStringLiteral("Screenshots"))) {
qWarning() << "Couldn't create folder at" << picturesDir.path() + QStringLiteral("/Screenshots") << "to take screenshot.";
return;
}
filePath += QStringLiteral("/Screenshots/Screenshot_%1.png").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss")));
// wait ~200 ms to wait for rest of animations
QTimer::singleShot(SCREENSHOT_DELAY, [=]() {
int lPipeFds[2];
if (pipe2(lPipeFds, O_CLOEXEC | O_NONBLOCK) != 0) {
if (pipe2(lPipeFds, O_CLOEXEC) != 0) {
qWarning() << "Could not take screenshot";
return;
}
// Take fullscreen screenshot, and no pointer
QDBusPendingCall pcall = m_screenshotInterface->screenshotFullscreen(QDBusUnixFileDescriptor(lPipeFds[1]), false, true);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pcall, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) {
if (watcher->isError()) {
const auto error = watcher->error();
qWarning() << "Error calling KWin DBus interface:" << error.name() << error.message();
}
watcher->deleteLater();
});
const auto lWatcher = new QFutureWatcher<QImage>(this);
QObject::connect(lWatcher, &QFutureWatcher<QImage>::finished, this, [lWatcher, filePath]() {
lWatcher->deleteLater();
const QImage lImage = lWatcher->result();
qDebug() << lImage;
if (!lImage.save(filePath, "PNG")) {
qWarning() << "Failed to save screenshot to" << filePath;
} else {
KNotification *notif = new KNotification("captured");
notif->setComponentName(QStringLiteral("plasma_phone_components"));
notif->setTitle(i18n("New Screenshot"));
notif->setUrls({filePath});
notif->setText(i18n("New screenshot saved to %1", filePath));
notif->sendEvent();
}
});
lWatcher->setFuture(QtConcurrent::run(readImage, lPipeFds[0]));
// We don't have access to the ScreenPool so we'll just take the first screen
auto pendingCall = m_screenshotInterface->CaptureScreen(qGuiApp->screens().constFirst()->name(), {}, QDBusUnixFileDescriptor(lPipeFds[1]));
close(lPipeFds[1]);
auto pipeFileDescriptor = lPipeFds[0];
auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, pipeFileDescriptor]() {
watcher->deleteLater();
const QDBusPendingReply<QVariantMap> reply = *watcher;
if (reply.isError()) {
qWarning() << "Screenshot request failed:" << reply.error().message();
} else {
handleMetaDataReceived(reply, pipeFileDescriptor);
}
});
});
}

View file

@ -13,7 +13,7 @@
#include <KSharedConfig>
#include "kscreeninterface.h"
#include "screenshotinterface.h"
#include "screenshot2interface.h"
class PhonePanel : public Plasma::Containment
{
@ -45,13 +45,14 @@ signals:
void isSystem24HourFormatChanged();
private:
void handleMetaDataReceived(const QVariantMap &metadata, int fd);
bool m_running = false;
KConfigWatcher::Ptr m_localeConfigWatcher;
KSharedConfig::Ptr m_localeConfig;
org::kde::KScreen *m_kscreenInterface;
org::kde::kwin::Screenshot *m_screenshotInterface;
OrgKdeKWinScreenShot2Interface *m_screenshotInterface;
};
#endif