diff --git a/src/Platforms/Platform.h b/src/Platforms/Platform.h index c478f8f..71b24ae 100644 --- a/src/Platforms/Platform.h +++ b/src/Platforms/Platform.h @@ -1,72 +1,70 @@ /* This file is part of Spectacle, the KDE screenshot utility * Copyright (C) 2019 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include #include class Platform: public QObject { Q_OBJECT public: enum class GrabMode { InvalidChoice = 0x00, AllScreens = 0x01, CurrentScreen = 0x02, ActiveWindow = 0x04, WindowUnderCursor = 0x08, TransientWithParent = 0x10 }; using GrabModes = QFlags; Q_FLAG(GrabModes) enum class ShutterMode { - Immediate = 0x00, - OnClick = 0x01 + Immediate = 0x01, + OnClick = 0x02 }; using ShutterModes = QFlags; Q_FLAG(ShutterModes) explicit Platform(QObject *parent = nullptr); virtual ~Platform() = default; virtual QString platformName() const = 0; virtual GrabModes supportedGrabModes() const = 0; virtual ShutterModes supportedShutterModes() const = 0; public Q_SLOTS: virtual void doGrab(ShutterMode theShutterMode, GrabMode theGrabMode, bool theIncludePointer, bool theIncludeDecorations) = 0; Q_SIGNALS: void newScreenshotTaken(const QPixmap &thePixmap); void newScreenshotFailed(); void windowTitleChanged(const QString &theWindowTitle); }; -Q_DECLARE_METATYPE(Platform::GrabMode) -Q_DECLARE_METATYPE(Platform::ShutterMode) Q_DECLARE_OPERATORS_FOR_FLAGS(Platform::GrabModes) Q_DECLARE_OPERATORS_FOR_FLAGS(Platform::ShutterModes) diff --git a/src/Platforms/PlatformLoader.cpp b/src/Platforms/PlatformLoader.cpp index 648d0f9..7769c6a 100644 --- a/src/Platforms/PlatformLoader.cpp +++ b/src/Platforms/PlatformLoader.cpp @@ -1,55 +1,52 @@ /* This file is part of Spectacle, the KDE screenshot utility * Copyright (C) 2019 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "Config.h" #include "PlatformLoader.h" #include "PlatformNull.h" #include "PlatformKWinWayland.h" #ifdef XCB_FOUND #include "PlatformXcb.h" #endif #include PlatformPtr loadPlatform() { - qRegisterMetaType(); - qRegisterMetaType(); - // We might be using the XCB platform (with Xwayland) in a wayland session, // but the X11 grabber won't work in that case. So force the Wayland grabber // in Wayland sessions. if (KWindowSystem::isPlatformWayland() || qstrcmp(qgetenv("XDG_SESSION_TYPE"), "wayland") == 0) { return std::make_unique(); } // Try checking if we're running under X11 now #ifdef XCB_FOUND if (KWindowSystem::isPlatformX11()) { return std::make_unique(); } #endif // If nothing else worked, return the null platform return std::make_unique(); } diff --git a/src/Platforms/PlatformXcb.cpp b/src/Platforms/PlatformXcb.cpp index 05ad3b7..5103d13 100644 --- a/src/Platforms/PlatformXcb.cpp +++ b/src/Platforms/PlatformXcb.cpp @@ -1,734 +1,734 @@ /* This file is part of Spectacle, the KDE screenshot utility * Copyright (C) 2019 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "PlatformXcb.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* -- XCB Image Smart Pointer ------------------------------------------------------------------ */ struct XcbImagePtrDeleter { void operator()(xcb_image_t *theXcbImage) const { if (theXcbImage) { xcb_image_destroy(theXcbImage); } } }; using XcbImagePtr = std::unique_ptr; /* -- On Click Native Event Filter ------------------------------------------------------------- */ class PlatformXcb::OnClickEventFilter: public QAbstractNativeEventFilter { public: explicit OnClickEventFilter(PlatformXcb *thePlatformPtr) : mPlatformPtr(thePlatformPtr) {} void setCaptureOptions(const Platform::GrabMode &theGrabMode, bool theIncludePointer, bool theIncludeDecorations) { mGrabMode = theGrabMode; mIncludePointer = theIncludePointer; mIncludeDecorations = theIncludeDecorations; } bool nativeEventFilter(const QByteArray &theEventType, void *theMessage, long * /* theResult */) override { if (theEventType == "xcb_generic_event_t") { auto lFirstEvent = static_cast(theMessage); switch (lFirstEvent->response_type & ~0x80) { case XCB_BUTTON_RELEASE: { // uninstall the eventfilter and release the mouse qApp->removeNativeEventFilter(this); xcb_ungrab_pointer(QX11Info::connection(), XCB_TIME_CURRENT_TIME); // decide whether to grab or abort. regrab the mouse on mouse-wheel events { auto lSecondEvent = static_cast(theMessage); if (lSecondEvent->detail == 1) { QTimer::singleShot(0, [this]() { mPlatformPtr->doGrabNow(mGrabMode, mIncludePointer, mIncludeDecorations); }); } else if (lSecondEvent->detail < 4) { emit mPlatformPtr->newScreenshotFailed(); } else { QTimer::singleShot(0, [this]() { mPlatformPtr->doGrabOnClick(mGrabMode, mIncludePointer, mIncludeDecorations); }); } } return true; } default: return false; } } return false; } private: PlatformXcb *mPlatformPtr; Platform::GrabMode mGrabMode { GrabMode::AllScreens }; bool mIncludePointer { true }; bool mIncludeDecorations { true }; }; /* -- General Plumbing ------------------------------------------------------------------------- */ PlatformXcb::PlatformXcb(QObject *theParent) : Platform(theParent), mNativeEventFilter(new OnClickEventFilter(this)) {} PlatformXcb::~PlatformXcb() { delete mNativeEventFilter; } QString PlatformXcb::platformName() const { return QStringLiteral("Xcb"); } Platform::GrabModes PlatformXcb::supportedGrabModes() const { Platform::GrabModes lSupportedModes({ GrabMode::AllScreens, GrabMode::ActiveWindow, GrabMode::WindowUnderCursor, GrabMode::TransientWithParent }); if (QApplication::screens().count() > 1) { lSupportedModes |= Platform::GrabMode::CurrentScreen; } return lSupportedModes; } Platform::ShutterModes PlatformXcb::supportedShutterModes() const { return { ShutterMode::Immediate | ShutterMode::OnClick }; } void PlatformXcb::doGrab(ShutterMode theShutterMode, GrabMode theGrabMode, bool theIncludePointer, bool theIncludeDecorations) { switch(theShutterMode) { case ShutterMode::Immediate: return doGrabNow(theGrabMode, theIncludePointer, theIncludeDecorations); case ShutterMode::OnClick: return doGrabOnClick(theGrabMode, theIncludePointer, theIncludeDecorations); } } /* -- Platform Utilities ----------------------------------------------------------------------- */ bool PlatformXcb::isKWinAvailable() { if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KWin"))) { QDBusInterface lIface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Effects"), QStringLiteral("org.kde.kwin.Effects")); QDBusReply lReply = lIface.call(QStringLiteral("isEffectLoaded"), QStringLiteral("screenshot")); return lReply.value(); } return false; } /* -- XCB Utilities ---------------------------------------------------------------------------- */ QPoint PlatformXcb::getCursorPosition() { // QCursor::pos() is not used because it requires additional calculations. // Its value is the offset to the origin of the current screen is in // device-independent pixels while the origin itself uses native pixels. auto lXcbConn = QX11Info::connection(); auto lPointerCookie = xcb_query_pointer_unchecked(lXcbConn, QX11Info::appRootWindow()); std::unique_ptr lPointerReply(xcb_query_pointer_reply(lXcbConn, lPointerCookie, nullptr)); return QPoint(lPointerReply->root_x, lPointerReply->root_y); } QRect PlatformXcb::getDrawableGeometry(xcb_drawable_t theDrawable) { auto lXcbConn = QX11Info::connection(); auto lGeoCookie = xcb_get_geometry_unchecked(lXcbConn, theDrawable); std::unique_ptr lGeoReply(xcb_get_geometry_reply(lXcbConn, lGeoCookie, nullptr)); if (!lGeoReply) { return QRect(); } return QRect(lGeoReply->x, lGeoReply->y, lGeoReply->width, lGeoReply->height); } xcb_window_t PlatformXcb::getWindowUnderCursor() { auto lXcbConn = QX11Info::connection(); auto lAppWin = QX11Info::appRootWindow(); const QByteArray lAtomName("WM_STATE"); auto lAtomCookie = xcb_intern_atom_unchecked(lXcbConn, 0, lAtomName.length(), lAtomName.constData()); auto lPointerCookie = xcb_query_pointer_unchecked(lXcbConn, lAppWin); std::unique_ptr lAtomReply(xcb_intern_atom_reply(lXcbConn, lAtomCookie, nullptr)); std::unique_ptr lPointerReply(xcb_query_pointer_reply(lXcbConn, lPointerCookie, nullptr)); if (lAtomReply->atom == XCB_ATOM_NONE) { return QX11Info::appRootWindow(); } // now start testing QStack lWindowStack; lWindowStack.push(lPointerReply->child); while (!lWindowStack.isEmpty()) { lAppWin = lWindowStack.pop(); // next, check if our window has the WM_STATE property set on // the window. if yes, return the window - we have found it auto lPropCookie = xcb_get_property_unchecked(lXcbConn, 0, lAppWin, lAtomReply->atom, XCB_ATOM_ANY, 0, 0); std::unique_ptr lPropReply(xcb_get_property_reply(lXcbConn, lPropCookie, nullptr)); if (lPropReply->type != XCB_ATOM_NONE) { return lAppWin; } // if we're here, this means the window is not the real window // we should start looking at its children auto lTreeCookie = xcb_query_tree_unchecked(lXcbConn, lAppWin); std::unique_ptr lTreeReply(xcb_query_tree_reply(lXcbConn, lTreeCookie, nullptr)); auto lWindowChildren = xcb_query_tree_children(lTreeReply.get()); auto lWindowChildrenLength = xcb_query_tree_children_length(lTreeReply.get()); for (int iIdx = lWindowChildrenLength - 1; iIdx >= 0; iIdx--) { lWindowStack.push(lWindowChildren[iIdx]); } } // return the window. it has geometry information for a crop return lPointerReply->child; } xcb_window_t PlatformXcb::getTransientWindowParent(xcb_window_t theChildWindow, QRect &theWindowRectOut, bool theIncludeDectorations) { NET::Properties lNetProp = theIncludeDectorations ? NET::WMFrameExtents : NET::WMGeometry; KWindowInfo lWindowInfo(theChildWindow, lNetProp, NET::WM2TransientFor); // add the current window to the image if (theIncludeDectorations) { theWindowRectOut = lWindowInfo.frameGeometry(); } else { theWindowRectOut = lWindowInfo.geometry(); } return lWindowInfo.transientFor(); } /* -- Image Processing Utilities --------------------------------------------------------------- */ QPixmap PlatformXcb::convertFromNative(xcb_image_t *theXcbImage) { auto lImageFormat = QImage::Format_Invalid; switch (theXcbImage->depth) { case 1: lImageFormat = QImage::Format_MonoLSB; break; case 16: lImageFormat = QImage::Format_RGB16; break; case 24: lImageFormat = QImage::Format_RGB32; break; case 30: lImageFormat = QImage::Format_BGR30; break; case 32: lImageFormat = QImage::Format_ARGB32_Premultiplied; break; default: return QPixmap(); // we don't know } // the RGB32 format requires data format 0xffRRGGBB, ensure that this fourth byte really is 0xff if (lImageFormat == QImage::Format_RGB32) { auto lData = reinterpret_cast(theXcbImage->data); for (size_t iIter = 0; iIter < theXcbImage->width * theXcbImage->height; iIter++) { lData[iIter] |= 0xff000000; } } QImage lImage(theXcbImage->data, theXcbImage->width, theXcbImage->height, lImageFormat); if (lImage.isNull()) { return QPixmap(); } // work around an abort in QImage::color if (lImage.format() == QImage::Format_MonoLSB) { lImage.setColorCount(2); lImage.setColor(0, QColor(Qt::white).rgb()); lImage.setColor(1, QColor(Qt::black).rgb()); } // the image is ready. Since the backing data from xcbImage could be freed // before the QPixmap goes away, a deep copy is necessary. return QPixmap::fromImage(lImage).copy(); } QPixmap PlatformXcb::blendCursorImage(QPixmap &thePixmap, const QRect theRect) { // If the cursor position lies outside the area, do not bother drawing a cursor. auto lCursorPos = getCursorPosition(); if (!theRect.contains(lCursorPos)) { return thePixmap; } // now we can get the image and start processing auto lXcbConn = QX11Info::connection(); auto lCursorCookie = xcb_xfixes_get_cursor_image_unchecked(lXcbConn); std::unique_ptr lCursorReply(xcb_xfixes_get_cursor_image_reply(lXcbConn, lCursorCookie, nullptr)); if (!lCursorReply) { return thePixmap; } // get the image and process it into a qimage auto lPixelData = xcb_xfixes_get_cursor_image_cursor_image(lCursorReply.get()); if (!lPixelData) { return thePixmap; } QImage lCursorImage(reinterpret_cast(lPixelData), lCursorReply->width, lCursorReply->height, QImage::Format_ARGB32_Premultiplied); // a small fix for the cursor position for fancier cursors lCursorPos -= QPoint(lCursorReply->xhot, lCursorReply->yhot); // now we translate the cursor point to our screen rectangle and do the painting lCursorPos -= QPoint(theRect.x(), theRect.y()); QPainter lPainter(&thePixmap); lPainter.drawImage(lCursorPos, lCursorImage); return thePixmap; } QPixmap PlatformXcb::postProcessPixmap(QPixmap &thePixmap, QRect theRect, bool theBlendPointer) { if (!(theBlendPointer)) { // note: this may be the null pixmap if an error occurred. return thePixmap; } return blendCursorImage(thePixmap, theRect); } /* -- Capture Helpers -------------------------------------------------------------------------- */ QPixmap PlatformXcb::getPixmapFromDrawable(xcb_drawable_t theXcbDrawable, const QRect &theRect) { auto lXcbConn = QX11Info::connection(); // proceed to get an image based on the geometry (in device pixels) XcbImagePtr lXcbImage( xcb_image_get( lXcbConn, theXcbDrawable, theRect.x(), theRect.y(), theRect.width(), theRect.height(), ~0, XCB_IMAGE_FORMAT_Z_PIXMAP ) ); // too bad, the capture failed. - if (lXcbImage) { + if (!lXcbImage) { return QPixmap(); } // now process the image auto lPixmap = convertFromNative(lXcbImage.get()); return lPixmap; } QPixmap PlatformXcb::getToplevelPixmap(QRect theRect, bool theBlendPointer) { auto lRootWindow = QX11Info::appRootWindow(); // treat a null rect as an alias for capturing fullscreen if (!theRect.isValid()) { theRect = getDrawableGeometry(lRootWindow); } else { QRegion lScreenRegion; for (auto lScreen: QGuiApplication::screens()) { auto lScreenRect = lScreen->geometry(); // Do not use setSize() here, because QSize::operator*=() // performs qRound() which can result in xcb_image_get() failing const auto lPixelRatio = lScreen->devicePixelRatio(); lScreenRect.setHeight(qFloor(lScreenRect.height() * lPixelRatio)); lScreenRect.setWidth(qFloor(lScreenRect.width() * lPixelRatio)); lScreenRegion += lScreenRect; } theRect = (lScreenRegion & theRect).boundingRect(); } auto lPixmap = getPixmapFromDrawable(lRootWindow, theRect); return postProcessPixmap(lPixmap, theRect, theBlendPointer); } QPixmap PlatformXcb::getWindowPixmap(xcb_window_t theWindow, bool theBlendPointer) { auto lXcbConn = QX11Info::connection(); // first get geometry information for our window auto lGeoCookie = xcb_get_geometry_unchecked(lXcbConn, theWindow); std::unique_ptr lGeoReply(xcb_get_geometry_reply(lXcbConn, lGeoCookie, nullptr)); QRect lWindowRect(lGeoReply->x, lGeoReply->y, lGeoReply->width, lGeoReply->height); // then proceed to get an image auto lPixmap = getPixmapFromDrawable(theWindow, lWindowRect); // translate window coordinates to global ones. auto lRootGeoCookie = xcb_get_geometry_unchecked(lXcbConn, lGeoReply->root); std::unique_ptr lRootGeoReply(xcb_get_geometry_reply(lXcbConn, lRootGeoCookie, nullptr)); auto lTranslateCookie = xcb_translate_coordinates_unchecked(lXcbConn, theWindow, lGeoReply->root, lRootGeoReply->x, lRootGeoReply->y); std::unique_ptr lTranslateReply(xcb_translate_coordinates_reply(lXcbConn, lTranslateCookie, nullptr)); // adjust local to global coordinates. lWindowRect.moveRight(lWindowRect.x() + lTranslateReply->dst_x); lWindowRect.moveTop(lWindowRect.y() + lTranslateReply->dst_y); // if the window capture failed, try to obtain one from the full screen. if (lPixmap.isNull()) { return getToplevelPixmap(lWindowRect, theBlendPointer); } return postProcessPixmap(lPixmap, lWindowRect, theBlendPointer); } void PlatformXcb::handleKWinScreenshotReply(quint64 theDrawable) { // obtain width and height and grab an image (x and y are always zero for pixmaps) auto lDrawable = static_cast(theDrawable); auto lRect = getDrawableGeometry(lDrawable); auto lPixmap = getPixmapFromDrawable(lDrawable, lRect); if (!lPixmap.isNull()) { emit newScreenshotTaken(lPixmap); return; } emit newScreenshotFailed(); } /* -- Grabber Methods -------------------------------------------------------------------------- */ void PlatformXcb::grabAllScreens(bool theIncludePointer) { auto lPixmap = getToplevelPixmap(QRect(), theIncludePointer); emit newScreenshotTaken(lPixmap); } void PlatformXcb::grabCurrentScreen(bool theIncludePointer) { auto lCursorPosition = QCursor::pos(); for (auto lScreen: QGuiApplication::screens()) { auto lScreenRect = lScreen->geometry(); if (!lScreenRect.contains(lCursorPosition)) { continue; } // the screen origin is in native pixels, but the size is device-dependent. // convert these also to native pixels. QRect lNativeScreenRect(lScreenRect.topLeft(), lScreenRect.size() * lScreen->devicePixelRatio()); auto lPixmap = getToplevelPixmap(lNativeScreenRect, theIncludePointer); emit newScreenshotTaken(lPixmap); return; } // no screen found with our cursor, fallback to capturing all screens grabAllScreens(theIncludePointer); } void PlatformXcb::grabApplicationWindow(xcb_window_t theWindow, bool theIncludePointer, bool theIncludeDecorations) { // if the user doesn't want decorations captured, we're in luck. This is // the easiest bit auto lPixmap = getWindowPixmap(theWindow, theIncludePointer); if (!theIncludeDecorations || theWindow == QX11Info::appRootWindow()) { emit newScreenshotTaken(lPixmap); return; } // if the user wants the window decorations, things get a little tricky. // we can't simply get a handle to the window manager frame window and // just grab it, because some compositing window managers (yes, kwin // included) do not render the window onto the frame but keep it in a // separate opengl buffer, so grabbing this window is going to simply // give us a transparent image with the frame and titlebar. // all is not lost. what we need to do is grab the image of the entire // desktop, find the geometry of the window including its frame, and // crop the root image accordingly. KWindowInfo lWindowInfo(theWindow, NET::WMFrameExtents); if (lWindowInfo.valid()) { auto lFrameGeom = lWindowInfo.frameGeometry(); lPixmap = getToplevelPixmap(lFrameGeom, theIncludePointer); } // fallback is window without the frame emit newScreenshotTaken(lPixmap); } void PlatformXcb::grabActiveWindow(bool theIncludePointer, bool theIncludeDecorations) { auto lActiveWindow = KWindowSystem::activeWindow(); //FIXME: updateWindowTitle(activeWindow); // if KWin is available, use the KWin DBus interfaces if (theIncludeDecorations && isKWinAvailable()) { auto lBus = QDBusConnection::sessionBus(); lBus.connect(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"), QStringLiteral("screenshotCreated"), this, SLOT(handleKWinScreenshotReply(quint64))); QDBusInterface lIface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); int lOpMask = 1; if (theIncludePointer) { lOpMask |= 1 << 1; } lIface.call(QStringLiteral("screenshotForWindow"), static_cast(lActiveWindow), lOpMask); return; } // otherwise, use the native functionality grabApplicationWindow(lActiveWindow, theIncludePointer, theIncludeDecorations); } void PlatformXcb::grabWindowUnderCursor(bool theIncludePointer, bool theIncludeDecorations) { auto lWindow = getWindowUnderCursor(); //FIXME: updateWindowTitle(windowUnderCursor); // if KWin is available, use the KWin DBus interfaces if (theIncludeDecorations && isKWinAvailable()) { auto lBus = QDBusConnection::sessionBus(); lBus.connect(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"), QStringLiteral("screenshotCreated"), this, SLOT(handleKWinScreenshotReply(quint64))); QDBusInterface lInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); int lOpMask = 1; if (theIncludePointer) { lOpMask |= 1 << 1; } lInterface.call(QStringLiteral("screenshotWindowUnderCursor"), lOpMask); return; } // otherwise, use the native functionality grabApplicationWindow(lWindow, theIncludePointer, theIncludeDecorations); } void PlatformXcb::grabTransientWithParent(bool theIncludePointer, bool theIncludeDecorations) { auto lWindow = getWindowUnderCursor(); //FIXME: updateWindowTitle(curWin); // grab the image early auto lPixmap = getToplevelPixmap(QRect(), false); // now that we know we have a transient window, let's // find other possible transient windows and the app window itself. QRegion lClipRegion; QSet lTransientWindows; auto lParentWindow = lWindow; const QRect lDesktopRect(0, 0, 1, 1); do { // find parent window and add the window to the visible region auto lWinId = lParentWindow; QRect lWinRect; lParentWindow = getTransientWindowParent(lWinId, lWinRect, theIncludeDecorations); lTransientWindows << lWinId; // Don't include the 1x1 pixel sized desktop window in the top left corner that is present // if the window is a QDialog without a parent. // BUG: 376350 if (lWinRect != lDesktopRect) { lClipRegion += lWinRect; } // Continue walking only if this is a transient window (having a parent) } while (lParentWindow != XCB_WINDOW_NONE && !lTransientWindows.contains(lParentWindow)); // All parents are known now, find other transient children. // Assume that the lowest window is behind everything else, then if a new // transient window is discovered, its children can then also be found. auto lWinList = KWindowSystem::stackingOrder(); for (auto lWinId: lWinList) { QRect lWinRect; auto lParentWindow = getTransientWindowParent(lWinId, lWinRect, theIncludeDecorations); // if the parent should be displayed, then show the child too if (lTransientWindows.contains(lParentWindow)) { if (!lTransientWindows.contains(lWinId)) { lTransientWindows << lWinId; lClipRegion += lWinRect; } } } // we can probably go ahead and generate the image now QImage lImage(lPixmap.size(), QImage::Format_ARGB32); lImage.fill(Qt::transparent); QPainter lPainter(&lImage); lPainter.setClipRegion(lClipRegion); lPainter.drawPixmap(0, 0, lPixmap); lPainter.end(); lPixmap = QPixmap::fromImage(lImage).copy(lClipRegion.boundingRect()); // why stop here, when we can render a 20px drop shadow all around it auto lShadowEffect = new QGraphicsDropShadowEffect; lShadowEffect->setOffset(0); lShadowEffect->setBlurRadius(20); auto lPixmapItem = new QGraphicsPixmapItem; lPixmapItem->setPixmap(lPixmap); lPixmapItem->setGraphicsEffect(lShadowEffect); QImage lShadowImage(lPixmap.size() + QSize(40, 40), QImage::Format_ARGB32); lShadowImage.fill(Qt::transparent); QPainter lShadowPainter(&lShadowImage); QGraphicsScene lGraphicsScene; lGraphicsScene.addItem(lPixmapItem); lGraphicsScene.render(&lShadowPainter, QRectF(), QRectF(-20, -20, lPixmap.width() + 40, lPixmap.height() + 40)); lShadowPainter.end(); // we can finish up now lPixmap = QPixmap::fromImage(lShadowImage); if (theIncludePointer) { auto lTopLeft = lClipRegion.boundingRect().topLeft() - QPoint(20, 20); lPixmap = blendCursorImage(lPixmap, QRect(lTopLeft, QSize(lPixmap.width(), lPixmap.height()))); } emit newScreenshotTaken(lPixmap); } void PlatformXcb::doGrabNow(const GrabMode &theGrabMode, bool theIncludePointer, bool theIncludeDecorations) { switch(theGrabMode) { case GrabMode::AllScreens: grabAllScreens(theIncludePointer); break; case GrabMode::CurrentScreen: grabCurrentScreen(theIncludePointer); break; case GrabMode::ActiveWindow: grabActiveWindow(theIncludePointer, theIncludeDecorations); break; case GrabMode::WindowUnderCursor: grabWindowUnderCursor(theIncludePointer, theIncludeDecorations); break; case GrabMode::TransientWithParent: grabTransientWithParent(theIncludePointer, theIncludeDecorations); break; case GrabMode::InvalidChoice: emit newScreenshotFailed(); } } void PlatformXcb::doGrabOnClick(const GrabMode &theGrabMode, bool theIncludePointer, bool theIncludeDecorations) { // get the cursor image xcb_cursor_t lXcbCursor = XCB_CURSOR_NONE; xcb_cursor_context_t *lXcbCursorCtx = nullptr; xcb_screen_t *lXcbAppScreen = xcb_aux_get_screen(QX11Info::connection(), QX11Info::appScreen()); if (xcb_cursor_context_new(QX11Info::connection(), lXcbAppScreen, &lXcbCursorCtx) >= 0) { QVector lCursorNames = { QByteArrayLiteral("cross"), QByteArrayLiteral("crosshair"), QByteArrayLiteral("diamond-cross"), QByteArrayLiteral("cross-reverse") }; for(const auto &lCursorName: lCursorNames) { xcb_cursor_t lCursor = xcb_cursor_load_cursor(lXcbCursorCtx, lCursorName.constData()); if (lCursor != XCB_CURSOR_NONE) { lXcbCursor = lCursor; break; } } } // grab the cursor xcb_grab_pointer_cookie_t grabPointerCookie = xcb_grab_pointer_unchecked( QX11Info::connection(), // xcb connection 0, // deliver events to owner? nope QX11Info::appRootWindow(), // window to grab pointer for (root) XCB_EVENT_MASK_BUTTON_RELEASE, // which events do I want XCB_GRAB_MODE_SYNC, // pointer grab mode XCB_GRAB_MODE_ASYNC, // keyboard grab mode (why is this even here) XCB_NONE, // confine pointer to which window (none) lXcbCursor, // cursor to change to for the duration of grab XCB_TIME_CURRENT_TIME // do this right now ); std::unique_ptr lGrabPointerReply(xcb_grab_pointer_reply(QX11Info::connection(), grabPointerCookie, nullptr)); // if the grab failed, take the screenshot right away if (lGrabPointerReply->status != XCB_GRAB_STATUS_SUCCESS) { doGrabNow(theGrabMode, theIncludePointer, theIncludeDecorations); return; } // fix things if our pointer grab causes a lockup and install our event filter mNativeEventFilter->setCaptureOptions(theGrabMode, theIncludePointer, theIncludeDecorations); xcb_allow_events(QX11Info::connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); qApp->installNativeEventFilter(mNativeEventFilter); // done. clean stuff up xcb_cursor_context_free(lXcbCursorCtx); xcb_free_cursor(QX11Info::connection(), lXcbCursor); } diff --git a/src/SpectacleCore.cpp b/src/SpectacleCore.cpp index 8784a78..44b3c29 100644 --- a/src/SpectacleCore.cpp +++ b/src/SpectacleCore.cpp @@ -1,355 +1,356 @@ /* * Copyright (C) 2015 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SpectacleCore.h" #include "spectacle_core_debug.h" #include "Config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include SpectacleCore::SpectacleCore(StartMode theStartMode, Spectacle::CaptureMode theCaptureMode, QString &theSaveFileName, qint64 theDelayMsec, bool theNotifyOnGrab, bool theCopyToClipboard, QObject *parent) : QObject(parent), mStartMode(theStartMode), mNotify(theNotifyOnGrab), mPlatform(loadPlatform()), mMainWindow(nullptr), mIsGuiInited(false), mCopyToClipboard(theCopyToClipboard) { auto lConfig = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup lGuiConfig(lConfig, "GuiConfig"); if (!(theSaveFileName.isEmpty() || theSaveFileName.isNull())) { if (QDir::isRelativePath(theSaveFileName)) { theSaveFileName = QDir::current().absoluteFilePath(theSaveFileName); } setFilename(theSaveFileName); } // essential connections connect(this, &SpectacleCore::errorMessage, this, &SpectacleCore::showErrorMessage); connect(mPlatform.get(), &Platform::newScreenshotTaken, this, &SpectacleCore::screenshotUpdated); connect(mPlatform.get(), &Platform::newScreenshotFailed, this, &SpectacleCore::screenshotFailed); auto lImmediateAvailable = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate); auto lOnClickAvailable = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::OnClick); if ((!lOnClickAvailable) && (theDelayMsec < 0)) { theDelayMsec = 0; } // reset last region if it should not be remembered across restarts auto lSpectacleConfig = SpectacleConfig::instance(); if(!lSpectacleConfig->alwaysRememberRegion()) { lSpectacleConfig->setCropRegion(QRect()); } // set up the export manager auto lExportManager = ExportManager::instance(); lExportManager->setCaptureMode(theCaptureMode); connect(lExportManager, &ExportManager::errorMessage, this, &SpectacleCore::showErrorMessage); connect(lExportManager, &ExportManager::imageSaved, this, &SpectacleCore::doCopyPath); connect(lExportManager, &ExportManager::forceNotify, this, &SpectacleCore::doNotify); connect(mPlatform.get(), &Platform::windowTitleChanged, lExportManager, &ExportManager::setWindowTitle); switch (theStartMode) { case StartMode::DBus: break; case StartMode::Background: { auto lMsec = (KWindowSystem::compositingActive() ? 200 : 50) + theDelayMsec; auto lShutterMode = lImmediateAvailable ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; auto lIncludePointer = lGuiConfig.readEntry("includePointer", true); auto lIncludeDecorations = lGuiConfig.readEntry("includeDecorations", true); const Platform::GrabMode lCaptureMode = toPlatformGrabMode(theCaptureMode); QTimer::singleShot(lMsec, [ this, lCaptureMode, lShutterMode, lIncludePointer, lIncludeDecorations ]() { mPlatform->doGrab(lShutterMode, lCaptureMode, lIncludePointer, lIncludeDecorations); }); } break; case StartMode::Gui: initGui(lGuiConfig.readEntry("includePointer", true), lGuiConfig.readEntry("includeDecorations", true)); break; } } QString SpectacleCore::filename() const { return mFileNameString; } void SpectacleCore::setFilename(const QString &filename) { mFileNameString = filename; mFileNameUrl = QUrl::fromUserInput(filename); } // Slots void SpectacleCore::dbusStartAgent() { qApp->setQuitOnLastWindowClosed(true); auto lConfig = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup lGuiConfig(lConfig, "GuiConfig"); auto lIncludePointer = lGuiConfig.readEntry("includePointer", true); auto lIncludeDecorations = lGuiConfig.readEntry("includeDecorations", true); if (!(mStartMode == StartMode::Gui)) { mStartMode = StartMode::Gui; initGui(lIncludePointer, lIncludeDecorations); } else { using Actions = SpectacleConfig::PrintKeyActionRunning; switch (SpectacleConfig::instance()->printKeyActionRunning()) { case Actions::TakeNewScreenshot: { auto lShutterMode = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate) ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; auto lGrabMode = toPlatformGrabMode(ExportManager::instance()->captureMode()); QTimer::singleShot(KWindowSystem::compositingActive() ? 200 : 50, [this, lShutterMode, lGrabMode, lIncludePointer, lIncludeDecorations]() { mPlatform->doGrab(lShutterMode, lGrabMode, lIncludePointer, lIncludeDecorations); }); break; } case Actions::FocusWindow: KWindowSystem::forceActiveWindow(mMainWindow->winId()); break; case Actions::StartNewInstance: QProcess newInstance; newInstance.setProgram(QStringLiteral("spectacle")); newInstance.startDetached(); break; } } } void SpectacleCore::takeNewScreenshot(Spectacle::CaptureMode theCaptureMode, int theTimeout, bool theIncludePointer, bool theIncludeDecorations) { ExportManager::instance()->setCaptureMode(theCaptureMode); auto lGrabMode = toPlatformGrabMode(theCaptureMode); if (theTimeout < 0) { mPlatform->doGrab(Platform::ShutterMode::OnClick, lGrabMode, theIncludePointer, theIncludeDecorations); + return; } // when compositing is enabled, we need to give it enough time for the window // to disappear and all the effects are complete before we take the shot. there's // no way of knowing how long the disappearing effects take, but as per default // settings (and unless the user has set an extremely slow effect), 200 // milliseconds is a good amount of wait time. auto lMsec = KWindowSystem::compositingActive() ? 200 : 50; QTimer::singleShot(theTimeout + lMsec, [this, lGrabMode, theIncludePointer, theIncludeDecorations]() { mPlatform->doGrab(Platform::ShutterMode::Immediate, lGrabMode, theIncludePointer, theIncludeDecorations); }); } void SpectacleCore::showErrorMessage(const QString &theErrString) { qCDebug(SPECTACLE_CORE_LOG) << "ERROR: " << theErrString; if (mStartMode == StartMode::Gui) { KMessageBox::error(nullptr, theErrString); } } void SpectacleCore::screenshotUpdated(const QPixmap &thePixmap) { auto lExportManager = ExportManager::instance(); lExportManager->setPixmap(thePixmap); lExportManager->updatePixmapTimestamp(); switch (mStartMode) { case StartMode::Background: case StartMode::DBus: { if (mNotify) { connect(lExportManager, &ExportManager::imageSaved, this, &SpectacleCore::doNotify); } if (mCopyToClipboard) { lExportManager->doCopyToClipboard(mNotify); } else { QUrl lSavePath = (mStartMode == StartMode::Background && mFileNameUrl.isValid() && mFileNameUrl.isLocalFile()) ? mFileNameUrl : QUrl(); lExportManager->doSave(lSavePath); } // if we notify, we emit allDone only if the user either dismissed the notification or pressed // the "Open" button, otherwise the app closes before it can react to it. if (!mNotify) { emit allDone(); } } break; case StartMode::Gui: mMainWindow->setScreenshotAndShow(thePixmap); } } void SpectacleCore::screenshotFailed() { switch (mStartMode) { case StartMode::Background: showErrorMessage(i18n("Screenshot capture canceled or failed")); emit allDone(); return; case StartMode::DBus: emit grabFailed(); emit allDone(); return; case StartMode::Gui: mMainWindow->show(); } } void SpectacleCore::doNotify(const QUrl &theSavedAt) { KNotification *lNotify = new KNotification(QStringLiteral("newScreenshotSaved")); switch(ExportManager::instance()->captureMode()) { case Spectacle::CaptureMode::AllScreens: lNotify->setTitle(i18nc("The entire screen area was captured, heading", "Full Screen Captured")); break; case Spectacle::CaptureMode::CurrentScreen: lNotify->setTitle(i18nc("The current screen was captured, heading", "Current Screen Captured")); break; case Spectacle::CaptureMode::ActiveWindow: lNotify->setTitle(i18nc("The active window was captured, heading", "Active Window Captured")); break; case Spectacle::CaptureMode::WindowUnderCursor: case Spectacle::CaptureMode::TransientWithParent: lNotify->setTitle(i18nc("The window under the mouse was captured, heading", "Window Under Cursor Captured")); break; case Spectacle::CaptureMode::RectangularRegion: lNotify->setTitle(i18nc("A rectangular region was captured, heading", "Rectangular Region Captured")); break; case Spectacle::CaptureMode::InvalidChoice: break; } // a speaking message is prettier than a URL, special case for copy to clipboard and the default pictures location const QString &lSavePath = theSavedAt.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); if (mCopyToClipboard) { lNotify->setText(i18n("A screenshot was saved to your clipboard.")); } else if (lSavePath == QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) { lNotify->setText(i18nc("Placeholder is filename", "A screenshot was saved as '%1' to your Pictures folder.", theSavedAt.fileName())); } else { lNotify->setText(i18n("A screenshot was saved as '%1' to '%2'.", theSavedAt.fileName(), lSavePath)); } if (!mCopyToClipboard) { lNotify->setUrls({theSavedAt}); lNotify->setDefaultAction(i18nc("Open the screenshot we just saved", "Open")); connect(lNotify, QOverload::of(&KNotification::activated), this, [this, theSavedAt](uint index) { if (index == 0) { new KRun(theSavedAt, nullptr); QTimer::singleShot(250, this, &SpectacleCore::allDone); } }); } connect(lNotify, &QObject::destroyed, this, &SpectacleCore::allDone); lNotify->sendEvent(); } void SpectacleCore::doCopyPath(const QUrl &savedAt) { if (SpectacleConfig::instance()->copySaveLocationToClipboard()) { qApp->clipboard()->setText(savedAt.toLocalFile()); } } void SpectacleCore::doStartDragAndDrop() { auto lExportManager = ExportManager::instance(); QUrl lTempFile = lExportManager->tempSave(); if (!lTempFile.isValid()) { return; } auto lMimeData = new QMimeData; lMimeData->setUrls(QList { lTempFile }); lMimeData->setData(QStringLiteral("application/x-kde-suggestedfilename"), QFile::encodeName(lTempFile.fileName())); auto lDragHandler = new QDrag(this); lDragHandler->setMimeData(lMimeData); lDragHandler->setPixmap(lExportManager->pixmap().scaled(256, 256, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); lDragHandler->exec(Qt::CopyAction); } // Private Platform::GrabMode SpectacleCore::toPlatformGrabMode(Spectacle::CaptureMode theCaptureMode) { switch(theCaptureMode) { case Spectacle::CaptureMode::InvalidChoice: return Platform::GrabMode::InvalidChoice; case Spectacle::CaptureMode::AllScreens: case Spectacle::CaptureMode::RectangularRegion: return Platform::GrabMode::AllScreens; case Spectacle::CaptureMode::TransientWithParent: return Platform::GrabMode::TransientWithParent; case Spectacle::CaptureMode::CurrentScreen: return Platform::GrabMode::CurrentScreen; case Spectacle::CaptureMode::ActiveWindow: return Platform::GrabMode::ActiveWindow; case Spectacle::CaptureMode::WindowUnderCursor: return Platform::GrabMode::WindowUnderCursor; } return Platform::GrabMode::InvalidChoice; } void SpectacleCore::initGui(bool theIncludePointer, bool theIncludeDecorations) { if (!mIsGuiInited) { mMainWindow = std::make_unique(mPlatform->supportedGrabModes(), mPlatform->supportedShutterModes()); connect(mMainWindow.get(), &KSMainWindow::newScreenshotRequest, this, &SpectacleCore::takeNewScreenshot); connect(mMainWindow.get(), &KSMainWindow::dragAndDropRequest, this, &SpectacleCore::doStartDragAndDrop); mIsGuiInited = true; auto lShutterMode = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate) ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; auto lGrabMode = toPlatformGrabMode(ExportManager::instance()->captureMode()); QTimer::singleShot(0, [this, lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations]() { mPlatform->doGrab(lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations); }); } }