diff --git a/CMakeLists.txt b/CMakeLists.txt index 75e3ba6..2b75638 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,114 +1,115 @@ # Spectacle project project(Spectacle) # KDE Application Version, managed by release script set(KDE_APPLICATIONS_VERSION_MAJOR "17") set(KDE_APPLICATIONS_VERSION_MINOR "03") set(KDE_APPLICATIONS_VERSION_MICRO "70") set(KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") set(SPECTACLE_VERSION ${KDE_APPLICATIONS_VERSION}) # minimum requirements cmake_minimum_required (VERSION 2.8.12 FATAL_ERROR) set(QT_MIN_VERSION "5.4.0") set(KF5_MIN_VERSION "5.25.0") set(PLASMA_MIN_VERSION "5.4.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set( CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ) # set up kf5 include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMSetupVersion) include(FeatureSummary) find_package( Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core + Concurrent Widgets DBus PrintSupport Quick ) find_package( KF5 ${KF5_MIN_VERSION} REQUIRED CoreAddons WidgetsAddons DBusAddons Notifications Config I18n KIO XmlGui WindowSystem DocTools Declarative ) # optional components find_package(KF5Kipi) if (KF5Kipi_FOUND) set(KIPI_FOUND 1) endif () find_package(KDEExperimentalPurpose) if (KDEExperimentalPurpose_FOUND) set(PURPOSE_FOUND 1) endif() find_package(XCB COMPONENTS XFIXES IMAGE UTIL CURSOR) set(XCB_COMPONENTS_ERRORS FALSE) if (XCB_FOUND) find_package(Qt5X11Extras ${QT_MIN_VERSION} REQUIRED) endif() set(XCB_COMPONENTS_FOUND TRUE) if(NOT XCB_XFIXES_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-XFIXES ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_IMAGE_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-IMAGE ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_UTIL_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-UTIL ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_CURSOR_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-CURSOR ") set(XCB_COMPONENTS_FOUND FALSE) endif() # fail build if none of the platform backends can be found if (NOT XCB_FOUND OR NOT XCB_COMPONENTS_FOUND) message(FATAL_ERROR "No suitable backend platform was found. Currenty supported platforms are: XCB Components Required: ${XCB_COMPONENTS_ERRORS}") endif() add_definitions(-DQT_NO_URL_CAST_FROM_STRING) # hand off to subdirectories add_subdirectory(src) add_subdirectory(dbus) add_subdirectory(desktop) add_subdirectory(icons) add_subdirectory(doc) # summaries feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a03b95..ed9bb48 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,103 +1,105 @@ # common - configure file and version definitions configure_file(Config.h.in ${CMAKE_CURRENT_BINARY_DIR}/Config.h) set(CMAKE_AUTORCC 1) # target set( SPECTACLE_SRCS_DEFAULT Main.cpp ExportManager.cpp SpectacleCore.cpp SpectacleConfig.cpp SpectacleDBusAdapter.cpp PlatformBackends/ImageGrabber.cpp PlatformBackends/DummyImageGrabber.cpp + PlatformBackends/KWinWaylandImageGrabber.cpp Gui/KSMainWindow.cpp Gui/KSWidget.cpp Gui/KSImageWidget.cpp Gui/ExportMenu.cpp Gui/SmartSpinBox.cpp Gui/SettingsDialog/SettingsDialog.cpp Gui/SettingsDialog/SettingsPage.cpp Gui/SettingsDialog/SaveOptionsPage.cpp Gui/SettingsDialog/GeneralOptionsPage.cpp QuickEditor/QuickEditor.cpp QuickEditor/QmlResources.qrc ) if(XCB_FOUND) set( SPECTACLE_SRCS_X11 PlatformBackends/X11ImageGrabber.cpp ) endif() if(KF5Kipi_FOUND) set( SPECTACLE_SRCS_KIPI KipiInterface/KSGKipiInterface.cpp KipiInterface/KSGKipiInfoShared.cpp KipiInterface/KSGKipiImageCollectionShared.cpp KipiInterface/KSGKipiImageCollectionSelector.cpp ) endif() set( SPECTACLE_SRCS_ALL ${SPECTACLE_SRCS_DEFAULT} ${SPECTACLE_SRCS_KIPI} ${SPECTACLE_SRCS_X11} ) add_executable( spectacle ${SPECTACLE_SRCS_ALL} ) # link libraries target_link_libraries( spectacle + Qt5::Concurrent Qt5::DBus Qt5::PrintSupport Qt5::Quick KF5::CoreAddons KF5::DBusAddons KF5::WidgetsAddons KF5::Notifications KF5::ConfigCore KF5::I18n KF5::KIOWidgets KF5::WindowSystem KF5::XmlGui KF5::Declarative ) if(XCB_FOUND) target_link_libraries( spectacle XCB::XFIXES XCB::IMAGE XCB::CURSOR XCB::UTIL Qt5::X11Extras ) endif() if(KF5Kipi_FOUND) target_link_libraries ( spectacle KF5::Kipi ) endif() if(KDEExperimentalPurpose_FOUND) target_link_libraries ( spectacle KDEExperimental::PurposeWidgets ) endif() install(TARGETS spectacle ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/PlatformBackends/KWinWaylandImageGrabber.cpp b/src/PlatformBackends/KWinWaylandImageGrabber.cpp new file mode 100644 index 0000000..ca9bc52 --- /dev/null +++ b/src/PlatformBackends/KWinWaylandImageGrabber.cpp @@ -0,0 +1,149 @@ +/* + * 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 + +KWinWaylandImageGrabber::KWinWaylandImageGrabber(QObject *parent) : + ImageGrabber(parent) +{ +} + +KWinWaylandImageGrabber::~KWinWaylandImageGrabber() = default; + +bool KWinWaylandImageGrabber::onClickGrabSupported() const +{ + return true; +} + +void KWinWaylandImageGrabber::grabFullScreen() +{ + // unsupported + emit pixmapChanged(QPixmap()); +} + +void KWinWaylandImageGrabber::grabCurrentScreen() +{ + // unsupported + emit pixmapChanged(QPixmap()); +} + +void KWinWaylandImageGrabber::grabActiveWindow() +{ + // unsupported + emit pixmapChanged(QPixmap()); +} + +void KWinWaylandImageGrabber::grabRectangularRegion() +{ + // unsupported + emit pixmapChanged(QPixmap()); +} + +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; +} + +void KWinWaylandImageGrabber::grabWindowUnderCursor() +{ + QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); + + int mask = 0; + if (mCaptureDecorations) { + mask = 1; + } + if (mCapturePointer) { + mask |= 1 << 1; + } + + int pipeFds[2]; + if (pipe2(pipeFds, O_CLOEXEC|O_NONBLOCK) != 0) { + emit imageGrabFailed(); + return; + } + const int pipeFd = pipeFds[0]; + + auto call = interface.asyncCall(QStringLiteral("interactive"), QVariant::fromValue(QDBusUnixFileDescriptor(pipeFds[1])), mask); + + auto readImage = [pipeFd] () -> QImage { + QByteArray content; + if (readData(pipeFd, content) != 0) { + close(pipeFd); + return QImage(); + } + close(pipeFd); + QDataStream ds(content); + QImage image; + ds >> image; + return image; + }; + 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)); + + close(pipeFds[1]); +} + +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; +} diff --git a/src/PlatformBackends/KWinWaylandImageGrabber.h b/src/PlatformBackends/KWinWaylandImageGrabber.h new file mode 100644 index 0000000..937d766 --- /dev/null +++ b/src/PlatformBackends/KWinWaylandImageGrabber.h @@ -0,0 +1,46 @@ +/* + * 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 = 0); + virtual ~KWinWaylandImageGrabber(); + + bool onClickGrabSupported() const Q_DECL_OVERRIDE; + + protected: + + void grabFullScreen() Q_DECL_OVERRIDE; + void grabCurrentScreen() Q_DECL_OVERRIDE; + void grabActiveWindow() Q_DECL_OVERRIDE; + void grabRectangularRegion() Q_DECL_OVERRIDE; + void grabWindowUnderCursor() Q_DECL_OVERRIDE; + void grabTransientWithParent() Q_DECL_OVERRIDE; + QPixmap blendCursorImage(const QPixmap &pixmap, int x, int y, int width, int height) Q_DECL_OVERRIDE; +}; + +#endif diff --git a/src/SpectacleCore.cpp b/src/SpectacleCore.cpp index 225368c..82c84a2 100644 --- a/src/SpectacleCore.cpp +++ b/src/SpectacleCore.cpp @@ -1,299 +1,303 @@ /* * 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 "SpectacleConfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Config.h" #include "PlatformBackends/DummyImageGrabber.h" #ifdef XCB_FOUND #include "PlatformBackends/X11ImageGrabber.h" #endif +#include "PlatformBackends/KWinWaylandImageGrabber.h" SpectacleCore::SpectacleCore(StartMode startMode, ImageGrabber::GrabMode grabMode, QString &saveFileName, qint64 delayMsec, bool notifyOnGrab, QObject *parent) : QObject(parent), mExportManager(ExportManager::instance()), mStartMode(startMode), mNotify(notifyOnGrab), mImageGrabber(nullptr), mMainWindow(nullptr), isGuiInited(false) { KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup guiConfig(config, "GuiConfig"); if (!(saveFileName.isEmpty() || saveFileName.isNull())) { if (QDir::isRelativePath(saveFileName)) { saveFileName = QDir::current().absoluteFilePath(saveFileName); } setFilename(saveFileName); } #ifdef XCB_FOUND if (KWindowSystem::isPlatformX11()) { mImageGrabber = new X11ImageGrabber; } #endif + if (!mImageGrabber && KWindowSystem::isPlatformWayland()) { + mImageGrabber = new KWinWaylandImageGrabber; + } if (!mImageGrabber) { mImageGrabber = new DummyImageGrabber; } mImageGrabber->setGrabMode(grabMode); mImageGrabber->setCapturePointer(guiConfig.readEntry("includePointer", true)); mImageGrabber->setCaptureDecorations(guiConfig.readEntry("includeDecorations", true)); if ((!(mImageGrabber->onClickGrabSupported())) && (delayMsec < 0)) { delayMsec = 0; } connect(mExportManager, &ExportManager::errorMessage, this, &SpectacleCore::showErrorMessage); connect(this, &SpectacleCore::errorMessage, this, &SpectacleCore::showErrorMessage); connect(mImageGrabber, &ImageGrabber::pixmapChanged, this, &SpectacleCore::screenshotUpdated); connect(mImageGrabber, &ImageGrabber::imageGrabFailed, this, &SpectacleCore::screenshotFailed); connect(mExportManager, &ExportManager::imageSaved, this, &SpectacleCore::doCopyPath); connect(mExportManager, &ExportManager::forceNotify, this, &SpectacleCore::doNotify); switch (startMode) { case DBusMode: break; case BackgroundMode: { int msec = (KWindowSystem::compositingActive() ? 200 : 50) + delayMsec; QTimer::singleShot(msec, mImageGrabber, &ImageGrabber::doImageGrab); } break; case GuiMode: initGui(); break; } } SpectacleCore::~SpectacleCore() { if (mMainWindow) { delete mMainWindow; } } // Q_PROPERTY stuff QString SpectacleCore::filename() const { return mFileNameString; } void SpectacleCore::setFilename(const QString &filename) { mFileNameString = filename; mFileNameUrl = QUrl::fromUserInput(filename); } ImageGrabber::GrabMode SpectacleCore::grabMode() const { return mImageGrabber->grabMode(); } void SpectacleCore::setGrabMode(const ImageGrabber::GrabMode &grabMode) { mImageGrabber->setGrabMode(grabMode); } // Slots void SpectacleCore::dbusStartAgent() { qApp->setQuitOnLastWindowClosed(true); if (!(mStartMode == GuiMode)) { mStartMode = GuiMode; return initGui(); } } void SpectacleCore::takeNewScreenshot(const ImageGrabber::GrabMode &mode, const int &timeout, const bool &includePointer, const bool &includeDecorations) { mImageGrabber->setGrabMode(mode); mImageGrabber->setCapturePointer(includePointer); mImageGrabber->setCaptureDecorations(includeDecorations); if (timeout < 0) { mImageGrabber->doOnClickGrab(); 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. const int msec = KWindowSystem::compositingActive() ? 200 : 50; QTimer::singleShot(timeout + msec, mImageGrabber, &ImageGrabber::doImageGrab); } void SpectacleCore::showErrorMessage(const QString &errString) { qDebug() << "ERROR: " << errString; if (mStartMode == GuiMode) { KMessageBox::error(0, errString); } } void SpectacleCore::screenshotUpdated(const QPixmap &pixmap) { mExportManager->setPixmap(pixmap); switch (mStartMode) { case BackgroundMode: case DBusMode: { if (mNotify) { connect(mExportManager, &ExportManager::imageSaved, this, &SpectacleCore::doNotify); } QUrl savePath = (mStartMode == BackgroundMode && mFileNameUrl.isValid() && mFileNameUrl.isLocalFile()) ? mFileNameUrl : QUrl(); mExportManager->doSave(savePath); // 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 GuiMode: mMainWindow->setScreenshotAndShow(pixmap); } } void SpectacleCore::screenshotFailed() { switch (mStartMode) { case BackgroundMode: showErrorMessage(i18n("Screenshot capture canceled or failed")); case DBusMode: emit grabFailed(); emit allDone(); return; case GuiMode: mMainWindow->show(); } } void SpectacleCore::doNotify(const QUrl &savedAt) { KNotification *notify = new KNotification(QStringLiteral("newScreenshotSaved")); switch(mImageGrabber->grabMode()) { case ImageGrabber::GrabMode::FullScreen: notify->setTitle(i18nc("The entire screen area was captured, heading", "Full Screen Captured")); break; case ImageGrabber::GrabMode::CurrentScreen: notify->setTitle(i18nc("The current screen was captured, heading", "Current Screen Captured")); break; case ImageGrabber::GrabMode::ActiveWindow: notify->setTitle(i18nc("The active window was captured, heading", "Active Window Captured")); break; case ImageGrabber::GrabMode::WindowUnderCursor: notify->setTitle(i18nc("The window under the mouse was captured, heading", "Window Under Cursor Captured")); break; case ImageGrabber::GrabMode::RectangularRegion: notify->setTitle(i18nc("A rectangular region was captured, heading", "Rectangular Region Captured")); break; default: break; } const QString &path = savedAt.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); // a speaking message is prettier than a URL, special case for the default pictures location if (path == QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) { notify->setText(i18nc("Placeholder is filename", "A screenshot was saved as '%1' to your Pictures folder.", savedAt.fileName())); } else { notify->setText(i18n("A screenshot was saved as '%1' to '%2'.", savedAt.fileName(), path)); } notify->setActions({i18nc("Open the screenshot we just saved", "Open")}); connect(notify, &KNotification::action1Activated, this, [this, savedAt] { new KRun(savedAt, nullptr); QTimer::singleShot(250, this, &SpectacleCore::allDone); }); connect(notify, &QObject::destroyed, this, &SpectacleCore::allDone); notify->sendEvent(); } void SpectacleCore::doCopyPath(const QUrl &savedAt) { if (SpectacleConfig::instance()->copySaveLocationToClipboard()) { qApp->clipboard()->setText(savedAt.toLocalFile()); } } void SpectacleCore::doStartDragAndDrop() { QUrl tempFile = mExportManager->tempSave(); if (!tempFile.isValid()) { return; } QMimeData *mimeData = new QMimeData; mimeData->setUrls(QList { tempFile }); mimeData->setImageData(mExportManager->pixmap()); mimeData->setData(QStringLiteral("application/x-kde-suggestedfilename"), QFile::encodeName(tempFile.fileName())); QDrag *dragHandler = new QDrag(this); dragHandler->setMimeData(mimeData); dragHandler->setPixmap(mExportManager->pixmap().scaled(256, 256, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); dragHandler->exec(); } // Private void SpectacleCore::initGui() { if (!isGuiInited) { mMainWindow = new KSMainWindow(mImageGrabber->onClickGrabSupported()); connect(mMainWindow, &KSMainWindow::newScreenshotRequest, this, &SpectacleCore::takeNewScreenshot); connect(mMainWindow, &KSMainWindow::dragAndDropRequest, this, &SpectacleCore::doStartDragAndDrop); isGuiInited = true; QMetaObject::invokeMethod(mImageGrabber, "doImageGrab", Qt::QueuedConnection); } }