diff --git a/CMakeLists.txt b/CMakeLists.txt index b7d053a..04d391d 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 "16") set(KDE_APPLICATIONS_VERSION_MINOR "12") set(KDE_APPLICATIONS_VERSION_MICRO "0") 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.18.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/Gui/KSMainWindow.cpp b/src/Gui/KSMainWindow.cpp index 4809c79..5a4dd77 100644 --- a/src/Gui/KSMainWindow.cpp +++ b/src/Gui/KSMainWindow.cpp @@ -1,333 +1,334 @@ /* * 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 "KSMainWindow.h" #include "Config.h" #include #include #include #include #include #include #include #ifdef XCB_FOUND #include #include #endif #include #include #include #include #include #include +#include #include "SettingsDialog/SettingsDialog.h" #include "ExportMenu.h" #include "ExportManager.h" #include "SpectacleConfig.h" KSMainWindow::KSMainWindow(bool onClickAvailable, QWidget *parent) : QDialog(parent), mKSWidget(new KSWidget), mDivider(new QFrame), mDialogButtonBox(new QDialogButtonBox), mSendToButton(new QPushButton), mClipboardButton(new QToolButton), mSaveButton(new QToolButton), mSaveMenu(new QMenu), mMessageWidget(new KMessageWidget), mExportMenu(new ExportMenu(this)), mOnClickAvailable(onClickAvailable) { // before we do anything, we need to set a window property // that skips the close/hide window animation on kwin. this // fixes a ghost image of the spectacle window that appears // on subsequent screenshots taken with the take new screenshot // button // // credits for this goes to Thomas Lübking #ifdef XCB_FOUND - if (qApp->platformName() == QStringLiteral("xcb")) { + if (KWindowSystem::isPlatformX11()) { // create a window if we haven't already. note that the QWidget constructor // should already have done this if (winId() == 0) { create(0, true, true); } // do the xcb shenanigans xcb_connection_t *xcbConn = QX11Info::connection(); const QByteArray effectName = QByteArrayLiteral("_KDE_NET_WM_SKIP_CLOSE_ANIMATION"); xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(xcbConn, false, effectName.length(), effectName.constData()); QScopedPointer atom(xcb_intern_atom_reply(xcbConn, atomCookie, nullptr)); if (atom.isNull()) { goto done; } uint32_t value = 1; xcb_change_property(xcbConn, XCB_PROP_MODE_REPLACE, winId(), atom->atom, XCB_ATOM_CARDINAL, 32, 1, &value); } #endif done: QMetaObject::invokeMethod(this, "init", Qt::QueuedConnection); } KSMainWindow::~KSMainWindow() {} // GUI init void KSMainWindow::init() { KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup guiConfig(config, "GuiConfig"); // window properties setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); QPoint location = guiConfig.readEntry("window-position", QPoint(50, 50)); move(location); // change window title on save connect(ExportManager::instance(), &ExportManager::imageSaved, this, &KSMainWindow::setScreenshotWindowTitle); // the KSGWidget connect(mKSWidget, &KSWidget::newScreenshotRequest, this, &KSMainWindow::captureScreenshot); connect(mKSWidget, &KSWidget::dragInitiated, this, &KSMainWindow::dragAndDropRequest); // the Button Bar mDialogButtonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::Discard); KGuiItem::assign(mSendToButton, KGuiItem(i18n("Export Image..."))); mSendToButton->setIcon(QIcon::fromTheme(QStringLiteral("application-x-executable"))); mDialogButtonBox->addButton(mSendToButton, QDialogButtonBox::ActionRole); mClipboardButton->setDefaultAction(KStandardAction::copy(this, SLOT(sendToClipboard()), this)); mClipboardButton->setText(i18n("Copy To Clipboard")); mClipboardButton->setToolTip(i18n("Copy the current screenshot image to the clipboard.")); mClipboardButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); mClipboardButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mDialogButtonBox->addButton(mClipboardButton, QDialogButtonBox::ActionRole); mSaveButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mSaveButton->setMenu(mSaveMenu); mSaveButton->setPopupMode(QToolButton::MenuButtonPopup); buildSaveMenu(); mDialogButtonBox->addButton(mSaveButton, QDialogButtonBox::ActionRole); QShortcut *shortcut = new QShortcut(QKeySequence(Qt::Key_Escape), mDialogButtonBox->button(QDialogButtonBox::Discard)); connect(shortcut, &QShortcut::activated, qApp, &QApplication::quit); connect(mDialogButtonBox->button(QDialogButtonBox::Discard), &QPushButton::clicked, qApp, &QApplication::quit); // the help menu KHelpMenu *helpMenu = new KHelpMenu(this, KAboutData::applicationData(), true); mDialogButtonBox->button(QDialogButtonBox::Help)->setMenu(helpMenu->menu()); // message widget connect(mMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &str) { QDesktopServices::openUrl(QUrl(str)); } ); // layouts mDivider->setFrameShape(QFrame::HLine); mDivider->setLineWidth(2); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(mKSWidget); layout->addWidget(mMessageWidget); layout->addWidget(mDivider); layout->addWidget(mDialogButtonBox); mMessageWidget->hide(); // populate our send-to actions mSendToButton->setMenu(mExportMenu); connect(mExportMenu, &ExportMenu::imageShared, this, &KSMainWindow::showImageSharedFeedback); // disable onClick mode if not available on the platform if (!mOnClickAvailable) { mKSWidget->disableOnClick(); } resize(QSize(840, 420).expandedTo(minimumSize())); // done with the init } void KSMainWindow::buildSaveMenu() { // first clear the menu mSaveMenu->clear(); // get our actions in order QAction *actionSave = KStandardAction::save(this, SLOT(save()), this); QAction *actionSaveAs = KStandardAction::saveAs(this, SLOT(saveAs()), this); QAction *actionSaveExit = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save &&& Exit"), this); actionSaveExit->setToolTip(i18n("Save screenshot in your Pictures directory and exit")); actionSaveExit->setShortcut(QKeySequence(QKeySequence::Quit)); connect(actionSaveExit, &QAction::triggered, this, &KSMainWindow::saveAndExit); // static or dynamic SpectacleConfig *cfgManager = SpectacleConfig::instance(); int switchState = cfgManager->useDynamicSaveButton() ? cfgManager->lastUsedSaveMode() : 0; // put the actions in order switch (switchState) { case 1: mSaveButton->setDefaultAction(actionSave); mSaveMenu->addAction(actionSaveExit); mSaveMenu->addAction(actionSaveAs); break; case 2: mSaveButton->setDefaultAction(actionSaveAs); mSaveMenu->addAction(actionSaveExit); mSaveMenu->addAction(actionSave); break; case 0: default: mSaveButton->setDefaultAction(actionSaveExit); mSaveMenu->addAction(actionSave); mSaveMenu->addAction(actionSaveAs); break; } // finish off building the menu mSaveMenu->addAction(KStandardAction::print(this, SLOT(showPrintDialog()), this)); mSaveMenu->addSeparator(); mSaveMenu->addAction(QIcon::fromTheme(QStringLiteral("applications-system")), i18n("Preferences"), this, SLOT(showPreferencesDialog())); } // overrides void KSMainWindow::moveEvent(QMoveEvent *event) { Q_UNUSED(event); KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup guiConfig(config, "GuiConfig"); guiConfig.writeEntry("window-position", pos()); guiConfig.sync(); } // slots void KSMainWindow::captureScreenshot(ImageGrabber::GrabMode mode, int timeout, bool includePointer, bool includeDecorations) { hide(); emit newScreenshotRequest(mode, timeout, includePointer, includeDecorations); } void KSMainWindow::setScreenshotAndShow(const QPixmap &pixmap) { mKSWidget->setScreenshotPixmap(pixmap); mExportMenu->imageUpdated(); setWindowTitle(i18nc("Unsaved Screenshot", "Unsaved[*]")); setWindowModified(true); KGuiItem::assign(mDialogButtonBox->button(QDialogButtonBox::Discard), KStandardGuiItem::discard()); show(); } void KSMainWindow::showPrintDialog() { QPrinter *printer = new QPrinter(QPrinter::HighResolution); QPrintDialog printDialog(printer, this); if (printDialog.exec() == QDialog::Accepted) { ExportManager::instance()->doPrint(printer); return; } delete printer; } void KSMainWindow::showImageSharedFeedback(bool error, const QString &message) { if (error) { mMessageWidget->setMessageType(KMessageWidget::Error); mMessageWidget->setText(i18n("There was a problem sharing the image: %1", message)); mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error"))); } else { mMessageWidget->setMessageType(KMessageWidget::Positive); mMessageWidget->setText(i18n("You can find the shared image at: %1", message)); mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))); } mMessageWidget->animatedShow(); QTimer::singleShot(20000, mMessageWidget, &KMessageWidget::animatedHide); } void KSMainWindow::sendToClipboard() { ExportManager::instance()->doCopyToClipboard(); mMessageWidget->setMessageType(KMessageWidget::Information); mMessageWidget->setText(i18n("The screenshot has been copied to the clipboard.")); mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); mMessageWidget->animatedShow(); QTimer::singleShot(10000, mMessageWidget, &KMessageWidget::animatedHide); } void KSMainWindow::showPreferencesDialog() { SettingsDialog prefDialog(this); prefDialog.exec(); } void KSMainWindow::setScreenshotWindowTitle(QUrl location) { setWindowTitle(location.fileName()); setWindowModified(false); KGuiItem::assign(mDialogButtonBox->button(QDialogButtonBox::Discard), KStandardGuiItem::quit()); } void KSMainWindow::save() { SpectacleConfig::instance()->setLastUsedSaveMode(1); buildSaveMenu(); ExportManager::instance()->doSave(); } void KSMainWindow::saveAs() { SpectacleConfig::instance()->setLastUsedSaveMode(2); buildSaveMenu(); ExportManager::instance()->doSaveAs(this); } void KSMainWindow::saveAndExit() { SpectacleConfig::instance()->setLastUsedSaveMode(0); qApp->setQuitOnLastWindowClosed(false); ExportManager::instance()->doSave(QUrl(), true); hide(); } diff --git a/src/PlatformBackends/KWinWaylandImageGrabber.cpp b/src/PlatformBackends/KWinWaylandImageGrabber.cpp new file mode 100644 index 0000000..2dec87d --- /dev/null +++ b/src/PlatformBackends/KWinWaylandImageGrabber.cpp @@ -0,0 +1,169 @@ +/* + * 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]); +} diff --git a/src/PlatformBackends/KWinWaylandImageGrabber.h b/src/PlatformBackends/KWinWaylandImageGrabber.h new file mode 100644 index 0000000..a998ed2 --- /dev/null +++ b/src/PlatformBackends/KWinWaylandImageGrabber.h @@ -0,0 +1,59 @@ +/* + * 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; + + 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/SpectacleCore.cpp b/src/SpectacleCore.cpp index 2a6e641..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 (qApp->platformName() == QStringLiteral("xcb")) { + 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); } }