diff --git a/src/effects/screenshot/CMakeLists.txt b/src/effects/screenshot/CMakeLists.txt index 2b02cf5062..23d826dbfb 100644 --- a/src/effects/screenshot/CMakeLists.txt +++ b/src/effects/screenshot/CMakeLists.txt @@ -5,4 +5,5 @@ set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} ../service_utils.cpp screenshot/screenshot.cpp + screenshot/screenshotdbusinterface1.cpp ) diff --git a/src/effects/screenshot/screenshot.cpp b/src/effects/screenshot/screenshot.cpp index ddd1d9e2c1..ddcf05500e 100644 --- a/src/effects/screenshot/screenshot.cpp +++ b/src/effects/screenshot/screenshot.cpp @@ -4,828 +4,48 @@ SPDX-FileCopyrightText: 2010 Martin Gräßlin SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "screenshot.h" +#include "screenshotdbusinterface1.h" + #include #include #include -#include -#include -#include -#include -#include -#include -#include + #include -#include + #include -#include -#include -#include - -#include -#include - -#include -#include "../service_utils.h" - -Q_DECLARE_METATYPE(QStringList) - -class ComparableQPoint : public QPoint -{ -public: - ComparableQPoint(QPoint& point): QPoint(point.x(), point.y()) - {} - - ComparableQPoint(QPoint point): QPoint(point.x(), point.y()) - {} - - ComparableQPoint(): QPoint() - {} - - // utility class that allows using QMap to sort its keys when they are QPoint - // so that the bottom and right points are after the top left ones - bool operator<(const ComparableQPoint &other) const { - return x() < other.x() || (x() == other.x() && y() < other.y()); - } -}; - namespace KWin { -const static QString s_errorAlreadyTaking = QStringLiteral("org.kde.kwin.Screenshot.Error.AlreadyTaking"); -const static QString s_errorAlreadyTakingMsg = QStringLiteral("A screenshot is already been taken"); -const static QString s_errorNotAuthorized = QStringLiteral("org.kde.kwin.Screenshot.Error.NoAuthorized"); -const static QString s_errorNotAuthorizedMsg = QStringLiteral("The process is not authorized to take a screenshot"); -const static QString s_errorFd = QStringLiteral("org.kde.kwin.Screenshot.Error.FileDescriptor"); -const static QString s_errorFdMsg = QStringLiteral("No valid file descriptor"); -const static QString s_errorCancelled = QStringLiteral("org.kde.kwin.Screenshot.Error.Cancelled"); -const static QString s_errorCancelledMsg = QStringLiteral("Screenshot got cancelled"); -const static QString s_errorInvalidArea = QStringLiteral("org.kde.kwin.Screenshot.Error.InvalidArea"); -const static QString s_errorInvalidAreaMsg = QStringLiteral("Invalid area requested"); -const static QString s_errorInvalidScreen = QStringLiteral("org.kde.kwin.Screenshot.Error.InvalidScreen"); -const static QString s_errorInvalidScreenMsg = QStringLiteral("Invalid screen requested"); -const static QString s_dbusInterfaceName = QStringLiteral("org.kde.kwin.Screenshot"); -const static QString s_errorScreenMissing = QStringLiteral("org.kde.kwin.Screenshot.Error.ScreenMissing"); -const static QString s_errorScreenMissingMsg = QStringLiteral("Screen not found"); - -bool ScreenShotEffect::supported() +struct ScreenShotWindowData { - return effects->compositingType() == XRenderCompositing || - (effects->isOpenGLCompositing() && GLRenderTarget::supported()); -} + QFutureInterface promise; + ScreenShotFlags flags; + EffectWindow *window = nullptr; +}; -ScreenShotEffect::ScreenShotEffect() - : m_scheduledScreenshot(nullptr) +struct ScreenShotAreaData { - connect(effects, &EffectsHandler::windowClosed, this, &ScreenShotEffect::windowClosed); - QDBusConnection::sessionBus().registerObject(QStringLiteral("/Screenshot"), this, QDBusConnection::ExportScriptableContents); -} + QFutureInterface promise; + ScreenShotFlags flags; + QRect area; + QImage result; + QList screens; +}; -ScreenShotEffect::~ScreenShotEffect() +struct ScreenShotScreenData { - QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Screenshot")); -} + QFutureInterface promise; + ScreenShotFlags flags; + EffectScreen *screen = nullptr; +}; -#ifdef KWIN_HAVE_XRENDER_COMPOSITING -static QImage xPictureToImage(xcb_render_picture_t srcPic, const QRect &geometry, xcb_image_t **xImage) -{ - xcb_connection_t *c = effects->xcbConnection(); - xcb_pixmap_t xpix = xcb_generate_id(c); - xcb_create_pixmap(c, 32, xpix, effects->x11RootWindow(), geometry.width(), geometry.height()); - XRenderPicture pic(xpix, 32); - xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, srcPic, XCB_RENDER_PICTURE_NONE, pic, - geometry.x(), geometry.y(), 0, 0, 0, 0, geometry.width(), geometry.height()); - xcb_flush(c); - *xImage = xcb_image_get(c, xpix, 0, 0, geometry.width(), geometry.height(), ~0, XCB_IMAGE_FORMAT_Z_PIXMAP); - QImage img((*xImage)->data, (*xImage)->width, (*xImage)->height, (*xImage)->stride, QImage::Format_ARGB32_Premultiplied); - // TODO: byte order might need swapping - xcb_free_pixmap(c, xpix); - return img.copy(); -} -#endif - -static QSize pickWindowSize(const QImage &image) -{ - xcb_connection_t *c = effects->xcbConnection(); - - // This will implicitly enable BIG-REQUESTS extension. - const uint32_t maximumRequestSize = xcb_get_maximum_request_length(c); - const xcb_setup_t *setup = xcb_get_setup(c); - - uint32_t requestSize = sizeof(xcb_put_image_request_t); - - // With BIG-REQUESTS extension an additional 32-bit field is inserted into - // the request so we better take it into account. - if (setup->maximum_request_length < maximumRequestSize) { - requestSize += 4; - } - - const uint32_t maximumDataSize = 4 * maximumRequestSize - requestSize; - const uint32_t bytesPerPixel = image.depth() >> 3; - const uint32_t bytesPerLine = image.bytesPerLine(); - - if (image.sizeInBytes() <= maximumDataSize) { - return image.size(); - } - - if (maximumDataSize < bytesPerLine) { - return QSize(maximumDataSize / bytesPerPixel, 1); - } - - return QSize(image.width(), maximumDataSize / bytesPerLine); -} - -static xcb_pixmap_t xpixmapFromImage(const QImage &image) -{ - xcb_connection_t *c = effects->xcbConnection(); - - xcb_pixmap_t pixmap = xcb_generate_id(c); - xcb_gcontext_t gc = xcb_generate_id(c); - - xcb_create_pixmap(c, image.depth(), pixmap, effects->x11RootWindow(), - image.width(), image.height()); - xcb_create_gc(c, gc, pixmap, 0, nullptr); - - const int bytesPerPixel = image.depth() >> 3; - - // Figure out how much data we can send with one invocation of xcb_put_image. - // In contrast to XPutImage, xcb_put_image doesn't implicitly split the data. - const QSize window = pickWindowSize(image); - - for (int i = 0; i < image.height(); i += window.height()) { - const int targetHeight = qMin(image.height() - i, window.height()); - const uint8_t *line = image.scanLine(i); - - for (int j = 0; j < image.width(); j += window.width()) { - const int targetWidth = qMin(image.width() - j, window.width()); - const uint8_t *bytes = line + j * bytesPerPixel; - const uint32_t byteCount = targetWidth * targetHeight * bytesPerPixel; - - xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, pixmap, - gc, targetWidth, targetHeight, j, i, 0, image.depth(), - byteCount, bytes); - } - } - - xcb_flush(c); - xcb_free_gc(c, gc); - - return pixmap; -} - -void ScreenShotEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) -{ - m_paintedScreen = data.screen(); - effects->paintScreen(mask, region, data); -} - -/** - * Translates the Point coordinates in m_cacheOutputsImages keys from the virtualGeometry - * To a new geometry taking into account the real size of the images (virtualSize * dpr), - * moving the siblings images on the right bottom of the image as necessary - */ -void ScreenShotEffect::computeCoordinatesAfterScaling() -{ - // prepare a translation table that will store oldPoint -> newPoint in the new coordinates - QMap translationMap; - for (auto i = m_cacheOutputsImages.keyBegin(); i != m_cacheOutputsImages.keyEnd(); ++i) { - translationMap.insert(*i, *i); - } - - for (auto i = m_cacheOutputsImages.constBegin(); i != m_cacheOutputsImages.constEnd(); ++i) { - const auto p = i.key(); - const auto img = i.value(); - const auto dpr = img.devicePixelRatio(); - if (!qFuzzyCompare(dpr, 1.0)) { - // must update all coordinates of next rects - const int deltaX = img.width() - (img.width() / dpr); - const int deltaY = img.height() - (img.height() / dpr); - - // for the next images on the right or bottom - // thanks to ComparableQPoint - for (auto i2 = m_cacheOutputsImages.constFind(p); - i2 != m_cacheOutputsImages.constEnd(); ++i2) { - - const auto point = i2.key(); - auto finalPoint = translationMap.value(point); - - if (point.x() >= img.width() + p.x() - deltaX) { - finalPoint.setX(finalPoint.x() + deltaX); - } - if (point.y() >= img.height() + p.y() - deltaY) { - finalPoint.setY(finalPoint.y() + deltaY); - } - // update final position point with the necessary deltas - translationMap.insert(point, finalPoint); - } - } - } - - // make the new coordinates effective - for (auto i = translationMap.keyBegin(); i != translationMap.keyEnd(); ++i) { - const auto key = *i; - const auto img = m_cacheOutputsImages.take(key); - m_cacheOutputsImages.insert(translationMap.value(key), img); - } -} - -void ScreenShotEffect::postPaintScreen() -{ - effects->postPaintScreen(); - if (m_scheduledScreenshot) { - WindowPaintData d(m_scheduledScreenshot); - double left = 0; - double top = 0; - double right = m_scheduledScreenshot->width(); - double bottom = m_scheduledScreenshot->height(); - if (m_scheduledScreenshot->hasDecoration() && m_type & INCLUDE_DECORATION) { - for (const WindowQuad &quad : qAsConst(d.quads)) { - // we need this loop to include the decoration padding - left = qMin(left, quad.left()); - top = qMin(top, quad.top()); - right = qMax(right, quad.right()); - bottom = qMax(bottom, quad.bottom()); - } - } else if (m_scheduledScreenshot->hasDecoration()) { - WindowQuadList newQuads; - left = m_scheduledScreenshot->width(); - top = m_scheduledScreenshot->height(); - right = 0; - bottom = 0; - for (const WindowQuad &quad : qAsConst(d.quads)) { - if (quad.type() == WindowQuadContents) { - newQuads << quad; - left = qMin(left, quad.left()); - top = qMin(top, quad.top()); - right = qMax(right, quad.right()); - bottom = qMax(bottom, quad.bottom()); - } - } - d.quads = newQuads; - } - const int width = right - left; - const int height = bottom - top; - bool validTarget = true; - QScopedPointer offscreenTexture; - QScopedPointer target; - if (effects->isOpenGLCompositing()) { - offscreenTexture.reset(new GLTexture(GL_RGBA8, width, height)); - offscreenTexture->setFilter(GL_LINEAR); - offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE); - target.reset(new GLRenderTarget(*offscreenTexture)); - validTarget = target->valid(); - } - if (validTarget) { - d.setXTranslation(-m_scheduledScreenshot->x() - left); - d.setYTranslation(-m_scheduledScreenshot->y() - top); - - // render window into offscreen texture - int mask = PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_TRANSLUCENT; - QImage img; - if (effects->isOpenGLCompositing()) { - GLRenderTarget::pushRenderTarget(target.data()); - glClearColor(0.0, 0.0, 0.0, 0.0); - glClear(GL_COLOR_BUFFER_BIT); - glClearColor(0.0, 0.0, 0.0, 1.0); - - QMatrix4x4 projection; - projection.ortho(QRect(0, 0, offscreenTexture->width(), offscreenTexture->height())); - d.setProjectionMatrix(projection); - - effects->drawWindow(m_scheduledScreenshot, mask, infiniteRegion(), d); - - // copy content from framebuffer into image - img = QImage(QSize(width, height), QImage::Format_ARGB32); - glReadnPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, img.sizeInBytes(), (GLvoid*)img.bits()); - GLRenderTarget::popRenderTarget(); - ScreenShotEffect::convertFromGLImage(img, width, height); - } -#ifdef KWIN_HAVE_XRENDER_COMPOSITING - xcb_image_t *xImage = nullptr; - if (effects->compositingType() == XRenderCompositing) { - setXRenderOffscreen(true); - effects->drawWindow(m_scheduledScreenshot, mask, QRegion(0, 0, width, height), d); - if (xRenderOffscreenTarget()) { - img = xPictureToImage(xRenderOffscreenTarget(), QRect(0, 0, width, height), &xImage); - } - setXRenderOffscreen(false); - } -#endif - - if (m_type & INCLUDE_CURSOR) { - grabPointerImage(img, m_scheduledScreenshot->x() + left, m_scheduledScreenshot->y() + top); - } - - if (m_windowMode == WindowMode::Xpixmap) { - const xcb_pixmap_t xpix = xpixmapFromImage(img); - emit screenshotCreated(xpix); - m_windowMode = WindowMode::NoCapture; - } else if (m_windowMode == WindowMode::File || m_windowMode == WindowMode::FileDescriptor) { - sendReplyImage(img); - } -#ifdef KWIN_HAVE_XRENDER_COMPOSITING - if (xImage) { - xcb_image_destroy(xImage); - } -#endif - } - m_scheduledScreenshot = nullptr; - } - - if (!m_scheduledGeometry.isNull()) { - if (m_paintedScreen) { - // special handling for per-output geometry rendering - const QRect intersection = m_scheduledGeometry.intersected(m_paintedScreen->geometry()); - if (intersection.isEmpty()) { - // doesn't intersect, not going onto this screenshot - return; - } - const qreal devicePixelRatio = m_nativeSize ? m_paintedScreen->devicePixelRatio() : 1.0; - QImage img = blitScreenshot(intersection, devicePixelRatio); - if (img.size() == (m_scheduledGeometry.size() * devicePixelRatio)) { - // we are done - sendReplyImage(img); - return; - } - img.setDevicePixelRatio(devicePixelRatio); - - m_cacheOutputsImages.insert(ComparableQPoint(m_paintedScreen->geometry().topLeft()), img); - - m_multipleOutputsRendered = m_multipleOutputsRendered.united(intersection); - if (m_multipleOutputsRendered.boundingRect() == m_scheduledGeometry) { - - if (m_orderImg.isEmpty()) { - // Recompute coordinates - if (m_nativeSize) { - computeCoordinatesAfterScaling(); - } - - // find the output image size - int width = 0; - int height = 0; - QMap::const_iterator i; - for (i = m_cacheOutputsImages.constBegin(); i != m_cacheOutputsImages.constEnd(); ++i) { - const auto pos = i.key(); - const auto img = i.value(); - - width = qMax(width, pos.x() + img.width()); - height = qMax(height, pos.y() + img.height()); - } - - QImage multipleOutputsImage = QImage(width, height, QImage::Format_ARGB32); - - QPainter p; - p.begin(&multipleOutputsImage); - - // reassemble images together - for (i = m_cacheOutputsImages.constBegin(); i != m_cacheOutputsImages.constEnd(); ++i) { - auto pos = i.key(); - auto img = i.value(); - // disable dpr rendering, we already took care of this - img.setDevicePixelRatio(1.0); - p.drawImage(pos, img); - } - p.end(); - - sendReplyImage(multipleOutputsImage); - } else { - sendReplyImages(); - } - } - - } else { - const QImage img = blitScreenshot(m_scheduledGeometry); - sendReplyImage(img); - } - } -} - -void ScreenShotEffect::sendReplyImage(const QImage &img) -{ - if (m_fd != -1) { - QtConcurrent::run( - [] (int fd, const QImage &img) { - QFile file; - if (file.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { - QDataStream ds(&file); - ds << img; - file.close(); - } else { - close(fd); - } - }, m_fd, img); - } else { - QDBusConnection::sessionBus().send(m_replyMessage.createReply(saveTempImage(img))); - } - - clearState(); -} - -void ScreenShotEffect::sendReplyImages() -{ - QList outputImages; - for (const QPoint &pos : qAsConst(m_orderImg)) { - auto it = m_cacheOutputsImages.constFind(pos); - if (it != m_cacheOutputsImages.constEnd()) { - outputImages.append(*it); - } - } - QtConcurrent::run( - [] (int fd, const QList &outputImages) { - QFile file; - if (file.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { - QDataStream ds(&file); - ds.setVersion(QDataStream::Qt_DefaultCompiledVersion); - ds << outputImages; - file.close(); - } else { - close(fd); - } - }, m_fd, outputImages); - - clearState(); -} - -void ScreenShotEffect::clearState() -{ - m_fd = -1; - m_scheduledGeometry = QRect(); - m_multipleOutputsRendered = QRegion(); - m_captureCursor = false; - m_windowMode = WindowMode::NoCapture; - m_cacheOutputsImages.clear(); - m_nativeSize = false; - m_orderImg.clear(); -} - -QString ScreenShotEffect::saveTempImage(const QImage &img) -{ - if (img.isNull()) { - return QString(); - } - QTemporaryFile temp(QDir::tempPath() + QDir::separator() + QLatin1String("kwin_screenshot_XXXXXX.png")); - temp.setAutoRemove(false); - if (!temp.open()) { - return QString(); - } - img.save(&temp); - temp.close(); - KNotification::event(KNotification::Notification, - i18nc("Notification caption that a screenshot got saved to file", "Screenshot"), - i18nc("Notification with path to screenshot file", "Screenshot saved to %1", temp.fileName()), - QStringLiteral("spectacle")); - return temp.fileName(); -} - -void ScreenShotEffect::screenshotWindowUnderCursor(int mask) -{ - if (isTakingScreenshot()) { - sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); - return; - } - m_type = (ScreenShotType)mask; - scheduleScreenshotWindowUnderCursor(); -} - -void ScreenShotEffect::scheduleScreenshotWindowUnderCursor() -{ - const QPoint cursor = effects->cursorPos(); - EffectWindowList order = effects->stackingOrder(); - EffectWindowList::const_iterator it = order.constEnd(), first = order.constBegin(); - while( it != first ) { - m_scheduledScreenshot = *(--it); - if (m_scheduledScreenshot->isOnCurrentDesktop() && - !m_scheduledScreenshot->isMinimized() && !m_scheduledScreenshot->isDeleted() && - m_scheduledScreenshot->geometry().contains(cursor)) - break; - m_scheduledScreenshot = nullptr; - } - if (m_scheduledScreenshot) { - m_windowMode = WindowMode::Xpixmap; - m_scheduledScreenshot->addRepaintFull(); - } -} - -void ScreenShotEffect::screenshotForWindow(qulonglong winid, int mask) -{ - m_type = (ScreenShotType) mask; - EffectWindow* w = effects->findWindow(winid); - if(w && !w->isMinimized() && !w->isDeleted()) { - m_windowMode = WindowMode::Xpixmap; - m_scheduledScreenshot = w; - m_scheduledScreenshot->addRepaintFull(); - } -} - -bool ScreenShotEffect::checkCall() const -{ - if (!calledFromDBus()) { - return false; - } - - const QDBusReply reply = connection().interface()->servicePid(message().service()); - if (reply.isValid()) { - const uint pid = reply.value(); - const auto interfaces = KWin::fetchRestrictedDBusInterfacesFromPid(pid); - if (!interfaces.contains(s_dbusInterfaceName)) { - sendErrorReply(s_errorNotAuthorized, s_errorNotAuthorizedMsg); - qCWarning(KWINEFFECTS) << "Process " << pid << " tried to take a screenshot without being granted to DBus interface" << s_dbusInterfaceName; - return false; - } - } else { - return false; - } - - if (isTakingScreenshot()) { - sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); - return false; - } - return true; -} - -QString ScreenShotEffect::interactive(int mask) -{ - if (!calledFromDBus()) { - return QString(); - } - if (isTakingScreenshot()) { - sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); - return QString(); - } - - m_type = (ScreenShotType) mask; - m_windowMode = WindowMode::File; - m_replyMessage = message(); - setDelayedReply(true); - effects->startInteractiveWindowSelection( - [this] (EffectWindow *w) { - hideInfoMessage(); - if (!w) { - QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(s_errorCancelled, s_errorCancelledMsg)); - m_windowMode = WindowMode::NoCapture; - return; - } else { - m_scheduledScreenshot = w; - m_scheduledScreenshot->addRepaintFull(); - } - }); - - showInfoMessage(InfoMessageMode::Window); - return QString(); -} - -void ScreenShotEffect::interactive(QDBusUnixFileDescriptor fd, int mask) -{ - if (!calledFromDBus()) { - return; - } - if (isTakingScreenshot()) { - sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); - return; - } - - m_fd = dup(fd.fileDescriptor()); - if (m_fd == -1) { - sendErrorReply(s_errorFd, s_errorFdMsg); - return; - } - m_type = (ScreenShotType) mask; - m_windowMode = WindowMode::FileDescriptor; - - effects->startInteractiveWindowSelection( - [this] (EffectWindow *w) { - hideInfoMessage(); - if (!w) { - close(m_fd); - m_fd = -1; - m_windowMode = WindowMode::NoCapture; - return; - } else { - m_scheduledScreenshot = w; - m_scheduledScreenshot->addRepaintFull(); - } - }); - - showInfoMessage(InfoMessageMode::Window); -} - -void ScreenShotEffect::showInfoMessage(InfoMessageMode mode) -{ - QString text; - switch (mode) { - case InfoMessageMode::Window: - text = i18n("Select window to screen shot with left click or enter.\nEscape or right click to cancel."); - break; - case InfoMessageMode::Screen: - text = i18n("Create screen shot with left click or enter.\nEscape or right click to cancel."); - break; - } - effects->showOnScreenMessage(text, QStringLiteral("spectacle")); -} - -void ScreenShotEffect::hideInfoMessage() -{ - effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); -} - -QString ScreenShotEffect::screenshotFullscreen(bool captureCursor) -{ - if (!checkCall()) { - return QString(); - } - m_replyMessage = message(); - setDelayedReply(true); - m_scheduledGeometry = effects->virtualScreenGeometry(); - m_captureCursor = captureCursor; - effects->addRepaintFull(); - return QString(); -} - -void ScreenShotEffect::screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor, bool shouldReturnNativeSize) -{ - if (!checkCall()) { - return; - } - m_fd = dup(fd.fileDescriptor()); - if (m_fd == -1) { - sendErrorReply(s_errorFd, s_errorFdMsg); - return; - } - m_captureCursor = captureCursor; - m_nativeSize = shouldReturnNativeSize; - - m_scheduledGeometry = effects->virtualScreenGeometry(); - effects->addRepaint(m_scheduledGeometry); -} - -QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor) -{ - if (!checkCall()) { - return QString(); - } - m_scheduledGeometry = effects->clientArea(FullScreenArea, screen, 0); - if (m_scheduledGeometry.isNull()) { - sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMsg); - return QString(); - } - m_captureCursor = captureCursor; - m_nativeSize = true; - m_replyMessage = message(); - setDelayedReply(true); - effects->addRepaint(m_scheduledGeometry); - return QString(); -} - -void ScreenShotEffect::screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor) -{ - if (!checkCall()) { - return; - } - m_fd = dup(fd.fileDescriptor()); - if (m_fd == -1) { - sendErrorReply(s_errorFd, s_errorFdMsg); - return; - } - m_captureCursor = captureCursor; - m_nativeSize = true; - - showInfoMessage(InfoMessageMode::Screen); - effects->startInteractivePositionSelection( - [this] (const QPoint &p) { - hideInfoMessage(); - if (p == QPoint(-1, -1)) { - // error condition - close(m_fd); - m_fd = -1; - } else { - m_scheduledGeometry = effects->clientArea(FullScreenArea, effects->screenNumber(p), 0); - if (m_scheduledGeometry.isNull()) { - close(m_fd); - m_fd = -1; - return; - } - effects->addRepaint(m_scheduledGeometry); - } - } - ); -} - -void ScreenShotEffect::screenshotScreens(QDBusUnixFileDescriptor fd, const QStringList &screensNames, bool captureCursor, bool shouldReturnNativeSize) -{ - if (!checkCall()) { - return; - } - m_fd = dup(fd.fileDescriptor()); - if (m_fd == -1) { - sendErrorReply(s_errorFd, s_errorFdMsg); - return; - } - m_captureCursor = captureCursor; - m_nativeSize = shouldReturnNativeSize; - m_orderImg = QList(); - m_scheduledGeometry = QRect(); - - const QList screens = QGuiApplication::screens(); - - for (const QScreen *screen : screens) { - const int indexName = screensNames.indexOf(screen->name()); - if (indexName != -1) { - const auto screenGeom = screen->geometry(); - if (!screenGeom.isValid()) { - close(m_fd); - clearState(); - sendErrorReply(s_errorScreenMissing, s_errorScreenMissingMsg + " : " + screen->name()); - return; - } - m_scheduledGeometry = m_scheduledGeometry.united(screenGeom); - m_orderImg.insert(indexName, screenGeom.topLeft()); - } - } - - if (m_orderImg.size() != screensNames.size()) { - close(m_fd); - clearState(); - sendErrorReply(s_errorScreenMissing, s_errorScreenMissingMsg); - return; - } - - effects->addRepaint(m_scheduledGeometry); -} - -QString ScreenShotEffect::screenshotArea(int x, int y, int width, int height, bool captureCursor) -{ - if (!checkCall()) { - return QString(); - } - m_scheduledGeometry = QRect(x, y, width, height); - if (m_scheduledGeometry.isNull() || m_scheduledGeometry.isEmpty()) { - m_scheduledGeometry = QRect(); - sendErrorReply(s_errorInvalidArea, s_errorInvalidAreaMsg); - return QString(); - } - m_captureCursor = captureCursor; - m_replyMessage = message(); - setDelayedReply(true); - effects->addRepaint(m_scheduledGeometry); - return QString(); -} - -QImage ScreenShotEffect::blitScreenshot(const QRect &geometry, const qreal scale) -{ - QImage img; - if (effects->isOpenGLCompositing()) - { - const QSize nativeSize = geometry.size() * scale; - - if (GLRenderTarget::blitSupported() && !GLPlatform::instance()->isGLES()) { - - img = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32); - GLTexture tex(GL_RGBA8, nativeSize.width(), nativeSize.height()); - GLRenderTarget target(tex); - target.blitFromFramebuffer(geometry); - // copy content from framebuffer into image - tex.bind(); - glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, static_cast(img.bits())); - tex.unbind(); - } else { - img = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32); - glReadPixels(0, 0, nativeSize.width(), nativeSize.height(), GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); - } - ScreenShotEffect::convertFromGLImage(img, nativeSize.width(), nativeSize.height()); - } - -#ifdef KWIN_HAVE_XRENDER_COMPOSITING - if (effects->compositingType() == XRenderCompositing) { - xcb_image_t *xImage = nullptr; - img = xPictureToImage(effects->xrenderBufferPicture(), geometry, &xImage); - if (xImage) { - xcb_image_destroy(xImage); - } - } -#endif - - if (m_captureCursor) { - grabPointerImage(img, geometry.x() * scale, geometry.y() * scale); - } - - return img; -} - -void ScreenShotEffect::grabPointerImage(QImage& snapshot, int offsetx, int offsety) -{ - const auto cursor = effects->cursorImage(); - if (cursor.image().isNull()) - return; - - QPainter painter(&snapshot); - painter.drawImage(effects->cursorPos() - cursor.hotSpot() - QPoint(offsetx, offsety), cursor.image()); -} - -void ScreenShotEffect::convertFromGLImage(QImage &img, int w, int h) +static void convertFromGLImage(QImage &img, int w, int h) { // from QtOpenGL/qgl.cpp // SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) @@ -856,31 +76,419 @@ void ScreenShotEffect::convertFromGLImage(QImage &img, int w, int h) img = img.mirrored(); } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +static void xImageCleanup(void *data) +{ + xcb_image_destroy(static_cast(data)); +} + +static QImage xPictureToImage(xcb_render_picture_t srcPic, const QRect &geometry) +{ + xcb_connection_t *c = effects->xcbConnection(); + xcb_pixmap_t xpix = xcb_generate_id(c); + xcb_create_pixmap(c, 32, xpix, effects->x11RootWindow(), geometry.width(), geometry.height()); + XRenderPicture pic(xpix, 32); + xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, srcPic, XCB_RENDER_PICTURE_NONE, pic, + geometry.x(), geometry.y(), 0, 0, 0, 0, geometry.width(), geometry.height()); + xcb_flush(c); + xcb_image_t *xImage = xcb_image_get(c, xpix, 0, 0, geometry.width(), geometry.height(), + ~0, XCB_IMAGE_FORMAT_Z_PIXMAP); + QImage img(xImage->data, xImage->width, xImage->height, xImage->stride, + QImage::Format_ARGB32_Premultiplied, xImageCleanup, xImage); + // TODO: byte order might need swapping + xcb_free_pixmap(c, xpix); + return img.copy(); +} +#endif + +bool ScreenShotEffect::supported() +{ + return effects->compositingType() == XRenderCompositing || + (effects->isOpenGLCompositing() && GLRenderTarget::supported()); +} + +ScreenShotEffect::ScreenShotEffect() + : m_dbusInterface1(new ScreenShotDBusInterface1(this)) +{ + connect(effects, &EffectsHandler::screenAdded, this, &ScreenShotEffect::handleScreenAdded); + connect(effects, &EffectsHandler::screenRemoved, this, &ScreenShotEffect::handleScreenRemoved); + connect(effects, &EffectsHandler::windowClosed, this, &ScreenShotEffect::handleWindowClosed); +} + +ScreenShotEffect::~ScreenShotEffect() +{ + cancelWindowScreenShots(); + cancelAreaScreenShots(); + cancelScreenScreenShots(); +} + +QFuture ScreenShotEffect::scheduleScreenShot(EffectScreen *screen, ScreenShotFlags flags) +{ + for (ScreenShotScreenData &data : m_screenScreenShots) { + if (data.screen == screen && data.flags == flags) { + return data.promise.future(); + } + } + + ScreenShotScreenData data; + data.screen = screen; + data.flags = flags; + + m_screenScreenShots.append(data); + effects->addRepaint(screen->geometry()); + + data.promise.reportStarted(); + return data.promise.future(); +} + +QFuture ScreenShotEffect::scheduleScreenShot(const QRect &area, ScreenShotFlags flags) +{ + for (ScreenShotAreaData &data : m_areaScreenShots) { + if (data.area == area && data.flags == flags) { + return data.promise.future(); + } + } + + ScreenShotAreaData data; + data.area = area; + data.flags = flags; + + const QList screens = effects->screens(); + for (EffectScreen *screen : screens) { + if (screen->geometry().intersects(area)) { + data.screens.append(screen); + } + } + + qreal devicePixelRatio = 1.0; + if (flags & ScreenShotNativeResolution) { + for (const EffectScreen *screen : qAsConst(data.screens)) { + if (screen->devicePixelRatio() > devicePixelRatio) { + devicePixelRatio = screen->devicePixelRatio(); + } + } + } + + data.result = QImage(area.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + data.result.fill(Qt::transparent); + data.result.setDevicePixelRatio(devicePixelRatio); + + m_areaScreenShots.append(data); + effects->addRepaint(area); + + data.promise.reportStarted(); + return data.promise.future(); +} + +QFuture ScreenShotEffect::scheduleScreenShot(EffectWindow *window, ScreenShotFlags flags) +{ + for (ScreenShotWindowData &data : m_windowScreenShots) { + if (data.window == window && data.flags == flags) { + return data.promise.future(); + } + } + + ScreenShotWindowData data; + data.window = window; + data.flags = flags; + + m_windowScreenShots.append(data); + window->addRepaintFull(); + + data.promise.reportStarted(); + return data.promise.future(); +} + +void ScreenShotEffect::cancelWindowScreenShots() +{ + while (!m_windowScreenShots.isEmpty()) { + ScreenShotWindowData screenshot = m_windowScreenShots.takeLast(); + screenshot.promise.reportCanceled(); + } +} + +void ScreenShotEffect::cancelAreaScreenShots() +{ + while (!m_areaScreenShots.isEmpty()) { + ScreenShotAreaData screenshot = m_areaScreenShots.takeLast(); + screenshot.promise.reportCanceled(); + } +} + +void ScreenShotEffect::cancelScreenScreenShots() +{ + while (!m_screenScreenShots.isEmpty()) { + ScreenShotScreenData screenshot = m_screenScreenShots.takeLast(); + screenshot.promise.reportCanceled(); + } +} + +void ScreenShotEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) +{ + m_paintedScreen = data.screen(); + effects->paintScreen(mask, region, data); +} + +void ScreenShotEffect::takeScreenShot(ScreenShotWindowData *screenshot) +{ + EffectWindow *window = screenshot->window; + + WindowPaintData d(window); + double left = 0; + double top = 0; + double right = window->width(); + double bottom = window->height(); + if (window->hasDecoration() && (screenshot->flags & ScreenShotIncludeDecoration)) { + for (const WindowQuad &quad : qAsConst(d.quads)) { + // we need this loop to include the decoration padding + left = qMin(left, quad.left()); + top = qMin(top, quad.top()); + right = qMax(right, quad.right()); + bottom = qMax(bottom, quad.bottom()); + } + } else if (window->hasDecoration()) { + WindowQuadList newQuads; + left = window->width(); + top = window->height(); + right = 0; + bottom = 0; + for (const WindowQuad &quad : qAsConst(d.quads)) { + if (quad.type() == WindowQuadContents) { + newQuads << quad; + left = qMin(left, quad.left()); + top = qMin(top, quad.top()); + right = qMax(right, quad.right()); + bottom = qMax(bottom, quad.bottom()); + } + } + d.quads = newQuads; + } + const int width = right - left; + const int height = bottom - top; + bool validTarget = true; + QScopedPointer offscreenTexture; + QScopedPointer target; + if (effects->isOpenGLCompositing()) { + offscreenTexture.reset(new GLTexture(GL_RGBA8, width, height)); + offscreenTexture->setFilter(GL_LINEAR); + offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE); + target.reset(new GLRenderTarget(*offscreenTexture)); + validTarget = target->valid(); + } + if (validTarget) { + d.setXTranslation(-window->x() - left); + d.setYTranslation(-window->y() - top); + + // render window into offscreen texture + int mask = PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_TRANSLUCENT; + QImage img; + if (effects->isOpenGLCompositing()) { + GLRenderTarget::pushRenderTarget(target.data()); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0.0, 0.0, 0.0, 1.0); + + QMatrix4x4 projection; + projection.ortho(QRect(0, 0, offscreenTexture->width(), offscreenTexture->height())); + d.setProjectionMatrix(projection); + + effects->drawWindow(window, mask, infiniteRegion(), d); + + // copy content from framebuffer into image + img = QImage(QSize(width, height), QImage::Format_ARGB32); + glReadnPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, img.sizeInBytes(), (GLvoid*)img.bits()); + GLRenderTarget::popRenderTarget(); + convertFromGLImage(img, width, height); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + setXRenderOffscreen(true); + effects->drawWindow(window, mask, QRegion(0, 0, width, height), d); + if (xRenderOffscreenTarget()) { + img = xPictureToImage(xRenderOffscreenTarget(), QRect(0, 0, width, height)); + } + setXRenderOffscreen(false); + } +#endif + + if (screenshot->flags & ScreenShotIncludeCursor) { + grabPointerImage(img, window->x() + left, window->y() + top); + } + + screenshot->promise.reportResult(img); + screenshot->promise.reportFinished(); + } else { + screenshot->promise.reportCanceled(); + } +} + +bool ScreenShotEffect::takeScreenShot(ScreenShotAreaData *screenshot) +{ + if (!m_paintedScreen) { + // On X11, all screens are painted simultaneously and there is no native HiDPI support. + QImage snapshot = blitScreenshot(screenshot->area); + if (screenshot->flags & ScreenShotIncludeCursor) { + grabPointerImage(snapshot, screenshot->area.x(), screenshot->area.y()); + } + screenshot->promise.reportResult(snapshot); + screenshot->promise.reportFinished(); + } else { + if (!screenshot->screens.contains(m_paintedScreen)) { + return false; + } + screenshot->screens.removeOne(m_paintedScreen); + + const QRect sourceRect = screenshot->area & m_paintedScreen->geometry(); + qreal sourceDevicePixelRatio = 1.0; + if (screenshot->flags & ScreenShotNativeResolution) { + sourceDevicePixelRatio = m_paintedScreen->devicePixelRatio(); + } + + const QImage snapshot = blitScreenshot(sourceRect, sourceDevicePixelRatio); + const QRect nativeArea(screenshot->area.topLeft(), + screenshot->area.size() * screenshot->result.devicePixelRatio()); + + QPainter painter(&screenshot->result); + painter.setWindow(nativeArea); + painter.drawImage(sourceRect, snapshot); + painter.end(); + + if (screenshot->screens.isEmpty()) { + if (screenshot->flags & ScreenShotIncludeCursor) { + grabPointerImage(screenshot->result, screenshot->area.x(), screenshot->area.y()); + } + screenshot->promise.reportResult(screenshot->result); + screenshot->promise.reportFinished(); + } + } + + return screenshot->promise.isFinished(); +} + +bool ScreenShotEffect::takeScreenShot(ScreenShotScreenData *screenshot) +{ + if (!m_paintedScreen || screenshot->screen == m_paintedScreen) { + qreal devicePixelRatio = 1.0; + if (screenshot->flags & ScreenShotNativeResolution) { + devicePixelRatio = screenshot->screen->devicePixelRatio(); + } + + QImage snapshot = blitScreenshot(screenshot->screen->geometry(), devicePixelRatio); + if (screenshot->flags & ScreenShotIncludeCursor) { + const int xOffset = screenshot->screen->geometry().width(); + const int yOffset = screenshot->screen->geometry().height(); + grabPointerImage(snapshot, xOffset, yOffset); + } + + screenshot->promise.reportResult(snapshot); + screenshot->promise.reportFinished(); + } + + return screenshot->promise.isFinished(); +} + +void ScreenShotEffect::postPaintScreen() +{ + effects->postPaintScreen(); + + while (!m_windowScreenShots.isEmpty()) { + ScreenShotWindowData screenshot = m_windowScreenShots.takeLast(); + takeScreenShot(&screenshot); + } + + for (int i = m_areaScreenShots.count() - 1; i >= 0; --i) { + if (takeScreenShot(&m_areaScreenShots[i])) { + m_areaScreenShots.removeAt(i); + } + } + + for (int i = m_screenScreenShots.count() - 1; i >= 0; --i) { + if (takeScreenShot(&m_screenScreenShots[i])) { + m_screenScreenShots.removeAt(i); + } + } +} + +QImage ScreenShotEffect::blitScreenshot(const QRect &geometry, const qreal scale) +{ + QImage img; + if (effects->isOpenGLCompositing()) + { + const QSize nativeSize = geometry.size() * scale; + + if (GLRenderTarget::blitSupported() && !GLPlatform::instance()->isGLES()) { + + img = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32); + GLTexture tex(GL_RGBA8, nativeSize.width(), nativeSize.height()); + GLRenderTarget target(tex); + target.blitFromFramebuffer(geometry); + // copy content from framebuffer into image + tex.bind(); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, static_cast(img.bits())); + tex.unbind(); + } else { + img = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32); + glReadPixels(0, 0, nativeSize.width(), nativeSize.height(), GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); + } + convertFromGLImage(img, nativeSize.width(), nativeSize.height()); + } + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + img = xPictureToImage(effects->xrenderBufferPicture(), geometry); + } +#endif + + img.setDevicePixelRatio(scale); + return img; +} + +void ScreenShotEffect::grabPointerImage(QImage& snapshot, int offsetx, int offsety) +{ + const auto cursor = effects->cursorImage(); + if (cursor.image().isNull()) + return; + + QPainter painter(&snapshot); + painter.drawImage(effects->cursorPos() - cursor.hotSpot() - QPoint(offsetx, offsety), cursor.image()); +} + bool ScreenShotEffect::isActive() const { - return (m_scheduledScreenshot != nullptr || !m_scheduledGeometry.isNull()) && !effects->isScreenLocked(); + return (!m_windowScreenShots.isEmpty() || !m_areaScreenShots.isEmpty() || !m_screenScreenShots.isEmpty()) + && !effects->isScreenLocked(); } -void ScreenShotEffect::windowClosed( EffectWindow* w ) +int ScreenShotEffect::requestedEffectChainPosition() const { - if (w == m_scheduledScreenshot) { - m_scheduledScreenshot = nullptr; - scheduleScreenshotWindowUnderCursor(); + return 50; +} + +void ScreenShotEffect::handleScreenAdded() +{ + cancelAreaScreenShots(); +} + +void ScreenShotEffect::handleScreenRemoved(EffectScreen *screen) +{ + cancelAreaScreenShots(); + + for (int i = m_screenScreenShots.count() - 1; i >= 0; --i) { + if (m_screenScreenShots[i].screen == screen) { + m_screenScreenShots[i].promise.reportCanceled(); + m_screenScreenShots.removeAt(i); + } } } -bool ScreenShotEffect::isTakingScreenshot() const +void ScreenShotEffect::handleWindowClosed(EffectWindow *window) { - if (!m_scheduledGeometry.isNull()) { - return true; + for (int i = m_windowScreenShots.count() - 1; i >= 0; --i) { + if (m_windowScreenShots[i].window == window) { + m_windowScreenShots[i].promise.reportCanceled(); + m_windowScreenShots.removeAt(i); + } } - if (m_windowMode != WindowMode::NoCapture) { - return true; - } - if (m_fd != -1) { - return true; - } - return false; } } // namespace diff --git a/src/effects/screenshot/screenshot.h b/src/effects/screenshot/screenshot.h index 93a9d04f8b..40b4bdbe1f 100644 --- a/src/effects/screenshot/screenshot.h +++ b/src/effects/screenshot/screenshot.h @@ -3,6 +3,7 @@ This file is part of the KDE project. SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ @@ -11,175 +12,95 @@ #define KWIN_SCREENSHOT_H #include -#include -#include -#include -#include -#include -#include -class ComparableQPoint; +#include +#include +#include +#include + namespace KWin { /** - * The screenshot effet allows to takes screenshot, by window, area, screen, etc... - * - * A using application must have "org.kde.kwin.Screenshot" in its X-KDE-DBUS-Restricted-Interfaces application service file field. + * This enum type is used to specify how a screenshot needs to be taken. */ -class ScreenShotEffect : public Effect, protected QDBusContext +enum ScreenShotFlag { + ScreenShotIncludeDecoration = 0x1, ///< Include window titlebar and borders + ScreenShotIncludeCursor = 0x2, ///< Include the cursor + ScreenShotNativeResolution = 0x4, ///< Take the screenshot at the native resolution +}; +Q_DECLARE_FLAGS(ScreenShotFlags, ScreenShotFlag) + +class ScreenShotDBusInterface1; +struct ScreenShotWindowData; +struct ScreenShotAreaData; +struct ScreenShotScreenData; + +/** + * The screenshot effet allows to takes screenshot, by window, area, screen, etc... + */ +class ScreenShotEffect : public Effect { Q_OBJECT - Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Screenshot") + public: - enum ScreenShotType { - INCLUDE_DECORATION = 1 << 0, - INCLUDE_CURSOR = 1 << 1 - }; ScreenShotEffect(); ~ScreenShotEffect() override; + /** + * Schedules a screenshot of the given @a screen. The returned QFuture can be used to query + * the image data. If the screen is removed before the screenshot is taken, the future will + * be cancelled. + */ + QFuture scheduleScreenShot(EffectScreen *screen, ScreenShotFlags flags = {}); + + /** + * Schedules a screenshot of the given @a area. The returned QFuture can be used to query the + * image data. + */ + QFuture scheduleScreenShot(const QRect &area, ScreenShotFlags flags = {}); + + /** + * Schedules a screenshot of the given @a window. The returned QFuture can be used to query + * the image data. If the window is removed before the screenshot is taken, the future will + * be cancelled. + */ + QFuture scheduleScreenShot(EffectWindow *window, ScreenShotFlags flags = {}); + void paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) override; void postPaintScreen() override; bool isActive() const override; - - int requestedEffectChainPosition() const override { - return 50; - } + int requestedEffectChainPosition() const override; static bool supported(); - static void convertFromGLImage(QImage &img, int w, int h); - void scheduleScreenshotWindowUnderCursor(); -public Q_SLOTS: - Q_SCRIPTABLE void screenshotForWindow(qulonglong winid, int mask = 0); - /** - * Starts an interactive window screenshot session. The user can select a window to - * screenshot. - * - * Once the window is selected the screenshot is saved into a file and the path gets - * returned to the DBus peer. - * - * @param mask The mask for what to include in the screenshot - */ - Q_SCRIPTABLE QString interactive(int mask = 0); - /** - * Starts an interactive window screenshot session. The user can select a window to - * screenshot. - * - * Once the window is selected the screenshot is saved into the @p fd passed to the - * method. It is intended to be used with a pipe, so that the invoking side can just - * read from the pipe. The image gets written into the fd using a QDataStream. - * - * @param fd File descriptor into which the screenshot should be saved - * @param mask The mask for what to include in the screenshot - */ - Q_SCRIPTABLE void interactive(QDBusUnixFileDescriptor fd, int mask = 0); - Q_SCRIPTABLE void screenshotWindowUnderCursor(int mask = 0); - /** - * Saves a screenshot of all screen into a file and returns the path to the file. - * Functionality requires hardware support, if not available a null string is returned. - * @param captureCursor Whether to include the cursor in the image - * @returns Path to stored screenshot, or null string in failure case. - */ - Q_SCRIPTABLE QString screenshotFullscreen(bool captureCursor = false); - /** - * Takes a full screen screenshot in a one file format. - * - * Once the screenshot is taken it gets saved into the @p fd passed to the - * method. It is intended to be used with a pipe, so that the invoking side can just - * read from the pipe. The image gets written into the fd using a QDataStream. - * - * @param fd File descriptor into which the screenshot should be saved - * @param captureCursor Whether to include the mouse cursor - * @param shouldReturnNativeSize Whether to return an image according to the virtualGeometry, or according to pixel on screen size - */ - Q_SCRIPTABLE void screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor = false, bool shouldReturnNativeSize = false); - /** - * Take a screenshot of the passed screens and return a QList in the fd response, - * an image for each screen in pixel-on-screen size when shouldReturnNativeSize is passed, or converted to using logicale size if not - * - * @param fd - * @param screensNames the names of the screens whose screenshot to return - * @param captureCursor - * @param shouldReturnNativeSize - */ - Q_SCRIPTABLE void screenshotScreens(QDBusUnixFileDescriptor fd, const QStringList &screensNames, bool captureCursor = false, bool shouldReturnNativeSize = false); - /** - * Saves a screenshot of the screen identified by @p screen into a file and returns the path to the file. - * Functionality requires hardware support, if not available a null string is returned. - * @param screen Number of screen as numbered by QDesktopWidget - * @param captureCursor Whether to include the cursor in the image - * @returns Path to stored screenshot, or null string in failure case. - */ - Q_SCRIPTABLE QString screenshotScreen(int screen, bool captureCursor = false); - /** - * Starts an interactive screenshot of a screen session. - * - * The user is asked to select the screen to screenshot. - * - * Once the screenshot is taken it gets saved into the @p fd passed to the - * method. It is intended to be used with a pipe, so that the invoking side can just - * read from the pipe. The image gets written into the fd using a QDataStream. - * - * @param fd File descriptor into which the screenshot should be saved - * @param captureCursor Whether to include the mouse cursor - */ - Q_SCRIPTABLE void screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor = false); - /** - * Saves a screenshot of the selected geometry into a file and returns the path to the file. - * Functionality requires hardware support, if not available a null string is returned. - * @param x Left upper x coord of region - * @param y Left upper y coord of region - * @param width Width of the region to screenshot - * @param height Height of the region to screenshot - * @param captureCursor Whether to include the cursor in the image - * @returns Path to stored screenshot, or null string in failure case. - */ - Q_SCRIPTABLE QString screenshotArea(int x, int y, int width, int height, bool captureCursor = false); - -Q_SIGNALS: - Q_SCRIPTABLE void screenshotCreated(qulonglong handle); private Q_SLOTS: - void windowClosed( KWin::EffectWindow* w ); + void handleWindowClosed(EffectWindow *window); + void handleScreenAdded(); + void handleScreenRemoved(EffectScreen *screen); private: + void takeScreenShot(ScreenShotWindowData *screenshot); + bool takeScreenShot(ScreenShotAreaData *screenshot); + bool takeScreenShot(ScreenShotScreenData *screenshot); + + void cancelWindowScreenShots(); + void cancelAreaScreenShots(); + void cancelScreenScreenShots(); + void grabPointerImage(QImage& snapshot, int offsetx, int offsety); QImage blitScreenshot(const QRect &geometry, const qreal scale = 1.0); - QString saveTempImage(const QImage &img); - void sendReplyImage(const QImage &img); - void sendReplyImages(); - void clearState(); - enum class InfoMessageMode { - Window, - Screen - }; - void showInfoMessage(InfoMessageMode mode); - void hideInfoMessage(); - bool isTakingScreenshot() const; - void computeCoordinatesAfterScaling(); - bool checkCall() const; - EffectWindow *m_scheduledScreenshot; - ScreenShotType m_type; - QRect m_scheduledGeometry; - QDBusMessage m_replyMessage; - QRegion m_multipleOutputsRendered; - QMap m_cacheOutputsImages; - QList m_orderImg; - bool m_captureCursor = false; - bool m_nativeSize = false; - enum class WindowMode { - NoCapture, - Xpixmap, - File, - FileDescriptor - }; - WindowMode m_windowMode = WindowMode::NoCapture; - int m_fd = -1; + QVector m_windowScreenShots; + QVector m_areaScreenShots; + QVector m_screenScreenShots; + + QScopedPointer m_dbusInterface1; EffectScreen *m_paintedScreen = nullptr; }; } // namespace +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::ScreenShotFlags) + #endif // KWIN_SCREENSHOT_H diff --git a/src/effects/screenshot/screenshotdbusinterface1.cpp b/src/effects/screenshot/screenshotdbusinterface1.cpp new file mode 100644 index 0000000000..56853e9a0e --- /dev/null +++ b/src/effects/screenshot/screenshotdbusinterface1.cpp @@ -0,0 +1,860 @@ +/* + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screenshotdbusinterface1.h" +#include "../service_utils.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ + +const static QString s_errorAlreadyTaking = QStringLiteral("org.kde.kwin.Screenshot.Error.AlreadyTaking"); +const static QString s_errorAlreadyTakingMsg = QStringLiteral("A screenshot is already been taken"); +const static QString s_errorNotAuthorized = QStringLiteral("org.kde.kwin.Screenshot.Error.NoAuthorized"); +const static QString s_errorNotAuthorizedMsg = QStringLiteral("The process is not authorized to take a screenshot"); +const static QString s_errorFd = QStringLiteral("org.kde.kwin.Screenshot.Error.FileDescriptor"); +const static QString s_errorFdMsg = QStringLiteral("No valid file descriptor"); +const static QString s_errorCancelled = QStringLiteral("org.kde.kwin.Screenshot.Error.Cancelled"); +const static QString s_errorCancelledMsg = QStringLiteral("Screenshot got cancelled"); +const static QString s_errorInvalidArea = QStringLiteral("org.kde.kwin.Screenshot.Error.InvalidArea"); +const static QString s_errorInvalidAreaMsg = QStringLiteral("Invalid area requested"); +const static QString s_errorInvalidScreen = QStringLiteral("org.kde.kwin.Screenshot.Error.InvalidScreen"); +const static QString s_errorInvalidScreenMsg = QStringLiteral("Invalid screen requested"); +const static QString s_dbusInterfaceName = QStringLiteral("org.kde.kwin.Screenshot"); +const static QString s_errorScreenMissing = QStringLiteral("org.kde.kwin.Screenshot.Error.ScreenMissing"); +const static QString s_errorScreenMissingMsg = QStringLiteral("Screen not found"); + +class ScreenShotSource1 : public QObject +{ + Q_OBJECT + +public: + explicit ScreenShotSource1(QObject *parent = nullptr); + + virtual QImage data() const = 0; + virtual void marshal(ScreenShotSink1 *sink) = 0; + + virtual bool isCancelled() const = 0; + virtual bool isCompleted() const = 0; + +Q_SIGNALS: + void cancelled(); + void completed(); +}; + +class ScreenShotSourceBasic1 : public ScreenShotSource1 +{ + Q_OBJECT + +public: + explicit ScreenShotSourceBasic1(const QFuture &future); + + bool isCancelled() const override; + bool isCompleted() const override; + QImage data() const override; + void marshal(ScreenShotSink1 *sink) override; + +private: + QFuture m_future; + QFutureWatcher *m_watcher; +}; + +class ScreenShotSourceScreen1 : public ScreenShotSourceBasic1 +{ + Q_OBJECT + +public: + ScreenShotSourceScreen1(ScreenShotEffect *effect, EffectScreen *screen, ScreenShotFlags flags); +}; + +class ScreenShotSourceArea1 : public ScreenShotSourceBasic1 +{ + Q_OBJECT + +public: + ScreenShotSourceArea1(ScreenShotEffect *effect, const QRect &area, ScreenShotFlags flags); +}; + +class ScreenShotSourceWindow1 : public ScreenShotSourceBasic1 +{ + Q_OBJECT + +public: + ScreenShotSourceWindow1(ScreenShotEffect *effect, EffectWindow *window, ScreenShotFlags flags); +}; + +class ScreenShotSourceMulti1 : public ScreenShotSource1 +{ + Q_OBJECT + +public: + explicit ScreenShotSourceMulti1(const QList &sources); + + bool isCancelled() const override; + bool isCompleted() const override; + QImage data() const override; + void marshal(ScreenShotSink1 *sink) override; + +private: + void handleSourceCancelled(); + void handleSourceCompleted(); + + QList m_sources; +}; + +class ScreenShotSink1 : public QObject +{ + Q_OBJECT + +public: + explicit ScreenShotSink1(ScreenShotDBusInterface1 *interface, + QDBusMessage replyMessage = QDBusMessage()); + + virtual void flush(const QImage &image); + virtual void flushMulti(const QList &images); + + virtual void cancel(); + +protected: + ScreenShotDBusInterface1 *m_interface; + QDBusMessage m_replyMessage; +}; + +class ScreenShotSinkPipe1 : public ScreenShotSink1 +{ + Q_OBJECT + +public: + ScreenShotSinkPipe1(ScreenShotDBusInterface1 *interface, int fileDescriptor, + QDBusMessage replyMessage = QDBusMessage()); + ~ScreenShotSinkPipe1() override; + + void flush(const QImage &image) override; + void flushMulti(const QList &images) override; + +private: + int m_fileDescriptor; +}; + +class ScreenShotSinkXpixmap1 : public ScreenShotSink1 +{ + Q_OBJECT + +public: + explicit ScreenShotSinkXpixmap1(ScreenShotDBusInterface1 *interface, + QDBusMessage replyMessage = QDBusMessage()); + + void flush(const QImage &image) override; +}; + +class ScreenShotSinkFile1 : public ScreenShotSink1 +{ + Q_OBJECT + +public: + explicit ScreenShotSinkFile1(ScreenShotDBusInterface1 *interface, + QDBusMessage replyMessage = QDBusMessage()); + + void flush(const QImage &image) override; +}; + +ScreenShotSource1::ScreenShotSource1(QObject *parent) + : QObject(parent) +{ +} + +ScreenShotSourceBasic1::ScreenShotSourceBasic1(const QFuture &future) + : m_future(future) +{ + m_watcher = new QFutureWatcher(this); + connect(m_watcher, &QFutureWatcher::finished, this, &ScreenShotSource1::completed); + connect(m_watcher, &QFutureWatcher::canceled, this, &ScreenShotSource1::cancelled); + m_watcher->setFuture(m_future); +} + +bool ScreenShotSourceBasic1::isCancelled() const +{ + return m_future.isCanceled(); +} + +bool ScreenShotSourceBasic1::isCompleted() const +{ + return m_future.isFinished(); +} + +QImage ScreenShotSourceBasic1::data() const +{ + return m_future.result(); +} + +void ScreenShotSourceBasic1::marshal(ScreenShotSink1 *sink) +{ + sink->flush(data()); +} + +ScreenShotSourceScreen1::ScreenShotSourceScreen1(ScreenShotEffect *effect, + EffectScreen *screen, + ScreenShotFlags flags) + : ScreenShotSourceBasic1(effect->scheduleScreenShot(screen, flags)) +{ +} + +ScreenShotSourceArea1::ScreenShotSourceArea1(ScreenShotEffect *effect, + const QRect &area, + ScreenShotFlags flags) + : ScreenShotSourceBasic1(effect->scheduleScreenShot(area, flags)) +{ +} + +ScreenShotSourceWindow1::ScreenShotSourceWindow1(ScreenShotEffect *effect, + EffectWindow *window, + ScreenShotFlags flags) + : ScreenShotSourceBasic1(effect->scheduleScreenShot(window, flags)) +{ +} + +ScreenShotSourceMulti1::ScreenShotSourceMulti1(const QList &sources) + : m_sources(sources) +{ + for (ScreenShotSource1 *source : sources) { + source->setParent(this); + + connect(source, &ScreenShotSource1::cancelled, + this, &ScreenShotSourceMulti1::handleSourceCancelled); + connect(source, &ScreenShotSource1::completed, + this, &ScreenShotSourceMulti1::handleSourceCompleted); + } +} + +bool ScreenShotSourceMulti1::isCancelled() const +{ + return std::any_of(m_sources.begin(), m_sources.end(), [](const ScreenShotSource1 *source) { + return source->isCancelled(); + }); +} + +bool ScreenShotSourceMulti1::isCompleted() const +{ + return std::all_of(m_sources.begin(), m_sources.end(), [](const ScreenShotSource1 *source) { + return source->isCompleted(); + }); +} + +QImage ScreenShotSourceMulti1::data() const +{ + return QImage(); // We don't allow nesting multi screenshot sources. +} + +void ScreenShotSourceMulti1::marshal(ScreenShotSink1 *sink) +{ + QList images; + images.reserve(m_sources.count()); + + for (ScreenShotSource1 *source : qAsConst(m_sources)) { + images.append(source->data()); + } + + sink->flushMulti(images); +} + +void ScreenShotSourceMulti1::handleSourceCancelled() +{ + emit cancelled(); +} + +void ScreenShotSourceMulti1::handleSourceCompleted() +{ + // If all sources are complete now, emit the completed signal. + if (isCompleted()) { + emit completed(); + } +} + +ScreenShotSink1::ScreenShotSink1(ScreenShotDBusInterface1 *interface, QDBusMessage message) + : m_interface(interface) + , m_replyMessage(message) +{ +} + +void ScreenShotSink1::flush(const QImage &image) +{ + Q_UNUSED(image) + qCWarning(KWINEFFECTS) << metaObject()->className() << "does not implement" << Q_FUNC_INFO; +} + +void ScreenShotSink1::flushMulti(const QList &images) +{ + Q_UNUSED(images) + qCWarning(KWINEFFECTS) << metaObject()->className() << "does not implement" << Q_FUNC_INFO; +} + +void ScreenShotSink1::cancel() +{ + if (m_replyMessage.isDelayedReply()) { + QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(s_errorCancelled, + s_errorCancelledMsg)); + } +} + +ScreenShotSinkPipe1::ScreenShotSinkPipe1(ScreenShotDBusInterface1 *interface, int fileDescriptor, + QDBusMessage replyMessage) + : ScreenShotSink1(interface, replyMessage) + , m_fileDescriptor(fileDescriptor) +{ +} + +ScreenShotSinkPipe1::~ScreenShotSinkPipe1() +{ + if (m_fileDescriptor != -1) { + close(m_fileDescriptor); + } +} + +void ScreenShotSinkPipe1::flush(const QImage &image) +{ + if (m_fileDescriptor == -1) { + return; + } + + QtConcurrent::run([](int fd, const QImage &image) { + QFile file; + if (file.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { + QDataStream ds(&file); + ds << image; + file.close(); + } else { + close(fd); + } + }, m_fileDescriptor, image); + + // The ownership of the pipe file descriptor has been moved to the worker thread. + m_fileDescriptor = -1; +} + +void ScreenShotSinkPipe1::flushMulti(const QList &images) +{ + if (m_fileDescriptor == -1) { + return; + } + + QtConcurrent::run([](int fd, const QList &images) { + QFile file; + if (file.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { + QDataStream ds(&file); + ds.setVersion(QDataStream::Qt_DefaultCompiledVersion); + ds << images; + file.close(); + } else { + close(fd); + } + }, m_fileDescriptor, images); + + // The ownership of the pipe file descriptor has been moved to the worker thread. + m_fileDescriptor = -1; +} + +ScreenShotSinkXpixmap1::ScreenShotSinkXpixmap1(ScreenShotDBusInterface1 *interface, + QDBusMessage replyMessage) + : ScreenShotSink1(interface, replyMessage) +{ +} + +static QSize pickWindowSize(const QImage &image) +{ + xcb_connection_t *c = effects->xcbConnection(); + + // This will implicitly enable BIG-REQUESTS extension. + const uint32_t maximumRequestSize = xcb_get_maximum_request_length(c); + const xcb_setup_t *setup = xcb_get_setup(c); + + uint32_t requestSize = sizeof(xcb_put_image_request_t); + + // With BIG-REQUESTS extension an additional 32-bit field is inserted into + // the request so we better take it into account. + if (setup->maximum_request_length < maximumRequestSize) { + requestSize += 4; + } + + const uint32_t maximumDataSize = 4 * maximumRequestSize - requestSize; + const uint32_t bytesPerPixel = image.depth() >> 3; + const uint32_t bytesPerLine = image.bytesPerLine(); + + if (image.sizeInBytes() <= maximumDataSize) { + return image.size(); + } + + if (maximumDataSize < bytesPerLine) { + return QSize(maximumDataSize / bytesPerPixel, 1); + } + + return QSize(image.width(), maximumDataSize / bytesPerLine); +} + +static xcb_pixmap_t xpixmapFromImage(const QImage &image) +{ + xcb_connection_t *c = effects->xcbConnection(); + + xcb_pixmap_t pixmap = xcb_generate_id(c); + xcb_gcontext_t gc = xcb_generate_id(c); + + xcb_create_pixmap(c, image.depth(), pixmap, effects->x11RootWindow(), + image.width(), image.height()); + xcb_create_gc(c, gc, pixmap, 0, nullptr); + + const int bytesPerPixel = image.depth() >> 3; + + // Figure out how much data we can send with one invocation of xcb_put_image. + // In contrast to XPutImage, xcb_put_image doesn't implicitly split the data. + const QSize window = pickWindowSize(image); + + for (int i = 0; i < image.height(); i += window.height()) { + const int targetHeight = qMin(image.height() - i, window.height()); + const uint8_t *line = image.scanLine(i); + + for (int j = 0; j < image.width(); j += window.width()) { + const int targetWidth = qMin(image.width() - j, window.width()); + const uint8_t *bytes = line + j * bytesPerPixel; + const uint32_t byteCount = targetWidth * targetHeight * bytesPerPixel; + + xcb_put_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, pixmap, gc, + targetWidth, targetHeight, j, i, 0, image.depth(), + byteCount, bytes); + } + } + + xcb_flush(c); + xcb_free_gc(c, gc); + + return pixmap; +} + +void ScreenShotSinkXpixmap1::flush(const QImage &image) +{ + const xcb_pixmap_t pixmap = xpixmapFromImage(image); + emit m_interface->screenshotCreated(pixmap); +} + +ScreenShotSinkFile1::ScreenShotSinkFile1(ScreenShotDBusInterface1 *interface, + QDBusMessage replyMessage) + : ScreenShotSink1(interface, replyMessage) +{ +} + +static QString saveTempImage(const QImage &image) +{ + if (image.isNull()) { + return QString(); + } + QTemporaryFile temp(QDir::tempPath() + QDir::separator() + QLatin1String("kwin_screenshot_XXXXXX.png")); + temp.setAutoRemove(false); + if (!temp.open()) { + return QString(); + } + image.save(&temp); + temp.close(); + KNotification::event(KNotification::Notification, + i18nc("Notification caption that a screenshot got saved to file", "Screenshot"), + i18nc("Notification with path to screenshot file", "Screenshot saved to %1", temp.fileName()), + QStringLiteral("spectacle")); + return temp.fileName(); +} + +void ScreenShotSinkFile1::flush(const QImage &image) +{ + QDBusConnection::sessionBus().send(m_replyMessage.createReply(saveTempImage(image))); +} + +ScreenShotDBusInterface1::ScreenShotDBusInterface1(ScreenShotEffect *effect, QObject *parent) + : QObject(parent) + , m_effect(effect) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Screenshot"), + this, + QDBusConnection::ExportScriptableContents); +} + +ScreenShotDBusInterface1::~ScreenShotDBusInterface1() +{ + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Screenshot")); +} + +bool ScreenShotDBusInterface1::checkCall() const +{ + if (!calledFromDBus()) { + return false; + } + + const QDBusReply reply = connection().interface()->servicePid(message().service()); + if (reply.isValid()) { + const uint pid = reply.value(); + const auto interfaces = KWin::fetchRestrictedDBusInterfacesFromPid(pid); + if (!interfaces.contains(s_dbusInterfaceName)) { + sendErrorReply(s_errorNotAuthorized, s_errorNotAuthorizedMsg); + qCWarning(KWINEFFECTS) << "Process" << pid << "tried to take a screenshot without being granted to DBus interface" << s_dbusInterfaceName; + return false; + } + } else { + return false; + } + + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return false; + } + return true; +} + +void ScreenShotDBusInterface1::screenshotWindowUnderCursor(int mask) +{ + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return; + } + + EffectWindow *hoveredWindow = nullptr; + + const QPoint cursor = effects->cursorPos(); + EffectWindowList order = effects->stackingOrder(); + EffectWindowList::const_iterator it = order.constEnd(), first = order.constBegin(); + while (it != first) { + hoveredWindow = *(--it); + if (hoveredWindow->isOnCurrentDesktop() + && !hoveredWindow->isMinimized() + && !hoveredWindow->isDeleted() + && hoveredWindow->geometry().contains(cursor)) { + break; + } + hoveredWindow = nullptr; + } + + if (hoveredWindow) { + takeScreenShot(hoveredWindow, ScreenShotFlags(mask), + new ScreenShotSinkXpixmap1(this, message())); + } +} + +void ScreenShotDBusInterface1::screenshotForWindow(qulonglong winId, int mask) +{ + EffectWindow *window = effects->findWindow(winId); + if (!window || window->isMinimized() || window->isDeleted()) { + return; + } + + takeScreenShot(window, ScreenShotFlags(mask), new ScreenShotSinkXpixmap1(this, message())); +} + +QString ScreenShotDBusInterface1::interactive(int mask) +{ + if (!calledFromDBus()) { + return QString(); + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return QString(); + } + + const QDBusMessage replyMessage = message(); + setDelayedReply(true); + + effects->startInteractiveWindowSelection([this, mask, replyMessage](EffectWindow *window) { + hideInfoMessage(); + if (!window) { + QDBusConnection::sessionBus().send(replyMessage.createErrorReply(s_errorCancelled, + s_errorCancelledMsg)); + return; + } + takeScreenShot(window, ScreenShotFlags(mask), new ScreenShotSinkFile1(this, replyMessage)); + }); + + showInfoMessage(InfoMessageMode::Window); + return QString(); +} + +void ScreenShotDBusInterface1::interactive(QDBusUnixFileDescriptor fd, int mask) +{ + if (!calledFromDBus()) { + return; + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return; + } + + const int fileDescriptor = dup(fd.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + + effects->startInteractiveWindowSelection([this, fileDescriptor, mask](EffectWindow *window) { + hideInfoMessage(); + if (!window) { + close(fileDescriptor); + return; + } + takeScreenShot(window, ScreenShotFlags(mask), + new ScreenShotSinkPipe1(this, fileDescriptor)); + }); + + showInfoMessage(InfoMessageMode::Window); +} + +QString ScreenShotDBusInterface1::screenshotFullscreen(bool captureCursor) +{ + if (!checkCall()) { + return QString(); + } + + ScreenShotFlags flags = ScreenShotFlags(); + if (captureCursor) { + flags |= ScreenShotIncludeCursor; + } + + takeScreenShot(effects->virtualScreenGeometry(), flags, new ScreenShotSinkFile1(this, message())); + + setDelayedReply(true); + return QString(); +} + +void ScreenShotDBusInterface1::screenshotFullscreen(QDBusUnixFileDescriptor fd, + bool captureCursor, bool shouldReturnNativeSize) +{ + if (!checkCall()) { + return; + } + + const int fileDescriptor = dup(fd.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + + ScreenShotFlags flags = ScreenShotFlags(); + if (captureCursor) { + flags |= ScreenShotIncludeCursor; + } + if (shouldReturnNativeSize) { + flags |= ScreenShotNativeResolution; + } + + takeScreenShot(effects->virtualScreenGeometry(), flags, + new ScreenShotSinkPipe1(this, fileDescriptor)); +} + +QString ScreenShotDBusInterface1::screenshotScreen(int screenId, bool captureCursor) +{ + if (!checkCall()) { + return QString(); + } + + EffectScreen *screen = effects->findScreen(screenId); + if (!screen) { + sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMsg); + return QString(); + } + + ScreenShotFlags flags = ScreenShotNativeResolution; + if (captureCursor) { + flags |= ScreenShotIncludeCursor; + } + + takeScreenShot(screen, flags, new ScreenShotSinkFile1(this, message())); + + setDelayedReply(true); + return QString(); +} + +void ScreenShotDBusInterface1::screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor) +{ + if (!checkCall()) { + return; + } + + const int fileDescriptor = dup(fd.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + + ScreenShotFlags flags = ScreenShotNativeResolution; + if (captureCursor) { + flags |= ScreenShotIncludeCursor; + } + + effects->startInteractivePositionSelection([this, fileDescriptor, flags](const QPoint &p) { + hideInfoMessage(); + if (p == QPoint(-1, -1)) { + close(fileDescriptor); + } else { + EffectScreen *screen = effects->screenAt(p); + if (!screen) { + close(fileDescriptor); + return; + } + takeScreenShot(screen, flags, new ScreenShotSinkPipe1(this, fileDescriptor)); + } + }); + showInfoMessage(InfoMessageMode::Screen); +} + +void ScreenShotDBusInterface1::screenshotScreens(QDBusUnixFileDescriptor fd, + const QStringList &screensNames, + bool captureCursor, bool shouldReturnNativeSize) +{ + if (!checkCall()) { + return; + } + + const int fileDescriptor = dup(fd.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFd, s_errorFdMsg); + return; + } + + ScreenShotFlags flags = ScreenShotFlags(); + if (captureCursor) { + flags |= ScreenShotIncludeCursor; + } + if (shouldReturnNativeSize) { + flags |= ScreenShotNativeResolution; + } + + QList screens; + screens.reserve(screensNames.count()); + + for (const QString &screenName : screensNames) { + EffectScreen *screen = effects->findScreen(screenName); + if (!screen) { + close(fileDescriptor); + sendErrorReply(s_errorScreenMissing, s_errorScreenMissingMsg + ": " + screenName); + return; + } + screens.append(screen); + } + + ScreenShotSinkPipe1 *sink = new ScreenShotSinkPipe1(this, fileDescriptor); + takeScreenShot(screens, flags, sink); +} + +QString ScreenShotDBusInterface1::screenshotArea(int x, int y, int width, int height, bool captureCursor) +{ + if (!checkCall()) { + return QString(); + } + + const QRect area(x, y, width, height); + if (area.isEmpty()) { + sendErrorReply(s_errorInvalidArea, s_errorInvalidAreaMsg); + return QString(); + } + + ScreenShotFlags flags = ScreenShotFlags(); + if (captureCursor) { + flags |= ScreenShotIncludeCursor; + } + + takeScreenShot(area, flags, new ScreenShotSinkFile1(this, message())); + setDelayedReply(true); + + return QString(); +} + +bool ScreenShotDBusInterface1::isTakingScreenshot() const +{ + return m_source; +} + +void ScreenShotDBusInterface1::showInfoMessage(InfoMessageMode mode) +{ + QString text; + switch (mode) { + case InfoMessageMode::Window: + text = i18n("Select window to screen shot with left click or enter.\nEscape or right click to cancel."); + break; + case InfoMessageMode::Screen: + text = i18n("Create screen shot with left click or enter.\nEscape or right click to cancel."); + break; + } + effects->showOnScreenMessage(text, QStringLiteral("spectacle")); +} + +void ScreenShotDBusInterface1::hideInfoMessage() +{ + effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); +} + +void ScreenShotDBusInterface1::handleSourceCancelled() +{ + m_sink->cancel(); + + m_source.reset(); + m_sink.reset(); +} + +void ScreenShotDBusInterface1::handleSourceCompleted() +{ + m_source->marshal(m_sink.data()); + + m_source.reset(); + m_sink.reset(); +} + +void ScreenShotDBusInterface1::bind(ScreenShotSink1 *sink, ScreenShotSource1 *source) +{ + m_sink.reset(sink); + m_source.reset(source); + + connect(m_source.data(), &ScreenShotSource1::cancelled, + this, &ScreenShotDBusInterface1::handleSourceCancelled); + connect(m_source.data(), &ScreenShotSource1::completed, + this, &ScreenShotDBusInterface1::handleSourceCompleted); +} + +void ScreenShotDBusInterface1::takeScreenShot(EffectScreen *screen, ScreenShotFlags flags, + ScreenShotSink1 *sink) +{ + bind(sink, new ScreenShotSourceScreen1(m_effect, screen, flags)); +} + +void ScreenShotDBusInterface1::takeScreenShot(const QList &screens, + ScreenShotFlags flags, ScreenShotSink1 *sink) +{ + QList sources; + sources.reserve(screens.count()); + for (EffectScreen *screen : screens) { + sources.append(new ScreenShotSourceScreen1(m_effect, screen, flags)); + } + + bind(sink, new ScreenShotSourceMulti1(sources)); +} + +void ScreenShotDBusInterface1::takeScreenShot(const QRect &area, ScreenShotFlags flags, + ScreenShotSink1 *sink) +{ + bind(sink, new ScreenShotSourceArea1(m_effect, area, flags)); +} + +void ScreenShotDBusInterface1::takeScreenShot(EffectWindow *window, ScreenShotFlags flags, + ScreenShotSink1 *sink) +{ + bind(sink, new ScreenShotSourceWindow1(m_effect, window, flags)); +} + +} // namespace KWin + +#include "screenshotdbusinterface1.moc" diff --git a/src/effects/screenshot/screenshotdbusinterface1.h b/src/effects/screenshot/screenshotdbusinterface1.h new file mode 100644 index 0000000000..77a8742c86 --- /dev/null +++ b/src/effects/screenshot/screenshotdbusinterface1.h @@ -0,0 +1,172 @@ +/* + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "screenshot.h" + +#include +#include +#include + +namespace KWin +{ + +class ScreenShotSink1; +class ScreenShotSource1; + +/** + * The ScreenshotDBusInterface1 class provides a d-bus api to take screenshots. This implements + * the org.kde.kwin.Screenshot interface. + * + * An application that requests a screenshot must have "org.kde.kwin.Screenshot" listed in its + * X-KDE-DBUS-Restricted-Interfaces desktop file field. + */ +class ScreenShotDBusInterface1 : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Screenshot") + +public: + explicit ScreenShotDBusInterface1(ScreenShotEffect *effect, QObject *parent = nullptr); + ~ScreenShotDBusInterface1() override; + +Q_SIGNALS: + /** + * This signal is emitted when a screenshot has been written in an Xpixmap with the + * specified @a handle. + */ + Q_SCRIPTABLE void screenshotCreated(qulonglong handle); + +public Q_SLOTS: + Q_SCRIPTABLE void screenshotForWindow(qulonglong winid, int mask = 0); + /** + * Starts an interactive window screenshot session. The user can select a window to + * screenshot. + * + * Once the window is selected the screenshot is saved into a file and the path gets + * returned to the DBus peer. + * + * @param mask The mask for what to include in the screenshot + */ + Q_SCRIPTABLE QString interactive(int mask = 0); + + /** + * Starts an interactive window screenshot session. The user can select a window to + * screenshot. + * + * Once the window is selected the screenshot is saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param mask The mask for what to include in the screenshot + */ + Q_SCRIPTABLE void interactive(QDBusUnixFileDescriptor fd, int mask = 0); + Q_SCRIPTABLE void screenshotWindowUnderCursor(int mask = 0); + + /** + * Saves a screenshot of all screen into a file and returns the path to the file. + * Functionality requires hardware support, if not available a null string is returned. + * @param captureCursor Whether to include the cursor in the image + * @returns Path to stored screenshot, or null string in failure case. + */ + Q_SCRIPTABLE QString screenshotFullscreen(bool captureCursor = false); + + /** + * Takes a full screen screenshot in a one file format. + * + * Once the screenshot is taken it gets saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param captureCursor Whether to include the mouse cursor + * @param shouldReturnNativeSize Whether to return an image according to the virtualGeometry, + * or according to pixel on screen size + */ + Q_SCRIPTABLE void screenshotFullscreen(QDBusUnixFileDescriptor fd, + bool captureCursor = false, + bool shouldReturnNativeSize = false); + + /** + * Take a screenshot of the passed screens and return a QList in the fd response, + * an image for each screen in pixel-on-screen size when shouldReturnNativeSize is passed, + * or converted to using logicale size if not + * + * @param fd + * @param screensNames the names of the screens whose screenshot to return + * @param captureCursor + * @param shouldReturnNativeSize + */ + Q_SCRIPTABLE void screenshotScreens(QDBusUnixFileDescriptor fd, + const QStringList &screensNames, + bool captureCursor = false, + bool shouldReturnNativeSize = false); + + /** + * Saves a screenshot of the screen identified by @p screen into a file and returns the path + * to the file. + * Functionality requires hardware support, if not available a null string is returned. + * @param screen Number of screen as numbered by QDesktopWidget + * @param captureCursor Whether to include the cursor in the image + * @returns Path to stored screenshot, or null string in failure case. + */ + Q_SCRIPTABLE QString screenshotScreen(int screen, bool captureCursor = false); + + /** + * Starts an interactive screenshot of a screen session. + * + * The user is asked to select the screen to screenshot. + * + * Once the screenshot is taken it gets saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param captureCursor Whether to include the mouse cursor + */ + Q_SCRIPTABLE void screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor = false); + + /** + * Saves a screenshot of the selected geometry into a file and returns the path to the file. + * Functionality requires hardware support, if not available a null string is returned. + * @param x Left upper x coord of region + * @param y Left upper y coord of region + * @param width Width of the region to screenshot + * @param height Height of the region to screenshot + * @param captureCursor Whether to include the cursor in the image + * @returns Path to stored screenshot, or null string in failure case. + */ + Q_SCRIPTABLE QString screenshotArea(int x, int y, int width, int height, bool captureCursor = false); + +private Q_SLOTS: + void handleSourceCompleted(); + void handleSourceCancelled(); + +private: + enum class InfoMessageMode { Window, Screen, }; + + void takeScreenShot(EffectScreen *screen, ScreenShotFlags flags, ScreenShotSink1 *sink); + void takeScreenShot(const QList &screens, ScreenShotFlags flags, ScreenShotSink1 *sink); + void takeScreenShot(const QRect &area, ScreenShotFlags flags, ScreenShotSink1 *sink); + void takeScreenShot(EffectWindow *window, ScreenShotFlags flags, ScreenShotSink1 *sink); + + void bind(ScreenShotSink1 *sink, ScreenShotSource1 *source); + + bool checkCall() const; + bool isTakingScreenshot() const; + + void showInfoMessage(InfoMessageMode mode); + void hideInfoMessage(); + + ScreenShotEffect *m_effect; + QScopedPointer m_sink; + QScopedPointer m_source; +}; + +} // namespace KWin