diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,7 +50,11 @@ find_package( Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core + Gui Widgets + Qml + Quick + QuickControls2 DBus PrintSupport Test diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,7 +39,15 @@ Gui/SettingsDialog/SettingsDialog.cpp Gui/SettingsDialog/GeneralOptionsPage.cpp Gui/SettingsDialog/ShortcutsOptionsPage.cpp + Gui/CaptureAreaComboBox/CaptureModeModel.cpp QuickEditor/QuickEditor.cpp + + Mobile/Components/ScreenshotItem.cpp + Mobile/Components/MobileBackend.cpp + Mobile/Components/Toolbar.cpp + Mobile/app.qrc + + SpectacleCommon.h ) kconfig_add_kcfg_files(SPECTACLE_SRCS_DEFAULT Gui/SettingsDialog/settings.kcfgc) @@ -76,6 +84,9 @@ spectacle Qt5::DBus Qt5::PrintSupport + Qt5::Qml + Qt5::Quick + Qt5::QuickControls2 KF5::CoreAddons KF5::DBusAddons KF5::WidgetsAddons diff --git a/src/Gui/CaptureAreaComboBox/CaptureModeModel.h b/src/Gui/CaptureAreaComboBox/CaptureModeModel.h new file mode 100644 --- /dev/null +++ b/src/Gui/CaptureAreaComboBox/CaptureModeModel.h @@ -0,0 +1,48 @@ +#ifndef CAPTURE_MODE_MODEL_H +#define CAPTURE_MODE_MODEL_H + +#include "Platforms/Platform.h" + +#include +#include +#include +#include + +#include + +class QAction; + +enum ComboBoxRoles +{ + ShortcutRole = Qt::UserRole + 1, + ActionRole +}; + +class CaptureModeModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit CaptureModeModel(const Platform::GrabModes &theGrabModes, QObject *parent = nullptr); + + QHash roleNames() const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE QVariantMap get(int index) const; + +public Q_SLOT: + void changeShortcut(QAction *action, const QKeySequence &seq); + +private: + struct ModelEntry + { + QString captureModeLabel; + QKeySequence captureModeShortcut; + QAction *action; + }; + + std::vector modelData; +}; +#endif //COMBO_BOX_MODEL_H \ No newline at end of file diff --git a/src/Gui/CaptureAreaComboBox/CaptureModeModel.cpp b/src/Gui/CaptureAreaComboBox/CaptureModeModel.cpp new file mode 100644 --- /dev/null +++ b/src/Gui/CaptureAreaComboBox/CaptureModeModel.cpp @@ -0,0 +1,92 @@ +#include "CaptureModeModel.h" +#include "ShortcutActions.h" + +#include + +#include +#include + +#include + +CaptureModeModel::CaptureModeModel(const Platform::GrabModes &theGrabModes, QObject *parent) : QAbstractListModel(parent) +{ + ShortcutActions* config = ShortcutActions::self(); + KGlobalAccel *accelerator = KGlobalAccel::self(); + + std::vector availableActions; + if (theGrabModes.testFlag(Platform::GrabMode::AllScreens)) + { + availableActions.push_back(config->shortcutActions()->action(QStringLiteral("FullScreenScreenShot"))); + } + if (theGrabModes.testFlag(Platform::GrabMode::CurrentScreen)) + { + availableActions.push_back(config->shortcutActions()->action(QStringLiteral("CurrentMonitorScreenShot"))); + } + if (theGrabModes.testFlag(Platform::GrabMode::ActiveWindow)) + { + availableActions.push_back(config->shortcutActions()->action(QStringLiteral("ActiveWindowScreenShot"))); + } + if (theGrabModes.testFlag(Platform::GrabMode::WindowUnderCursor)) + { + availableActions.push_back(config->shortcutActions()->action(QStringLiteral("WindowUnderCursorScreenShot"))); + } + availableActions.push_back(config->shortcutActions()->action(QStringLiteral("RectangularRegionScreenShot"))); + + for (QAction *action : availableActions) + { + QString label = action ? action->text().remove(QStringLiteral("Capture ")) : QString(); + QKeySequence shortcut = accelerator->hasShortcut(action) ? accelerator->shortcut(action)[0] : QKeySequence(); + modelData.push_back({label, shortcut, action}); + } + connect(accelerator, &KGlobalAccel::globalShortcutChanged, this, &CaptureModeModel::changeShortcut); +} + +int CaptureModeModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return modelData.size(); +} + +QHash CaptureModeModel::roleNames() const +{ + QHash roleNames = QAbstractListModel::roleNames(); + roleNames.insert(ShortcutRole, "shortcut"); + roleNames.insert(ActionRole, "action"); + return roleNames; +} + +QVariant CaptureModeModel::data(const QModelIndex &index, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + return modelData[index.row()].captureModeLabel; + case ShortcutRole: + return modelData[index.row()].captureModeShortcut.toString(); + case ActionRole: + return modelData[index.row()].action->data(); + } + return QVariant{}; +} + +void CaptureModeModel::changeShortcut(QAction *action, const QKeySequence &seq) +{ + auto modelEntryIterator = std::find_if(modelData.begin(), modelData.end(), [action](const ModelEntry &entry) { + return (entry.action == action); + }); + if (modelEntryIterator != modelData.end()) + (*modelEntryIterator).captureModeShortcut = seq; +} + +QVariantMap CaptureModeModel::get(int index) const { + QVariantMap data {}; + + QHashIterator it(this->roleNames()); + QModelIndex idx = this->index(index, 0); + + while (it.hasNext()) { + it.next(); + data[QString::fromStdString(it.value().toStdString())] = idx.data(it.key()); + } + return data; +} \ No newline at end of file diff --git a/src/Main.cpp b/src/Main.cpp --- a/src/Main.cpp +++ b/src/Main.cpp @@ -38,6 +38,8 @@ QApplication lApp(argc, argv); + QQmlApplicationEngine engine {}; + lApp.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true); lApp.setAttribute(Qt::AA_UseHighDpiPixmaps, true); @@ -96,7 +98,8 @@ // are we running in background or dbus mode? - SpectacleCore::StartMode lStartMode = SpectacleCore::StartMode::Gui; + SpectacleCore::StartMode lStartMode = qgetenv("QT_QUICK_CONTROLS_MOBILE").isEmpty() ? SpectacleCore::StartMode::Gui : SpectacleCore::StartMode::Mobile; + bool lNotify = true; bool lCopyToClipboard = false; qint64 lDelayMsec = 0; @@ -139,15 +142,14 @@ case SpectacleCore::StartMode::DBus: lApp.setQuitOnLastWindowClosed(false); break; - case SpectacleCore::StartMode::Gui: - break; + default: break; } // release the kraken - SpectacleCore lCore(lStartMode, lCaptureMode, lFileName, lDelayMsec, lNotify, lCopyToClipboard); - QObject::connect(&lCore, &SpectacleCore::allDone, qApp, &QApplication::quit); - QObject::connect(qApp, &QApplication::aboutToQuit, Settings::self(), &Settings::save); + SpectacleCore lCore(engine, lStartMode, lCaptureMode, lFileName, lDelayMsec, lNotify, lCopyToClipboard); + QObject::connect(&lCore, &SpectacleCore::allDone, qApp, &QGuiApplication::quit); + QObject::connect(qApp, &QGuiApplication::aboutToQuit, Settings::self(), &Settings::save); // create the dbus connections @@ -161,6 +163,5 @@ QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Spectacle")); // fire it up - return lApp.exec(); } diff --git a/src/Mobile/Components/MobileBackend.h b/src/Mobile/Components/MobileBackend.h new file mode 100644 --- /dev/null +++ b/src/Mobile/Components/MobileBackend.h @@ -0,0 +1,28 @@ +#ifndef MOBILE_BACKEND_H +#define MOBILE_BACKEND_H + +#include +#include + +#include "SpectacleCommon.h" + +class MobileBackend: public QObject { + Q_OBJECT + Q_PROPERTY(QPixmap screenshot READ getScreenshot WRITE setScreenshot NOTIFY screenshotChanged) + +public: + explicit MobileBackend(QObject* parent = nullptr); + + const QPixmap& getScreenshot() const; + void setScreenshot(const QPixmap& screenshot); + +Q_SIGNALS: + void newScreenshotRequest(Spectacle::CaptureMode theCaptureMode, int theTimeout, bool theIncludePointer, bool theIncludeDecorations); + void screenshotChanged(); + void captureOnClickSet(bool); + +private: + QPixmap screenshot = QPixmap{}; +}; + +#endif //SCREENSHOT_MANAGER \ No newline at end of file diff --git a/src/Mobile/Components/MobileBackend.cpp b/src/Mobile/Components/MobileBackend.cpp new file mode 100644 --- /dev/null +++ b/src/Mobile/Components/MobileBackend.cpp @@ -0,0 +1,12 @@ +#include "MobileBackend.h" + +MobileBackend::MobileBackend(QObject* parent) : QObject(parent) {} + +const QPixmap& MobileBackend::getScreenshot() const { + return this->screenshot; +} + +void MobileBackend::setScreenshot(const QPixmap& screenshot) { + this->screenshot = screenshot; + emit screenshotChanged(); +} \ No newline at end of file diff --git a/src/Mobile/Components/ScreenshotItem.h b/src/Mobile/Components/ScreenshotItem.h new file mode 100644 --- /dev/null +++ b/src/Mobile/Components/ScreenshotItem.h @@ -0,0 +1,27 @@ +#ifndef SCREENSHOT_ITEM_H +#define SCREENSHOT_ITEM_H + +#include +#include +#include +#include + +class ScreenshotItem : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(QPixmap pixmap READ getPixmap WRITE setPixmap NOTIFY pixmapChanged) +public: + ScreenshotItem(QQuickItem *parent = nullptr); + + void paint(QPainter *painter) override; + + void setPixmap(const QPixmap& image); + const QPixmap& getPixmap() const; +Q_SIGNALS: + void pixmapChanged(); + +private: + QPixmap pixmap; +}; + +#endif //SCREENSHOT_ITEM_H \ No newline at end of file diff --git a/src/Mobile/Components/ScreenshotItem.cpp b/src/Mobile/Components/ScreenshotItem.cpp new file mode 100644 --- /dev/null +++ b/src/Mobile/Components/ScreenshotItem.cpp @@ -0,0 +1,24 @@ +#include "ScreenshotItem.h" + +#include + +ScreenshotItem::ScreenshotItem(QQuickItem *parent) : QQuickPaintedItem(parent) {} + +void ScreenshotItem::paint(QPainter *painter) { + QRectF bounding_rect = boundingRect(); + + const qreal scale = qApp->devicePixelRatio(); + QPixmap scaledPixmap = pixmap.scaled(size().toSize() * scale, Qt::KeepAspectRatio, Qt::SmoothTransformation); + scaledPixmap.setDevicePixelRatio(scale); + + painter->drawPixmap(bounding_rect.center() - scaledPixmap.rect().center(), scaledPixmap); +} + +const QPixmap& ScreenshotItem::getPixmap() const { + return this->pixmap; +} + +void ScreenshotItem::setPixmap(const QPixmap& pixmap) { + this->pixmap = pixmap; + update(); +} \ No newline at end of file diff --git a/src/Mobile/Components/Toolbar.h b/src/Mobile/Components/Toolbar.h new file mode 100644 --- /dev/null +++ b/src/Mobile/Components/Toolbar.h @@ -0,0 +1,38 @@ +#ifndef TOOLBAR_H +#define TOOLBAR_H + +#include + +class Toolbar : public QQuickPaintedItem { + Q_OBJECT + + Q_PROPERTY(QPointF notchCenter READ getNotchCenter WRITE setNotchCenter NOTIFY notchCenterChanged) + Q_PROPERTY(double notchRadius READ getNotchRadius WRITE setNotchRadius NOTIFY notchRadiusChanged) + Q_PROPERTY(double cornerRadius READ getCornerRadius WRITE setCornerRadius NOTIFY cornerRadiusChanged) + +public: + Toolbar(QQuickPaintedItem* parent = nullptr); + + void paint(QPainter *painter) override; + + const QPointF& getNotchCenter() const; + void setNotchCenter(const QPointF& notchCenter); + + double getNotchRadius() const; + void setNotchRadius(double notchRadius); + + double getCornerRadius() const; + void setCornerRadius(double cornerRadius); + +Q_SIGNALS: + void notchCenterChanged(); + void notchRadiusChanged(); + void cornerRadiusChanged(); + +private: + QPointF notchCenter; + double notchRadius = 30; + double cornerRadius = 4; +}; + +#endif //TOOLBAR_H \ No newline at end of file diff --git a/src/Mobile/Components/Toolbar.cpp b/src/Mobile/Components/Toolbar.cpp new file mode 100644 --- /dev/null +++ b/src/Mobile/Components/Toolbar.cpp @@ -0,0 +1,75 @@ +#include "Toolbar.h" + +#include +#include +#include + +Toolbar::Toolbar(QQuickPaintedItem* parent) +: QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void Toolbar::paint(QPainter* painter) +{ + QPainterPath path; + const double w = width(); + const double h = height(); + const double cx = notchCenter.x(); + const double cy = notchCenter.y(); + const double nr = notchRadius; + const double nd = notchRadius * 2; + const double cr = cornerRadius; + const double cd = cr * 2; + const double xDelta = std::sqrt(nr * nr + 2 * nr * cr - cy * cy + 2 * cy * cr); + const qreal angle = std::atan2(cr - cy, xDelta) * 180.0 / M_PI; + + path.arcTo(cx - xDelta - cr, 0, cd, cd, 90, -90 + angle); + path.arcTo(cx - nr, cy - nr, nd, nd, -180 + angle, 180 - angle * 2); + path.arcTo(cx + xDelta - cr, 0, cd, cd, 180 - angle, -90 + angle); + + path.lineTo(w, 0); + path.lineTo(w, h); + path.lineTo(path.currentPosition().x(), h); + path.lineTo(0, h); + painter->fillPath(path, QColor(QStringLiteral("blue"))); +} + +double Toolbar::getNotchRadius() const { + return notchRadius; +} + +void Toolbar::setNotchRadius(double notchRadius) { + if (qFuzzyCompare(this->notchRadius, notchRadius)) + return; + + this->notchRadius = notchRadius; + emit notchRadiusChanged(); + update(); +} + +const QPointF& Toolbar::getNotchCenter() const { + return notchCenter; +} + +void Toolbar::setNotchCenter(const QPointF& notchCenter) { + if (this->notchCenter == notchCenter) + return; + + this->notchCenter = notchCenter; + emit notchCenterChanged(); + update(); +} + +double Toolbar::getCornerRadius() const { + return cornerRadius; +} + +void Toolbar::setCornerRadius(double cornerRadius) +{ + if (qFuzzyCompare(this->cornerRadius, cornerRadius)) + return; + + this->cornerRadius = cornerRadius; + emit cornerRadiusChanged(); + update(); +} \ No newline at end of file diff --git a/src/Mobile/app.qrc b/src/Mobile/app.qrc new file mode 100644 --- /dev/null +++ b/src/Mobile/app.qrc @@ -0,0 +1,10 @@ + + + +package/contents/ui/BottomToolbar.qml +package/contents/ui/ConfigurationDrawer.qml +package/contents/ui/FabButton.qml +package/contents/ui/main.qml +package/contents/ui/ShareDrawer.qml + + \ No newline at end of file diff --git a/src/Mobile/package/contents/ui/BottomToolbar.qml b/src/Mobile/package/contents/ui/BottomToolbar.qml new file mode 100644 --- /dev/null +++ b/src/Mobile/package/contents/ui/BottomToolbar.qml @@ -0,0 +1,53 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtGraphicalEffects 1.14 + +import Spectacle 1.0 as SpectacleMobile + +ToolBar { + id: root + + signal configureButtonClicked() + signal shareButtonClicked() + + RowLayout { + anchors.fill: parent + + ToolButton { + id: configureButton + + icon.name: "configure" + onClicked: configureButtonClicked() + } + + Item { + Layout.fillWidth: true + } + + ToolButton { + icon.name: "document-share" + onClicked: shareButtonClicked(); + } + + ToolButton { + icon.name: "edit-copy" + onClicked: { exportManager.doCopyToClipboard(); showPassiveNotification("Screenshot copied to clipboard", 1000)} + } + + ToolButton { + icon.name: "document-save" + onClicked: exportManager.doSaveAs(null, false) + } + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: SpectacleMobile.ToolBar { + width: bottomToolbar .width; height: bottomToolbar .height + notchCenter: Qt.point(root.width/2, 0); + cornerRadius: 10 + notchRadius: (fab.width/2 + 10) * fab.scale + } + } +} \ No newline at end of file diff --git a/src/Mobile/package/contents/ui/ConfigurationDrawer.qml b/src/Mobile/package/contents/ui/ConfigurationDrawer.qml new file mode 100644 --- /dev/null +++ b/src/Mobile/package/contents/ui/ConfigurationDrawer.qml @@ -0,0 +1,110 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 + +import org.kde.kirigami 2.11 as Kirigami + +Drawer { + id: root + + leftPadding: 10 + topPadding: 10 + rightPadding: 10 + bottomPadding: 10 + + edge: Qt.BottomEdge + dragMargin: 0 + + property var captureMode: captureModeModel.get(captureModeComboBox.currentIndex).action + property alias delay: screenshotDelaySlider.value + property alias captureOnClick: captureOnClickCheckbox.checked + + Column { + spacing: 10 + + Label { + text: "Capture Mode" + } + + Column { + leftPadding: 25 + spacing: 5 + + ComboBox { + id: captureModeComboBox + + property int horizontalSpacing: 25 + + textRole: "display" + //valueRole: "action" + + model: captureModeModel + + delegate: ItemDelegate { + width: parent.width + + Label { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + leftPadding: 5 + text: model.display + } + + Label { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + rightPadding: 5 + text: model.shortcut + } + } + + TextMetrics { + id: textMetrics + font: captureModeComboBox.font + } + + onModelChanged: { + let maxLeftWidth = 0; + let maxRightWidth = 0; + for(let i = 0; i < model.rowCount(); i++) { + textMetrics.text = model.get(i).display; + maxLeftWidth = Math.max(textMetrics.width, maxLeftWidth); + textMetrics.text = model.get(i).shortcut; + maxRightWidth = Math.max(textMetrics.width, maxRightWidth); + } + width = maxLeftWidth + maxRightWidth + horizontalSpacing + leftPadding + rightPadding; + } + } + + Label { + text: "Delay " + screenshotDelaySlider.value + " seconds" + } + + Slider { + id: screenshotDelaySlider + + enabled: !captureOnClick + + from: 0 + to: 30 + stepSize: 1 + } + } + + Label { + text: "Options" + } + + Column { + leftPadding: 25 + + CheckBox { + id: captureOnClickCheckbox + text: "Capture on touch" + + onClicked: mobileBackend.captureOnClickSet(checked) + } + } + } +} \ No newline at end of file diff --git a/src/Mobile/package/contents/ui/FabButton.qml b/src/Mobile/package/contents/ui/FabButton.qml new file mode 100644 --- /dev/null +++ b/src/Mobile/package/contents/ui/FabButton.qml @@ -0,0 +1,33 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 + +import Spectacle 1.0 as SpectacleMobile + +RoundButton { + id: root + + signal animationFinished() + + icon.name: "camera-photo" + focusPolicy: Qt.NoFocus + + onClicked: hideAnimation.start(); + + NumberAnimation on scale { + id: hideAnimation + + running: false + to: 0 + duration: 200 + + onFinished: waitForAnimationFinished.start(); + } + + + Timer { + id: waitForAnimationFinished + + interval: 100 + onTriggered: animationFinished() + } +} \ No newline at end of file diff --git a/src/Mobile/package/contents/ui/ShareDrawer.qml b/src/Mobile/package/contents/ui/ShareDrawer.qml new file mode 100644 --- /dev/null +++ b/src/Mobile/package/contents/ui/ShareDrawer.qml @@ -0,0 +1,50 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 + +import org.kde.kirigami 2.11 as Kirigami +import org.kde.purpose 1.0 as Purpose + +Drawer { + id: root + + leftPadding: 10 + topPadding: 10 + rightPadding: 10 + bottomPadding: 10 + + edge: Qt.BottomEdge + dragMargin: 0 + + Purpose.AlternativesView { + id: view + + anchors.fill: parent + + pluginType: "Export" + + clip: true + + delegate: Kirigami.BasicListItem { + label: display + icon: iconName + onClicked: view.createJob (index) + } + + onFinished: closeTimer.start() + } + + Timer { + id: closeTimer + + interval: 250 + onTriggered: root.close() + } + + onOpened: { + view.inputData = { + "urls": [exportManager.tempSave().toString()], + "mimeType": "image/*" + } + } +} + diff --git a/src/Mobile/package/contents/ui/main.qml b/src/Mobile/package/contents/ui/main.qml new file mode 100644 --- /dev/null +++ b/src/Mobile/package/contents/ui/main.qml @@ -0,0 +1,84 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Window 2.14 + +import org.kde.kirigami 2.11 as Kirigami + +import Spectacle 1.0 as SpectacleMobile + +Kirigami.ApplicationWindow { + id: root + + width: 350; height: 640 + + pageStack.initialPage: Kirigami.Page { + title: "Spectacle Mobile" + + SpectacleMobile.ScreenshotItem { + anchors.fill: parent + + pixmap: mobileBackend.screenshot + } + + Connections { + target: mobileBackend + onScreenshotChanged: { + fab.scale = 1; + root.show() + } + } + + padding: 5 + } + + ConfigurationDrawer { + id: configurationDrawer + + width: parent.width + } + + ShareDrawer { + id: shareSheet + + width: parent.width + height: 200 + } + + FabButton { + id: fab + + anchors.horizontalCenter: parent.horizontalCenter + y: bottomToolbar.y - height/2 + z: 10 + + onAnimationFinished: { + root.hide() + waitForWindowHidden.start(); + } + + Timer { + id: waitForWindowHidden + + interval: 250 + onTriggered: { + let captureMode = configurationDrawer.captureMode; + let delay = configurationDrawer.captureOnClick ? -1 : configurationDrawer.delay * 1000; + mobileBackend.newScreenshotRequest(captureMode, delay, false, false); + } + } + } + + footer: BottomToolbar { + id: bottomToolbar + Kirigami.Theme.backgroundColor: "#2980b9" + + onConfigureButtonClicked: configurationDrawer.open() + onShareButtonClicked: shareSheet.open() + } + + Component.onCompleted: { + setX(Screen.width / 2 - width / 2); + setY(Screen.height / 2 - height / 2); + } +} \ No newline at end of file diff --git a/src/ShortcutActions.cpp b/src/ShortcutActions.cpp --- a/src/ShortcutActions.cpp +++ b/src/ShortcutActions.cpp @@ -18,6 +18,8 @@ #include "ShortcutActions.h" +#include "SpectacleCommon.h" + #include #include @@ -46,21 +48,36 @@ { QAction *action = new QAction(i18n("Capture Entire Desktop")); action->setObjectName(QStringLiteral("FullScreenScreenShot")); + action->setProperty("isConfigurationAction", true); + action->setData(Spectacle::CaptureMode::AllScreens); mActions.addAction(action->objectName(), action); } { QAction *action = new QAction(i18n("Capture Current Monitor")); action->setObjectName(QStringLiteral("CurrentMonitorScreenShot")); + action->setProperty("isConfigurationAction", true); + action->setData(Spectacle::CaptureMode::CurrentScreen); mActions.addAction(action->objectName(), action); } { QAction *action = new QAction(i18n("Capture Active Window")); action->setObjectName(QStringLiteral("ActiveWindowScreenShot")); + action->setProperty("isConfigurationAction", true); + action->setData(Spectacle::CaptureMode::ActiveWindow); + mActions.addAction(action->objectName(), action); + } + { + QAction *action = new QAction(i18n("Capture Window Under Cursor")); + action->setObjectName(QStringLiteral("WindowUnderCursorScreenShot")); + action->setProperty("isConfigurationAction", true); + action->setData(Spectacle::CaptureMode::WindowUnderCursor); mActions.addAction(action->objectName(), action); } { QAction *action = new QAction(i18n("Capture Rectangular Region")); action->setObjectName(QStringLiteral("RectangularRegionScreenShot")); + action->setProperty("isConfigurationAction", true); + action->setData(Spectacle::CaptureMode::RectangularRegion); mActions.addAction(action->objectName(), action); } } diff --git a/src/SpectacleCommon.h b/src/SpectacleCommon.h --- a/src/SpectacleCommon.h +++ b/src/SpectacleCommon.h @@ -21,14 +21,20 @@ #pragma once +#include + namespace Spectacle { + Q_NAMESPACE + enum CaptureMode { - InvalidChoice = -1, - AllScreens = 0, - CurrentScreen = 1, - ActiveWindow = 2, - WindowUnderCursor = 3, + InvalidChoice = -1, + AllScreens = 0, + CurrentScreen = 1, + ActiveWindow = 2, + WindowUnderCursor = 3, TransientWithParent = 4, - RectangularRegion = 5 + RectangularRegion = 5 }; + + Q_ENUM_NS(CaptureMode) } diff --git a/src/SpectacleCore.h b/src/SpectacleCore.h --- a/src/SpectacleCore.h +++ b/src/SpectacleCore.h @@ -22,12 +22,15 @@ #pragma once #include +#include #include "ExportManager.h" #include "Gui/KSMainWindow.h" #include "QuickEditor/QuickEditor.h" #include "Platforms/PlatformLoader.h" +#include "Mobile/Components/MobileBackend.h" + #include namespace KWayland { @@ -45,13 +48,9 @@ public: - enum class StartMode { - Gui = 0, - DBus = 1, - Background = 2 - }; + enum class StartMode { Gui, Mobile, DBus, Background}; - explicit SpectacleCore(StartMode theStartMode, + explicit SpectacleCore(QQmlApplicationEngine& engine, StartMode theStartMode, Spectacle::CaptureMode theCaptureMode, QString &theSaveFileName, qint64 theDelayMsec, @@ -88,7 +87,10 @@ Platform::GrabMode toPlatformGrabMode(Spectacle::CaptureMode theCaptureMode); void setUpShortcuts(); - StartMode mStartMode; + QQmlApplicationEngine& engine; + MobileBackend mobileBackend; + + StartMode mStartMode; bool mNotify; QString mFileNameString; QUrl mFileNameUrl; diff --git a/src/SpectacleCore.cpp b/src/SpectacleCore.cpp --- a/src/SpectacleCore.cpp +++ b/src/SpectacleCore.cpp @@ -22,8 +22,13 @@ #include "spectacle_core_debug.h" #include "Config.h" +#include "SpectacleCommon.h" #include "settings.h" #include "ShortcutActions.h" +#include "Mobile/Components/ScreenshotItem.h" +#include "Mobile/Components/MobileBackend.h" +#include "Mobile/Components/Toolbar.h" +#include "Gui/CaptureAreaComboBox/CaptureModeModel.h" #include #include @@ -44,21 +49,25 @@ #include #include -SpectacleCore::SpectacleCore(StartMode theStartMode, +#include +#include + +SpectacleCore::SpectacleCore(QQmlApplicationEngine& engine, 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), - mWaylandPlasmashell(nullptr) + QObject *parent) : QObject(parent), + engine(engine), + mobileBackend(), + mStartMode(theStartMode), + mNotify(theNotifyOnGrab), + mPlatform(loadPlatform()), + mMainWindow(nullptr), + mIsGuiInited(false), + mCopyToClipboard(theCopyToClipboard), + mWaylandPlasmashell(nullptr) { if (!(theSaveFileName.isEmpty() || theSaveFileName.isNull())) { if (QDir::isRelativePath(theSaveFileName)) { @@ -109,6 +118,9 @@ connection->roundtrip(); } + // Needs to be done up here + setUpShortcuts(); + switch (theStartMode) { case StartMode::DBus: break; @@ -123,11 +135,10 @@ }); } break; - case StartMode::Gui: + default: initGui(Settings::includePointer(), Settings::includeDecorations()); break; } - setUpShortcuts(); } void SpectacleCore::setUpShortcuts() @@ -200,7 +211,8 @@ ExportManager::instance()->setCaptureMode(theCaptureMode); auto lGrabMode = toPlatformGrabMode(theCaptureMode); - if (theTimeout < 0) { + if (theTimeout < 0) + { mPlatform->doGrab(Platform::ShutterMode::OnClick, lGrabMode, theIncludePointer, theIncludeDecorations); return; } @@ -278,15 +290,26 @@ break; case StartMode::Gui: mMainWindow->setScreenshotAndShow(thePixmap); + break; + case StartMode::Mobile: + mobileBackend.setScreenshot(thePixmap); + break; + } + if (mStartMode == StartMode::Gui || mStartMode == StartMode::Mobile) { bool autoSaveImage = Settings::autoSaveImage(); bool copyImageToClipboard = Settings::copyImageToClipboard(); - if (autoSaveImage && copyImageToClipboard) { + if (autoSaveImage && copyImageToClipboard) + { lExportManager->doSaveAndCopy(); - } else if (autoSaveImage) { + } + else if (autoSaveImage) + { lExportManager->doSave(); - } else if (copyImageToClipboard) { + } + else if (copyImageToClipboard) + { lExportManager->doCopyToClipboard(false); } } @@ -303,13 +326,17 @@ case StartMode::Background: showErrorMessage(i18n("Screenshot capture canceled or failed")); emit allDone(); - return; + break; case StartMode::DBus: emit grabFailed(); emit allDone(); - return; + break; case StartMode::Gui: mMainWindow->setScreenshotAndShow(QPixmap()); + break; + case StartMode::Mobile: + mobileBackend.setScreenshot(QPixmap()); + break; } } @@ -419,21 +446,40 @@ return Platform::GrabMode::InvalidChoice; } + void SpectacleCore::initGui(bool theIncludePointer, bool theIncludeDecorations) { - if (!mIsGuiInited) { + if (mIsGuiInited) + return; + + if (mStartMode == StartMode::Mobile) { + qmlRegisterType("Spectacle", 1, 0, "ScreenshotItem"); + qmlRegisterType("Spectacle", 1, 0, "ToolBar"); + qRegisterMetaType("CaptureMode"); + + connect(&mobileBackend, &MobileBackend::newScreenshotRequest, this, &SpectacleCore::takeNewScreenshot); + connect(&mobileBackend, &MobileBackend::captureOnClickSet, &Settings::setOnClickChecked); + + engine.rootContext()->setContextProperty(QStringLiteral("mobileBackend"), &mobileBackend); + engine.rootContext()->setContextProperty(QStringLiteral("captureModeModel"), new CaptureModeModel{mPlatform->supportedGrabModes()}); + engine.rootContext()->setContextProperty(QStringLiteral("exportManager"), ExportManager::instance()); + + engine.setBaseUrl(QUrl(QStringLiteral("qrc:/package/contents/ui/"))); + engine.load(QUrl(QStringLiteral("qrc:/package/contents/ui/main.qml"))); + } + else { 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; + mIsGuiInited = true; - auto lShutterMode = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate) ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; - auto lGrabMode = toPlatformGrabMode(ExportManager::instance()->captureMode()); + auto lShutterMode = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate) ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; + auto lGrabMode = toPlatformGrabMode(ExportManager::instance()->captureMode()); - QTimer::singleShot(0, this, [this, lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations]() { - mPlatform->doGrab(lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations); - }); - } + QTimer::singleShot(0, this, [this, lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations]() { + mPlatform->doGrab(lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations); + }); }