1 /* This file is part of Spectacle, the KDE screenshot utility
2 * SPDX-FileCopyrightText: 2016 Martin Graesslin <mgraesslin@kde.org>
3
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "PlatformKWinWayland.h"
8
9 #include <QApplication>
10 #include <QDBusInterface>
11 #include <QDBusPendingCall>
12 #include <QDBusUnixFileDescriptor>
13 #include <QFutureWatcher>
14 #include <QGuiApplication>
15 #include <QImage>
16 #include <QPixmap>
17 #include <QScreen>
18 #include <QtConcurrent>
19 #include <qplatformdefs.h>
20
21 #include <array>
22
23 /* -- Static Helpers --------------------------------------------------------------------------- */
24
readData(int fd,QByteArray & data)25 static bool readData(int fd, QByteArray &data)
26 {
27 fd_set readset;
28 FD_ZERO(&readset);
29 FD_SET(fd, &readset);
30 struct timeval timeout;
31 timeout.tv_sec = 30;
32 timeout.tv_usec = 0;
33 char buf[4096 * 16];
34
35 while (true) {
36 int ready = select(FD_SETSIZE, &readset, nullptr, nullptr, &timeout);
37 if (ready < 0) {
38 qWarning() << "PlatformKWinWayland readData: select() failed" << strerror(errno);
39 return false;
40 } else if (ready == 0) {
41 qWarning("PlatformKWinWayland readData: timeout reading from pipe");
42 return false;
43 } else {
44 int n = read(fd, buf, sizeof buf);
45
46 if (n < 0) {
47 qWarning() << "PlatformKWinWayland readData: read() failed" << strerror(errno);
48 return false;
49 } else if (n == 0) {
50 return true;
51 } else {
52 data.append(buf, n);
53 }
54 }
55 }
56
57 Q_UNREACHABLE();
58 }
59
readImage(int thePipeFd)60 static QImage readImage(int thePipeFd)
61 {
62 QByteArray lContent;
63 if (!readData(thePipeFd, lContent)) {
64 close(thePipeFd);
65 return QImage();
66 }
67 close(thePipeFd);
68
69 QDataStream lDataStream(lContent);
70 QImage lImage;
71 lDataStream >> lImage;
72 return lImage;
73 }
74
readImages(int thePipeFd)75 static QVector<QImage> readImages(int thePipeFd)
76 {
77 QByteArray lContent;
78 if (!readData(thePipeFd, lContent)) {
79 close(thePipeFd);
80 return QVector<QImage>();
81 }
82 close(thePipeFd);
83
84 QDataStream lDataStream(lContent);
85 lDataStream.setVersion(QDataStream::Qt_DefaultCompiledVersion);
86
87 QImage lImage;
88 QVector<QImage> imgs;
89 while (!lDataStream.atEnd()) {
90 lDataStream >> lImage;
91 if (!lImage.isNull()) {
92 imgs << lImage;
93 }
94 }
95
96 return imgs;
97 }
98
99 /* -- General Plumbing ------------------------------------------------------------------------- */
100
PlatformKWinWayland(QObject * parent)101 PlatformKWinWayland::PlatformKWinWayland(QObject *parent)
102 : Platform(parent)
103 {
104 }
105
platformName() const106 QString PlatformKWinWayland::platformName() const
107 {
108 return QStringLiteral("KWinWayland");
109 }
110
111 static std::array<int, 3> s_plasmaVersion = {-1, -1, -1};
112
findPlasmaMinorVersion()113 std::array<int, 3> findPlasmaMinorVersion()
114 {
115 if (s_plasmaVersion == std::array<int, 3>{-1, -1, -1}) {
116 auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"),
117 QStringLiteral("/MainApplication"),
118 QStringLiteral("org.freedesktop.DBus.Properties"),
119 QStringLiteral("Get"));
120
121 message.setArguments({QStringLiteral("org.qtproject.Qt.QCoreApplication"), QStringLiteral("applicationVersion")});
122
123 const auto resultMessage = QDBusConnection::sessionBus().call(message);
124 if (resultMessage.type() != QDBusMessage::ReplyMessage) {
125 qWarning() << "Error querying plasma version" << resultMessage.errorName() << resultMessage.errorMessage();
126 return s_plasmaVersion;
127 }
128 QDBusVariant val = resultMessage.arguments().at(0).value<QDBusVariant>();
129
130 const QString rawVersion = val.variant().value<QString>();
131 const QVector<QStringRef> splitted = rawVersion.splitRef(QLatin1Char('.'));
132 if (splitted.size() != 3) {
133 qWarning() << "error parsing plasma version";
134 return s_plasmaVersion;
135 }
136 bool ok;
137 int plasmaMajorVersion = splitted[0].toInt(&ok);
138 if (!ok) {
139 qWarning() << "error parsing plasma major version";
140 return s_plasmaVersion;
141 }
142 int plasmaMinorVersion = splitted[1].toInt(&ok);
143 if (!ok) {
144 qWarning() << "error parsing plasma minor version";
145 return s_plasmaVersion;
146 }
147 int plasmaPatchVersion = splitted[2].toInt(&ok);
148 if (!ok) {
149 qWarning() << "error parsing plasma patch version";
150 return s_plasmaVersion;
151 }
152 s_plasmaVersion = {plasmaMajorVersion, plasmaMinorVersion, plasmaPatchVersion};
153 }
154 return s_plasmaVersion;
155 }
156
supportedGrabModes() const157 Platform::GrabModes PlatformKWinWayland::supportedGrabModes() const
158 {
159 Platform::GrabModes lSupportedModes({Platform::GrabMode::AllScreens, GrabMode::WindowUnderCursor});
160 QList<QScreen *> screens = QApplication::screens();
161
162 // TODO remove sometime after Plasma 5.21 is released
163 // We can handle rectangular selection one one screen not scale factor
164 // on Plasma < 5.21
165 if (screenshotScreensAvailable() || (screens.count() == 1 && screens.first()->devicePixelRatio() == 1)) {
166 lSupportedModes |= Platform::GrabMode::PerScreenImageNative;
167 }
168
169 // TODO remove sometime after Plasma 5.20 is released
170 auto plasmaVersion = findPlasmaMinorVersion();
171 if (plasmaVersion.at(0) != -1 && (plasmaVersion.at(0) != 5 || (plasmaVersion.at(1) >= 20))) {
172 lSupportedModes |= Platform::GrabMode::AllScreensScaled;
173 }
174
175 if (screens.count() > 1) {
176 lSupportedModes |= Platform::GrabMode::CurrentScreen;
177 }
178 return lSupportedModes;
179 }
180
screenshotScreensAvailable() const181 bool PlatformKWinWayland::screenshotScreensAvailable() const
182 {
183 // TODO remove sometime after Plasma 5.21 is released
184 auto plasmaVersion = findPlasmaMinorVersion();
185 // Screenshot screenshotScreens dbus interface requires Plasma 5.21
186 if (plasmaVersion.at(0) != -1 && (plasmaVersion.at(0) != 5 || (plasmaVersion.at(1) >= 21 || (plasmaVersion.at(1) == 20 && plasmaVersion.at(2) >= 80)))) {
187 return true;
188 } else {
189 return false;
190 }
191 }
192
supportedShutterModes() const193 Platform::ShutterModes PlatformKWinWayland::supportedShutterModes() const
194 {
195 // TODO remove sometime after Plasma 5.20 is released
196 auto plasmaVersion = findPlasmaMinorVersion();
197 if (plasmaVersion.at(0) != -1 && (plasmaVersion.at(0) != 5 || (plasmaVersion.at(1) >= 20))) {
198 return {ShutterMode::Immediate};
199 } else {
200 return {ShutterMode::OnClick};
201 }
202 }
203
doGrab(ShutterMode,GrabMode theGrabMode,bool theIncludePointer,bool theIncludeDecorations)204 void PlatformKWinWayland::doGrab(ShutterMode /* theShutterMode */, GrabMode theGrabMode, bool theIncludePointer, bool theIncludeDecorations)
205 {
206 switch (theGrabMode) {
207 case GrabMode::AllScreens:
208 doGrabHelper(QStringLiteral("screenshotFullscreen"), theIncludePointer, true);
209 return;
210 case GrabMode::AllScreensScaled:
211 doGrabHelper(QStringLiteral("screenshotFullscreen"), theIncludePointer, false);
212 return;
213
214 case GrabMode::PerScreenImageNative: {
215 const QList<QScreen *> screens = QGuiApplication::screens();
216 QStringList screenNames;
217 screenNames.reserve(screens.count());
218 for (const auto screen : screens) {
219 screenNames << screen->name();
220 }
221 if (screenshotScreensAvailable()) {
222 doGrabImagesHelper(QStringLiteral("screenshotScreens"), screenNames, theIncludePointer, true);
223 } else {
224 // TODO remove sometime after Plasma 5.21 is released
225 // Use the dbus call screenshotFullscreen to get a single screen screenshot and treat it as a list of images
226 doGrabImagesHelper(QStringLiteral("screenshotFullscreen"), theIncludePointer, true);
227 }
228 return;
229 }
230 case GrabMode::CurrentScreen: {
231 doGrabHelper(QStringLiteral("screenshotScreen"), theIncludePointer);
232 return;
233 }
234 case GrabMode::WindowUnderCursor: {
235 int lOpMask = theIncludeDecorations ? 1 : 0;
236 if (theIncludePointer) {
237 lOpMask |= 1 << 1;
238 }
239 doGrabHelper(QStringLiteral("interactive"), lOpMask);
240 return;
241 }
242 case GrabMode::InvalidChoice:
243 case GrabMode::ActiveWindow:
244 case GrabMode::TransientWithParent:
245 Q_EMIT newScreenshotFailed();
246 return;
247 }
248 }
249
250 /* -- Grab Helpers ----------------------------------------------------------------------------- */
251
startReadImage(int theReadPipe)252 void PlatformKWinWayland::startReadImage(int theReadPipe)
253 {
254 auto lWatcher = new QFutureWatcher<QImage>(this);
255 QObject::connect(lWatcher, &QFutureWatcher<QImage>::finished, this, [lWatcher, this]() {
256 lWatcher->deleteLater();
257 const QImage lImage = lWatcher->result();
258 if (lImage.isNull()) {
259 Q_EMIT newScreenshotFailed();
260 } else {
261 Q_EMIT newScreenshotTaken(QPixmap::fromImage(lImage));
262 }
263 });
264 lWatcher->setFuture(QtConcurrent::run(readImage, theReadPipe));
265 }
266
startReadImages(int theReadPipe)267 void PlatformKWinWayland::startReadImages(int theReadPipe)
268 {
269 auto lWatcher = new QFutureWatcher<QVector<QImage>>(this);
270 QObject::connect(lWatcher, &QFutureWatcher<QVector<QImage>>::finished, this, [lWatcher, this]() {
271 lWatcher->deleteLater();
272 auto result = lWatcher->result();
273 if (result.isEmpty()) {
274 Q_EMIT newScreenshotFailed();
275 } else {
276 Q_EMIT newScreensScreenshotTaken(result);
277 }
278 });
279 lWatcher->setFuture(QtConcurrent::run(readImages, theReadPipe));
280 }
281
282 template<typename... ArgType>
callDBus(const QString & theGrabMethod,int theWriteFile,ArgType...arguments)283 void PlatformKWinWayland::callDBus(const QString &theGrabMethod, int theWriteFile, ArgType... arguments)
284 {
285 QDBusInterface lInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"));
286 QDBusPendingCall pcall = lInterface.asyncCall(theGrabMethod, QVariant::fromValue(QDBusUnixFileDescriptor(theWriteFile)), arguments...);
287 checkDbusPendingCall(pcall);
288 }
289
checkDbusPendingCall(const QDBusPendingCall & pcall)290 void PlatformKWinWayland::checkDbusPendingCall(const QDBusPendingCall &pcall)
291 {
292 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pcall, this);
293 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
294 if (watcher->isError()) {
295 const auto error = watcher->error();
296 qWarning() << "Error calling KWin DBus interface:" << error.name() << error.message();
297 Q_EMIT newScreenshotFailed();
298 }
299 watcher->deleteLater();
300 });
301 }
302
303 template<typename... ArgType>
doGrabHelper(const QString & theGrabMethod,ArgType...arguments)304 void PlatformKWinWayland::doGrabHelper(const QString &theGrabMethod, ArgType... arguments)
305 {
306 int lPipeFds[2];
307 if (pipe2(lPipeFds, O_CLOEXEC | O_NONBLOCK) != 0) {
308 Q_EMIT newScreenshotFailed();
309 return;
310 }
311
312 callDBus(theGrabMethod, lPipeFds[1], arguments...);
313 startReadImage(lPipeFds[0]);
314
315 close(lPipeFds[1]);
316 }
317
318 template<typename... ArgType>
doGrabImagesHelper(const QString & theGrabMethod,ArgType...arguments)319 void PlatformKWinWayland::doGrabImagesHelper(const QString &theGrabMethod, ArgType... arguments)
320 {
321 int lPipeFds[2];
322 if (pipe2(lPipeFds, O_CLOEXEC | O_NONBLOCK) != 0) {
323 Q_EMIT newScreenshotFailed();
324 return;
325 }
326
327 callDBus(theGrabMethod, lPipeFds[1], arguments...);
328 startReadImages(lPipeFds[0]);
329
330 close(lPipeFds[1]);
331 }
332