diff --git a/src/core/UBApplicationController.cpp b/src/core/UBApplicationController.cpp index 7fc8bf989..c86f88caf 100644 --- a/src/core/UBApplicationController.cpp +++ b/src/core/UBApplicationController.cpp @@ -620,6 +620,19 @@ void UBApplicationController::hideDesktop() emit desktopMode(false); } +void UBApplicationController::showGlassPane(bool show) +{ + if (isShowingDesktop()) + { + uninotesController()->drawingView()->setVisible(show); + + if (show) + { + UBPlatformUtils::keepOnTop(); + } + } +} + void UBApplicationController::setMirrorSourceWidget(QWidget* pWidget) { if (mMirror) diff --git a/src/core/UBApplicationController.h b/src/core/UBApplicationController.h index dce6dddce..70688ded0 100644 --- a/src/core/UBApplicationController.h +++ b/src/core/UBApplicationController.h @@ -132,8 +132,8 @@ class UBApplicationController : public QObject // defaulting to false to match QAction triggered(bool checked = false) void showDesktop(bool dontSwitchFrontProcess = false); - void hideDesktop(); + void showGlassPane(bool show); void useMultiScreen(bool use); diff --git a/src/core/UBDisplayManager.cpp b/src/core/UBDisplayManager.cpp index 58d425c19..c754e9fdf 100644 --- a/src/core/UBDisplayManager.cpp +++ b/src/core/UBDisplayManager.cpp @@ -672,18 +672,13 @@ qreal UBDisplayManager::logicalDpi(ScreenRole role) const return scr ? scr->logicalDotsPerInch() : 96.; } -void UBDisplayManager::grab(ScreenRole role, std::function callback, QRect rect) const +void UBDisplayManager::grab(ScreenRole role, std::function callback, bool crop) const { QScreen* scr = screen(role); if (scr) { - if (!rect.isValid()) - { - rect = QRect(QPoint(0, 0), scr->geometry().size()); - } - - UBPlatformUtils::grabScreen(scr, callback, rect); + UBPlatformUtils::grabScreen(scr, callback, crop); } else { diff --git a/src/core/UBDisplayManager.h b/src/core/UBDisplayManager.h index ad9ac2d59..acc42553b 100644 --- a/src/core/UBDisplayManager.h +++ b/src/core/UBDisplayManager.h @@ -107,7 +107,7 @@ class UBDisplayManager : public QObject qreal physicalDpi(ScreenRole role) const; qreal logicalDpi(ScreenRole role) const; - void grab(ScreenRole role, std::function callback, QRect rect = {}) const; + void grab(ScreenRole role, std::function callback, bool crop = false) const; QPixmap grabGlobal(QRect rect) const; void initScreensByRole(); diff --git a/src/desktop/UBDesktopAnnotationController.cpp b/src/desktop/UBDesktopAnnotationController.cpp index 14948b3a7..163af5752 100644 --- a/src/desktop/UBDesktopAnnotationController.cpp +++ b/src/desktop/UBDesktopAnnotationController.cpp @@ -359,13 +359,7 @@ void UBDesktopAnnotationController::showWindow() UBDrawingController::drawingController()->setStylusTool(mDesktopStylusTool); -#ifndef Q_OS_LINUX UBPlatformUtils::showFullScreen(mTransparentDrawingView); -#else - // this is necessary to avoid hiding the panels on Unity and Cinnamon - // if finer control is necessary, use qgetenv("XDG_CURRENT_DESKTOP") - mTransparentDrawingView->show(); -#endif UBPlatformUtils::hideMenuBarAndDock(); mDesktopPalette->appear(); @@ -470,7 +464,9 @@ void UBDesktopAnnotationController::customCapture() mDesktopPalette->disappearForCapture(); - getScreenPixmap([this](QPixmap pixmap){ + const auto grabCanCrop = UBPlatformUtils::grabCanCrop(); + + getScreenPixmap([this, grabCanCrop](QPixmap pixmap){ auto finishCapture = [this](){ mDesktopPalette->appear(); mCustomCaptureClicked = false; @@ -481,7 +477,7 @@ void UBDesktopAnnotationController::customCapture() QPixmap screenshot = pixmap.isNull() ? clipboardScreenshot() : pixmap; // On Wayland with xdg-desktop-portal, the user already chose the area. - if (UBPlatformUtils::sessionType() == UBPlatformUtils::WAYLAND && !screenshot.isNull()) + if (grabCanCrop && !screenshot.isNull()) { emit imageCaptured(screenshot, false); finishCapture(); @@ -497,7 +493,7 @@ void UBDesktopAnnotationController::customCapture() } finishCapture(); - }); + }, grabCanCrop); } } @@ -519,22 +515,15 @@ void UBDesktopAnnotationController::screenCapture() QPixmap screenshot = pixmap.isNull() ? clipboardScreenshot() : pixmap; - if (UBPlatformUtils::sessionType() == UBPlatformUtils::WAYLAND && !screenshot.isNull()) - { - emit imageCaptured(screenshot, false); - finishCapture(); - return; - } - emit imageCaptured(screenshot, false); finishCapture(); - }); + }, false); } -void UBDesktopAnnotationController::getScreenPixmap(std::function callback) +void UBDesktopAnnotationController::getScreenPixmap(std::function callback, bool crop) { - UBApplication::displayManager->grab(ScreenRole::Control, callback); + UBApplication::displayManager->grab(ScreenRole::Control, callback, crop); } diff --git a/src/desktop/UBDesktopAnnotationController.h b/src/desktop/UBDesktopAnnotationController.h index b217a663a..1dc6f59bb 100644 --- a/src/desktop/UBDesktopAnnotationController.h +++ b/src/desktop/UBDesktopAnnotationController.h @@ -99,7 +99,7 @@ class UBDesktopAnnotationController : public QObject void restoreUniboard(); protected: - void getScreenPixmap(std::function callback); + void getScreenPixmap(std::function callback, bool crop); UBBoardView* mTransparentDrawingView; std::shared_ptr mTransparentDrawingScene; diff --git a/src/frameworks/CMakeLists.txt b/src/frameworks/CMakeLists.txt index 1b12387e2..daf7bdbcf 100644 --- a/src/frameworks/CMakeLists.txt +++ b/src/frameworks/CMakeLists.txt @@ -23,10 +23,14 @@ target_sources(openboard PRIVATE if(CMAKE_SYSTEM_NAME STREQUAL Linux) target_sources(openboard PRIVATE - UBDesktopPortal.cpp - UBDesktopPortal.h - UBPipewireSink.cpp - UBPipewireSink.h + linux/UBScreenCastDesktopPortalWrapper.cpp + linux/UBScreenCastDesktopPortalWrapper.h + linux/UBScreenshotDesktopPortalWrapper.cpp + linux/UBScreenshotDesktopPortalWrapper.h + linux/UBDesktopPortalTokenGenerator.cpp + linux/UBDesktopPortalTokenGenerator.h + linux/UBPipewireSink.cpp + linux/UBPipewireSink.h UBPlatformUtils_linux.cpp ) endif() diff --git a/src/frameworks/UBPlatformUtils.h b/src/frameworks/UBPlatformUtils.h index b090c723c..2728bc66c 100644 --- a/src/frameworks/UBPlatformUtils.h +++ b/src/frameworks/UBPlatformUtils.h @@ -213,7 +213,8 @@ class UBPlatformUtils static void setFrontProcess(); static void showFullScreen(QWidget * pWidget); static void showOSK(bool show); - static void grabScreen(QScreen* screen, std::function callback, QRect rect = {}); + static void grabScreen(QScreen* screen, std::function callback, bool crop = false); + static bool grabCanCrop(); #ifdef Q_OS_OSX static void SetMacLocaleByIdentifier(const QString& id); diff --git a/src/frameworks/UBPlatformUtils_linux.cpp b/src/frameworks/UBPlatformUtils_linux.cpp index 62e804ebb..704306bb3 100644 --- a/src/frameworks/UBPlatformUtils_linux.cpp +++ b/src/frameworks/UBPlatformUtils_linux.cpp @@ -36,7 +36,7 @@ #include -#include "frameworks/UBDesktopPortal.h" +#include "frameworks/linux/UBScreenshotDesktopPortalWrapper.h" #include "frameworks/UBFileSystemUtils.h" #include "core/UBApplication.h" #include "core/UBDisplayManager.h" @@ -45,6 +45,40 @@ static OnboardListener* listener = nullptr; +// utility function to detect desktop environment +enum DesktopEnvironment +{ + UNKNOWN, + GNOME, + KDE, + OTHER +}; + +static DesktopEnvironment desktopEnvironment() +{ + static DesktopEnvironment desktop{UNKNOWN}; + + if (desktop == UNKNOWN) + { + const auto xdgCurrentDesktop{QProcessEnvironment::systemEnvironment().value("XDG_CURRENT_DESKTOP", "")}; + + if (xdgCurrentDesktop.compare("gnome", Qt::CaseInsensitive) == 0) + { + desktop = GNOME; + } + else if (xdgCurrentDesktop.compare("kde", Qt::CaseInsensitive) == 0) + { + desktop = KDE; + } + else + { + desktop = OTHER; + } + } + + return desktop; +} + void UBPlatformUtils::init() { initializeKeyboardLayouts(); @@ -535,28 +569,33 @@ void UBPlatformUtils::showOSK(bool show) } } -void UBPlatformUtils::grabScreen(QScreen* screen, std::function callback, QRect rect) +void UBPlatformUtils::grabScreen(QScreen* screen, std::function callback, bool crop) { if (sessionType() == WAYLAND) { - UBDesktopPortal* portal = new UBDesktopPortal; + auto* portal = new UBScreenshotDesktopPortalWrapper; - QObject::connect(portal, &UBDesktopPortal::screenGrabbed, portal, [portal,callback](QPixmap screenshot){ + QObject::connect(portal, &UBScreenshotDesktopPortalWrapper::screenGrabbed, portal, [portal,callback](QPixmap screenshot){ callback(screenshot); portal->deleteLater(); }); - portal->grabScreen(screen, rect); + portal->grabScreen(screen, crop); } else { // see https://doc.qt.io/qt-6.2/qtwidgets-desktop-screenshot-example.html // for using window id 0 - QPixmap pixmap = screen->grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height()); + QPixmap pixmap = screen->grabWindow(0); callback(pixmap); } } +bool UBPlatformUtils::grabCanCrop() +{ + return sessionType() == WAYLAND && desktopEnvironment() == GNOME; +} + UBPlatformUtils::SessionType UBPlatformUtils::sessionType() { /* @@ -596,7 +635,9 @@ void UBPlatformUtils::keepOnTop() return; } - auto path = "/" + result.arguments().first().toString(); + const auto kdeSesionVersion = QProcessEnvironment::systemEnvironment().value("KDE_SESSION_VERSION", "0").toInt(); + const auto prefix = kdeSesionVersion <= 5 ? "/" : "/Scripting/Script"; + const auto path = prefix + result.arguments().first().toString(); QDBusInterface script("org.kde.KWin", path); script.call("run"); diff --git a/src/frameworks/UBPlatformUtils_mac.mm b/src/frameworks/UBPlatformUtils_mac.mm index 513bc3b89..bd68356ae 100644 --- a/src/frameworks/UBPlatformUtils_mac.mm +++ b/src/frameworks/UBPlatformUtils_mac.mm @@ -752,8 +752,14 @@ click menu item (nbItems - 4)\n\ } } -void UBPlatformUtils::grabScreen(QScreen* screen, std::function callback, QRect rect) +void UBPlatformUtils::grabScreen(QScreen* screen, std::function callback, bool crop) { - QPixmap pixmap = screen->grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height()); + Q_UNUSED(crop) + QPixmap pixmap = screen->grabWindow(0); callback(pixmap); } + +bool UBPlatformUtils::grabCanCrop() +{ + return false; +} diff --git a/src/frameworks/UBPlatformUtils_win.cpp b/src/frameworks/UBPlatformUtils_win.cpp index 9c144b255..d3fa17c96 100644 --- a/src/frameworks/UBPlatformUtils_win.cpp +++ b/src/frameworks/UBPlatformUtils_win.cpp @@ -495,8 +495,14 @@ void UBPlatformUtils::showOSK(bool show) } } -void UBPlatformUtils::grabScreen(QScreen* screen, std::function callback, QRect rect) +void UBPlatformUtils::grabScreen(QScreen* screen, std::function callback, bool crop) { - QPixmap pixmap = screen->grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height()); + Q_UNUSED(crop) + QPixmap pixmap = screen->grabWindow(0); callback(pixmap); } + +bool UBPlatformUtils::grabCanCrop() +{ + return false; +} diff --git a/src/frameworks/frameworks.pri b/src/frameworks/frameworks.pri index a14e74972..5b67ed786 100644 --- a/src/frameworks/frameworks.pri +++ b/src/frameworks/frameworks.pri @@ -25,22 +25,24 @@ SOURCES += src/frameworks/UBGeometryUtils.cpp \ win32 { SOURCES += src/frameworks/UBPlatformUtils_win.cpp -} +} -macx { +macx { OBJECTIVE_SOURCES += src/frameworks/UBPlatformUtils_mac.mm - -} + +} linux-g++* { - HEADERS += src/frameworks/UBDesktopPortal.h \ - src/frameworks/UBPipewireSink.h + HEADERS += src/frameworks/linux/UBScreenCastDesktopPortalWrapper.h \ + src/frameworks/linux/UBScreenshotDesktopPortalWrapper.h \ + src/frameworks/linux/UBPipewireSink.h SOURCES += src/frameworks/UBPlatformUtils_linux.cpp \ - src/frameworks/UBDesktopPortal.cpp \ - src/frameworks/UBPipewireSink.cpp + src/frameworks/linux/UBScreenCastDesktopPortalWrapper.cpp \ + src/frameworks/linux/UBScreenshotDesktopPortalWrapper.cpp \ + src/frameworks/linux/UBPipewireSink.cpp CONFIG += link_pkgconfig PKGCONFIG += libpipewire-0.3 diff --git a/src/frameworks/linux/UBDesktopPortalTokenGenerator.cpp b/src/frameworks/linux/UBDesktopPortalTokenGenerator.cpp new file mode 100644 index 000000000..50b4d36ab --- /dev/null +++ b/src/frameworks/linux/UBDesktopPortalTokenGenerator.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015-2026 Département de l'Instruction Publique (DIP-SEM) + * + * This file is part of OpenBoard. + * + * OpenBoard is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, + * with a specific linking exception for the OpenSSL project's + * "OpenSSL" library (or with modified versions of it that use the + * same license as the "OpenSSL" library). + * + * OpenBoard 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 General Public License + * along with OpenBoard. If not, see . + */ + + +#include "UBDesktopPortalTokenGenerator.h" + +#include + + +QString UBDesktopPortalTokenGenerator::generateToken() +{ + const auto randomValue = QRandomGenerator::global()->generate(); + return "OB_" + QString::number(randomValue, 16); +} diff --git a/src/frameworks/linux/UBDesktopPortalTokenGenerator.h b/src/frameworks/linux/UBDesktopPortalTokenGenerator.h new file mode 100644 index 000000000..6e3b12ef5 --- /dev/null +++ b/src/frameworks/linux/UBDesktopPortalTokenGenerator.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015-2026 Département de l'Instruction Publique (DIP-SEM) + * + * This file is part of OpenBoard. + * + * OpenBoard is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, + * with a specific linking exception for the OpenSSL project's + * "OpenSSL" library (or with modified versions of it that use the + * same license as the "OpenSSL" library). + * + * OpenBoard 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 General Public License + * along with OpenBoard. If not, see . + */ + + +#pragma once + +#include + +class UBDesktopPortalTokenGenerator +{ +public: + UBDesktopPortalTokenGenerator() = delete; + + static QString generateToken(); +}; + diff --git a/src/frameworks/UBPipewireSink.cpp b/src/frameworks/linux/UBPipewireSink.cpp similarity index 100% rename from src/frameworks/UBPipewireSink.cpp rename to src/frameworks/linux/UBPipewireSink.cpp diff --git a/src/frameworks/UBPipewireSink.h b/src/frameworks/linux/UBPipewireSink.h similarity index 100% rename from src/frameworks/UBPipewireSink.h rename to src/frameworks/linux/UBPipewireSink.h diff --git a/src/frameworks/UBDesktopPortal.cpp b/src/frameworks/linux/UBScreenCastDesktopPortalWrapper.cpp similarity index 55% rename from src/frameworks/UBDesktopPortal.cpp rename to src/frameworks/linux/UBScreenCastDesktopPortalWrapper.cpp index 468630065..7d7c34733 100644 --- a/src/frameworks/UBDesktopPortal.cpp +++ b/src/frameworks/linux/UBScreenCastDesktopPortalWrapper.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2025 Département de l'Instruction Publique (DIP-SEM) + * Copyright (C) 2015-2026 Département de l'Instruction Publique (DIP-SEM) * * This file is part of OpenBoard. * @@ -20,7 +20,7 @@ */ -#include "UBDesktopPortal.h" +#include "UBScreenCastDesktopPortalWrapper.h" #include #include @@ -29,81 +29,22 @@ #include #include #include -#include -#include -#include #include #include -#include "board/UBBoardView.h" -#include "core/UBApplication.h" -#include "core/UBApplicationController.h" #include "core/UBSettings.h" -#include "desktop/UBDesktopAnnotationController.h" -#include "frameworks/UBPlatformUtils.h" +#include "frameworks/linux/UBDesktopPortalTokenGenerator.h" enum : uint { MONITOR = 1, WINDOW = 2, VIRTUAL = 4 } SourceType; enum : uint { HIDDEN = 1, EMBEDDED = 2, METADATA = 4 } CursorMode; enum : uint { TRANSIENT = 0, APPLICATION = 1, PERSISTENT = 2 } PersistMode; -Q_DECLARE_METATYPE(UBDesktopPortal::Stream) -Q_DECLARE_METATYPE(UBDesktopPortal::Streams) - -QPixmap UBDesktopPortal::loadScreenshotFromUri(const QUrl& uri) const -{ - if (uri.isEmpty()) - { - return {}; - } - - QFile file(uri.toLocalFile()); - - if (!file.exists()) - { - qDebug() << "Screenshot image file does not exist" << uri; - return {}; - } - - QPixmap pixmap{file.fileName()}; - file.remove(); - qDebug() << "DesktopPortal: loaded screenshot file" << uri; - return pixmap; -} - -QPixmap UBDesktopPortal::readClipboardScreenshot() const -{ - auto clipboard = QGuiApplication::clipboard(); - - if (!clipboard) - { - qDebug() << "DesktopPortal: no clipboard available"; - return {}; - } - - QImage cbImage = clipboard->image(); - - if (!cbImage.isNull()) - { - QPixmap fromImage = QPixmap::fromImage(cbImage); - qDebug() << "DesktopPortal: clipboard image found" << "size" << fromImage.size(); - return fromImage; - } - - QPixmap cbPixmap = clipboard->pixmap(); - - if (!cbPixmap.isNull()) - { - qDebug() << "DesktopPortal: clipboard pixmap found" << "size" << cbPixmap.size(); - return cbPixmap; - } - - qDebug() << "DesktopPortal: clipboard empty"; - return {}; -} +Q_DECLARE_METATYPE(UBScreenCastDesktopPortalWrapper::Stream) +Q_DECLARE_METATYPE(UBScreenCastDesktopPortalWrapper::Streams) -QDBusArgument &operator << (QDBusArgument &arg, const UBDesktopPortal::Stream &stream) +QDBusArgument &operator << (QDBusArgument &arg, const UBScreenCastDesktopPortalWrapper::Stream &stream) { arg.beginStructure(); arg << stream.node_id; @@ -121,7 +62,7 @@ QDBusArgument &operator << (QDBusArgument &arg, const UBDesktopPortal::Stream &s return arg; } -const QDBusArgument &operator >> (const QDBusArgument &arg, UBDesktopPortal::Stream &stream) +const QDBusArgument &operator >> (const QDBusArgument &arg, UBScreenCastDesktopPortalWrapper::Stream &stream) { arg.beginStructure(); arg >> stream.node_id; @@ -144,9 +85,9 @@ const QDBusArgument &operator >> (const QDBusArgument &arg, UBDesktopPortal::Str return arg; } -QDBusArgument &operator << (QDBusArgument &arg, const UBDesktopPortal::Streams &streams) +QDBusArgument &operator << (QDBusArgument &arg, const UBScreenCastDesktopPortalWrapper::Streams &streams) { - arg.beginArray(qMetaTypeId()); + arg.beginArray(qMetaTypeId()); for (const auto &stream : streams) { arg << stream; @@ -155,14 +96,13 @@ QDBusArgument &operator << (QDBusArgument &arg, const UBDesktopPortal::Streams & return arg; } - -const QDBusArgument &operator >> (const QDBusArgument &arg, UBDesktopPortal::Streams &streams) +const QDBusArgument &operator >> (const QDBusArgument &arg, UBScreenCastDesktopPortalWrapper::Streams &streams) { streams.clear(); arg.beginArray(); while (!arg.atEnd()) { - UBDesktopPortal::Stream stream; + UBScreenCastDesktopPortalWrapper::Stream stream; arg >> stream; streams.append(stream); } @@ -170,14 +110,14 @@ const QDBusArgument &operator >> (const QDBusArgument &arg, UBDesktopPortal::Str return arg; } -UBDesktopPortal::UBDesktopPortal(QObject* parent) +UBScreenCastDesktopPortalWrapper::UBScreenCastDesktopPortalWrapper(QObject* parent) : QObject{parent} { qDBusRegisterMetaType(); qDBusRegisterMetaType(); } -UBDesktopPortal::~UBDesktopPortal() +UBScreenCastDesktopPortalWrapper::~UBScreenCastDesktopPortalWrapper() { if (mScreencastPortal) { @@ -185,63 +125,7 @@ UBDesktopPortal::~UBDesktopPortal() } } -void UBDesktopPortal::grabScreen(QScreen* screen, const QRect& rect) -{ - mScreenRect = screen->geometry(); - mInteractiveScreenshot = false; - - if (!rect.isNull()) - { - mScreenRect = rect.translated(mScreenRect.topLeft()); - } - - qDebug() << "DesktopPortal: grabScreen" - << "screen" << (screen ? screen->name() : QStringLiteral("")) - << "geom" << mScreenRect - << "rect" << rect; - - QDBusInterface screenshotPortal("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Screenshot"); - - if (screenshotPortal.isValid()) - { - QMap options; - options["handle_token"] = createRequestToken(); - options["interactive"] = true; // required by some portals (e.g. GNOME) - mInteractiveScreenshot = true; - QDBusReply reply = screenshotPortal.call("Screenshot", "", options); - QDBusObjectPath objectPath = reply.value(); - QString path = objectPath.path(); - qDebug() << "DesktopPortal: Screenshot reply valid" << reply.isValid() << "path" << path << "options" << options; - - if (!reply.isValid()) - { - qWarning() << "DesktopPortal: Screenshot call failed" << reply.error(); - emit screenGrabbed(QPixmap{}); - return; - } - - const bool connected = QDBusConnection::sessionBus().connect( - "", - path, - "org.freedesktop.portal.Request", - "Response", - "ua{sv}", - this, - SLOT(handleScreenshotResponse(uint,QMap))); - - if (!connected) - { - qWarning() << "DesktopPortal: failed to connect to screenshot response signal"; - } - } - else - { - qDebug() << "No valid screenshot portal"; - emit screenGrabbed(QPixmap{}); - } -} - -void UBDesktopPortal::startScreenCast(bool withCursor) +void UBScreenCastDesktopPortalWrapper::startScreenCast(bool withCursor) { mWithCursor = withCursor; @@ -253,9 +137,9 @@ void UBDesktopPortal::startScreenCast(bool withCursor) } // Create ScreenCast session - QString requestToken = createRequestToken(); - QMap options; - options["session_handle_token"] = createSessionToken(); + QString requestToken = UBDesktopPortalTokenGenerator::generateToken(); + QVariantMap options; + options["session_handle_token"] = UBDesktopPortalTokenGenerator::generateToken(); options["handle_token"] = requestToken; QDBusConnection::sessionBus().connect("", mRequestPath + requestToken, "org.freedesktop.portal.Request", "Response", "ua{sv}", this, @@ -272,7 +156,7 @@ void UBDesktopPortal::startScreenCast(bool withCursor) } } -void UBDesktopPortal::stopScreenCast() +void UBScreenCastDesktopPortalWrapper::stopScreenCast() { if (mSession.isEmpty()) { @@ -295,67 +179,7 @@ void UBDesktopPortal::stopScreenCast() mSession.clear(); } -void UBDesktopPortal::handleScreenshotResponse(uint code, const QMap& results) -{ - qDebug() << "DesktopPortal: handleScreenshotResponse code" << code << "results" << results; - - QPixmap pixmap; - auto finalizeScreenshot = [this](const QPixmap& pixmap){ - if (pixmap.isNull()) - { - qDebug() << "DesktopPortal: screenshot pixmap still null, aborting"; - emit screenGrabbed(QPixmap{}); - return; - } - - QPixmap screenshot; - - if (mInteractiveScreenshot) - { - // portal already applied the selection - screenshot = pixmap; - } - else - { - QRect targetRect = mScreenRect; - targetRect.moveTo(0, 0); // portal images are local to the grab - targetRect &= pixmap.rect(); - screenshot = pixmap.copy(targetRect); - qDebug() << "DesktopPortal: cropping screenshot" << "targetRect" << targetRect; - } - - qDebug() << "DesktopPortal: emitting screenshot" - << "interactive" << mInteractiveScreenshot - << "source size" << pixmap.size() - << "final size" << screenshot.size(); - emit screenGrabbed(screenshot); - }; - - const QUrl uri(results.value("uri").toUrl()); - qDebug() << "DesktopPortal: screenshot URI" << uri; - - pixmap = loadScreenshotFromUri(uri); - - // GNOME may only place the shot on the clipboard when capturing a full screen - if (pixmap.isNull()) - { - pixmap = readClipboardScreenshot(); - } - - if (pixmap.isNull()) - { - // Clipboard might not be populated yet on some portals (e.g. GNOME fullscreen); retry shortly. - QTimer::singleShot(500, this, [this, finalizeScreenshot](){ - QPixmap delayed = readClipboardScreenshot(); - finalizeScreenshot(delayed); - }); - return; - } - - finalizeScreenshot(pixmap); -} - -void UBDesktopPortal::handleCreateSessionResponse(uint response, const QVariantMap& results) +void UBScreenCastDesktopPortalWrapper::handleCreateSessionResponse(uint response, const QVariantMap& results) { if (response != 0) { @@ -375,8 +199,8 @@ void UBDesktopPortal::handleCreateSessionResponse(uint response, const QVariantM } // Select sources - QString requestToken = createRequestToken(); - QMap options; + QString requestToken = UBDesktopPortalTokenGenerator::generateToken(); + QVariantMap options; options["multiple"] = false; options["types"] = MONITOR; options["cursor_mode"] = mWithCursor ? EMBEDDED : HIDDEN; @@ -409,7 +233,7 @@ void UBDesktopPortal::handleCreateSessionResponse(uint response, const QVariantM } } -void UBDesktopPortal::handleSelectSourcesResponse(uint response, const QVariantMap& results) +void UBScreenCastDesktopPortalWrapper::handleSelectSourcesResponse(uint response, const QVariantMap& results) { Q_UNUSED(results); @@ -429,7 +253,7 @@ void UBDesktopPortal::handleSelectSourcesResponse(uint response, const QVariantM } // Start ScreenCast - QString requestToken = createRequestToken(); + QString requestToken = UBDesktopPortalTokenGenerator::generateToken(); QMap options; options["handle_token"] = requestToken; @@ -450,7 +274,7 @@ void UBDesktopPortal::handleSelectSourcesResponse(uint response, const QVariantM showGlassPane(false); } -void UBDesktopPortal::handleStartResponse(uint response, const QVariantMap& results) +void UBScreenCastDesktopPortalWrapper::handleStartResponse(uint response, const QVariantMap& results) { // Show annotation drawing view in desktop mode after portal dialog was closed showGlassPane(true); @@ -503,7 +327,7 @@ void UBDesktopPortal::handleStartResponse(uint response, const QVariantMap& resu } // Open PipeWire Remote - QMap options; + QVariantMap options; const QDBusReply reply = portal->call("OpenPipeWireRemote", QDBusObjectPath(mSession), options); if (!reply.isValid()) @@ -518,7 +342,7 @@ void UBDesktopPortal::handleStartResponse(uint response, const QVariantMap& resu emit streamStarted(fd, stream.node_id); } -QDBusInterface* UBDesktopPortal::screencastPortal() +QDBusInterface* UBScreenCastDesktopPortalWrapper::screencastPortal() { if (!mScreencastPortal) { @@ -547,32 +371,3 @@ QDBusInterface* UBDesktopPortal::screencastPortal() emit screenCastAborted(); return nullptr; } - -QString UBDesktopPortal::createSessionToken() const -{ - static int sessionTokenCounter = 0; - - sessionTokenCounter += 1; - return QString("obsess%1").arg(sessionTokenCounter); -} - -QString UBDesktopPortal::createRequestToken() const -{ - static int requestTokenCounter = 0; - - requestTokenCounter += 1; - return QString("obreq%1").arg(requestTokenCounter); -} - -void UBDesktopPortal::showGlassPane(bool show) const -{ - if (UBApplication::applicationController->isShowingDesktop()) - { - UBApplication::applicationController->uninotesController()->drawingView()->setVisible(show); - - if (show) - { - UBPlatformUtils::keepOnTop(); - } - } -} diff --git a/src/frameworks/UBDesktopPortal.h b/src/frameworks/linux/UBScreenCastDesktopPortalWrapper.h similarity index 71% rename from src/frameworks/UBDesktopPortal.h rename to src/frameworks/linux/UBScreenCastDesktopPortalWrapper.h index 1be0c6bef..0b94a9260 100644 --- a/src/frameworks/UBDesktopPortal.h +++ b/src/frameworks/linux/UBScreenCastDesktopPortalWrapper.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2025 Département de l'Instruction Publique (DIP-SEM) + * Copyright (C) 2015-2026 Département de l'Instruction Publique (DIP-SEM) * * This file is part of OpenBoard. * @@ -23,16 +23,13 @@ #pragma once #include -#include #include #include -#include // forward class QDBusInterface; -class QScreen; -class UBDesktopPortal : public QObject +class UBScreenCastDesktopPortalWrapper : public QObject { Q_OBJECT @@ -44,38 +41,28 @@ class UBDesktopPortal : public QObject } Stream; typedef QList Streams; - explicit UBDesktopPortal(QObject* parent = nullptr); - virtual ~UBDesktopPortal(); - - void grabScreen(QScreen* screen, const QRect& rect = {}); + explicit UBScreenCastDesktopPortalWrapper(QObject* parent = nullptr); + virtual ~UBScreenCastDesktopPortalWrapper(); public slots: void startScreenCast(bool withCursor); void stopScreenCast(); signals: - void screenGrabbed(QPixmap pixmap); void streamStarted(int fd, int nodeId); void screenCastAborted(); + void showGlassPane(bool show); private slots: - void handleScreenshotResponse(uint code, const QMap& results); void handleCreateSessionResponse(uint response, const QVariantMap& results); void handleSelectSourcesResponse(uint response, const QVariantMap& results); void handleStartResponse(uint response, const QVariantMap& results); private: QDBusInterface* screencastPortal(); - QString createSessionToken() const; - QString createRequestToken() const; - void showGlassPane(bool show) const; - QPixmap loadScreenshotFromUri(const QUrl& uri) const; - QPixmap readClipboardScreenshot() const; private: - QRect mScreenRect; bool mWithCursor{false}; - bool mInteractiveScreenshot{false}; QString mSession; QString mRequestPath; uint mScreencastPortalVersion{0}; diff --git a/src/frameworks/linux/UBScreenshotDesktopPortalWrapper.cpp b/src/frameworks/linux/UBScreenshotDesktopPortalWrapper.cpp new file mode 100644 index 000000000..e7d82a381 --- /dev/null +++ b/src/frameworks/linux/UBScreenshotDesktopPortalWrapper.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2015-2026 Département de l'Instruction Publique (DIP-SEM) + * + * This file is part of OpenBoard. + * + * OpenBoard is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, + * with a specific linking exception for the OpenSSL project's + * "OpenSSL" library (or with modified versions of it that use the + * same license as the "OpenSSL" library). + * + * OpenBoard 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 General Public License + * along with OpenBoard. If not, see . + */ + + +#include "UBScreenshotDesktopPortalWrapper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "frameworks/linux/UBDesktopPortalTokenGenerator.h" + + +UBScreenshotDesktopPortalWrapper::UBScreenshotDesktopPortalWrapper(QObject* parent) + : QObject{parent} +{ +} + +UBScreenshotDesktopPortalWrapper::~UBScreenshotDesktopPortalWrapper() +{ +} + +void UBScreenshotDesktopPortalWrapper::grabScreen(QScreen* screen, bool interactive) +{ + mScreenRect = screen->geometry(); + mInteractiveScreenshot = interactive; + + qDebug() << "DesktopPortal: grabScreen" + << "screen" << (screen ? screen->name() : QStringLiteral("")) + << "geom" << mScreenRect + << "interactive" << interactive; + + QDBusInterface screenshotPortal("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Screenshot"); + + if (screenshotPortal.isValid()) + { + QMap options; + options["handle_token"] = UBDesktopPortalTokenGenerator::generateToken(); + options["interactive"] = mInteractiveScreenshot; // required by some portals (e.g. GNOME) + QDBusReply reply = screenshotPortal.call("Screenshot", "", options); + QDBusObjectPath objectPath = reply.value(); + QString path = objectPath.path(); + qDebug() << "DesktopPortal: Screenshot reply valid" << reply.isValid() << "path" << path << "options" << options; + + if (!reply.isValid()) + { + qWarning() << "DesktopPortal: Screenshot call failed" << reply.error(); + emit screenGrabbed(QPixmap{}); + return; + } + + const bool connected = QDBusConnection::sessionBus().connect( + "", + path, + "org.freedesktop.portal.Request", + "Response", + "ua{sv}", + this, + SLOT(handleScreenshotResponse(uint,QMap))); + + if (!connected) + { + qWarning() << "DesktopPortal: failed to connect to screenshot response signal"; + } + } + else + { + qDebug() << "No valid screenshot portal"; + emit screenGrabbed(QPixmap{}); + } +} + +void UBScreenshotDesktopPortalWrapper::handleScreenshotResponse(uint code, const QVariantMap& results) +{ + qDebug() << "DesktopPortal: handleScreenshotResponse code" << code << "results" << results; + + QPixmap pixmap; + auto finalizeScreenshot = [this](const QPixmap& pixmap){ + if (pixmap.isNull()) + { + qDebug() << "DesktopPortal: screenshot pixmap still null, aborting"; + emit screenGrabbed(QPixmap{}); + return; + } + + QPixmap screenshot; + + if (mInteractiveScreenshot) + { + // portal already applied the selection + screenshot = pixmap; + } + else + { + QRect targetRect = mScreenRect; + // NOTE have to disable this again. + // On KDE, the screenshot contains all screens, and I have to cut the desired screen out of this. + // What happens for non-interactive screenshots on GNOME? Is this important at all? +// targetRect.moveTo(0, 0); // portal images are local to the grab + targetRect &= pixmap.rect(); + screenshot = pixmap.copy(targetRect); + qDebug() << "DesktopPortal: cropping screenshot" << "targetRect" << targetRect; + } + + qDebug() << "DesktopPortal: emitting screenshot" + << "interactive" << mInteractiveScreenshot + << "source size" << pixmap.size() + << "final size" << screenshot.size(); + emit screenGrabbed(screenshot); + }; + + const QUrl uri(results.value("uri").toUrl()); + qDebug() << "DesktopPortal: screenshot URI" << uri; + + pixmap = loadScreenshotFromUri(uri); + + // GNOME may only place the shot on the clipboard when capturing a full screen + if (pixmap.isNull()) + { + pixmap = readClipboardScreenshot(); + } + + if (pixmap.isNull()) + { + // Clipboard might not be populated yet on some portals (e.g. GNOME fullscreen); retry shortly. + QTimer::singleShot(500, this, [this, finalizeScreenshot](){ + QPixmap delayed = readClipboardScreenshot(); + finalizeScreenshot(delayed); + }); + return; + } + + finalizeScreenshot(pixmap); +} + +QPixmap UBScreenshotDesktopPortalWrapper::loadScreenshotFromUri(const QUrl& uri) const +{ + if (uri.isEmpty()) + { + return {}; + } + + QFile file(uri.toLocalFile()); + + if (!file.exists()) + { + qDebug() << "Screenshot image file does not exist" << uri; + return {}; + } + + QPixmap pixmap{file.fileName()}; + file.remove(); + qDebug() << "DesktopPortal: loaded screenshot file" << uri; + return pixmap; +} + +QPixmap UBScreenshotDesktopPortalWrapper::readClipboardScreenshot() const +{ + const auto clipboard = QGuiApplication::clipboard(); + + if (!clipboard) + { + qDebug() << "DesktopPortal: no clipboard available"; + return {}; + } + + QImage cbImage = clipboard->image(); + + if (!cbImage.isNull()) + { + QPixmap fromImage = QPixmap::fromImage(cbImage); + qDebug() << "DesktopPortal: clipboard image found" << "size" << fromImage.size(); + return fromImage; + } + + QPixmap cbPixmap = clipboard->pixmap(); + + if (!cbPixmap.isNull()) + { + qDebug() << "DesktopPortal: clipboard pixmap found" << "size" << cbPixmap.size(); + return cbPixmap; + } + + qDebug() << "DesktopPortal: clipboard empty"; + return {}; +} diff --git a/src/frameworks/linux/UBScreenshotDesktopPortalWrapper.h b/src/frameworks/linux/UBScreenshotDesktopPortalWrapper.h new file mode 100644 index 000000000..4b37b02de --- /dev/null +++ b/src/frameworks/linux/UBScreenshotDesktopPortalWrapper.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015-2026 Département de l'Instruction Publique (DIP-SEM) + * + * This file is part of OpenBoard. + * + * OpenBoard is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, + * with a specific linking exception for the OpenSSL project's + * "OpenSSL" library (or with modified versions of it that use the + * same license as the "OpenSSL" library). + * + * OpenBoard 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 General Public License + * along with OpenBoard. If not, see . + */ + + +#pragma once + +#include +#include +#include +#include +#include + +// forward +class QScreen; + +class UBScreenshotDesktopPortalWrapper : public QObject +{ + Q_OBJECT + +public: + explicit UBScreenshotDesktopPortalWrapper(QObject* parent = nullptr); + virtual ~UBScreenshotDesktopPortalWrapper(); + +public slots: + void grabScreen(QScreen* screen, bool interactive); + +signals: + void screenGrabbed(QPixmap pixmap); + +private slots: + void handleScreenshotResponse(uint code, const QVariantMap& results); + +private: + QPixmap loadScreenshotFromUri(const QUrl& uri) const; + QPixmap readClipboardScreenshot() const; + +private: + QRect mScreenRect; + bool mInteractiveScreenshot{false}; +}; diff --git a/src/gui/UBScreenMirror.cpp b/src/gui/UBScreenMirror.cpp index 04b699094..e8c9239fd 100644 --- a/src/gui/UBScreenMirror.cpp +++ b/src/gui/UBScreenMirror.cpp @@ -35,8 +35,8 @@ #include "board/UBBoardController.h" #ifdef Q_OS_LINUX -#include "frameworks/UBDesktopPortal.h" -#include "frameworks/UBPipewireSink.h" +#include "frameworks/linux/UBScreenCastDesktopPortalWrapper.h" +#include "frameworks/linux/UBPipewireSink.h" #include "frameworks/UBPlatformUtils.h" #endif @@ -115,12 +115,14 @@ void UBScreenMirror::startScreenCast() // use UBDesktopPortal if (!mPortal) { - mPortal = new UBDesktopPortal(this); + mPortal = new UBScreenCastDesktopPortalWrapper(this); - connect(mPortal, &UBDesktopPortal::streamStarted, this, &UBScreenMirror::playStream); - connect(mPortal, &UBDesktopPortal::screenCastAborted, this, [](){ + connect(mPortal, &UBScreenCastDesktopPortalWrapper::streamStarted, this, &UBScreenMirror::playStream); + connect(mPortal, &UBScreenCastDesktopPortalWrapper::screenCastAborted, this, [](){ UBApplication::applicationController->mirroringEnabled(false); }); + connect(mPortal, &UBScreenCastDesktopPortalWrapper::showGlassPane, + UBApplication::applicationController, &UBApplicationController::showGlassPane); } mPortal->startScreenCast(true); diff --git a/src/gui/UBScreenMirror.h b/src/gui/UBScreenMirror.h index 92923c9b6..11518fce1 100644 --- a/src/gui/UBScreenMirror.h +++ b/src/gui/UBScreenMirror.h @@ -34,7 +34,7 @@ #include // forward -class UBDesktopPortal; +class UBScreenCastDesktopPortalWrapper; class UBScreenMirror : public QWidget @@ -70,7 +70,7 @@ class UBScreenMirror : public QWidget bool mIsStarted{false}; #ifdef Q_OS_LINUX - UBDesktopPortal* mPortal{nullptr}; + UBScreenCastDesktopPortalWrapper* mPortal{nullptr}; #endif }; diff --git a/src/podcast/UBPodcastController.cpp b/src/podcast/UBPodcastController.cpp index b15caa706..53c08b16b 100644 --- a/src/podcast/UBPodcastController.cpp +++ b/src/podcast/UBPodcastController.cpp @@ -64,8 +64,8 @@ #include "ffmpeg/UBFFmpegVideoEncoder.h" #include "ffmpeg/UBMicrophoneInput.h" #elif defined(Q_OS_LINUX) - #include "frameworks/UBDesktopPortal.h" - #include "frameworks/UBPipewireSink.h" + #include "frameworks/linux/UBScreenCastDesktopPortalWrapper.h" + #include "frameworks/linux/UBPipewireSink.h" #include "ffmpeg/UBFFmpegVideoEncoder.h" #include "ffmpeg/UBMicrophoneInput.h" #endif @@ -683,21 +683,23 @@ void UBPodcastController::applicationDesktopMode(bool displayed) // create a ScreenCast session if (!mPortal) { - mPortal = new UBDesktopPortal{this}; + mPortal = new UBScreenCastDesktopPortalWrapper{this}; // delete portal if screencast was aborted - connect(mPortal, &UBDesktopPortal::screenCastAborted, this, [this](){ + connect(mPortal, &UBScreenCastDesktopPortalWrapper::screenCastAborted, this, [this](){ mPortal->deleteLater(); mPortal = nullptr; }); + connect(mPortal, &UBScreenCastDesktopPortalWrapper::showGlassPane, + UBApplication::applicationController, &UBApplicationController::showGlassPane); } if (mRecordingState == Recording) { mPipewireSink = new UBPipewireSink{this}; - disconnect(mPortal, &UBDesktopPortal::streamStarted, mPortal, &UBDesktopPortal::stopScreenCast); - connect(mPortal, &UBDesktopPortal::streamStarted, mPipewireSink, &UBPipewireSink::start); + disconnect(mPortal, &UBScreenCastDesktopPortalWrapper::streamStarted, mPortal, &UBScreenCastDesktopPortalWrapper::stopScreenCast); + connect(mPortal, &UBScreenCastDesktopPortalWrapper::streamStarted, mPipewireSink, &UBPipewireSink::start); connect(mPipewireSink, &UBPipewireSink::gotImage, this, [this](QImage image){ if (mPipewireSink) { @@ -708,7 +710,7 @@ void UBPodcastController::applicationDesktopMode(bool displayed) else { // stop stream, we just selected the screen - connect(mPortal, &UBDesktopPortal::streamStarted, mPortal, &UBDesktopPortal::stopScreenCast); + connect(mPortal, &UBScreenCastDesktopPortalWrapper::streamStarted, mPortal, &UBScreenCastDesktopPortalWrapper::stopScreenCast); } mPortal->startScreenCast(true); diff --git a/src/podcast/UBPodcastController.h b/src/podcast/UBPodcastController.h index 89143892b..3c8e9a058 100644 --- a/src/podcast/UBPodcastController.h +++ b/src/podcast/UBPodcastController.h @@ -40,7 +40,7 @@ class UBGraphicsScene; class WebView; class UBPodcastRecordingPalette; -class UBDesktopPortal; +class UBScreenCastDesktopPortalWrapper; class UBPipewireSink; @@ -192,7 +192,7 @@ class UBPodcastController : public QObject QString mPodcastRecordingPath; #ifdef Q_OS_LINUX - UBDesktopPortal* mPortal{nullptr}; + UBScreenCastDesktopPortalWrapper* mPortal{nullptr}; UBPipewireSink* mPipewireSink{nullptr}; #endif };