File indexing completed on 2025-02-02 04:26:13

0001 /*
0002     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "ImagePlatformKWin.h"
0008 #include "ExportManager.h"
0009 
0010 #include <KWindowSystem>
0011 
0012 #include <QDBusConnection>
0013 #include <QDBusConnectionInterface>
0014 #include <QDBusPendingCall>
0015 #include <QDBusPendingCallWatcher>
0016 #include <QDBusReply>
0017 #include <QDBusUnixFileDescriptor>
0018 #include <QFile>
0019 #include <QFileDevice>
0020 #include <QFuture>
0021 #include <QFutureWatcher>
0022 #include <QGuiApplication>
0023 #include <QPixmap>
0024 #include <QScreen>
0025 #include <QTimer>
0026 #include <QtConcurrentRun>
0027 
0028 #include <errno.h>
0029 #include <fcntl.h>
0030 #include <string.h>
0031 #include <unistd.h>
0032 
0033 using namespace Qt::StringLiterals;
0034 
0035 static QVariantMap screenShotFlagsToVardict(ImagePlatformKWin::ScreenShotFlags flags)
0036 {
0037     QVariantMap options;
0038 
0039     if (flags & ImagePlatformKWin::ScreenShotFlag::IncludeCursor) {
0040         options.insert(u"include-cursor"_s, true);
0041     }
0042     if (flags & ImagePlatformKWin::ScreenShotFlag::IncludeDecoration) {
0043         options.insert(u"include-decoration"_s, true);
0044     }
0045 
0046     bool includeShadow = flags & ImagePlatformKWin::ScreenShotFlag::IncludeShadow;
0047     options.insert(u"include-shadow"_s, includeShadow);
0048 
0049     if (flags & ImagePlatformKWin::ScreenShotFlag::NativeSize) {
0050         options.insert(u"native-resolution"_s, true);
0051     }
0052 
0053     return options;
0054 }
0055 
0056 static const QString s_screenShotService = u"org.kde.KWin.ScreenShot2"_s;
0057 static const QString s_screenShotObjectPath = u"/org/kde/KWin/ScreenShot2"_s;
0058 static const QString s_screenShotInterface = u"org.kde.KWin.ScreenShot2"_s;
0059 
0060 template<typename... ArgType>
0061 ScreenShotSource2::ScreenShotSource2(const QString &methodName, ArgType... arguments)
0062 {
0063     // Do not set the O_NONBLOCK flag. Code that reads data from the pipe assumes
0064     // that read() will block if there is no any data yet.
0065     int pipeFds[2];
0066     if (pipe2(pipeFds, O_CLOEXEC) == -1) {
0067         QTimer::singleShot(0, this, &ScreenShotSource2::errorOccurred);
0068         qWarning() << "pipe2() failed:" << strerror(errno);
0069         return;
0070     }
0071 
0072     QDBusMessage message = QDBusMessage::createMethodCall(s_screenShotService, s_screenShotObjectPath, s_screenShotInterface, methodName);
0073 
0074     QVariantList dbusArguments{arguments...};
0075     dbusArguments.append(QVariant::fromValue(QDBusUnixFileDescriptor(pipeFds[1])));
0076     message.setArguments(dbusArguments);
0077 
0078     QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
0079     close(pipeFds[1]);
0080     m_pipeFileDescriptor = pipeFds[0];
0081 
0082     auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
0083     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() {
0084         watcher->deleteLater();
0085         const QDBusPendingReply<QVariantMap> reply = *watcher;
0086 
0087         if (reply.isError()) {
0088             qWarning() << "Screenshot request failed:" << reply.error().message();
0089             if (reply.error().name() == u"org.kde.KWin.ScreenShot2.Error.Cancelled"_s) {
0090                 // don't show error on user cancellation
0091                 Q_EMIT finished(m_result);
0092             } else {
0093                 Q_EMIT errorOccurred();
0094             }
0095         } else {
0096             handleMetaDataReceived(reply);
0097         }
0098     });
0099 }
0100 
0101 ScreenShotSource2::~ScreenShotSource2()
0102 {
0103     if (m_pipeFileDescriptor != -1) {
0104         close(m_pipeFileDescriptor);
0105     }
0106 }
0107 
0108 QImage ScreenShotSource2::result() const
0109 {
0110     return m_result;
0111 }
0112 
0113 static QImage allocateImage(const QVariantMap &metadata)
0114 {
0115     bool ok;
0116 
0117     const uint width = metadata.value(u"width"_s).toUInt(&ok);
0118     if (!ok) {
0119         return QImage();
0120     }
0121 
0122     const uint height = metadata.value(u"height"_s).toUInt(&ok);
0123     if (!ok) {
0124         return QImage();
0125     }
0126 
0127     const uint format = metadata.value(u"format"_s).toUInt(&ok);
0128     if (!ok) {
0129         return QImage();
0130     }
0131 
0132     return QImage(width, height, QImage::Format(format));
0133 }
0134 
0135 static QImage readImage(int fileDescriptor, const QVariantMap &metadata)
0136 {
0137     QFile file;
0138     if (!file.open(fileDescriptor, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
0139         close(fileDescriptor);
0140         return QImage();
0141     }
0142 
0143     QImage result = allocateImage(metadata);
0144     if (result.isNull()) {
0145         return QImage();
0146     }
0147 
0148     const auto windowId = metadata.value(u"windowId"_s).toString();
0149     // No point in storing the windowId in the image since it means nothing to users
0150     // and can't be used if the window is closed.
0151     if (!windowId.isEmpty()) {
0152         QDBusMessage message = QDBusMessage::createMethodCall(u"org.kde.KWin"_s,
0153                                                               u"/KWin"_s,
0154                                                               u"org.kde.KWin"_s,
0155                                                               u"getWindowInfo"_s);
0156         message.setArguments({windowId});
0157         const QDBusReply<QVariantMap> reply = QDBusConnection::sessionBus().call(message);
0158         if (reply.isValid()) {
0159             const auto &windowTitle = reply.value().value(u"caption"_s).toString();
0160             if (!windowTitle.isEmpty()) {
0161                 result.setText(u"windowTitle"_s, windowTitle);
0162                 ExportManager::instance()->setWindowTitle(windowTitle);
0163             }
0164         }
0165     }
0166 
0167     bool ok = false;
0168     qreal scale = metadata.value(u"scale"_s).toReal(&ok);
0169     if (ok) {
0170         // NOTE: KWin X11Output DPR is always 1. This is intentional.
0171         // https://bugs.kde.org/show_bug.cgi?id=474778
0172         if (KWindowSystem::isPlatformX11() && scale == 1) {
0173             scale = qGuiApp->devicePixelRatio();
0174         }
0175         result.setDevicePixelRatio(scale);
0176     }
0177 
0178     const auto screen = metadata.value(u"screen"_s).toString();
0179     if (!screen.isEmpty()) {
0180         result.setText(u"screen"_s, screen);
0181     }
0182 
0183     QDataStream stream(&file);
0184     stream.readRawData(reinterpret_cast<char *>(result.bits()), result.sizeInBytes());
0185 
0186     return result;
0187 }
0188 
0189 void ScreenShotSource2::handleMetaDataReceived(const QVariantMap &metadata)
0190 {
0191     const QString type = metadata.value(u"type"_s).toString();
0192     if (type != "raw"_L1) {
0193         qWarning() << "Unsupported metadata type:" << type;
0194         return;
0195     }
0196 
0197     auto watcher = new QFutureWatcher<QImage>(this);
0198     connect(watcher, &QFutureWatcher<QImage>::finished, this, [this, watcher]() {
0199         watcher->deleteLater();
0200         m_result = watcher->result();
0201         if (m_result.isNull()) {
0202             Q_EMIT errorOccurred();
0203         } else {
0204             Q_EMIT finished(m_result);
0205         }
0206     });
0207     watcher->setFuture(QtConcurrent::run(readImage, m_pipeFileDescriptor, metadata));
0208 
0209     // The ownership of the pipe file descriptor has been moved to the worker thread.
0210     m_pipeFileDescriptor = -1;
0211 }
0212 
0213 ScreenShotSourceArea2::ScreenShotSourceArea2(const QRect &area, ImagePlatformKWin::ScreenShotFlags flags)
0214     : ScreenShotSource2(u"CaptureArea"_s,
0215                         qint32(area.x()),
0216                         qint32(area.y()),
0217                         quint32(area.width()),
0218                         quint32(area.height()),
0219                         screenShotFlagsToVardict(flags))
0220 {
0221 }
0222 
0223 ScreenShotSourceInteractive2::ScreenShotSourceInteractive2(ImagePlatformKWin::InteractiveKind kind, ImagePlatformKWin::ScreenShotFlags flags)
0224     : ScreenShotSource2(u"CaptureInteractive"_s, quint32(kind), screenShotFlagsToVardict(flags))
0225 {
0226 }
0227 
0228 ScreenShotSourceScreen2::ScreenShotSourceScreen2(const QScreen *screen, ImagePlatformKWin::ScreenShotFlags flags)
0229 // NOTE: As of Qt 6.4, QScreen::name() is not guaranteed to match the result of any native APIs.
0230 // It should not be used to uniquely identify a screen, but it happens to work on X11 and Wayland.
0231 // KWin's ScreenShot2 DBus API uses QScreen::name() as identifiers for screens.
0232     : ScreenShotSource2(u"CaptureScreen"_s, screen->name(), screenShotFlagsToVardict(flags))
0233 {
0234 }
0235 
0236 ScreenShotSourceActiveWindow2::ScreenShotSourceActiveWindow2(ImagePlatformKWin::ScreenShotFlags flags)
0237     : ScreenShotSource2(u"CaptureActiveWindow"_s, screenShotFlagsToVardict(flags))
0238 {
0239 }
0240 
0241 ScreenShotSourceActiveScreen2::ScreenShotSourceActiveScreen2(ImagePlatformKWin::ScreenShotFlags flags)
0242     : ScreenShotSource2(u"CaptureActiveScreen"_s, screenShotFlagsToVardict(flags))
0243 {
0244 }
0245 
0246 ScreenShotSourceWorkspace2::ScreenShotSourceWorkspace2(ImagePlatformKWin::ScreenShotFlags flags)
0247     : ScreenShotSource2(u"CaptureWorkspace"_s, screenShotFlagsToVardict(flags))
0248 {
0249 }
0250 
0251 ImagePlatformKWin::ImagePlatformKWin(QObject *parent)
0252     : ImagePlatform(parent)
0253 {
0254     auto message = QDBusMessage::createMethodCall(u"org.kde.KWin.ScreenShot2"_s,
0255                                                   u"/org/kde/KWin/ScreenShot2"_s,
0256                                                   u"org.freedesktop.DBus.Properties"_s,
0257                                                   u"Get"_s);
0258     message.setArguments({u"org.kde.KWin.ScreenShot2"_s, u"Version"_s});
0259 
0260     const QDBusMessage reply = QDBusConnection::sessionBus().call(message);
0261     if (reply.type() == QDBusMessage::ReplyMessage) {
0262         m_apiVersion = reply.arguments().constFirst().value<QDBusVariant>().variant().toUInt();
0263     }
0264 
0265     updateSupportedGrabModes();
0266     connect(qGuiApp, &QGuiApplication::screenAdded, this, &ImagePlatformKWin::updateSupportedGrabModes);
0267     connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ImagePlatformKWin::updateSupportedGrabModes);
0268 }
0269 
0270 ImagePlatform::GrabModes ImagePlatformKWin::supportedGrabModes() const
0271 {
0272     return m_grabModes;
0273 }
0274 
0275 void ImagePlatformKWin::updateSupportedGrabModes()
0276 {
0277     ImagePlatform::GrabModes grabModes = GrabMode::AllScreens | GrabMode::WindowUnderCursor | GrabMode::PerScreenImageNative;
0278 
0279     if (m_apiVersion >= 2) {
0280         grabModes |= GrabMode::ActiveWindow;
0281     }
0282 
0283     if (QGuiApplication::screens().count() > 1) {
0284         grabModes |= GrabMode::CurrentScreen | GrabMode::AllScreensScaled;
0285     }
0286 
0287     if (m_grabModes != grabModes) {
0288         m_grabModes = grabModes;
0289         Q_EMIT supportedGrabModesChanged();
0290     }
0291 }
0292 
0293 ImagePlatform::ShutterModes ImagePlatformKWin::supportedShutterModes() const
0294 {
0295     return ShutterMode::Immediate;
0296 }
0297 
0298 void ImagePlatformKWin::doGrab(ShutterMode, GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow)
0299 {
0300     ScreenShotFlags flags = ScreenShotFlag::NativeSize;
0301 
0302     flags.setFlag(ScreenShotFlag::IncludeShadow, includeShadow);
0303 
0304     if (includeDecorations) {
0305         flags |= ScreenShotFlag::IncludeDecoration;
0306     }
0307     if (includePointer) {
0308         flags |= ScreenShotFlag::IncludeCursor;
0309     }
0310 
0311     switch (grabMode) {
0312     case GrabMode::AllScreens:
0313         takeScreenShotWorkspace(flags);
0314         break;
0315     case GrabMode::CurrentScreen:
0316         if (m_apiVersion >= 2) {
0317             takeScreenShotActiveScreen(flags);
0318         } else {
0319             takeScreenShotInteractive(InteractiveKind::Screen, flags);
0320         }
0321         break;
0322     case GrabMode::ActiveWindow:
0323         takeScreenShotActiveWindow(flags);
0324         break;
0325     case GrabMode::TransientWithParent:
0326     case GrabMode::WindowUnderCursor:
0327         takeScreenShotInteractive(InteractiveKind::Window, flags);
0328         break;
0329     case GrabMode::AllScreensScaled:
0330         takeScreenShotWorkspace(flags & ~ScreenShotFlags(ScreenShotFlag::NativeSize));
0331         break;
0332     case GrabMode::PerScreenImageNative:
0333         takeScreenShotCroppable(flags);
0334         break;
0335     case GrabMode::NoGrabModes:
0336         Q_EMIT newScreenshotFailed();
0337         break;
0338     }
0339 }
0340 
0341 void ImagePlatformKWin::trackSource(ScreenShotSource2 *source)
0342 {
0343     connect(source, &ScreenShotSource2::finished, this, [this, source](const QImage &image) {
0344         source->deleteLater();
0345         Q_EMIT newScreenshotTaken(image);
0346     });
0347     connect(source, &ScreenShotSource2::errorOccurred, this, [this, source]() {
0348         source->deleteLater();
0349         Q_EMIT newScreenshotFailed();
0350     });
0351 }
0352 
0353 void ImagePlatformKWin::trackCroppableSource(ScreenShotSourceWorkspace2 *source)
0354 {
0355     connect(source, &ScreenShotSourceWorkspace2::finished, this, [this, source](const QImage &image) {
0356         source->deleteLater();
0357         Q_EMIT newCroppableScreenshotTaken(image);
0358     });
0359     connect(source, &ScreenShotSourceWorkspace2::errorOccurred, this, [this, source]() {
0360         source->deleteLater();
0361         Q_EMIT newScreenshotFailed();
0362     });
0363 }
0364 
0365 void ImagePlatformKWin::takeScreenShotArea(const QRect &area, ScreenShotFlags flags)
0366 {
0367     trackSource(new ScreenShotSourceArea2(area, flags));
0368 }
0369 
0370 void ImagePlatformKWin::takeScreenShotInteractive(InteractiveKind kind, ScreenShotFlags flags)
0371 {
0372     trackSource(new ScreenShotSourceInteractive2(kind, flags));
0373 }
0374 
0375 void ImagePlatformKWin::takeScreenShotActiveWindow(ScreenShotFlags flags)
0376 {
0377     trackSource(new ScreenShotSourceActiveWindow2(flags));
0378 }
0379 
0380 void ImagePlatformKWin::takeScreenShotActiveScreen(ScreenShotFlags flags)
0381 {
0382     trackSource(new ScreenShotSourceActiveScreen2(flags));
0383 }
0384 
0385 void ImagePlatformKWin::takeScreenShotWorkspace(ScreenShotFlags flags)
0386 {
0387     trackSource(new ScreenShotSourceWorkspace2(flags));
0388 }
0389 
0390 void ImagePlatformKWin::takeScreenShotCroppable(ScreenShotFlags flags)
0391 {
0392     trackCroppableSource(new ScreenShotSourceWorkspace2(flags));
0393 }
0394 
0395 #include "moc_ImagePlatformKWin.cpp"