File indexing completed on 2024-04-28 05:36:53

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Red Hat Inc
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  *
0006  * SPDX-FileCopyrightText: 2018 Jan Grulich <jgrulich@redhat.com>
0007  */
0008 
0009 #include "screenshotdialog.h"
0010 #include "screenshotdialog_debug.h"
0011 
0012 #include <QPushButton>
0013 
0014 #include <KLocalizedString>
0015 #include <QDBusConnection>
0016 #include <QDBusMessage>
0017 #include <QDBusReply>
0018 #include <QDBusUnixFileDescriptor>
0019 #include <QFutureWatcher>
0020 #include <QStandardItemModel>
0021 #include <QTimer>
0022 #include <QWindow>
0023 #include <QtConcurrentRun>
0024 #include <qplatformdefs.h>
0025 
0026 #include <fcntl.h>
0027 #include <poll.h>
0028 #include <unistd.h>
0029 
0030 static int readData(int fd, QByteArray &data)
0031 {
0032     char buffer[4096];
0033     pollfd pfds[1];
0034     pfds[0].fd = fd;
0035     pfds[0].events = POLLIN;
0036 
0037     while (true) {
0038         // give user 30 sec to click a window, afterwards considered as error
0039         const int ready = poll(pfds, 1, 30000);
0040         if (ready < 0) {
0041             if (errno != EINTR) {
0042                 qWarning() << "poll() failed:" << strerror(errno);
0043                 return -1;
0044             }
0045         } else if (ready == 0) {
0046             qDebug() << "failed to read screenshot: timeout";
0047             return -1;
0048         } else if (pfds[0].revents & POLLIN) {
0049             const int n = QT_READ(fd, buffer, sizeof(buffer));
0050 
0051             if (n < 0) {
0052                 qWarning() << "read() failed:" << strerror(errno);
0053                 return -1;
0054             } else if (n == 0) {
0055                 return 0;
0056             } else if (n > 0) {
0057                 data.append(buffer, n);
0058             }
0059         } else if (pfds[0].revents & POLLHUP) {
0060             return 0;
0061         } else {
0062             qWarning() << "failed to read screenshot: pipe is broken";
0063             return -1;
0064         }
0065     }
0066 
0067     Q_UNREACHABLE();
0068 }
0069 
0070 static QImage allocateImage(const QVariantMap &metadata)
0071 {
0072     bool ok;
0073 
0074     const uint width = metadata.value(QStringLiteral("width")).toUInt(&ok);
0075     if (!ok) {
0076         return QImage();
0077     }
0078 
0079     const uint height = metadata.value(QStringLiteral("height")).toUInt(&ok);
0080     if (!ok) {
0081         return QImage();
0082     }
0083 
0084     const uint format = metadata.value(QStringLiteral("format")).toUInt(&ok);
0085     if (!ok) {
0086         return QImage();
0087     }
0088 
0089     return QImage(width, height, QImage::Format(format));
0090 }
0091 
0092 static QImage readImage(int pipeFd, const QVariantMap &metadata)
0093 {
0094     QByteArray content;
0095     if (readData(pipeFd, content) != 0) {
0096         close(pipeFd);
0097         return QImage();
0098     }
0099     close(pipeFd);
0100 
0101     QImage result = allocateImage(metadata);
0102     if (result.isNull()) {
0103         return QImage();
0104     }
0105 
0106     QDataStream ds(content);
0107     ds.readRawData(reinterpret_cast<char *>(result.bits()), result.sizeInBytes());
0108     return result;
0109 }
0110 
0111 ScreenshotDialog::ScreenshotDialog(QObject *parent)
0112     : QuickDialog(parent)
0113 {
0114     QStandardItemModel *model = new QStandardItemModel(this);
0115     model->appendRow(new QStandardItem(i18n("Full Screen")));
0116     model->appendRow(new QStandardItem(i18n("Current Screen")));
0117     model->appendRow(new QStandardItem(i18n("Active Window")));
0118     create(QStringLiteral("qrc:/ScreenshotDialog.qml"),
0119            {
0120                {"app", QVariant::fromValue<QObject *>(this)},
0121                {"screenshotTypesModel", QVariant::fromValue<QObject *>(model)},
0122            });
0123 }
0124 
0125 QImage ScreenshotDialog::image() const
0126 {
0127     return m_image;
0128 }
0129 
0130 void ScreenshotDialog::takeScreenshotNonInteractive()
0131 {
0132     QFuture<QImage> future = takeScreenshot();
0133     if (!future.isStarted()) {
0134         return;
0135     }
0136 
0137     future.waitForFinished();
0138     m_image = future.result();
0139 }
0140 
0141 void ScreenshotDialog::takeScreenshotInteractive()
0142 {
0143     const QFuture<QImage> future = takeScreenshot();
0144     if (!future.isStarted()) {
0145         return;
0146     }
0147 
0148     QFutureWatcher<QImage> *watcher = new QFutureWatcher<QImage>(this);
0149     QObject::connect(watcher, &QFutureWatcher<QImage>::finished, this, [watcher, this] {
0150         watcher->deleteLater();
0151         m_image = watcher->result();
0152         m_theDialog->setProperty("screenshotImage", m_image);
0153     });
0154 
0155     watcher->setFuture(future);
0156 }
0157 
0158 QFuture<QImage> ScreenshotDialog::takeScreenshot()
0159 {
0160     int pipeFds[2];
0161     if (pipe2(pipeFds, O_CLOEXEC | O_NONBLOCK) != 0) {
0162         Q_EMIT failed();
0163         return {};
0164     }
0165 
0166     QVariantMap options;
0167     options.insert(QStringLiteral("native-resolution"), true); // match xdg-desktop-portal-gnome, and fixes flameshot!3164
0168     if (m_theDialog->property("withCursor").toBool()) {
0169         options.insert(QStringLiteral("include-cursor"), true);
0170     }
0171     if (m_theDialog->property("withBorders").toBool()) {
0172         options.insert(QStringLiteral("include-decoration"), true);
0173     }
0174 
0175     QString method;
0176     switch (ScreenshotType(m_theDialog->property("screenshotType").toInt())) {
0177     case FullScreen:
0178         method = QStringLiteral("CaptureWorkspace");
0179         break;
0180     case CurrentScreen:
0181         method = QStringLiteral("CaptureActiveScreen");
0182         break;
0183     case ActiveWindow:
0184         method = QStringLiteral("CaptureActiveWindow");
0185         break;
0186     }
0187 
0188     QVariantList arguments;
0189     arguments.append(options);
0190     arguments.append(QVariant::fromValue(QDBusUnixFileDescriptor(pipeFds[1])));
0191 
0192     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin.ScreenShot2"),
0193                                                           QStringLiteral("/org/kde/KWin/ScreenShot2"),
0194                                                           QStringLiteral("org.kde.KWin.ScreenShot2"),
0195                                                           method);
0196     message.setArguments(arguments);
0197 
0198     QDBusReply<QVariantMap> reply = QDBusConnection::sessionBus().call(message);
0199     ::close(pipeFds[1]);
0200     if (!reply.isValid()) {
0201         qCWarning(XdgDesktopPortalKdeScreenshotDialog) << method << "failed:" << reply.error();
0202         close(pipeFds[0]);
0203         return QFuture<QImage>();
0204     }
0205 
0206     return QtConcurrent::run(readImage, pipeFds[0], reply);
0207 }