diff --git a/src/Gui/KSWidget.cpp b/src/Gui/KSWidget.cpp index 1a3ee2d..fe9edb6 100644 --- a/src/Gui/KSWidget.cpp +++ b/src/Gui/KSWidget.cpp @@ -1,248 +1,252 @@ /* * 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 "KSWidget.h" #include "spectacle_gui_debug.h" #include "KSImageWidget.h" #include "SmartSpinBox.h" #include "SpectacleConfig.h" #include #include #include #include #include #include #include #include #include KSWidget::KSWidget(const QVector& supportedModes, QWidget *parent) : QWidget(parent) { // get a handle to the configuration manager SpectacleConfig *configManager = SpectacleConfig::instance(); // we'll init the widget that holds the image first mImageWidget = new KSImageWidget(this); mImageWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); connect(mImageWidget, &KSImageWidget::dragInitiated, this, &KSWidget::dragInitiated); // the capture mode options first mCaptureModeLabel = new QLabel(i18n("Capture Mode"), this); mCaptureArea = new QComboBox(this); + QString fullScreenLabel = QApplication::screens().length() == 1 + ? i18n("Full Screen") + : i18n("Full Screen (All Monitors)"); + if (supportedModes.contains(ImageGrabber::FullScreen)) - mCaptureArea->insertItem(1, i18n("Full Screen (All Monitors)"), ImageGrabber::FullScreen); + mCaptureArea->insertItem(1, fullScreenLabel, ImageGrabber::FullScreen); if (supportedModes.contains(ImageGrabber::CurrentScreen)) mCaptureArea->insertItem(2, i18n("Current Screen"), ImageGrabber::CurrentScreen); if (supportedModes.contains(ImageGrabber::ActiveWindow)) mCaptureArea->insertItem(3, i18n("Active Window"), ImageGrabber::ActiveWindow); if (supportedModes.contains(ImageGrabber::WindowUnderCursor)) mCaptureArea->insertItem(4, i18n("Window Under Cursor"), ImageGrabber::WindowUnderCursor); if (supportedModes.contains(ImageGrabber::RectangularRegion)) mCaptureArea->insertItem(5, i18n("Rectangular Region"), ImageGrabber::RectangularRegion); mCaptureArea->setMinimumWidth(240); connect(mCaptureArea, static_cast(&QComboBox::currentIndexChanged), this, &KSWidget::captureModeChanged); mDelayMsec = new SmartSpinBox(this); mDelayMsec->setDecimals(1); mDelayMsec->setSingleStep(1.0); mDelayMsec->setMinimum(0.0); mDelayMsec->setMaximum(999.9); mDelayMsec->setSpecialValueText(i18n("No Delay")); mDelayMsec->setMinimumWidth(160); connect(mDelayMsec, static_cast(&SmartSpinBox::valueChanged), configManager, &SpectacleConfig::setCaptureDelay); mCaptureOnClick = new QCheckBox(i18n("On Click"), this); mCaptureOnClick->setToolTip(i18n("Wait for a mouse click before capturing the screenshot image")); connect(mCaptureOnClick, &QCheckBox::stateChanged, this, &KSWidget::onClickStateChanged); connect(mCaptureOnClick, &QCheckBox::clicked, configManager, &SpectacleConfig::setOnClickChecked); mDelayLayout = new QHBoxLayout; mDelayLayout->addWidget(mDelayMsec); mDelayLayout->addWidget(mCaptureOnClick); mCaptureModeForm = new QFormLayout; mCaptureModeForm->addRow(i18n("Area:"), mCaptureArea); mCaptureModeForm->addRow(i18n("Delay:"), mDelayLayout); mCaptureModeForm->setContentsMargins(24, 0, 0, 0); // options (mouse pointer, window decorations, quit after saving or copying) mContentOptionsLabel = new QLabel(this); mContentOptionsLabel->setText(i18n("Options")); mMousePointer = new QCheckBox(i18n("Include mouse pointer"), this); mMousePointer->setToolTip(i18n("Show the mouse cursor in the screenshot image")); connect(mMousePointer, &QCheckBox::clicked, configManager, &SpectacleConfig::setIncludePointerChecked); mWindowDecorations = new QCheckBox(i18n("Include window titlebar and borders"), this); mWindowDecorations->setToolTip(i18n("Show the window title bar, the minimize/maximize/close buttons, and the window border")); mWindowDecorations->setEnabled(false); connect(mWindowDecorations, &QCheckBox::clicked, configManager, &SpectacleConfig::setIncludeDecorationsChecked); mCaptureTransientOnly = new QCheckBox(i18n("Capture the current pop-up only"), this); mCaptureTransientOnly->setToolTip(i18n("Capture only the current pop-up window (like a menu, tooltip etc).\n" "If disabled, the pop-up is captured along with the parent window")); mCaptureTransientOnly->setEnabled(false); connect(mCaptureTransientOnly, &QCheckBox::clicked, configManager, &SpectacleConfig::setCaptureTransientWindowOnlyChecked); mQuitAfterSaveOrCopy = new QCheckBox(i18n("Quit after Save or Copy"), this); mQuitAfterSaveOrCopy->setToolTip(i18n("Quit Spectacle after saving or copying the image")); connect(mQuitAfterSaveOrCopy, &QCheckBox::clicked, configManager, &SpectacleConfig::setQuitAfterSaveOrCopyChecked); mContentOptionsForm = new QVBoxLayout; mContentOptionsForm->addWidget(mMousePointer); mContentOptionsForm->addWidget(mWindowDecorations); mContentOptionsForm->addWidget(mCaptureTransientOnly); mContentOptionsForm->addWidget(mQuitAfterSaveOrCopy); mContentOptionsForm->setContentsMargins(24, 0, 0, 0); // the take a new screenshot button mTakeScreenshotButton = new QPushButton(i18n("Take a New Screenshot"), this); mTakeScreenshotButton->setIcon(QIcon::fromTheme(QStringLiteral("spectacle"))); mTakeScreenshotButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); mTakeScreenshotButton->setFocus(); connect(mTakeScreenshotButton, &QPushButton::clicked, this, &KSWidget::newScreenshotClicked); QShortcut *takeScreenshotShortcut = new QShortcut(QKeySequence(QKeySequence::New), mTakeScreenshotButton); connect(takeScreenshotShortcut, &QShortcut::activated, [this]() { mTakeScreenshotButton->animateClick(100); }); // finally, finish up the layouts mRightLayout = new QVBoxLayout; mRightLayout->addStretch(1); mRightLayout->addWidget(mCaptureModeLabel); mRightLayout->addLayout(mCaptureModeForm); mRightLayout->addStretch(1); mRightLayout->addWidget(mContentOptionsLabel); mRightLayout->addLayout(mContentOptionsForm); mRightLayout->addStretch(10); mRightLayout->addWidget(mTakeScreenshotButton, 1, Qt::AlignHCenter); mRightLayout->setContentsMargins(10, 0, 0, 10); mMainLayout = new QGridLayout(this); mMainLayout->addWidget(mImageWidget, 0, 0, 1, 1); mMainLayout->addLayout(mRightLayout, 0, 1, 1, 1); mMainLayout->setColumnMinimumWidth(0, 320); mMainLayout->setColumnMinimumWidth(1, 320); // and read in the saved checkbox states and capture mode indices mMousePointer->setChecked (configManager->includePointerChecked()); mWindowDecorations->setChecked (configManager->includeDecorationsChecked()); mCaptureOnClick->setChecked (configManager->onClickChecked()); mCaptureTransientOnly->setChecked (configManager->captureTransientWindowOnlyChecked()); mQuitAfterSaveOrCopy->setChecked (configManager->quitAfterSaveOrCopyChecked()); if (configManager->captureMode()>=0) mCaptureArea->setCurrentIndex (configManager->captureMode()); mDelayMsec->setValue (configManager->captureDelay()); // done } int KSWidget::imagePaddingWidth() const { int rightLayoutLeft = 0; int rightLayoutRight = 0; int mainLayoutRight = 0; mRightLayout->getContentsMargins(&rightLayoutLeft, nullptr, &rightLayoutRight, nullptr); mMainLayout->getContentsMargins(nullptr, nullptr, &mainLayoutRight, nullptr); int paddingWidth = (rightLayoutLeft + rightLayoutRight + mainLayoutRight); paddingWidth += mRightLayout->contentsRect().width(); paddingWidth += 2 * SpectacleImage::SHADOW_RADIUS; // image drop shadow return paddingWidth; } // public slots void KSWidget::setScreenshotPixmap(const QPixmap &pixmap) { mImageWidget->setScreenshot(pixmap); } void KSWidget::disableOnClick() { mCaptureOnClick->setEnabled(false); mDelayMsec->setEnabled(true); } // private slots void KSWidget::newScreenshotClicked() { int delay = mCaptureOnClick->isChecked() ? -1 : (mDelayMsec->value() * 1000); ImageGrabber::GrabMode mode = static_cast(mCaptureArea->currentData().toInt()); if (mode == ImageGrabber::WindowUnderCursor && !(mCaptureTransientOnly->isChecked())) { mode = ImageGrabber::TransientWithParent; } emit newScreenshotRequest(mode, delay, mMousePointer->isChecked(), mWindowDecorations->isChecked()); } void KSWidget::onClickStateChanged(int state) { if (state == Qt::Checked) { mDelayMsec->setEnabled(false); } else if (state == Qt::Unchecked) { mDelayMsec->setEnabled(true); } } void KSWidget::captureModeChanged(int index) { SpectacleConfig::instance()->setCaptureMode(index); ImageGrabber::GrabMode mode = static_cast(mCaptureArea->itemData(index).toInt()); switch (mode) { case ImageGrabber::WindowUnderCursor: mWindowDecorations->setEnabled(true); mCaptureTransientOnly->setEnabled(true); break; case ImageGrabber::ActiveWindow: mWindowDecorations->setEnabled(true); mCaptureTransientOnly->setEnabled(false); break; case ImageGrabber::FullScreen: case ImageGrabber::CurrentScreen: case ImageGrabber::RectangularRegion: mWindowDecorations->setEnabled(false); mCaptureTransientOnly->setEnabled(false); break; case ImageGrabber::TransientWithParent: case ImageGrabber::InvalidChoice: default: qCWarning(SPECTACLE_GUI_LOG) << "Skipping invalid or unreachable enum value"; break; } } diff --git a/src/PlatformBackends/KWinWaylandImageGrabber.cpp b/src/PlatformBackends/KWinWaylandImageGrabber.cpp index ecd0300..5851465 100644 --- a/src/PlatformBackends/KWinWaylandImageGrabber.cpp +++ b/src/PlatformBackends/KWinWaylandImageGrabber.cpp @@ -1,168 +1,177 @@ /* * Copyright (C) 2016 Martin Graesslin * * 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 "KWinWaylandImageGrabber.h" #include #include #include #include #include #include #include #include static int readData(int fd, QByteArray &data) { // implementation based on QtWayland file qwaylanddataoffer.cpp char buf[4096]; int retryCount = 0; int n; while (true) { n = QT_READ(fd, buf, sizeof buf); // give user 30 sec to click a window, afterwards considered as error if (n == -1 && (errno == EAGAIN) && ++retryCount < 30000) { usleep(1000); } else { break; } } if (n > 0) { data.append(buf, n); n = readData(fd, data); } return n; } static QImage readImage(int pipeFd) { QByteArray content; if (readData(pipeFd, content) != 0) { close(pipeFd); return QImage(); } close(pipeFd); QDataStream ds(content); QImage image; ds >> image; return image; } KWinWaylandImageGrabber::KWinWaylandImageGrabber(QObject *parent) : ImageGrabber(parent) { } KWinWaylandImageGrabber::~KWinWaylandImageGrabber() = default; bool KWinWaylandImageGrabber::onClickGrabSupported() const { return true; } void KWinWaylandImageGrabber::grabFullScreen() { grab(Mode::FullScreen, mCapturePointer); } void KWinWaylandImageGrabber::grabCurrentScreen() { grab(Mode::CurrentScreen, mCapturePointer); } void KWinWaylandImageGrabber::grabActiveWindow() { // unsupported emit pixmapChanged(QPixmap()); } void KWinWaylandImageGrabber::grabRectangularRegion() { // unsupported emit pixmapChanged(QPixmap()); } void KWinWaylandImageGrabber::grabWindowUnderCursor() { int mask = 0; if (mCaptureDecorations) { mask = 1; } if (mCapturePointer) { mask |= 1 << 1; } grab(Mode::Window, mask); } void KWinWaylandImageGrabber::grabTransientWithParent() { // unsupported, perform grab window under cursor grabWindowUnderCursor(); } QPixmap KWinWaylandImageGrabber::blendCursorImage(const QPixmap &pixmap, int x, int y, int width, int height) { Q_UNUSED(x) Q_UNUSED(y) Q_UNUSED(width) Q_UNUSED(height) return pixmap; } void KWinWaylandImageGrabber::startReadImage(int readPipe) { QFutureWatcher *watcher = new QFutureWatcher(this); QObject::connect(watcher, &QFutureWatcher::finished, this, [watcher, this] { watcher->deleteLater(); const QImage img = watcher->result(); emit pixmapChanged(QPixmap::fromImage(img)); } ); watcher->setFuture(QtConcurrent::run(readImage, readPipe)); } template void KWinWaylandImageGrabber::callDBus(Mode mode, int writeFd, T argument) { QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); static const QMap s_hash = { {Mode::Window, QStringLiteral("interactive")}, {Mode::CurrentScreen, QStringLiteral("screenshotScreen")}, {Mode::FullScreen, QStringLiteral("screenshotFullscreen")} }; auto it = s_hash.find(mode); Q_ASSERT(it != s_hash.end()); interface.asyncCall(it.value(), QVariant::fromValue(QDBusUnixFileDescriptor(writeFd)), argument); } template void KWinWaylandImageGrabber::grab(Mode mode, T argument) { int pipeFds[2]; if (pipe2(pipeFds, O_CLOEXEC|O_NONBLOCK) != 0) { emit imageGrabFailed(); return; } callDBus(mode, pipeFds[1], argument); startReadImage(pipeFds[0]); close(pipeFds[1]); } + +QVector KWinWaylandImageGrabber::supportedModes() const +{ + if (QApplication::screens().count() == 1) { + return {FullScreen, WindowUnderCursor, TransientWithParent}; + } + + return {FullScreen, CurrentScreen, WindowUnderCursor, TransientWithParent}; +} diff --git a/src/PlatformBackends/KWinWaylandImageGrabber.h b/src/PlatformBackends/KWinWaylandImageGrabber.h index 76e98af..e7fa442 100644 --- a/src/PlatformBackends/KWinWaylandImageGrabber.h +++ b/src/PlatformBackends/KWinWaylandImageGrabber.h @@ -1,60 +1,60 @@ /* * Copyright (C) 2016 Martin Graesslin * * 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. */ #ifndef KWINWAYLANDIMAGEGRABBER_H #define KWINWAYLANDIMAGEGRABBER_H #include "ImageGrabber.h" class KWinWaylandImageGrabber : public ImageGrabber { Q_OBJECT public: explicit KWinWaylandImageGrabber(QObject * parent = nullptr); ~KWinWaylandImageGrabber() override; - QVector supportedModes() const override { return {FullScreen, CurrentScreen, /*ActiveWindow, */WindowUnderCursor, TransientWithParent/*, RectangularRegion*/}; } + QVector supportedModes() const override; bool onClickGrabSupported() const override; protected: void grabFullScreen() override; void grabCurrentScreen() override; void grabActiveWindow() override; void grabRectangularRegion() override; void grabWindowUnderCursor() override; void grabTransientWithParent() override; QPixmap blendCursorImage(const QPixmap &pixmap, int x, int y, int width, int height) override; private: void startReadImage(int readPipe); enum class Mode { Window, CurrentScreen, FullScreen }; template void callDBus(Mode mode, int writeFd, T argument); template void grab(Mode mode, T argument); }; #endif diff --git a/src/PlatformBackends/X11ImageGrabber.cpp b/src/PlatformBackends/X11ImageGrabber.cpp index c55e733..4fbb89f 100644 --- a/src/PlatformBackends/X11ImageGrabber.cpp +++ b/src/PlatformBackends/X11ImageGrabber.cpp @@ -1,753 +1,762 @@ /* * Copyright (C) 2015 Boudhayan Gupta * * Contains code from kxutils.cpp, part of KWindowSystem. Copyright * notices reproduced below: * * Copyright (C) 2008 Lubos Lunak (l.lunak@kde.org) * Copyright (C) 2013 Martin Gräßlin * * 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 "X11ImageGrabber.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include X11ImageGrabber::X11ImageGrabber(QObject *parent) : ImageGrabber(parent) { mNativeEventFilter = new OnClickEventFilter(this); } X11ImageGrabber::~X11ImageGrabber() { delete mNativeEventFilter; } // for onClick grab OnClickEventFilter::OnClickEventFilter(X11ImageGrabber *grabber) : QAbstractNativeEventFilter(), mImageGrabber(grabber) {} bool OnClickEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, long *result) { Q_UNUSED(result); if (eventType == "xcb_generic_event_t") { xcb_generic_event_t *ev = static_cast(message); switch (ev->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 { xcb_button_release_event_t *ev2 = static_cast(message); if (ev2->detail == 1) { QMetaObject::invokeMethod(mImageGrabber, "doImageGrab", Qt::QueuedConnection); } else if (ev2->detail < 4) { emit mImageGrabber->imageGrabFailed(); } else { QMetaObject::invokeMethod(mImageGrabber, "doOnClickGrab", Qt::QueuedConnection); } } return true; default: return false; } } return false; } bool X11ImageGrabber::onClickGrabSupported() const { return true; } void X11ImageGrabber::doOnClickGrab() { // get the cursor image xcb_cursor_t xcbCursor = XCB_CURSOR_NONE; xcb_cursor_context_t *xcbCursorCtx; xcb_screen_t *xcbAppScreen = xcb_aux_get_screen(QX11Info::connection(), QX11Info::appScreen()); if (xcb_cursor_context_new(QX11Info::connection(), xcbAppScreen, &xcbCursorCtx) >= 0) { QVector cursorNames = { QByteArrayLiteral("cross"), QByteArrayLiteral("crosshair"), QByteArrayLiteral("diamond-cross"), QByteArrayLiteral("cross-reverse") }; Q_FOREACH (const QByteArray &cursorName, cursorNames) { xcb_cursor_t cursor = xcb_cursor_load_cursor(xcbCursorCtx, cursorName.constData()); if (cursor != XCB_CURSOR_NONE) { xcbCursor = cursor; 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) xcbCursor, // cursor to change to for the duration of grab XCB_TIME_CURRENT_TIME // do this right now ); CScopedPointer grabPointerReply(xcb_grab_pointer_reply(QX11Info::connection(), grabPointerCookie, nullptr)); // if the grab failed, take the screenshot right away if (grabPointerReply->status != XCB_GRAB_STATUS_SUCCESS) { doImageGrab(); return; } // fix things if our pointer grab causes a lockup xcb_allow_events(QX11Info::connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); // and install our event filter qApp->installNativeEventFilter(mNativeEventFilter); // done. clean stuff up xcb_cursor_context_free(xcbCursorCtx); xcb_free_cursor(QX11Info::connection(), xcbCursor); } // image conversion routine QPixmap X11ImageGrabber::convertFromNative(xcb_image_t *xcbImage) { QImage::Format format = QImage::Format_Invalid; switch (xcbImage->depth) { case 1: format = QImage::Format_MonoLSB; break; case 16: format = QImage::Format_RGB16; break; case 24: format = QImage::Format_RGB32; break; case 30: format = QImage::Format_BGR30; break; case 32: format = 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 (format == QImage::Format_RGB32) { quint32 *data = reinterpret_cast(xcbImage->data); for (int i = 0; i < xcbImage->width * xcbImage->height; i++) { data[i] |= 0xff000000; } } QImage image(xcbImage->data, xcbImage->width, xcbImage->height, format); if (image.isNull()) { return QPixmap(); } // work around an abort in QImage::color if (image.format() == QImage::Format_MonoLSB) { image.setColorCount(2); image.setColor(0, QColor(Qt::white).rgb()); image.setColor(1, QColor(Qt::black).rgb()); } // 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(image).copy(); } // utility functions // Note: x, y, width and height are measured in device pixels QPixmap X11ImageGrabber::blendCursorImage(const QPixmap &pixmap, int x, int y, int width, int height) { // If the cursor position lies outside the area, do not bother drawing a cursor. QPoint cursorPos = getNativeCursorPosition(); QRect screenRect(x, y, width, height); if (!screenRect.contains(cursorPos)) { return pixmap; } // now we can get the image and start processing xcb_connection_t *xcbConn = QX11Info::connection(); xcb_xfixes_get_cursor_image_cookie_t cursorCookie = xcb_xfixes_get_cursor_image_unchecked(xcbConn); CScopedPointer cursorReply(xcb_xfixes_get_cursor_image_reply(xcbConn, cursorCookie, nullptr)); if (cursorReply.isNull()) { return pixmap; } quint32 *pixelData = xcb_xfixes_get_cursor_image_cursor_image(cursorReply.data()); if (!pixelData) { return pixmap; } // process the image into a QImage QImage cursorImage = QImage((quint8 *)pixelData, cursorReply->width, cursorReply->height, QImage::Format_ARGB32_Premultiplied); // a small fix for the cursor position for fancier cursors cursorPos -= QPoint(cursorReply->xhot, cursorReply->yhot); // now we translate the cursor point to our screen rectangle cursorPos -= QPoint(x, y); // and do the painting QPixmap blendedPixmap = pixmap; QPainter painter(&blendedPixmap); painter.drawImage(cursorPos, cursorImage); // and done return blendedPixmap; } QPixmap X11ImageGrabber::getPixmapFromDrawable(xcb_drawable_t drawableId, const QRect &rect) { xcb_connection_t *xcbConn = QX11Info::connection(); // proceed to get an image based on the geometry (in device pixels) QScopedPointer xcbImage( xcb_image_get( xcbConn, drawableId, rect.x(), rect.y(), rect.width(), rect.height(), ~0, XCB_IMAGE_FORMAT_Z_PIXMAP ) ); // too bad, the capture failed. if (xcbImage.isNull()) { return QPixmap(); } // now process the image QPixmap nativePixmap = convertFromNative(xcbImage.data()); return nativePixmap; } // finalize the grabbed pixmap where we know the absolute position QPixmap X11ImageGrabber::postProcessPixmap(const QPixmap &pixmap, QRect rect, bool blendPointer) { if (!(blendPointer)) { // note: this may be the null pixmap if an error occurred. return pixmap; } return blendCursorImage(pixmap, rect.x(), rect.y(), rect.width(), rect.height()); } QPixmap X11ImageGrabber::getToplevelPixmap(QRect rect, bool blendPointer) { xcb_window_t rootWindow = QX11Info::appRootWindow(); // Treat a null rect as an alias for capturing fullscreen if (!rect.isValid()) { rect = getDrawableGeometry(rootWindow); } else { QRegion screenRegion; for (auto screen : QGuiApplication::screens()) { QRect screenRect = screen->geometry(); // Do not use setSize() here, because QSize::operator*=() // performs qRound() which can result in xcb_image_get() failing const qreal dpr = screen->devicePixelRatio(); screenRect.setHeight(qFloor(screenRect.height() * dpr)); screenRect.setWidth(qFloor(screenRect.width() * dpr)); screenRegion += screenRect; } rect = (screenRegion & rect).boundingRect(); } QPixmap nativePixmap = getPixmapFromDrawable(rootWindow, rect); return postProcessPixmap(nativePixmap, rect, blendPointer); } QPixmap X11ImageGrabber::getWindowPixmap(xcb_window_t window, bool blendPointer) { xcb_connection_t *xcbConn = QX11Info::connection(); // first get geometry information for our window xcb_get_geometry_cookie_t geomCookie = xcb_get_geometry_unchecked(xcbConn, window); CScopedPointer geomReply(xcb_get_geometry_reply(xcbConn, geomCookie, nullptr)); QRect rect(geomReply->x, geomReply->y, geomReply->width, geomReply->height); // then proceed to get an image QPixmap nativePixmap = getPixmapFromDrawable(window, rect); // Translate window coordinates to global ones. xcb_get_geometry_cookie_t geomRootCookie = xcb_get_geometry_unchecked(xcbConn, geomReply->root); CScopedPointer geomRootReply(xcb_get_geometry_reply(xcbConn, geomRootCookie, nullptr)); xcb_translate_coordinates_cookie_t translateCookie = xcb_translate_coordinates_unchecked( xcbConn, window, geomReply->root, geomRootReply->x, geomRootReply->y); CScopedPointer translateReply( xcb_translate_coordinates_reply(xcbConn, translateCookie, nullptr)); // Adjust local to global coordinates. rect.moveRight(rect.x() + translateReply->dst_x); rect.moveTop(rect.y() + translateReply->dst_y); // If the window capture failed, try to obtain one from the full screen. if (nativePixmap.isNull()) { return getToplevelPixmap(rect, blendPointer); } return postProcessPixmap(nativePixmap, rect, blendPointer); } bool X11ImageGrabber::isKWinAvailable() { if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KWin"))) { QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Effects"), QStringLiteral("org.kde.kwin.Effects")); QDBusReply reply = interface.call(QStringLiteral("isEffectLoaded"), QStringLiteral("screenshot")); return reply.value(); } return false; } void X11ImageGrabber::KWinDBusScreenshotHelper(quint64 pixmapId) { // obtain width and height and grab an image (x and y are always zero for pixmaps) QRect rect = getDrawableGeometry((xcb_drawable_t)pixmapId); mPixmap = getPixmapFromDrawable((xcb_drawable_t)pixmapId, rect); if (!mPixmap.isNull()) { emit pixmapChanged(mPixmap); return; } // Cannot retrieve pixmap from KWin, just fallback to fullscreen capture. We // could try to detect the original action (window under cursor or active // window), but that is too complex for this edge case. grabFullScreen(); } void X11ImageGrabber::rectangleSelectionCancelled() { QObject *sender = QObject::sender(); sender->disconnect(); sender->deleteLater(); emit imageGrabFailed(); } void X11ImageGrabber::rectangleSelectionConfirmed(const QPixmap &pixmap) { QObject *sender = QObject::sender(); sender->disconnect(); sender->deleteLater(); mPixmap = pixmap; emit pixmapChanged(mPixmap); } // grabber methods void X11ImageGrabber::updateWindowTitle(xcb_window_t window) { QString windowTitle = KWindowSystem::readNameProperty(window, XA_WM_NAME); emit windowTitleChanged(windowTitle); } void X11ImageGrabber::grabFullScreen() { mPixmap = getToplevelPixmap(QRect(), mCapturePointer); emit pixmapChanged(mPixmap); } void X11ImageGrabber::grabTransientWithParent() { xcb_window_t curWin = getRealWindowUnderCursor(); updateWindowTitle(curWin); // grab the image early mPixmap = 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 clipRegion; QSet transients; xcb_window_t parentWinId = curWin; const QRect desktopRect(0, 0, 1, 1); do { // find parent window and add the window to the visible region xcb_window_t winId = parentWinId; QRect winRect; parentWinId = getTransientWindowParent(winId, winRect); transients << winId; // 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 (winRect != desktopRect) { clipRegion += winRect; } // Continue walking only if this is a transient window (having a parent) } while (parentWinId != XCB_WINDOW_NONE && !transients.contains(parentWinId)); // 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. QList winList = KWindowSystem::stackingOrder(); for (auto winId : winList) { QRect winRect; xcb_window_t parentWinId = getTransientWindowParent(winId, winRect); // if the parent should be displayed, then show the child too if (transients.contains(parentWinId)) { if (!transients.contains(winId)) { transients << winId; clipRegion += winRect; } } } // we can probably go ahead and generate the image now QImage tempImage(mPixmap.size(), QImage::Format_ARGB32); tempImage.fill(Qt::transparent); QPainter tempPainter(&tempImage); tempPainter.setClipRegion(clipRegion); tempPainter.drawPixmap(0, 0, mPixmap); tempPainter.end(); mPixmap = QPixmap::fromImage(tempImage).copy(clipRegion.boundingRect()); // why stop here, when we can render a 20px drop shadow all around it QGraphicsDropShadowEffect *effect = new QGraphicsDropShadowEffect; effect->setOffset(0); effect->setBlurRadius(20); QGraphicsPixmapItem *item = new QGraphicsPixmapItem; item->setPixmap(mPixmap); item->setGraphicsEffect(effect); QImage shadowImage(mPixmap.size() + QSize(40, 40), QImage::Format_ARGB32); shadowImage.fill(Qt::transparent); QPainter shadowPainter(&shadowImage); QGraphicsScene scene; scene.addItem(item); scene.render(&shadowPainter, QRectF(), QRectF(-20, -20, mPixmap.width() + 40, mPixmap.height() + 40)); shadowPainter.end(); // we can finish up now mPixmap = QPixmap::fromImage(shadowImage); if (mCapturePointer) { QPoint topLeft = clipRegion.boundingRect().topLeft() - QPoint(20, 20); mPixmap = blendCursorImage(mPixmap, topLeft.x(), topLeft.y(), mPixmap.width(), mPixmap.height()); } emit pixmapChanged(mPixmap); } void X11ImageGrabber::grabActiveWindow() { xcb_window_t activeWindow = KWindowSystem::activeWindow(); updateWindowTitle(activeWindow); // if KWin is available, use the KWin DBus interfaces if (mCaptureDecorations && isKWinAvailable()) { QDBusConnection bus = QDBusConnection::sessionBus(); bus.connect(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"), QStringLiteral("screenshotCreated"), this, SLOT(KWinDBusScreenshotHelper(quint64))); QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); int mask = 1; if (mCapturePointer) { mask |= 1 << 1; } interface.call(QStringLiteral("screenshotForWindow"), (quint64)activeWindow, mask); return; } // otherwise, use the native functionality return grabApplicationWindowHelper(activeWindow); } void X11ImageGrabber::grabWindowUnderCursor() { const xcb_window_t windowUnderCursor = getRealWindowUnderCursor(); updateWindowTitle(windowUnderCursor); // if KWin is available, use the KWin DBus interfaces if (mCaptureDecorations && isKWinAvailable()) { QDBusConnection bus = QDBusConnection::sessionBus(); bus.connect(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot"), QStringLiteral("screenshotCreated"), this, SLOT(KWinDBusScreenshotHelper(quint64))); QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); int mask = 1; if (mCapturePointer) { mask |= 1 << 1; } interface.call(QStringLiteral("screenshotWindowUnderCursor"), mask); return; } // else, go native return grabApplicationWindowHelper(windowUnderCursor); } void X11ImageGrabber::grabApplicationWindowHelper(xcb_window_t window) { // if the user doesn't want decorations captured, we're in luck. This is // the easiest bit mPixmap = getWindowPixmap(window, mCapturePointer); if (!mCaptureDecorations || window == QX11Info::appRootWindow()) { emit pixmapChanged(mPixmap); 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 info(window, NET::WMFrameExtents); if (info.valid()) { QRect frameGeom = info.frameGeometry(); mPixmap = getToplevelPixmap(frameGeom, mCapturePointer); } // fallback is window without the frame emit pixmapChanged(mPixmap); } QRect X11ImageGrabber::getDrawableGeometry(xcb_drawable_t drawable) { xcb_connection_t *xcbConn = QX11Info::connection(); xcb_get_geometry_cookie_t geomCookie = xcb_get_geometry_unchecked(xcbConn, drawable); CScopedPointer geomReply(xcb_get_geometry_reply(xcbConn, geomCookie, nullptr)); if (geomReply.isNull()) { return QRect(); } return QRect(geomReply->x, geomReply->y, geomReply->width, geomReply->height); } void X11ImageGrabber::grabCurrentScreen() { QPoint cursorPosition = QCursor::pos(); for (auto screen : QGuiApplication::screens()) { QRect screenRect = screen->geometry(); if (!screenRect.contains(cursorPosition)) { continue; } // The screen origin is in native pixels, but the size is device-dependent. Convert these also to native pixels. QRect nativeScreenRect(screenRect.topLeft(), screenRect.size() * screen->devicePixelRatio()); mPixmap = getToplevelPixmap(nativeScreenRect, mCapturePointer); emit pixmapChanged(mPixmap); return; } // No screen found with our cursor, fallback to capturing full screen grabFullScreen(); } void X11ImageGrabber::grabRectangularRegion() { const auto pixmap = getToplevelPixmap(QRect(), mCapturePointer); if (!pixmap.isNull()) { QuickEditor *editor = new QuickEditor(pixmap); connect(editor, &QuickEditor::grabDone, this, &X11ImageGrabber::rectangleSelectionConfirmed); connect(editor, &QuickEditor::grabCancelled, this, &X11ImageGrabber::rectangleSelectionCancelled); } else { emit pixmapChanged(pixmap); } } xcb_window_t X11ImageGrabber::getRealWindowUnderCursor() { xcb_connection_t *xcbConn = QX11Info::connection(); xcb_window_t curWin = QX11Info::appRootWindow(); const QByteArray atomName("WM_STATE"); xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(xcbConn, 0, atomName.length(), atomName.constData()); xcb_query_pointer_cookie_t pointerCookie = xcb_query_pointer_unchecked(xcbConn, curWin); CScopedPointer atomReply(xcb_intern_atom_reply(xcbConn, atomCookie, nullptr)); CScopedPointer pointerReply(xcb_query_pointer_reply(xcbConn, pointerCookie, nullptr)); if (atomReply->atom == XCB_ATOM_NONE) { return QX11Info::appRootWindow(); } // now start testing QStack windowStack; windowStack.push(pointerReply->child); while (!windowStack.isEmpty()) { curWin = windowStack.pop(); // next, check if our window has the WM_STATE property set on // the window. if yes, return the window - we have found it xcb_get_property_cookie_t propertyCookie = xcb_get_property_unchecked(xcbConn, 0, curWin, atomReply->atom, XCB_ATOM_ANY, 0, 0); CScopedPointer propertyReply(xcb_get_property_reply(xcbConn, propertyCookie, nullptr)); if (propertyReply->type != XCB_ATOM_NONE) { return curWin; } // if we're here, this means the window is not the real window // we should start looking at its children xcb_query_tree_cookie_t treeCookie = xcb_query_tree_unchecked(xcbConn, curWin); CScopedPointer treeReply(xcb_query_tree_reply(xcbConn, treeCookie, nullptr)); xcb_window_t *winChildren = xcb_query_tree_children(treeReply.data()); int winChildrenLength = xcb_query_tree_children_length(treeReply.data()); for (int i = winChildrenLength - 1; i >= 0; i--) { windowStack.push(winChildren[i]); } } // return the window. it has geometry information for a crop return pointerReply->child; } // obtain the size of the given window, returning the window ID of the parent xcb_window_t X11ImageGrabber::getTransientWindowParent(xcb_window_t winId, QRect &outRect) { NET::Properties properties = mCaptureDecorations ? NET::WMFrameExtents : NET::WMGeometry; KWindowInfo winInfo(winId, properties, NET::WM2TransientFor); // add the current window to the image if (mCaptureDecorations) { outRect = winInfo.frameGeometry(); } else { outRect = winInfo.geometry(); } return winInfo.transientFor(); } QPoint X11ImageGrabber::getNativeCursorPosition() { // 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. xcb_connection_t *xcbConn = QX11Info::connection(); xcb_query_pointer_cookie_t pointerCookie = xcb_query_pointer_unchecked(xcbConn, QX11Info::appRootWindow()); CScopedPointer pointerReply(xcb_query_pointer_reply(xcbConn, pointerCookie, nullptr)); return QPoint(pointerReply->root_x, pointerReply->root_y); } + +QVector X11ImageGrabber::supportedModes() const +{ + if (QApplication::screens().count() == 1) { + return {FullScreen, ActiveWindow, WindowUnderCursor, TransientWithParent, RectangularRegion}; + } + + return {FullScreen, CurrentScreen, ActiveWindow, WindowUnderCursor, TransientWithParent, RectangularRegion}; +} diff --git a/src/PlatformBackends/X11ImageGrabber.h b/src/PlatformBackends/X11ImageGrabber.h index a9a4b71..78ac805 100644 --- a/src/PlatformBackends/X11ImageGrabber.h +++ b/src/PlatformBackends/X11ImageGrabber.h @@ -1,105 +1,105 @@ /* * 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. */ #ifndef X11IMAGEGRABBER_H #define X11IMAGEGRABBER_H #include #include #include #include "ImageGrabber.h" class X11ImageGrabber; class OnClickEventFilter : public QAbstractNativeEventFilter { public: explicit OnClickEventFilter(X11ImageGrabber *grabber); bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) override; private: X11ImageGrabber *mImageGrabber; }; class X11ImageGrabber : public ImageGrabber { Q_OBJECT public: explicit X11ImageGrabber(QObject * parent = nullptr); ~X11ImageGrabber() override; - QVector supportedModes() const override { return {FullScreen, CurrentScreen, ActiveWindow, WindowUnderCursor, TransientWithParent, RectangularRegion}; } + QVector supportedModes() const override; bool onClickGrabSupported() const override; protected: void grabFullScreen() override; void grabCurrentScreen() override; void grabActiveWindow() override; void grabRectangularRegion() override; void grabWindowUnderCursor() override; void grabTransientWithParent() override; QPixmap blendCursorImage(const QPixmap &pixmap, int x, int y, int width, int height) override; private Q_SLOTS: void KWinDBusScreenshotHelper(quint64 window); void rectangleSelectionConfirmed(const QPixmap &pixmap); void rectangleSelectionCancelled(); public Q_SLOTS: void doOnClickGrab() override; private: bool isKWinAvailable(); xcb_window_t getRealWindowUnderCursor(); void grabApplicationWindowHelper(xcb_window_t window); QRect getDrawableGeometry(xcb_drawable_t drawable); QPixmap postProcessPixmap(const QPixmap &pixmap, QRect rect, bool blendPointer); QPixmap getPixmapFromDrawable(xcb_drawable_t drawableId, const QRect &rect); QPixmap getToplevelPixmap(QRect rect, bool blendPointer); QPixmap getWindowPixmap(xcb_window_t window, bool blendPointer); QPixmap convertFromNative(xcb_image_t *xcbImage); xcb_window_t getTransientWindowParent(xcb_window_t winId, QRect &outRect); QPoint getNativeCursorPosition(); OnClickEventFilter *mNativeEventFilter; void updateWindowTitle(xcb_window_t window); }; template using CScopedPointer = QScopedPointer; struct ScopedPointerXcbImageDeleter { static inline void cleanup(xcb_image_t *xcbImage) { if (xcbImage) { xcb_image_destroy(xcbImage); } } }; #endif // X11IMAGEGRABBER_H