File indexing completed on 2025-02-02 04:26:13
0001 /* This file is part of Spectacle, the KDE screenshot utility 0002 * SPDX-FileCopyrightText: 2019 Boudhayan Gupta <bgupta@kde.org> 0003 0004 * SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "ImagePlatformXcb.h" 0008 0009 #include <xcb/randr.h> 0010 #include <xcb/xcb_cursor.h> 0011 #include <xcb/xcb_util.h> 0012 #include <xcb/xfixes.h> 0013 0014 #include <X11/Xatom.h> 0015 #include <X11/Xdefs.h> 0016 0017 #include <QAbstractNativeEventFilter> 0018 #include <QApplication> 0019 #include <QDBusConnection> 0020 #include <QDBusConnectionInterface> 0021 #include <QDBusInterface> 0022 #include <QGraphicsDropShadowEffect> 0023 #include <QGraphicsPixmapItem> 0024 #include <QGraphicsScene> 0025 #include <QPainter> 0026 #include <QScreen> 0027 #include <QSet> 0028 #include <QStack> 0029 #include <QTimer> 0030 #include <private/qtx11extras_p.h> 0031 #include <QtMath> 0032 0033 #include <KWindowInfo> 0034 #include <KWindowSystem> 0035 #include <KX11Extras> 0036 0037 using namespace Qt::StringLiterals; 0038 0039 #include <memory> 0040 0041 /* -- XCB Image Smart Pointer ------------------------------------------------------------------ */ 0042 0043 struct XcbImagePtrDeleter { 0044 void operator()(xcb_image_t *xcbImage) const 0045 { 0046 if (xcbImage) { 0047 xcb_image_destroy(xcbImage); 0048 } 0049 } 0050 }; 0051 using XcbImagePtr = std::unique_ptr<xcb_image_t, XcbImagePtrDeleter>; 0052 0053 struct CFreeDeleter { 0054 void operator()(void *ptr) const 0055 { 0056 free(ptr); 0057 } 0058 }; 0059 template<typename Reply> 0060 using XcbReplyPtr = std::unique_ptr<Reply, CFreeDeleter>; 0061 0062 /* -- On Click Native Event Filter ------------------------------------------------------------- */ 0063 0064 class ImagePlatformXcb::OnClickEventFilter : public QAbstractNativeEventFilter 0065 { 0066 public: 0067 explicit OnClickEventFilter(ImagePlatformXcb *platformPtr) 0068 : m_platformPtr(platformPtr) 0069 { 0070 } 0071 0072 void setCaptureOptions(ImagePlatform::GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) 0073 { 0074 m_grabMode = grabMode; 0075 m_includePointer = includePointer; 0076 m_includeDecorations = includeDecorations; 0077 m_includeShadow = includeShadow; 0078 } 0079 0080 bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr * /*result*/) override 0081 { 0082 if (eventType == "xcb_generic_event_t") { 0083 auto firstEvent = static_cast<xcb_generic_event_t *>(message); 0084 0085 switch (firstEvent->response_type & ~0x80) { 0086 case XCB_BUTTON_RELEASE: { 0087 // uninstall the eventfilter and release the mouse 0088 qApp->removeNativeEventFilter(this); 0089 xcb_ungrab_pointer(QX11Info::connection(), XCB_TIME_CURRENT_TIME); 0090 0091 // decide whether to grab or abort. regrab the mouse on mouse-wheel events 0092 { 0093 auto secondEvent = static_cast<xcb_button_release_event_t *>(message); 0094 if (secondEvent->detail == 1) { 0095 QTimer::singleShot(0, nullptr, [this]() { 0096 m_platformPtr->doGrabNow(m_grabMode, m_includePointer, m_includeDecorations, m_includeShadow); 0097 }); 0098 } else if (secondEvent->detail == 2 || secondEvent->detail == 3) { 0099 // 2: middle click, 3: right click; both cancel 0100 Q_EMIT m_platformPtr->newScreenshotTaken(); 0101 } else if (secondEvent->detail < 4) { 0102 Q_EMIT m_platformPtr->newScreenshotFailed(); 0103 } else { 0104 QTimer::singleShot(0, nullptr, [this]() { 0105 m_platformPtr->doGrabOnClick(m_grabMode, m_includePointer, m_includeDecorations, m_includeShadow); 0106 }); 0107 } 0108 } 0109 return true; 0110 } 0111 default: 0112 return false; 0113 } 0114 } 0115 return false; 0116 } 0117 0118 private: 0119 ImagePlatformXcb *m_platformPtr; 0120 ImagePlatform::GrabMode m_grabMode{GrabMode::AllScreens}; 0121 bool m_includePointer{true}; 0122 bool m_includeDecorations{true}; 0123 bool m_includeShadow{true}; 0124 }; 0125 0126 /* -- General Plumbing ------------------------------------------------------------------------- */ 0127 0128 ImagePlatformXcb::ImagePlatformXcb(QObject *parent) 0129 : ImagePlatform(parent) 0130 , m_nativeEventFilter(new OnClickEventFilter(this)) 0131 { 0132 updateSupportedGrabModes(); 0133 connect(qGuiApp, &QGuiApplication::screenAdded, this, &ImagePlatformXcb::updateSupportedGrabModes); 0134 connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ImagePlatformXcb::updateSupportedGrabModes); 0135 } 0136 0137 ImagePlatformXcb::~ImagePlatformXcb() 0138 { 0139 } 0140 0141 ImagePlatform::GrabModes ImagePlatformXcb::supportedGrabModes() const 0142 { 0143 return m_grabModes; 0144 } 0145 0146 void ImagePlatformXcb::updateSupportedGrabModes() 0147 { 0148 ImagePlatform::GrabModes grabModes = { 0149 GrabMode::AllScreens, GrabMode::ActiveWindow, 0150 GrabMode::WindowUnderCursor, GrabMode::TransientWithParent, 0151 GrabMode::PerScreenImageNative 0152 }; 0153 0154 if (QApplication::screens().count() > 1) { 0155 grabModes |= ImagePlatform::GrabMode::CurrentScreen; 0156 } 0157 0158 if (m_grabModes != grabModes) { 0159 m_grabModes = grabModes; 0160 Q_EMIT supportedGrabModesChanged(); 0161 } 0162 } 0163 0164 ImagePlatform::ShutterModes ImagePlatformXcb::supportedShutterModes() const 0165 { 0166 return {ShutterMode::Immediate | ShutterMode::OnClick}; 0167 } 0168 0169 void ImagePlatformXcb::doGrab(ShutterMode shutterMode, GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) 0170 { 0171 switch (shutterMode) { 0172 case ShutterMode::Immediate: { 0173 doGrabNow(grabMode, includePointer, includeDecorations, includeShadow); 0174 return; 0175 } 0176 case ShutterMode::OnClick: { 0177 doGrabOnClick(grabMode, includePointer, includeDecorations, includeShadow); 0178 return; 0179 } 0180 } 0181 } 0182 0183 /* -- Platform Utilities ----------------------------------------------------------------------- */ 0184 0185 void ImagePlatformXcb::updateWindowTitle(xcb_window_t window) 0186 { 0187 auto title = KX11Extras::readNameProperty(window, XA_WM_NAME); 0188 Q_EMIT windowTitleChanged(title); 0189 } 0190 0191 bool ImagePlatformXcb::isKWinAvailable() 0192 { 0193 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.KWin"_s)) { 0194 QDBusInterface iface(u"org.kde.KWin"_s, u"/Effects"_s, u"org.kde.kwin.Effects"_s); 0195 QDBusReply<bool> reply = iface.call(u"isEffectLoaded"_s, u"screenshot"_s); 0196 return reply.value(); 0197 } 0198 return false; 0199 } 0200 0201 /* -- XCB Utilities ---------------------------------------------------------------------------- */ 0202 0203 QPoint ImagePlatformXcb::getCursorPosition() 0204 { 0205 // QCursor::pos() is not used because it requires additional calculations. 0206 // Its value is the offset to the origin of the current screen is in 0207 // device-independent pixels while the origin itself uses native pixels. 0208 0209 auto xcbConn = QX11Info::connection(); 0210 auto pointerCookie = xcb_query_pointer_unchecked(xcbConn, QX11Info::appRootWindow()); 0211 XcbReplyPtr<xcb_query_pointer_reply_t> pointerReply(xcb_query_pointer_reply(xcbConn, pointerCookie, nullptr)); 0212 0213 return QPoint(pointerReply->root_x, pointerReply->root_y); 0214 } 0215 0216 QRect ImagePlatformXcb::getDrawableGeometry(xcb_drawable_t drawable) 0217 { 0218 auto xcbConn = QX11Info::connection(); 0219 auto geoCookie = xcb_get_geometry_unchecked(xcbConn, drawable); 0220 XcbReplyPtr<xcb_get_geometry_reply_t> geoReply(xcb_get_geometry_reply(xcbConn, geoCookie, nullptr)); 0221 if (!geoReply) { 0222 return QRect(); 0223 } 0224 return QRect(geoReply->x, geoReply->y, geoReply->width, geoReply->height); 0225 } 0226 0227 xcb_window_t ImagePlatformXcb::getWindowUnderCursor() 0228 { 0229 auto xcbConn = QX11Info::connection(); 0230 auto appWin = QX11Info::appRootWindow(); 0231 0232 const QByteArray atomName("WM_STATE"); 0233 auto atomCookie = xcb_intern_atom_unchecked(xcbConn, 0, atomName.length(), atomName.constData()); 0234 auto pointerCookie = xcb_query_pointer_unchecked(xcbConn, appWin); 0235 XcbReplyPtr<xcb_intern_atom_reply_t> atomReply(xcb_intern_atom_reply(xcbConn, atomCookie, nullptr)); 0236 XcbReplyPtr<xcb_query_pointer_reply_t> pointerReply(xcb_query_pointer_reply(xcbConn, pointerCookie, nullptr)); 0237 0238 if (atomReply->atom == XCB_ATOM_NONE) { 0239 return QX11Info::appRootWindow(); 0240 } 0241 0242 // now start testing 0243 QStack<xcb_window_t> windowStack; 0244 windowStack.push(pointerReply->child); 0245 0246 while (!windowStack.isEmpty()) { 0247 appWin = windowStack.pop(); 0248 0249 // next, check if our window has the WM_STATE property set on 0250 // the window. if yes, return the window - we have found it 0251 auto propCookie = xcb_get_property_unchecked(xcbConn, 0, appWin, atomReply->atom, XCB_ATOM_ANY, 0, 0); 0252 XcbReplyPtr<xcb_get_property_reply_t> propReply(xcb_get_property_reply(xcbConn, propCookie, nullptr)); 0253 0254 if (propReply->type != XCB_ATOM_NONE) { 0255 return appWin; 0256 } 0257 0258 // if we're here, this means the window is not the real window 0259 // we should start looking at its children 0260 auto treeCookie = xcb_query_tree_unchecked(xcbConn, appWin); 0261 XcbReplyPtr<xcb_query_tree_reply_t> treeReply(xcb_query_tree_reply(xcbConn, treeCookie, nullptr)); 0262 auto windowChildren = xcb_query_tree_children(treeReply.get()); 0263 auto windowChildrenLength = xcb_query_tree_children_length(treeReply.get()); 0264 0265 for (int iIdx = windowChildrenLength - 1; iIdx >= 0; iIdx--) { 0266 windowStack.push(windowChildren[iIdx]); 0267 } 0268 } 0269 0270 // return the window. it has geometry information for a crop 0271 return pointerReply->child; 0272 } 0273 0274 xcb_window_t ImagePlatformXcb::getTransientWindowParent(xcb_window_t childWindow, QRect &windowRectOut, bool includeDecorations) 0275 { 0276 NET::Properties netProp = includeDecorations ? NET::WMFrameExtents : NET::WMGeometry; 0277 KWindowInfo windowInfo(childWindow, netProp, NET::WM2TransientFor); 0278 0279 // add the current window to the image 0280 if (includeDecorations) { 0281 windowRectOut = windowInfo.frameGeometry(); 0282 } else { 0283 windowRectOut = windowInfo.geometry(); 0284 } 0285 return windowInfo.transientFor(); 0286 } 0287 0288 QList<QRect> ImagePlatformXcb::getScreenRects() 0289 { 0290 QList<QRect> screenRects; 0291 auto xcbConn = QX11Info::connection(); 0292 auto appWin = QX11Info::appRootWindow(); 0293 auto monitorsCookie = xcb_randr_get_monitors(xcbConn, appWin, 1); 0294 XcbReplyPtr<xcb_randr_get_monitors_reply_t> monitorsReply(xcb_randr_get_monitors_reply(xcbConn, monitorsCookie, nullptr)); 0295 auto it = xcb_randr_get_monitors_monitors_iterator(monitorsReply.get()); 0296 while (it.rem) { 0297 auto monitorInfo = it.data; 0298 screenRects += {monitorInfo->x, monitorInfo->y, monitorInfo->width, monitorInfo->height}; 0299 xcb_randr_monitor_info_next(&it); 0300 } 0301 return screenRects; 0302 } 0303 0304 /* -- Image Processing Utilities --------------------------------------------------------------- */ 0305 0306 QImage ImagePlatformXcb::addDropShadow(QImage &image) 0307 { 0308 // Create a new image that is 20px wider and 20px taller than the original image 0309 QImage shadowImage(image.size() + QSize(40, 40), QImage::Format_ARGB32); 0310 shadowImage.fill(Qt::transparent); 0311 0312 // Create a painter for the shadow image 0313 QPainter shadowPainter(&shadowImage); 0314 0315 // Create a pixmap item for the original image 0316 auto pixmapItem = new QGraphicsPixmapItem; 0317 pixmapItem->setPixmap(QPixmap::fromImage(image)); 0318 0319 // Create a drop shadow effect for the pixmap item 0320 auto shadowEffect = new QGraphicsDropShadowEffect; 0321 shadowEffect->setOffset(0); 0322 shadowEffect->setBlurRadius(20); 0323 pixmapItem->setGraphicsEffect(shadowEffect); 0324 0325 // Create a graphics scene and add the pixmap item to it 0326 QGraphicsScene graphicsScene; 0327 graphicsScene.addItem(pixmapItem); 0328 0329 // Render the graphics scene to the shadow image 0330 graphicsScene.render(&shadowPainter, QRectF(), QRectF(-20, -20, image.width() + 40, image.height() + 40)); 0331 shadowPainter.end(); 0332 0333 // Return the shadow image 0334 return shadowImage; 0335 } 0336 0337 QImage ImagePlatformXcb::convertFromNative(xcb_image_t *xcbImage) 0338 { 0339 auto imageFormat = QImage::Format_Invalid; 0340 switch (xcbImage->depth) { 0341 case 1: 0342 imageFormat = QImage::Format_MonoLSB; 0343 break; 0344 case 16: 0345 imageFormat = QImage::Format_RGB16; 0346 break; 0347 case 24: 0348 imageFormat = QImage::Format_RGB32; 0349 break; 0350 case 30: 0351 imageFormat = QImage::Format_RGB30; 0352 break; 0353 case 32: 0354 imageFormat = QImage::Format_ARGB32_Premultiplied; 0355 break; 0356 default: 0357 return {}; // we don't know 0358 } 0359 0360 // the RGB32 format requires data format 0xffRRGGBB, ensure that this fourth byte really is 0xff 0361 if (imageFormat == QImage::Format_RGB32) { 0362 auto data = reinterpret_cast<quint32 *>(xcbImage->data); 0363 for (size_t iIter = 0; iIter < xcbImage->width * xcbImage->height; iIter++) { 0364 data[iIter] |= 0xff000000; 0365 } 0366 } 0367 0368 QImage image(xcbImage->data, xcbImage->width, xcbImage->height, imageFormat); 0369 if (image.isNull()) { 0370 return {}; 0371 } 0372 0373 // work around an abort in QImage::color 0374 if (image.format() == QImage::Format_MonoLSB) { 0375 image.setColorCount(2); 0376 image.setColor(0, QColor(Qt::white).rgb()); 0377 image.setColor(1, QColor(Qt::black).rgb()); 0378 } 0379 0380 // the image is ready. Since the backing data from xcbImage could be freed 0381 // before the image goes away, a deep copy is necessary. 0382 return image.copy(); 0383 } 0384 0385 QImage ImagePlatformXcb::blendCursorImage(QImage &image, const QRect rect) 0386 { 0387 // If the cursor position lies outside the area, do not bother drawing a cursor. 0388 0389 auto cursorPos = getCursorPosition(); 0390 if (!rect.contains(cursorPos)) { 0391 return image; 0392 } 0393 0394 // now we can get the image and start processing 0395 auto xcbConn = QX11Info::connection(); 0396 0397 auto cursorCookie = xcb_xfixes_get_cursor_image_unchecked(xcbConn); 0398 XcbReplyPtr<xcb_xfixes_get_cursor_image_reply_t> cursorReply(xcb_xfixes_get_cursor_image_reply(xcbConn, cursorCookie, nullptr)); 0399 if (!cursorReply) { 0400 return image; 0401 } 0402 0403 // get the image and process it into a qimage 0404 auto pixelData = xcb_xfixes_get_cursor_image_cursor_image(cursorReply.get()); 0405 if (!pixelData) { 0406 return image; 0407 } 0408 QImage cursorImage(reinterpret_cast<quint8 *>(pixelData), cursorReply->width, cursorReply->height, QImage::Format_ARGB32_Premultiplied); 0409 0410 // a small fix for the cursor position for fancier cursors 0411 cursorPos -= QPoint(cursorReply->xhot, cursorReply->yhot); 0412 0413 // now we translate the cursor point to our screen rectangle and do the painting 0414 cursorPos -= QPoint(rect.x(), rect.y()); 0415 QPainter painter(&image); 0416 painter.drawImage(cursorPos, cursorImage); 0417 return image; 0418 } 0419 0420 QImage ImagePlatformXcb::postProcessImage(QImage &image, QRect rect, bool blendPointer) 0421 { 0422 if (!(blendPointer)) { 0423 // note: this may be a null image if an error occurred. 0424 return image; 0425 } 0426 return blendCursorImage(image, rect); 0427 } 0428 0429 /* -- Capture Helpers -------------------------------------------------------------------------- */ 0430 0431 QImage ImagePlatformXcb::getImageFromDrawable(xcb_drawable_t xcbDrawable, const QRect &rect) 0432 { 0433 auto xcbConn = QX11Info::connection(); 0434 0435 // proceed to get an image based on the geometry (in device pixels) 0436 XcbImagePtr xcbImage(xcb_image_get(xcbConn, xcbDrawable, rect.x(), rect.y(), rect.width(), rect.height(), ~0, XCB_IMAGE_FORMAT_Z_PIXMAP)); 0437 0438 // too bad, the capture failed. 0439 if (!xcbImage) { 0440 return {}; 0441 } 0442 0443 // now process the image 0444 return convertFromNative(xcbImage.get()); 0445 } 0446 0447 QImage ImagePlatformXcb::getToplevelImage(QRect rect, bool blendPointer) 0448 { 0449 auto rootWindow = QX11Info::appRootWindow(); 0450 0451 // treat a null rect as an alias for capturing fullscreen 0452 if (!rect.isValid()) { 0453 rect = getDrawableGeometry(rootWindow); 0454 } else { 0455 QRegion screenRegion; 0456 const auto screenRects = getScreenRects(); 0457 for (auto &screenRect :screenRects) { 0458 screenRegion += screenRect; 0459 } 0460 rect = (screenRegion & rect).boundingRect(); 0461 } 0462 0463 auto image = getImageFromDrawable(rootWindow, rect); 0464 return postProcessImage(image, rect, blendPointer); 0465 } 0466 0467 QImage ImagePlatformXcb::getWindowImage(xcb_window_t window, bool blendPointer) 0468 { 0469 auto xcbConn = QX11Info::connection(); 0470 0471 // first get geometry information for our window 0472 auto geoCookie = xcb_get_geometry_unchecked(xcbConn, window); 0473 XcbReplyPtr<xcb_get_geometry_reply_t> geoReply(xcb_get_geometry_reply(xcbConn, geoCookie, nullptr)); 0474 QRect windowRect(geoReply->x, geoReply->y, geoReply->width, geoReply->height); 0475 0476 // then proceed to get an image 0477 auto image = getImageFromDrawable(window, windowRect); 0478 0479 // translate window coordinates to global ones. 0480 auto rootGeoCookie = xcb_get_geometry_unchecked(xcbConn, geoReply->root); 0481 XcbReplyPtr<xcb_get_geometry_reply_t> rootGeoReply(xcb_get_geometry_reply(xcbConn, rootGeoCookie, nullptr)); 0482 auto translateCookie = xcb_translate_coordinates_unchecked(xcbConn, window, geoReply->root, rootGeoReply->x, rootGeoReply->y); 0483 XcbReplyPtr<xcb_translate_coordinates_reply_t> translateReply(xcb_translate_coordinates_reply(xcbConn, translateCookie, nullptr)); 0484 0485 // adjust local to global coordinates. 0486 windowRect.moveRight(windowRect.x() + translateReply->dst_x); 0487 windowRect.moveTop(windowRect.y() + translateReply->dst_y); 0488 0489 // if the window capture failed, try to obtain one from the full screen. 0490 if (image.isNull()) { 0491 return getToplevelImage(windowRect, blendPointer); 0492 } 0493 return postProcessImage(image, windowRect, blendPointer); 0494 } 0495 0496 void ImagePlatformXcb::handleKWinScreenshotReply(quint64 drawable) 0497 { 0498 QDBusConnection::sessionBus().disconnect(u"org.kde.KWin"_s, 0499 u"/Screenshot"_s, 0500 u"org.kde.kwin.Screenshot"_s, 0501 u"screenshotCreated"_s, 0502 this, 0503 SLOT(handleKWinScreenshotReply(quint64))); 0504 0505 // obtain width and height and grab an image (x and y are always zero for drawables) 0506 auto xcbDrawable = static_cast<xcb_drawable_t>(drawable); 0507 auto rect = getDrawableGeometry(xcbDrawable); 0508 auto image = getImageFromDrawable(xcbDrawable, rect); 0509 0510 if (!image.isNull()) { 0511 image.setDevicePixelRatio(qGuiApp->devicePixelRatio()); 0512 Q_EMIT newScreenshotTaken(image); 0513 return; 0514 } 0515 Q_EMIT newScreenshotFailed(); 0516 } 0517 0518 /* -- Grabber Methods -------------------------------------------------------------------------- */ 0519 0520 void ImagePlatformXcb::grabAllScreens(bool includePointer, bool crop) 0521 { 0522 auto image = getToplevelImage(QRect(), includePointer); 0523 image.setDevicePixelRatio(qGuiApp->devicePixelRatio()); 0524 if (crop) { 0525 Q_EMIT newCroppableScreenshotTaken(image); 0526 return; 0527 } 0528 Q_EMIT newScreenshotTaken(image); 0529 } 0530 0531 void ImagePlatformXcb::grabCurrentScreen(bool includePointer) 0532 { 0533 auto cursorPosition = QCursor::pos(); 0534 const auto screenRects = getScreenRects(); 0535 for (auto &screenRect : screenRects) { 0536 // On X11, QScreen::geometry's position is not scaled, but the size is scaled and rounded. 0537 QRect qScreenRect(screenRect.topLeft(), screenRect.size() / qGuiApp->devicePixelRatio()); 0538 if (!qScreenRect.contains(cursorPosition)) { 0539 continue; 0540 } 0541 0542 // the screen origin is in native pixels, but the size is device-dependent. 0543 // convert these also to native pixels. 0544 auto image = getToplevelImage(screenRect, includePointer); 0545 image.setDevicePixelRatio(qGuiApp->devicePixelRatio()); 0546 Q_EMIT newScreenshotTaken(image); 0547 return; 0548 } 0549 0550 // no screen found with our cursor, fallback to capturing all screens 0551 grabAllScreens(includePointer); 0552 } 0553 0554 void ImagePlatformXcb::grabApplicationWindow(xcb_window_t window, bool includePointer, bool includeDecorations) 0555 { 0556 // if the user doesn't want decorations captured, we're in luck. This is 0557 // the easiest bit 0558 0559 auto image = getWindowImage(window, includePointer); 0560 image.setDevicePixelRatio(qGuiApp->devicePixelRatio()); 0561 if (!includeDecorations || window == QX11Info::appRootWindow()) { 0562 Q_EMIT newScreenshotTaken(image); 0563 return; 0564 } 0565 0566 // if the user wants the window decorations, things get a little tricky. 0567 // we can't simply get a handle to the window manager frame window and 0568 // just grab it, because some compositing window managers (yes, kwin 0569 // included) do not render the window onto the frame but keep it in a 0570 // separate opengl buffer, so grabbing this window is going to simply 0571 // give us a transparent image with the frame and titlebar. 0572 0573 // all is not lost. what we need to do is grab the image of the entire 0574 // desktop, find the geometry of the window including its frame, and 0575 // crop the root image accordingly. 0576 0577 KWindowInfo windowInfo(window, NET::WMFrameExtents); 0578 if (windowInfo.valid()) { 0579 auto frameGeom = windowInfo.frameGeometry(); 0580 image = getToplevelImage(frameGeom, includePointer); 0581 } 0582 0583 // fallback is window without the frame 0584 Q_EMIT newScreenshotTaken(image); 0585 } 0586 0587 void ImagePlatformXcb::grabActiveWindow(bool includePointer, bool includeDecorations, bool includeShadow) 0588 { 0589 auto activeWindow = KX11Extras::activeWindow(); 0590 updateWindowTitle(activeWindow); 0591 0592 // if KWin is available, use the KWin DBus interfaces 0593 if (includeDecorations && isKWinAvailable()) { 0594 auto bus = QDBusConnection::sessionBus(); 0595 bus.connect(u"org.kde.KWin"_s, 0596 u"/Screenshot"_s, 0597 u"org.kde.kwin.Screenshot"_s, 0598 u"screenshotCreated"_s, 0599 this, 0600 SLOT(handleKWinScreenshotReply(quint64))); 0601 QDBusInterface iface(u"org.kde.KWin"_s, u"/Screenshot"_s, u"org.kde.kwin.Screenshot"_s); 0602 0603 int opMask = 1; 0604 if (includePointer) { 0605 opMask |= 1 << 1; 0606 } 0607 if (includeShadow) { 0608 opMask |= 1 << 2; 0609 } 0610 iface.call(QStringLiteral("screenshotForWindow"), static_cast<quint64>(activeWindow), opMask); 0611 0612 return; 0613 } 0614 0615 // otherwise, use the native functionality 0616 grabApplicationWindow(activeWindow, includePointer, includeDecorations); 0617 } 0618 0619 void ImagePlatformXcb::grabWindowUnderCursor(bool includePointer, bool includeDecorations, bool includeShadow) 0620 { 0621 auto window = getWindowUnderCursor(); 0622 updateWindowTitle(window); 0623 0624 // if KWin is available, use the KWin DBus interfaces 0625 if (includeDecorations && isKWinAvailable()) { 0626 auto bus = QDBusConnection::sessionBus(); 0627 bus.connect(u"org.kde.KWin"_s, 0628 u"/Screenshot"_s, 0629 u"org.kde.kwin.Screenshot"_s, 0630 u"screenshotCreated"_s, 0631 this, 0632 SLOT(handleKWinScreenshotReply(quint64))); 0633 QDBusInterface interface(u"org.kde.KWin"_s, u"/Screenshot"_s, u"org.kde.kwin.Screenshot"_s); 0634 0635 int opMask = 1; 0636 if (includePointer) { 0637 opMask |= 1 << 1; 0638 } 0639 if (includeShadow) { 0640 opMask |= 1 << 2; 0641 } 0642 interface.call(u"screenshotWindowUnderCursor"_s, opMask); 0643 0644 return; 0645 } 0646 0647 // otherwise, use the native functionality 0648 grabApplicationWindow(window, includePointer, includeDecorations); 0649 } 0650 0651 void ImagePlatformXcb::grabTransientWithParent(bool includePointer, bool includeDecorations, bool includeShadow) 0652 { 0653 auto window = getWindowUnderCursor(); 0654 updateWindowTitle(window); 0655 0656 // grab the image early 0657 auto image = getToplevelImage(QRect(), false); 0658 image.setDevicePixelRatio(qGuiApp->devicePixelRatio()); 0659 0660 // now that we know we have a transient window, let's 0661 // find other possible transient windows and the app window itself. 0662 QRegion clipRegion; 0663 QSet<xcb_window_t> transientWindows; 0664 auto parentWindow = window; 0665 const QRect desktopRect(0, 0, 1, 1); 0666 do { 0667 // find parent window and add the window to the visible region 0668 auto winId = parentWindow; 0669 QRect winRect; 0670 parentWindow = getTransientWindowParent(winId, winRect, includeDecorations); 0671 transientWindows << winId; 0672 0673 // Don't include the 1x1 pixel sized desktop window in the top left corner that is present 0674 // if the window is a QDialog without a parent. 0675 // BUG: 376350 0676 if (winRect != desktopRect) { 0677 clipRegion += winRect; 0678 } 0679 0680 // Continue walking only if this is a transient window (having a parent) 0681 } while (parentWindow != XCB_WINDOW_NONE && !transientWindows.contains(parentWindow)); 0682 0683 // All parents are known now, find other transient children. 0684 // Assume that the lowest window is behind everything else, then if a new 0685 // transient window is discovered, its children can then also be found. 0686 auto winList = KX11Extras::stackingOrder(); 0687 for (auto winId : winList) { 0688 QRect winRect; 0689 auto parentWindow = getTransientWindowParent(winId, winRect, includeDecorations); 0690 0691 // if the parent should be displayed, then show the child too 0692 if (transientWindows.contains(parentWindow)) { 0693 if (!transientWindows.contains(winId)) { 0694 transientWindows << winId; 0695 clipRegion += winRect; 0696 } 0697 } 0698 } 0699 0700 // we can probably go ahead and generate the image now 0701 QImage clippedImage(image.size(), QImage::Format_ARGB32); 0702 clippedImage.fill(Qt::transparent); 0703 0704 QPainter painter(&clippedImage); 0705 painter.setClipRegion(clipRegion); 0706 painter.drawImage(0, 0, image); 0707 painter.end(); 0708 image = clippedImage.copy(clipRegion.boundingRect()); 0709 0710 // add a drop shadow if requested 0711 if (includeShadow) { 0712 image = addDropShadow(image); 0713 } 0714 0715 if (includePointer) { 0716 auto topLeft = clipRegion.boundingRect().topLeft() - QPoint(20, 20); 0717 image = blendCursorImage(image, QRect(topLeft, QSize(image.width(), image.height()))); 0718 } 0719 0720 Q_EMIT newScreenshotTaken(image); 0721 } 0722 0723 void ImagePlatformXcb::doGrabNow(GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) 0724 { 0725 if (grabMode & ~(ActiveWindow | WindowUnderCursor | TransientWithParent)) { 0726 // Notify that window title is empty since we are not picking a window. 0727 Q_EMIT windowTitleChanged(); 0728 } 0729 switch (grabMode) { 0730 case GrabMode::AllScreens: 0731 case GrabMode::AllScreensScaled: 0732 grabAllScreens(includePointer); 0733 break; 0734 case GrabMode::PerScreenImageNative: { 0735 grabAllScreens(includePointer, true); 0736 break; 0737 } 0738 case GrabMode::CurrentScreen: 0739 grabCurrentScreen(includePointer); 0740 break; 0741 case GrabMode::ActiveWindow: 0742 grabActiveWindow(includePointer, includeDecorations, includeShadow); 0743 break; 0744 case GrabMode::WindowUnderCursor: 0745 grabWindowUnderCursor(includePointer, includeDecorations, includeShadow); 0746 break; 0747 case GrabMode::TransientWithParent: 0748 grabTransientWithParent(includePointer, includeDecorations, includeShadow); 0749 break; 0750 case GrabMode::NoGrabModes: 0751 Q_EMIT newScreenshotFailed(); 0752 } 0753 } 0754 0755 void ImagePlatformXcb::doGrabOnClick(GrabMode grabMode, bool includePointer, bool includeDecorations, bool includeShadow) 0756 { 0757 // get the cursor image 0758 xcb_cursor_t xcbCursor = XCB_CURSOR_NONE; 0759 xcb_cursor_context_t *xcbCursorCtx = nullptr; 0760 xcb_screen_t *xcbAppScreen = xcb_aux_get_screen(QX11Info::connection(), QX11Info::appScreen()); 0761 0762 if (xcb_cursor_context_new(QX11Info::connection(), xcbAppScreen, &xcbCursorCtx) >= 0) { 0763 QList<QByteArray> cursorNames = {"cross"_ba, "crosshair"_ba, "diamond-cross"_ba, "cross-reverse"_ba}; 0764 0765 for (const auto &cursorName : cursorNames) { 0766 xcb_cursor_t cursor = xcb_cursor_load_cursor(xcbCursorCtx, cursorName.constData()); 0767 if (cursor != XCB_CURSOR_NONE) { 0768 xcbCursor = cursor; 0769 break; 0770 } 0771 } 0772 } 0773 0774 // grab the cursor 0775 xcb_grab_pointer_cookie_t grabPointerCookie = xcb_grab_pointer_unchecked(QX11Info::connection(), // xcb connection 0776 0, // deliver events to owner? nope 0777 QX11Info::appRootWindow(), // window to grab pointer for (root) 0778 XCB_EVENT_MASK_BUTTON_RELEASE, // which events do I want 0779 XCB_GRAB_MODE_SYNC, // pointer grab mode 0780 XCB_GRAB_MODE_ASYNC, // keyboard grab mode (why is this even here) 0781 XCB_NONE, // confine pointer to which window (none) 0782 xcbCursor, // cursor to change to for the duration of grab 0783 XCB_TIME_CURRENT_TIME // do this right now 0784 ); 0785 XcbReplyPtr<xcb_grab_pointer_reply_t> grabPointerReply(xcb_grab_pointer_reply(QX11Info::connection(), grabPointerCookie, nullptr)); 0786 0787 // if the grab failed, take the screenshot right away 0788 if (grabPointerReply->status != XCB_GRAB_STATUS_SUCCESS) { 0789 doGrabNow(grabMode, includePointer, includeDecorations, includeShadow); 0790 return; 0791 } 0792 0793 // fix things if our pointer grab causes a lockup and install our event filter 0794 m_nativeEventFilter->setCaptureOptions(grabMode, includePointer, includeDecorations, includeShadow); 0795 xcb_allow_events(QX11Info::connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); 0796 qApp->installNativeEventFilter(m_nativeEventFilter.get()); 0797 0798 // done. clean stuff up 0799 xcb_cursor_context_free(xcbCursorCtx); 0800 xcb_free_cursor(QX11Info::connection(), xcbCursor); 0801 } 0802 0803 #include "moc_ImagePlatformXcb.cpp"