diff --git a/CMakeLists.txt b/CMakeLists.txt index 37d24da..cb8c882 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,42 +1,64 @@ project(xdg-desktop-portal-kde) cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR) set(QT_MIN_VERSION "5.4.0") set(PROJECT_VERSION "5.12.80") set(PROJECT_VERSION_MAJOR 5) ################# set KDE specific information ################# find_package(ECM 1.3.0 REQUIRED NO_MODULE) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMPackageConfigHelpers) include(ECMOptionalAddSubdirectory) include(FeatureSummary) +include(FindPkgConfig) + +# remove? +pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.1) +pkg_check_modules(SPA REQUIRED libspa-0.1) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +find_package(gbm REQUIRED) +set_package_properties(gbm PROPERTIES DESCRIPTION "GBM - Generic Buffer Management" + TYPE REQUIRED + PURPOSE "Required for egl ouput of drm backend." +) + +find_package(epoxy REQUIRED) +include_directories(${epoxy_INCLUDE_DIRS}) +set_package_properties(epoxy PROPERTIES DESCRIPTION "libepoxy" + URL "http://github.com/anholt/libepoxy" + TYPE REQUIRED + PURPOSE "OpenGL dispatch library for GBM backend" +) + find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core DBus PrintSupport Widgets ) find_package(KF5 REQUIRED CoreAddons I18n Notifications + Wayland ) -add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0 -DQT_NO_KEYWORDS) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0) add_definitions(-DQT_USE_FAST_CONCATENATION -DQT_USE_FAST_OPERATOR_PLUS) remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY) add_subdirectory(data) add_subdirectory(src) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/cmake/modules/Findepoxy.cmake b/cmake/modules/Findepoxy.cmake new file mode 100644 index 0000000..dfd8c3c --- /dev/null +++ b/cmake/modules/Findepoxy.cmake @@ -0,0 +1,56 @@ +# - Try to find libepoxy +# Once done this will define +# +# epoxy_FOUND - System has libepoxy +# epoxy_LIBRARY - The libepoxy library +# epoxy_INCLUDE_DIR - The libepoxy include dir +# epoxy_DEFINITIONS - Compiler switches required for using libepoxy +# epoxy_HAS_GLX - Whether GLX support is available + +# Copyright (c) 2014 Fredrik Höglund +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the University nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +if (NOT WIN32) + find_package(PkgConfig) + pkg_check_modules(PKG_epoxy QUIET epoxy) + + set(epoxy_DEFINITIONS ${PKG_epoxy_CFLAGS}) + + find_path(epoxy_INCLUDE_DIR NAMES epoxy/gl.h HINTS ${PKG_epoxy_INCLUDEDIR} ${PKG_epoxy_INCLUDE_DIRS}) + find_library(epoxy_LIBRARY NAMES epoxy HINTS ${PKG_epoxy_LIBDIR} ${PKG_epoxy_LIBRARY_DIRS}) + find_file(epoxy_GLX_HEADER NAMES epoxy/glx.h HINTS ${epoxy_INCLUDE_DIR}) + + if (epoxy_GLX_HEADER STREQUAL "epoxy_GLX_HEADER-NOTFOUND") + set(epoxy_HAS_GLX FALSE CACHE BOOL "whether glx is available") + else () + set(epoxy_HAS_GLX TRUE CACHE BOOL "whether glx is available") + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(epoxy DEFAULT_MSG epoxy_LIBRARY epoxy_INCLUDE_DIR) + + mark_as_advanced(epoxy_INCLUDE_DIR epoxy_LIBRARY epoxy_HAS_GLX) +endif() diff --git a/cmake/modules/Findgbm.cmake b/cmake/modules/Findgbm.cmake new file mode 100644 index 0000000..6dfc895 --- /dev/null +++ b/cmake/modules/Findgbm.cmake @@ -0,0 +1,125 @@ +#.rst: +# Findgbm +# ------- +# +# Try to find gbm on a Unix system. +# +# This will define the following variables: +# +# ``gbm_FOUND`` +# True if (the requested version of) gbm is available +# ``gbm_VERSION`` +# The version of gbm +# ``gbm_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``gbm::gbm`` +# target +# ``gbm_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``gbm_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``gbm_FOUND`` is TRUE, it will also define the following imported target: +# +# ``gbm::gbm`` +# The gbm library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by Findgbm.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use Findgbm.cmake") +endif() + +if(NOT WIN32) + # Use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + pkg_check_modules(PKG_gbm QUIET gbm) + + set(gbm_DEFINITIONS ${PKG_gbm_CFLAGS_OTHER}) + set(gbm_VERSION ${PKG_gbm_VERSION}) + + find_path(gbm_INCLUDE_DIR + NAMES + gbm.h + HINTS + ${PKG_gbm_INCLUDE_DIRS} + ) + find_library(gbm_LIBRARY + NAMES + gbm + HINTS + ${PKG_gbm_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(gbm + FOUND_VAR + gbm_FOUND + REQUIRED_VARS + gbm_LIBRARY + gbm_INCLUDE_DIR + VERSION_VAR + gbm_VERSION + ) + + if(gbm_FOUND AND NOT TARGET gbm::gbm) + add_library(gbm::gbm UNKNOWN IMPORTED) + set_target_properties(gbm::gbm PROPERTIES + IMPORTED_LOCATION "${gbm_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${gbm_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${gbm_INCLUDE_DIR}" + ) + endif() + + mark_as_advanced(gbm_LIBRARY gbm_INCLUDE_DIR) + + # compatibility variables + set(gbm_LIBRARIES ${gbm_LIBRARY}) + set(gbm_INCLUDE_DIRS ${gbm_INCLUDE_DIR}) + set(gbm_VERSION_STRING ${gbm_VERSION}) + +else() + message(STATUS "Findgbm.cmake cannot find gbm on Windows systems.") + set(gbm_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(gbm PROPERTIES + URL "http://www.mesa3d.org" + DESCRIPTION "Mesa gbm library." +) diff --git a/data/kde.portal b/data/kde.portal index 5f3d143..fc25232 100644 --- a/data/kde.portal +++ b/data/kde.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.kde -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Print +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.ScreenCast UseIn=KDE diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e669ae3..2064e25 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,39 +1,52 @@ add_definitions(-DTRANSLATION_DOMAIN="xdg-desktop-portal-kde") include_directories( ${Qt5PrintSupport_PRIVATE_INCLUDE_DIRS} + ${PIPEWIRE_INCLUDE_DIRS} ${SPA_INCLUDE_DIRS} ${GLIB_INCLUDE_DIRS} + ${gbm_INCLUDE_DIRS} ) set(xdg_desktop_portal_kde_SRCS xdg-desktop-portal-kde.cpp access.cpp accessdialog.cpp appchooser.cpp appchooserdialog.cpp desktopportal.cpp email.cpp filechooser.cpp inhibit.cpp notification.cpp print.cpp request.cpp + screencast.cpp + screencaststream.cpp + screenchooserdialog.cpp + session.cpp ) ki18n_wrap_ui(xdg_desktop_portal_kde_SRCS accessdialog.ui appchooserdialog.ui + screenchooserdialog.ui ) add_executable(xdg-desktop-portal-kde ${xdg_desktop_portal_kde_SRCS}) target_link_libraries(xdg-desktop-portal-kde Qt5::Core Qt5::DBus Qt5::PrintSupport Qt5::Widgets KF5::CoreAddons KF5::I18n KF5::Notifications + KF5::WaylandClient + ${PIPEWIRE_LIBRARIES} + ${SPA_LIBRARIES} + ${GLIB_LIBRARIES} + ${epoxy_LIBRARY} + gbm::gbm ) install(TARGETS xdg-desktop-portal-kde DESTINATION ${KDE_INSTALL_LIBEXECDIR}) diff --git a/src/access.cpp b/src/access.cpp index e467a1f..70359b9 100644 --- a/src/access.cpp +++ b/src/access.cpp @@ -1,86 +1,86 @@ /* * Copyright © 2017 Red Hat, Inc * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "access.h" #include "accessdialog.h" #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAccess, "xdg-desktop-portal-kde-access") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAccess, "xdp-kde-access") AccessPortal::AccessPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { } AccessPortal::~AccessPortal() { } uint AccessPortal::AccessDialog(const QDBusObjectPath &handle, const QString &app_id, const QString &parent_window, const QString &title, const QString &subtitle, const QString &body, const QVariantMap &options, QVariantMap &results) { qCDebug(XdgDesktopPortalKdeAccess) << "AccessDialog called with parameters:"; qCDebug(XdgDesktopPortalKdeAccess) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeAccess) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeAccess) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdeAccess) << " title: " << title; qCDebug(XdgDesktopPortalKdeAccess) << " subtitle: " << subtitle; qCDebug(XdgDesktopPortalKdeAccess) << " body: " << body; qCDebug(XdgDesktopPortalKdeAccess) << " options: " << options; auto accessDialog = new ::AccessDialog(); accessDialog->setBody(body); accessDialog->setTitle(title); accessDialog->setSubtitle(subtitle); if (options.contains(QLatin1String("modal"))) { accessDialog->setModal(options.value(QLatin1String("modal")).toBool()); } if (options.contains(QLatin1String("deny_label"))) { accessDialog->setRejectLabel(options.value(QLatin1String("deny_label")).toString()); } if (options.contains(QLatin1String("grant_label"))) { accessDialog->setAcceptLabel(options.value(QLatin1String("grant_label")).toString()); } if (options.contains(QLatin1String("icon"))) { accessDialog->setIcon(options.value(QLatin1String("icon")).toString()); } // TODO choices if (accessDialog->exec()) { accessDialog->deleteLater(); return 0; } accessDialog->deleteLater(); return 1; } diff --git a/src/accessdialog.cpp b/src/accessdialog.cpp index 01228ee..b56ace4 100644 --- a/src/accessdialog.cpp +++ b/src/accessdialog.cpp @@ -1,80 +1,80 @@ /* * Copyright © 2017 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "accessdialog.h" #include "ui_accessdialog.h" #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAccessDialog, "xdg-desktop-portal-kde-access-dialog") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAccessDialog, "xdp-kde-access-dialog") AccessDialog::AccessDialog(QDialog *parent, Qt::WindowFlags flags) : QDialog(parent, flags) , m_dialog(new Ui::AccessDialog) { m_dialog->setupUi(this); connect(m_dialog->buttonBox, &QDialogButtonBox::accepted, this, &AccessDialog::accept); connect(m_dialog->buttonBox, &QDialogButtonBox::rejected, this, &AccessDialog::reject); m_dialog->iconLabel->setPixmap(QIcon::fromTheme(QLatin1String("dialog-question")).pixmap(QSize(64, 64))); setWindowTitle(i18n("Request device access")); } AccessDialog::~AccessDialog() { delete m_dialog; } void AccessDialog::setAcceptLabel(const QString &label) { m_dialog->buttonBox->button(QDialogButtonBox::Ok)->setText(label); } void AccessDialog::setBody(const QString &body) { m_dialog->bodyLabel->setText(body); } void AccessDialog::setIcon(const QString &icon) { m_dialog->iconLabel->setPixmap(QIcon::fromTheme(icon).pixmap(QSize(64, 64))); } void AccessDialog::setRejectLabel(const QString &label) { m_dialog->buttonBox->button(QDialogButtonBox::Cancel)->setText(label); } void AccessDialog::setSubtitle(const QString &subtitle) { m_dialog->subtitleLabel->setText(subtitle); } void AccessDialog::setTitle(const QString &title) { m_dialog->titleLabel->setText(title); } diff --git a/src/appchooser.cpp b/src/appchooser.cpp index 0494166..ea5e174 100644 --- a/src/appchooser.cpp +++ b/src/appchooser.cpp @@ -1,73 +1,73 @@ /* * Copyright © 2016 Red Hat, Inc * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "appchooser.h" #include "appchooserdialog.h" #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAppChooser, "xdg-desktop-portal-kde-app-chooser") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAppChooser, "xdp-kde-app-chooser") AppChooserPortal::AppChooserPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { } AppChooserPortal::~AppChooserPortal() { } uint AppChooserPortal::ChooseApplication(const QDBusObjectPath &handle, const QString &app_id, const QString &parent_window, const QStringList &choices, const QVariantMap &options, QVariantMap &results) { qCDebug(XdgDesktopPortalKdeAppChooser) << "ChooseApplication called with parameters:"; qCDebug(XdgDesktopPortalKdeAppChooser) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeAppChooser) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeAppChooser) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdeAppChooser) << " choices: " << choices; qCDebug(XdgDesktopPortalKdeAppChooser) << " options: " << options; QString latestChoice; if (options.contains(QLatin1String("last_choice"))) { latestChoice = options.value(QLatin1String("last_choice")).toString(); } AppChooserDialog *appDialog = new AppChooserDialog(choices); if (!latestChoice.isEmpty()) { appDialog->setSelectedApplication(latestChoice); } if (appDialog->exec()) { results.insert(QLatin1String("choice"), appDialog->selectedApplication()); appDialog->deleteLater(); return 0; } appDialog->deleteLater(); return 1; } diff --git a/src/appchooserdialog.cpp b/src/appchooserdialog.cpp index 29c32f2..c590df4 100644 --- a/src/appchooserdialog.cpp +++ b/src/appchooserdialog.cpp @@ -1,100 +1,100 @@ /* * Copyright © 2017 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "appchooserdialog.h" #include "ui_appchooserdialog.h" #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAppChooserDialog, "xdg-desktop-portal-kde-app-chooser-dialog") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAppChooserDialog, "xdp-kde-app-chooser-dialog") AppChooserDialog::AppChooserDialog(const QStringList &choices, QDialog *parent, Qt::WindowFlags flags) : QDialog(parent, flags) , m_dialog(new Ui::AppChooserDialog) , m_choices(choices) { m_dialog->setupUi(this); Q_FOREACH (const QString &choice, m_choices) { const QString desktopFile = choice + QLatin1String(".desktop"); const QStringList desktopFilesLocations = QStandardPaths::locateAll(QStandardPaths::ApplicationsLocation, desktopFile, QStandardPaths::LocateFile); Q_FOREACH (const QString desktopFile, desktopFilesLocations) { QString applicationIcon; QString applicationName; QSettings settings(desktopFile, QSettings::IniFormat); settings.beginGroup(QLatin1String("Desktop Entry")); if (settings.contains(QLatin1String("X-GNOME-FullName"))) { applicationName = settings.value(QLatin1String("X-GNOME-FullName")).toString(); } else { applicationName = settings.value(QLatin1String("Name")).toString(); } applicationIcon = settings.value(QLatin1String("Icon")).toString(); QListWidgetItem *widgetItem = new QListWidgetItem(m_dialog->appView); widgetItem->setData(Qt::UserRole, QVariant::fromValue(choice)); // FIXME GTK icons will work only with Qt 5.7 widgetItem->setIcon(QIcon::fromTheme(applicationIcon)); widgetItem->setText(applicationName); } } connect(m_dialog->buttonBox, &QDialogButtonBox::accepted, this, &AppChooserDialog::accept); connect(m_dialog->buttonBox, &QDialogButtonBox::rejected, this, &AppChooserDialog::reject); connect(m_dialog->appView, &QListWidget::itemDoubleClicked, this, &AppChooserDialog::accept); connect(m_dialog->searchEdit, &QLineEdit::textChanged, this, &AppChooserDialog::searchTextChanged); m_dialog->buttonBox->button(QDialogButtonBox::Ok)->setText(i18n("Select")); setWindowTitle(i18n("Select application")); } AppChooserDialog::~AppChooserDialog() { delete m_dialog; } QString AppChooserDialog::selectedApplication() const { return m_dialog->appView->currentItem()->data(Qt::UserRole).toString(); } void AppChooserDialog::setSelectedApplication(const QString &applicationName) { for (int i = 0; i < m_dialog->appView->count(); i++) { QListWidgetItem *widgetItem = m_dialog->appView->item(i); if (widgetItem->data(Qt::UserRole).toString() == applicationName) { m_dialog->appView->setCurrentItem(widgetItem, QItemSelectionModel::Select); } } } void AppChooserDialog::searchTextChanged(const QString &text) { for (int i = 0; i < m_dialog->appView->count(); i++) { QListWidgetItem *widgetItem = m_dialog->appView->item(i); if (text.isEmpty()) { widgetItem->setHidden(false); } else { widgetItem->setHidden(!widgetItem->text().toLower().contains(text.toLower())); } } } diff --git a/src/desktopportal.cpp b/src/desktopportal.cpp index 49881c9..1bc57c1 100644 --- a/src/desktopportal.cpp +++ b/src/desktopportal.cpp @@ -1,45 +1,46 @@ /* * Copyright © 2016 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "desktopportal.h" #include #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeDesktopPortal, "xdg-desktop-portal-kde-desktop-portal") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeDesktopPortal, "xdp-kde-desktop-portal") DesktopPortal::DesktopPortal(QObject *parent) : QObject(parent) , m_access(new AccessPortal(this)) , m_appChooser(new AppChooserPortal(this)) , m_email(new EmailPortal(this)) , m_fileChooser(new FileChooserPortal(this)) , m_inhibit(new InhibitPortal(this)) , m_notification(new NotificationPortal(this)) , m_print(new PrintPortal(this)) + , m_screenCast(new ScreenCastPortal(this)) { } DesktopPortal::~DesktopPortal() { } diff --git a/src/desktopportal.h b/src/desktopportal.h index 0fe1b4c..409f98d 100644 --- a/src/desktopportal.h +++ b/src/desktopportal.h @@ -1,53 +1,55 @@ /* * Copyright © 2016 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #ifndef XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H #define XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H #include #include #include "access.h" #include "appchooser.h" #include "email.h" #include "filechooser.h" #include "inhibit.h" #include "notification.h" #include "print.h" +#include "screencast.h" class DesktopPortal : public QObject { Q_OBJECT public: explicit DesktopPortal(QObject *parent = nullptr); ~DesktopPortal(); private: AccessPortal *m_access; AppChooserPortal *m_appChooser; EmailPortal *m_email; FileChooserPortal *m_fileChooser; InhibitPortal *m_inhibit; NotificationPortal *m_notification; PrintPortal *m_print; + ScreenCastPortal *m_screenCast; }; #endif // XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H diff --git a/src/email.cpp b/src/email.cpp index 397cd4e..e597b4a 100644 --- a/src/email.cpp +++ b/src/email.cpp @@ -1,60 +1,60 @@ /* * Copyright © 2017 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "email.h" #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeEmail, "xdg-desktop-portal-kde-email") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeEmail, "xdp-kde-email") EmailPortal::EmailPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { } EmailPortal::~EmailPortal() { } uint EmailPortal::ComposeEmail(const QDBusObjectPath &handle, const QString &app_id, const QString &window, const QVariantMap &options, QVariantMap &results) { Q_UNUSED(results) qCDebug(XdgDesktopPortalKdeEmail) << "ComposeEmail called with parameters:"; qCDebug(XdgDesktopPortalKdeEmail) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeEmail) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeEmail) << " window: " << window; qCDebug(XdgDesktopPortalKdeEmail) << " options: " << options; QString attachmentString; const QStringList attachments = options.value(QLatin1String("attachments")).toStringList(); Q_FOREACH (const QString &attachment, attachments) { attachmentString += QStringLiteral("&attachment=%1").arg(attachment); } const QString mailtoUrl = QStringLiteral("mailto:%1?subject=%2&body=%3%4").arg(options.value(QLatin1String("address")).toString()) .arg(options.value(QLatin1String("subject")).toString()) .arg(options.value(QLatin1String("body")).toString()) .arg(attachmentString); return QDesktopServices::openUrl(QUrl(mailtoUrl)); } diff --git a/src/filechooser.cpp b/src/filechooser.cpp index 0938705..69d4984 100644 --- a/src/filechooser.cpp +++ b/src/filechooser.cpp @@ -1,291 +1,291 @@ /* * Copyright © 2016 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "filechooser.h" #include #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeFileChooser, "xdg-desktop-portal-kde-file-chooser") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeFileChooser, "xdp-kde-file-chooser") // Keep in sync with qflatpakfiledialog from flatpak-platform-plugin Q_DECLARE_METATYPE(FileChooserPortal::Filter); Q_DECLARE_METATYPE(FileChooserPortal::Filters); Q_DECLARE_METATYPE(FileChooserPortal::FilterList); Q_DECLARE_METATYPE(FileChooserPortal::FilterListList); QDBusArgument &operator << (QDBusArgument &arg, const FileChooserPortal::Filter &filter) { arg.beginStructure(); arg << filter.type << filter.filterString; arg.endStructure(); return arg; } const QDBusArgument &operator >> (const QDBusArgument &arg, FileChooserPortal::Filter &filter) { uint type; QString filterString; arg.beginStructure(); arg >> type >> filterString; filter.type = type; filter.filterString = filterString; arg.endStructure(); return arg; } QDBusArgument &operator << (QDBusArgument &arg, const FileChooserPortal::FilterList &filterList) { arg.beginStructure(); arg << filterList.userVisibleName << filterList.filters; arg.endStructure(); return arg; } const QDBusArgument &operator >> (const QDBusArgument &arg, FileChooserPortal::FilterList &filterList) { QString userVisibleName; FileChooserPortal::Filters filters; arg.beginStructure(); arg >> userVisibleName >> filters; filterList.userVisibleName = userVisibleName; filterList.filters = filters; arg.endStructure(); return arg; } FileChooserPortal::FileChooserPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); } FileChooserPortal::~FileChooserPortal() { } uint FileChooserPortal::OpenFile(const QDBusObjectPath &handle, const QString &app_id, const QString &parent_window, const QString &title, const QVariantMap &options, QVariantMap &results) { Q_UNUSED(app_id); qCDebug(XdgDesktopPortalKdeFileChooser) << "OpenFile called with parameters:"; qCDebug(XdgDesktopPortalKdeFileChooser) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeFileChooser) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdeFileChooser) << " title: " << title; qCDebug(XdgDesktopPortalKdeFileChooser) << " options: " << options; bool modalDialog = true; bool multipleFiles = false; QString acceptLabel; QStringList nameFilters; QStringList mimeTypeFilters; /* TODO * choices a(ssa(ss)s) * List of serialized combo boxes to add to the file chooser. * * For each element, the first string is an ID that will be returned with the response, te second string is a user-visible label. * The a(ss) is the list of choices, each being a is an ID and a user-visible label. The final string is the initial selection, * or "", to let the portal decide which choice will be initially selected. None of the strings, except for the initial selection, should be empty. * * As a special case, passing an empty array for the list of choices indicates a boolean choice that is typically displayed as a check button, using "true" and "false" as the choices. * Example: [('encoding', 'Encoding', [('utf8', 'Unicode (UTF-8)'), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false')] */ if (options.contains(QLatin1String("accept_label"))) { acceptLabel = options.value(QLatin1String("accept_label")).toString(); } if (options.contains(QLatin1String("modal"))) { modalDialog = options.value(QLatin1String("modal")).toBool(); } if (options.contains(QLatin1String("multiple"))) { multipleFiles = options.value(QLatin1String("multiple")).toBool(); } if (options.contains(QLatin1String("filters"))) { FilterListList filterListList = qdbus_cast(options.value(QLatin1String("filters"))); Q_FOREACH (const FilterList &filterList, filterListList) { QStringList filterStrings; Q_FOREACH (const Filter &filterStruct, filterList.filters) { if (filterStruct.type == 0) { filterStrings << filterStruct.filterString; } else { mimeTypeFilters << filterStruct.filterString; } } if (!filterStrings.isEmpty()) { nameFilters << QString("%1 (%2)").arg(filterList.userVisibleName).arg(filterStrings.join(QLatin1String(" "))); } } } QFileDialog *fileDialog = new QFileDialog(); fileDialog->setWindowTitle(title); fileDialog->setModal(modalDialog); fileDialog->setFileMode(multipleFiles ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile); fileDialog->setLabelText(QFileDialog::Accept, !acceptLabel.isEmpty() ? acceptLabel : i18n("Open")); if (!nameFilters.isEmpty()) { fileDialog->setNameFilters(nameFilters); } if (!mimeTypeFilters.isEmpty()) { fileDialog->setMimeTypeFilters(mimeTypeFilters); } if (fileDialog->exec() == QDialog::Accepted) { QStringList files; Q_FOREACH (const QString &filename, fileDialog->selectedFiles()) { QUrl url = QUrl::fromLocalFile(filename); files << url.toDisplayString(); } results.insert(QLatin1String("uris"), files); fileDialog->deleteLater(); return 0; } fileDialog->deleteLater(); return 1; } uint FileChooserPortal::SaveFile(const QDBusObjectPath &handle, const QString &app_id, const QString &parent_window, const QString &title, const QVariantMap &options, QVariantMap &results) { Q_UNUSED(app_id); qCDebug(XdgDesktopPortalKdeFileChooser) << "SaveFile called with parameters:"; qCDebug(XdgDesktopPortalKdeFileChooser) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeFileChooser) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdeFileChooser) << " title: " << title; qCDebug(XdgDesktopPortalKdeFileChooser) << " options: " << options; bool modalDialog = true; QString acceptLabel; QString currentName; QString currentFolder; QString currentFile; QStringList nameFilters; QStringList mimeTypeFilters; // TODO parse options - choices if (options.contains(QLatin1String("modal"))) { modalDialog = options.value(QLatin1String("modal")).toBool(); } if (options.contains(QLatin1String("accept_label"))) { acceptLabel = options.value(QLatin1String("accept_label")).toString(); } if (options.contains(QLatin1String("current_name"))) { currentName = options.value(QLatin1String("current_name")).toString(); } if (options.contains(QLatin1String("current_folder"))) { currentFolder = options.value(QLatin1String("current_folder")).toByteArray(); } if (options.contains(QLatin1String("current_file"))) { currentFile = options.value(QLatin1String("current_file")).toByteArray(); } if (options.contains(QLatin1String("filters"))) { FilterListList filterListList = qdbus_cast(options.value(QLatin1String("filters"))); Q_FOREACH (const FilterList &filterList, filterListList) { QStringList filterStrings; Q_FOREACH (const Filter &filterStruct, filterList.filters) { if (filterStruct.type == 0) { filterStrings << filterStruct.filterString; } else { mimeTypeFilters << filterStruct.filterString; } } if (!filterStrings.isEmpty()) { nameFilters << QString("%1 (%2)").arg(filterList.userVisibleName).arg(filterStrings.join(QLatin1String(" "))); } } } QFileDialog *fileDialog = new QFileDialog(); fileDialog->setWindowTitle(title); fileDialog->setModal(modalDialog); fileDialog->setAcceptMode(QFileDialog::AcceptSave); // TODO: Looks Qt doesn't have API for this // if (!currentName.isEmpty()) { // fileDialog->selectFile(currentName); // } if (!currentFolder.isEmpty()) { fileDialog->setDirectoryUrl(QUrl(currentFolder)); } if (!currentFile.isEmpty()) { fileDialog->selectFile(currentFile); } if (!acceptLabel.isEmpty()) { fileDialog->setLabelText(QFileDialog::Accept, acceptLabel); } if (!nameFilters.isEmpty()) { fileDialog->setNameFilters(nameFilters); } if (!mimeTypeFilters.isEmpty()) { fileDialog->setMimeTypeFilters(mimeTypeFilters); } if (fileDialog->exec() == QDialog::Accepted) { QStringList files; Q_FOREACH (const QString &filename, fileDialog->selectedFiles()) { QUrl url = QUrl::fromLocalFile(filename); files << url.toDisplayString(); } results.insert(QLatin1String("uris"), files); fileDialog->deleteLater(); return 0; } fileDialog->deleteLater(); return 1; } diff --git a/src/inhibit.cpp b/src/inhibit.cpp index 3bad185..6af47b9 100644 --- a/src/inhibit.cpp +++ b/src/inhibit.cpp @@ -1,80 +1,80 @@ /* * Copyright © 2017 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "inhibit.h" #include "request.h" #include #include #include #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeInhibit, "xdg-desktop-portal-kde-inhibit") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeInhibit, "xdp-kde-inhibit") InhibitPortal::InhibitPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { } InhibitPortal::~InhibitPortal() { } void InhibitPortal::Inhibit(const QDBusObjectPath &handle, const QString &app_id, const QString &window, uint flags, const QVariantMap &options) { qCDebug(XdgDesktopPortalKdeInhibit) << "Inhibit called with parameters:"; qCDebug(XdgDesktopPortalKdeInhibit) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdeInhibit) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeInhibit) << " window: " << window; qCDebug(XdgDesktopPortalKdeInhibit) << " flags: " << flags; qCDebug(XdgDesktopPortalKdeInhibit) << " options: " << options; QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.kde.Solid.PowerManagement"), QLatin1String("/org/kde/Solid/PowerManagement/PolicyAgent"), QLatin1String("org.kde.Solid.PowerManagement.PolicyAgent"), QLatin1String("AddInhibition")); // interrupt session (1) message << (uint)1 << app_id << options.value(QLatin1String("reason")).toString(); QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); connect(watcher, &QDBusPendingCallWatcher::finished, [handle, this] (QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; if (reply.isError()) { qCDebug(XdgDesktopPortalKdeInhibit) << "Inhibition error: " << reply.error().message(); } else { QDBusConnection sessionBus = QDBusConnection::sessionBus(); Request *request = new Request(this, QLatin1String("org.freedesktop.impl.portal.Inhibit"), QVariant(reply.value())); if (sessionBus.registerVirtualObject(handle.path(), request, QDBusConnection::VirtualObjectRegisterOption::SubPath)) { connect(request, &Request::closeRequested, [request, handle] () { QDBusConnection::sessionBus().unregisterObject(handle.path()); request->deleteLater(); }); } else { qCDebug(XdgDesktopPortalKdeInhibit) << sessionBus.lastError().message(); qCDebug(XdgDesktopPortalKdeInhibit) << "Failed to register request object with inhibition"; request->deleteLater(); } } }); } diff --git a/src/notification.cpp b/src/notification.cpp index f052707..046008e 100644 --- a/src/notification.cpp +++ b/src/notification.cpp @@ -1,144 +1,144 @@ /* * Copyright © 2016 Red Hat, Inc * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "notification.h" #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeNotification, "xdg-desktop-portal-kde-notification") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeNotification, "xdp-kde-notification") NotificationPortal::NotificationPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { } NotificationPortal::~NotificationPortal() { } void NotificationPortal::AddNotification(const QString &app_id, const QString &id, const QVariantMap ¬ification) { qCDebug(XdgDesktopPortalKdeNotification) << "AddNotification called with parameters:"; qCDebug(XdgDesktopPortalKdeNotification) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeNotification) << " id: " << id; qCDebug(XdgDesktopPortalKdeNotification) << " notification: " << notification; // We have to use "notification" as an ID because any other ID will not be configured KNotification *notify = new KNotification(QLatin1String("notification"), KNotification::CloseOnTimeout | KNotification::DefaultEvent, this); if (notification.contains(QLatin1String("title"))) { notify->setTitle(notification.value(QLatin1String("title")).toString()); } if (notification.contains(QLatin1String("body"))) { notify->setText(notification.value(QLatin1String("body")).toString()); } if (notification.contains(QLatin1String("icon"))) { notify->setIconName(notification.value(QLatin1String("icon")).toString()); } if (notification.contains(QLatin1String("priority"))) { // TODO KNotification has no option for priority } if (notification.contains(QLatin1String("default-action"))) { // TODO KNotification has no option for default action } if (notification.contains(QLatin1String("default-action-target"))) { // TODO KNotification has no option for default action } if (notification.contains(QLatin1String("buttons"))) { QList buttons; QDBusArgument dbusArgument = notification.value(QLatin1String("buttons")).value(); while (!dbusArgument.atEnd()) { dbusArgument >> buttons; } QStringList actions; Q_FOREACH (const QVariantMap &button, buttons) { actions << button.value(QLatin1String("label")).toString(); } if (!actions.isEmpty()) { notify->setActions(actions); } } notify->setProperty("app_id", app_id); notify->setProperty("id", id); connect(notify, static_cast(&KNotification::activated), this, &NotificationPortal::notificationActivated); connect(notify, &KNotification::closed, this, &NotificationPortal::notificationClosed); notify->sendEvent(); m_notifications.insert(QString("%1:%2").arg(app_id, id), notify); } void NotificationPortal::notificationActivated(uint action) { KNotification *notify = qobject_cast(sender()); if (!notify) { return; } const QString appId = notify->property("app_id").toString(); const QString id = notify->property("id").toString(); qCDebug(XdgDesktopPortalKdeNotification) << "Notification activated:"; qCDebug(XdgDesktopPortalKdeNotification) << " app_id: " << appId; qCDebug(XdgDesktopPortalKdeNotification) << " id: " << id; qCDebug(XdgDesktopPortalKdeNotification) << " action: " << action; QDBusMessage message = QDBusMessage::createSignal(QLatin1String("/org/freedesktop/portal/desktop"), QLatin1String("org.freedesktop.impl.portal.Notification"), QLatin1String("ActionInvoked")); message << appId << id << QString::number(action) << QVariantList(); QDBusConnection::sessionBus().send(message); } void NotificationPortal::RemoveNotification(const QString &app_id, const QString &id) { qCDebug(XdgDesktopPortalKdeNotification) << "RemoveNotification called with parameters:"; qCDebug(XdgDesktopPortalKdeNotification) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdeNotification) << " id: " << id; KNotification *notify = m_notifications.take(QString("%1:%2").arg(app_id, id)); if (notify) { notify->close(); notify->deleteLater(); } } void NotificationPortal::notificationClosed() { KNotification *notify = qobject_cast(sender()); if (!notify) { return; } const QString appId = notify->property("app_id").toString(); const QString id = notify->property("id").toString(); m_notifications.remove(QString("%1:%2").arg(appId, id)); notify->deleteLater(); } diff --git a/src/print.cpp b/src/print.cpp index d797007..2fca78f 100644 --- a/src/print.cpp +++ b/src/print.cpp @@ -1,977 +1,977 @@ /* * Copyright © 2016-2017 Jan Grulich * Copyright © 2007,2010 by John Layt * Copyright © 2007 Alex Merry * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "print.h" #include #include #include #include #include #include #include // #include #include #include #include -Q_LOGGING_CATEGORY(XdgDesktopPortalKdePrint, "xdg-desktop-portal-kde-print") +Q_LOGGING_CATEGORY(XdgDesktopPortalKdePrint, "xdp-kde-print") // INFO: code below is copied from Qt as there is no public API for converting key to PageSizeId // TODO seems to by Windows only stuff // Standard sources data // struct StandardPaperSource { // QPrinter::PaperSource sourceNum; // const char *source; // } // // static const StandardPaperSource qt_paperSources[] = { // {QPrinter::Auto, "Auto"}, // {QPrinter::Cassette, "Cassette"}, // {QPrinter::Envelope, "Envelope"}, // {QPrinter::EnvelopeManual, "EnvelopeManual"}, // {QPrinter::FormSource, "FormSource"}, // {QPrinter::LargeCapacity, "LargeCapacity"}, // {QPrinter::LargeFormat, "AnyLargeFormat"}, // {QPrinter::Lower, "Lower"}, // {QPrinter::Middle, "Middle"}, // {QPrinter::Manual, "Manual"}, // {QPrinter::Manual, "ManualFeed"}, // {QPrinter::OnlyOne, "OnlyOne"}, // = QPrint::Upper // {QPrinter::Tractor, "Tractor"}, // {QPrinter::SmallFormat, "AnySmallFormat"}, // {QPrinter::Upper, "Upper"}, // }; // Standard sizes data struct StandardPageSize { QPageSize::PageSizeId id; const char *mediaOption; // PPD standard mediaOption ID }; // Standard page sizes taken from the Postscript PPD Standard v4.3 // See http://partners.adobe.com/public/developer/en/ps/5003.PPD_Spec_v4.3.pdf // Excludes all Transverse and Rotated sizes // NB!This table needs to be in sync with QPageSize::PageSizeId const static StandardPageSize qt_pageSizes[] = { // Existing Qt sizes including ISO, US, ANSI and other standards {QPageSize::A4, "A4"}, {QPageSize::B5, "ISOB5"}, {QPageSize::Letter, "Letter"}, {QPageSize::Legal, "Legal"}, {QPageSize::Executive, "Executive.7.5x10in"}, // Qt size differs from Postscript / Windows {QPageSize::A0, "A0"}, {QPageSize::A1, "A1"}, {QPageSize::A2, "A2"}, {QPageSize::A3, "A3"}, {QPageSize::A5, "A5"}, {QPageSize::A6, "A6"}, {QPageSize::A7, "A7"}, {QPageSize::A8, "A8"}, {QPageSize::A9, "A9"}, {QPageSize::B0, "ISOB0"}, {QPageSize::B1, "ISOB1"}, {QPageSize::B10, "ISOB10"}, {QPageSize::B2, "ISOB2"}, {QPageSize::B3, "ISOB3"}, {QPageSize::B4, "ISOB4"}, {QPageSize::B6, "ISOB6"}, {QPageSize::B7, "ISOB7"}, {QPageSize::B8, "ISOB8"}, {QPageSize::B9, "ISOB9"}, {QPageSize::C5E, "EnvC5"}, {QPageSize::Comm10E, "Env10"}, {QPageSize::DLE, "EnvDL"}, {QPageSize::Folio, "Folio"}, {QPageSize::Ledger, "Ledger"}, {QPageSize::Tabloid, "Tabloid"}, {QPageSize::Custom, "Custom"}, // Special case to keep in sync with QPageSize::PageSizeId // ISO Standard Sizes {QPageSize::A10, "A10"}, {QPageSize::A3Extra, "A3Extra"}, {QPageSize::A4Extra, "A4Extra"}, {QPageSize::A4Plus, "A4Plus"}, {QPageSize::A4Small, "A4Small"}, {QPageSize::A5Extra, "A5Extra"}, {QPageSize::B5Extra, "ISOB5Extra"}, // JIS Standard Sizes {QPageSize::JisB0, "B0"}, {QPageSize::JisB1, "B1"}, {QPageSize::JisB2, "B2"}, {QPageSize::JisB3, "B3"}, {QPageSize::JisB4, "B4"}, {QPageSize::JisB5, "B5"}, {QPageSize::JisB6, "B6"}, {QPageSize::JisB7, "B7"}, {QPageSize::JisB8, "B8"}, {QPageSize::JisB9, "B9"}, {QPageSize::JisB10, "B10"}, // ANSI / US Standard sizes {QPageSize::AnsiC, "AnsiC"}, {QPageSize::AnsiD, "AnsiD"}, {QPageSize::AnsiE, "AnsiE"}, {QPageSize::LegalExtra, "LegalExtra"}, {QPageSize::LetterExtra, "LetterExtra"}, {QPageSize::LetterPlus, "LetterPlus"}, {QPageSize::LetterSmall, "LetterSmall"}, {QPageSize::TabloidExtra, "TabloidExtra"}, // Architectural sizes {QPageSize::ArchA, "ARCHA"}, {QPageSize::ArchB, "ARCHB"}, {QPageSize::ArchC, "ARCHC"}, {QPageSize::ArchD, "ARCHD"}, {QPageSize::ArchE, "ARCHE"}, // Inch-based Sizes {QPageSize::Imperial7x9, "7x9"}, {QPageSize::Imperial8x10, "8x10"}, {QPageSize::Imperial9x11, "9x11"}, {QPageSize::Imperial9x12, "9x12"}, {QPageSize::Imperial10x11, "10x11"}, {QPageSize::Imperial10x13, "10x13"}, {QPageSize::Imperial10x14, "10x14"}, {QPageSize::Imperial12x11, "12x11"}, {QPageSize::Imperial15x11, "15x11"}, // Other Page Sizes {QPageSize::ExecutiveStandard, "Executive"}, // Qt size differs from Postscript / Windows {QPageSize::Note, "Note"}, {QPageSize::Quarto, "Quarto"}, {QPageSize::Statement, "Statement"}, {QPageSize::SuperA, "SuperA"}, {QPageSize::SuperB, "SuperB"}, {QPageSize::Postcard, "Postcard"}, {QPageSize::DoublePostcard, "DoublePostcard"}, {QPageSize::Prc16K, "PRC16K"}, {QPageSize::Prc32K, "PRC32K"}, {QPageSize::Prc32KBig, "PRC32KBig"}, // Fan Fold Sizes {QPageSize::FanFoldUS, "FanFoldUS"}, {QPageSize::FanFoldGerman, "FanFoldGerman"}, {QPageSize::FanFoldGermanLegal, "FanFoldGermanLegal"}, // ISO Envelopes {QPageSize::EnvelopeB4, "EnvISOB4"}, {QPageSize::EnvelopeB5, "EnvISOB5"}, {QPageSize::EnvelopeB6, "EnvISOB6"}, {QPageSize::EnvelopeC0, "EnvC0"}, {QPageSize::EnvelopeC1, "EnvC1"}, {QPageSize::EnvelopeC2, "EnvC2"}, {QPageSize::EnvelopeC3, "EnvC3"}, {QPageSize::EnvelopeC4, "EnvC4"}, {QPageSize::EnvelopeC6, "EnvC6"}, {QPageSize::EnvelopeC65, "EnvC65"}, {QPageSize::EnvelopeC7, "EnvC7"}, // US Envelopes {QPageSize::Envelope9, "Env9"}, {QPageSize::Envelope11, "Env11"}, {QPageSize::Envelope12, "Env12"}, {QPageSize::Envelope14, "Env14"}, {QPageSize::EnvelopeMonarch, "EnvMonarch"}, {QPageSize::EnvelopePersonal, "EnvPersonal"}, // Other Envelopes {QPageSize::EnvelopeChou3, "EnvChou3"}, {QPageSize::EnvelopeChou4, "EnvChou4"}, {QPageSize::EnvelopeInvite, "EnvInvite"}, {QPageSize::EnvelopeItalian, "EnvItalian"}, {QPageSize::EnvelopeKaku2, "EnvKaku2"}, {QPageSize::EnvelopeKaku3, "EnvKaku3"}, {QPageSize::EnvelopePrc1, "EnvPRC1"}, {QPageSize::EnvelopePrc2, "EnvPRC2"}, {QPageSize::EnvelopePrc3, "EnvPRC3"}, {QPageSize::EnvelopePrc4, "EnvPRC4"}, {QPageSize::EnvelopePrc5, "EnvPRC5"}, {QPageSize::EnvelopePrc6, "EnvPRC6"}, {QPageSize::EnvelopePrc7, "EnvPRC7"}, {QPageSize::EnvelopePrc8, "EnvPRC8"}, {QPageSize::EnvelopePrc9, "EnvPRC9"}, {QPageSize::EnvelopePrc10, "EnvPRC10"}, {QPageSize::EnvelopeYou4, "EnvYou4"} }; // Return key name for PageSize static QString qt_keyForPageSizeId(QPageSize::PageSizeId id) { return QString::fromLatin1(qt_pageSizes[id].mediaOption); } // Return id name for PPD Key static QPageSize::PageSizeId qt_idForPpdKey(const QString &ppdKey) { if (ppdKey.isEmpty()) { return QPageSize::Custom; } for (int i = 0; i <= int(QPageSize::LastPageSize); ++i) { if (QLatin1String(qt_pageSizes[i].mediaOption) == ppdKey) { return qt_pageSizes[i].id; } } return QPageSize::Custom; } PrintPortal::PrintPortal(QObject *parent) : QDBusAbstractAdaptor(parent) { } PrintPortal::~PrintPortal() { } uint PrintPortal::Print(const QDBusObjectPath &handle, const QString &app_id, const QString &parent_window, const QString &title, const QDBusUnixFileDescriptor &fd, const QVariantMap &options, QVariantMap &results) { qCDebug(XdgDesktopPortalKdePrint) << "Print called with parameters:"; qCDebug(XdgDesktopPortalKdePrint) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdePrint) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdePrint) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdePrint) << " title: " << title; qCDebug(XdgDesktopPortalKdePrint) << " fd: " << fd.fileDescriptor(); qCDebug(XdgDesktopPortalKdePrint) << " options: " << options; QFile fileToPrint; if (fileToPrint.open(fd.fileDescriptor(), QIODevice::ReadOnly)) { QPrinter *printer = nullptr; // Use printer associated with token if possible if (options.contains(QLatin1String("token"))) { printer = m_printers.value(options.value(QLatin1String("token")).toUInt()); } else { // Use the last configured printer otherwise if (m_printers.count()) { printer = m_printers.last(); } } if (!printer) { qCDebug(XdgDesktopPortalKdePrint) << "Failed to print: no QPrinter what can be used for printing"; return 1; } // We are going to print to a file if (!printer->outputFileName().isEmpty()) { if (QFile::exists(printer->outputFileName())) { QFile::remove(printer->outputFileName()); } QByteArray pdfContent = fileToPrint.readAll(); QFile outputFile(printer->outputFileName()); if (outputFile.open(QIODevice::ReadWrite)) { outputFile.write(pdfContent); outputFile.close(); } else { qCDebug(XdgDesktopPortalKdePrint) << "Failed to print: couldn't open the file for writing"; } fileToPrint.close(); return 0; // TODO poscript support? // Print to a printer via lpr command } else { // The code below is copied from Okular bool useCupsOptions = cupsAvailable(); QString exe; QStringList argList; //Decide what executable to use to print with, need the CUPS version of lpr if available //Some distros name the CUPS version of lpr as lpr-cups or lpr.cups so try those first //before default to lpr, or failing that to lp if (!QStandardPaths::findExecutable(QStringLiteral("lpr-cups")).isEmpty()) { exe = QStringLiteral("lpr-cups"); } else if (!QStandardPaths::findExecutable(QStringLiteral("lpr.cups")).isEmpty()) { exe = QStringLiteral("lpr.cups"); } else if (!QStandardPaths::findExecutable(QStringLiteral("lpr")).isEmpty()) { exe = QStringLiteral("lpr"); } else if (!QStandardPaths::findExecutable(QStringLiteral("lp")).isEmpty()) { exe = QStringLiteral("lp"); } else { qCDebug(XdgDesktopPortalKdePrint) << "Failed to print: couldn't run lpr command for printing"; return 1; } QTemporaryFile tempFile; if (tempFile.open()) { tempFile.write(fileToPrint.readAll()); tempFile.close(); } else { qCDebug(XdgDesktopPortalKdePrint) << "Failed to print: couldn't create temporary file for printing"; return 1; } argList = printArguments(printer, useCupsOptions, exe, printer->orientation()) << tempFile.fileName(); // qCDebug(XdgDesktopPortalKdePrint) << "Executing" << exe << "with arguments" << argList << tempFile.fileName(); int retValue = KProcess::execute(exe, argList); if (retValue <= 0) { qCDebug(XdgDesktopPortalKdePrint) << "Failed to print: running KProcess failed"; return 1; } return retValue; } } else { qCDebug(XdgDesktopPortalKdePrint) << "Failed to print: couldn't not read from fd"; return 1; } return 0; } uint PrintPortal::PreparePrint(const QDBusObjectPath &handle, const QString &app_id, const QString &parent_window, const QString &title, const QVariantMap &settings, const QVariantMap &page_setup, const QVariantMap &options, QVariantMap &results) { qCDebug(XdgDesktopPortalKdePrint) << "PreparePrint called with parameters:"; qCDebug(XdgDesktopPortalKdePrint) << " handle: " << handle.path(); qCDebug(XdgDesktopPortalKdePrint) << " app_id: " << app_id; qCDebug(XdgDesktopPortalKdePrint) << " parent_window: " << parent_window; qCDebug(XdgDesktopPortalKdePrint) << " title: " << title; qCDebug(XdgDesktopPortalKdePrint) << " settings: " << settings; qCDebug(XdgDesktopPortalKdePrint) << " page_setup: " << page_setup; qCDebug(XdgDesktopPortalKdePrint) << " options: " << options; // Create new one QPrinter *printer = new QPrinter(); // First we have to load pre-configured options // Process settings (used by printer) if (settings.contains(QLatin1String("orientation"))) { // TODO: what is the difference between this and the one in page setup } if (settings.contains(QLatin1String("paper-format"))) { // TODO: what is the difference between this and the one in page setup } if (settings.contains(QLatin1String("paper-width"))) { // TODO: what is the difference between this and the one in page setup } if (settings.contains(QLatin1String("paper-height"))) { // TODO: what is the difference between this and the one in page setup } if (settings.contains(QLatin1String("n-copies"))) { printer->setCopyCount(settings.value(QLatin1String("n-copies")).toString().toInt()); } if (settings.contains(QLatin1String("default-source"))) { // TODO seems to be windows only stuff } if (settings.contains(QLatin1String("quality"))) { // TODO doesn't seem to be used by Qt } if (settings.contains(QLatin1String("resolution"))) { printer->setResolution(settings.value(QLatin1String("resolution")).toString().toInt()); } if (settings.contains(QLatin1String("use-color"))) { printer->setColorMode(settings.value(QLatin1String("use-color")).toString() == QLatin1String("yes") ? QPrinter::Color : QPrinter::GrayScale); } if (settings.contains(QLatin1String("duplex"))) { const QString duplex = settings.value(QLatin1String("duplex")).toString(); if (duplex == QLatin1String("simplex")) { printer->setDuplex(QPrinter::DuplexNone); } else if (duplex == QLatin1String("horizontal")) { printer->setDuplex(QPrinter::DuplexShortSide); } else if (duplex == QLatin1String("vertical")) { printer->setDuplex(QPrinter::DuplexLongSide); } } if (settings.contains(QLatin1String("collate"))) { printer->setCollateCopies(settings.value(QLatin1String("collate")).toString() == QLatin1String("yes")); } if (settings.contains(QLatin1String("reverse"))) { printer->setPageOrder(settings.value(QLatin1String("reverse")).toString() == QLatin1String("yes") ? QPrinter::LastPageFirst : QPrinter::FirstPageFirst); } if (settings.contains(QLatin1String("media-type"))) { // TODO doesn't seem to be used by Qt } if (settings.contains(QLatin1String("dither"))) { // TODO doesn't seem to be used by Qt } if (settings.contains(QLatin1String("scale"))) { // TODO doesn't seem to be used by Qt } if (settings.contains(QLatin1String("print-pages"))) { const QString printPages = settings.value(QLatin1String("print-pages")).toString(); if (printPages == QLatin1String("all")) { printer->setPrintRange(QPrinter::AllPages); } else if (printPages == QLatin1String("selection")) { printer->setPrintRange(QPrinter::Selection); } else if (printPages == QLatin1String("current")) { printer->setPrintRange(QPrinter::CurrentPage); } else if (printPages == QLatin1String("ranges")) { printer->setPrintRange(QPrinter::PageRange); } } if (settings.contains(QLatin1String("page-ranges"))) { const QString range = settings.value(QLatin1String("page-ranges")).toString(); // Gnome supports format like 1-5,7,9,11-15, however Qt support only e.g.1-15 // so get the first and the last value const QStringList ranges = range.split(QLatin1Char(',')); if (ranges.count()) { QStringList firstRangeValues = ranges.first().split(QLatin1Char('-')); QStringList lastRangeValues = ranges.last().split(QLatin1Char('-')); printer->setFromTo(firstRangeValues.first().toInt(), lastRangeValues.last().toInt()); } } if (settings.contains(QLatin1String("page-set"))) { // WARNING Qt internal private API, anyway the print dialog doesn't seem to // read these propertis, but I'll leave it here in case this changes in future const QString pageSet = settings.value(QLatin1String("page-set")).toString(); if (pageSet == QLatin1String("all")) { QCUPSSupport::setPageSet(printer, QCUPSSupport::AllPages); } else if (pageSet == QLatin1String("even")) { QCUPSSupport::setPageSet(printer, QCUPSSupport::EvenPages); } else if (pageSet == QLatin1String("odd")) { QCUPSSupport::setPageSet(printer, QCUPSSupport::OddPages); } } if (settings.contains(QLatin1String("finishings"))) { // TODO doesn't seem to be used by Qt } QCUPSSupport::PagesPerSheet pagesPerSheet = QCUPSSupport::OnePagePerSheet; QCUPSSupport::PagesPerSheetLayout pagesPerSheetLayout = QCUPSSupport::LeftToRightTopToBottom; if (settings.contains(QLatin1String("number-up"))) { // WARNING Qt internal private API, anyway the print dialog doesn't seem to // read these propertis, but I'll leave it here in case this changes in future const QString numberUp = settings.value(QLatin1String("number-up")).toString(); if (numberUp == QLatin1String("1")) { pagesPerSheet = QCUPSSupport::OnePagePerSheet; } else if (numberUp == QLatin1String("2")) { pagesPerSheet = QCUPSSupport::TwoPagesPerSheet; } else if (numberUp == QLatin1String("4")) { pagesPerSheet = QCUPSSupport::FourPagesPerSheet; } else if (numberUp == QLatin1String("6")) { pagesPerSheet = QCUPSSupport::SixPagesPerSheet; } else if (numberUp == QLatin1String("9")) { pagesPerSheet = QCUPSSupport::NinePagesPerSheet; } else if (numberUp == QLatin1String("16")) { pagesPerSheet = QCUPSSupport::SixteenPagesPerSheet; } } if (settings.contains(QLatin1String("number-up-layout"))) { // WARNING Qt internal private API, anyway the print dialog doesn't seem to // read these propertis, but I'll leave it here in case this changes in future const QString layout = settings.value(QLatin1String("number-up-layout")).toString(); if (layout == QLatin1String("lrtb")) { pagesPerSheetLayout = QCUPSSupport::LeftToRightTopToBottom; } else if (layout == QLatin1String("lrbt")) { pagesPerSheetLayout = QCUPSSupport::LeftToRightBottomToTop; } else if (layout == QLatin1String("rltb")) { pagesPerSheetLayout = QCUPSSupport::RightToLeftTopToBottom; } else if (layout == QLatin1String("rlbt")) { pagesPerSheetLayout = QCUPSSupport::RightToLeftBottomToTop; } else if (layout == QLatin1String("tblr")) { pagesPerSheetLayout = QCUPSSupport::TopToBottomLeftToRight; } else if (layout == QLatin1String("tbrl")) { pagesPerSheetLayout = QCUPSSupport::TopToBottomRightToLeft; } else if (layout == QLatin1String("btlr")) { pagesPerSheetLayout = QCUPSSupport::BottomToTopLeftToRight; } else if (layout == QLatin1String("btrl")) { pagesPerSheetLayout = QCUPSSupport::BottomToTopRightToLeft; } } QCUPSSupport::setPagesPerSheetLayout(printer, pagesPerSheet, pagesPerSheetLayout); if (settings.contains(QLatin1String("output-bin"))) { // TODO not sure what this setting represents } if (settings.contains(QLatin1String("resolution-x"))) { // TODO possible to set only full resolution, but I can count the total // resolution I guess, anyway doesn't seem to be used by the print dialog } if (settings.contains(QLatin1String("resolution-y"))) { // TODO possible to set only full resolution, but I can count the total // resolution I guess, anyway doesn't seem to be used by the print dialog } if (settings.contains(QLatin1String("printer-lpi"))) { // TODO not possible to set, maybe count it? } if (settings.contains(QLatin1String("output-basename"))) { // TODO processed in output-uri // printer->setOutputFileName(settings.value(QLatin1String("output-basename")).toString()); } if (settings.contains(QLatin1String("output-file-format"))) { // TODO only PDF supported by Qt so when printing to file we can use PDF only // btw. this should be already set automatically because we set output file name printer->setOutputFormat(QPrinter::PdfFormat); } if (settings.contains(QLatin1String("output-uri"))) { const QUrl uri = QUrl(settings.value(QLatin1String("output-uri")).toString()); // Check whether the uri is not just a directory name and whether we don't need to // append output-basename if (settings.contains(QLatin1String("output-basename"))) { const QString basename = settings.value(QLatin1String("output-basename")).toString(); if (!uri.toDisplayString().endsWith(basename) && uri.toDisplayString().endsWith(QLatin1Char('/'))) { printer->setOutputFileName(uri.toDisplayString() + basename); } } else { printer->setOutputFileName(uri.toDisplayString()); } } // Process page setup QMarginsF pageMargins = printer->pageLayout().margins(QPageLayout::Millimeter); if (page_setup.contains(QLatin1String("PPDName"))) { printer->setPageSize(QPageSize(qt_idForPpdKey(page_setup.value(QLatin1String("PPDName")).toString()))); } if (page_setup.contains(QLatin1String("Name"))) { // TODO: Try to use name instead of PPDName, at least I think it's how it's supposed to be used if (!page_setup.contains(QLatin1String("PPDName"))) { printer->setPageSize(QPageSize(qt_idForPpdKey(page_setup.value(QLatin1String("PPDName")).toString()))); } } if (page_setup.contains(QLatin1String("DisplayName"))) { // TODO: This should just set a different name for the standardized one I guess printer->setPageSize(QPageSize(printer->pageLayout().pageSize().size(QPageSize::Millimeter), QPageSize::Millimeter, page_setup.value(QLatin1String("DisplayName")).toString())); } if (page_setup.contains(QLatin1String("Width")) && page_setup.contains(QLatin1String("Height"))) { QSizeF paperSize; paperSize.setHeight(page_setup.value(QLatin1String("Height")).toReal()); paperSize.setWidth(page_setup.value(QLatin1String("Width")).toReal()); printer->setPageSize(QPageSize(paperSize, QPageSize::Millimeter, page_setup.value(QLatin1String("DisplayName")).toString())); } if (page_setup.contains(QLatin1String("MarginTop"))) { pageMargins.setTop(page_setup.value(QLatin1String("MarginTop")).toReal()); } if (page_setup.contains(QLatin1String("MarginBottom"))) { pageMargins.setBottom(page_setup.value(QLatin1String("MarginBottom")).toReal()); } if (page_setup.contains(QLatin1String("MarginLeft"))) { pageMargins.setLeft(page_setup.value(QLatin1String("MarginLeft")).toReal()); } if (page_setup.contains(QLatin1String("MarginRight"))) { pageMargins.setRight(page_setup.value(QLatin1String("MarginLeft")).toReal()); } if (page_setup.contains(QLatin1String("Orientation"))) { const QString orientation = page_setup.value(QLatin1String("Orientation")).toString(); if (orientation == QLatin1String("landscape") || orientation == QLatin1String("reverse_landscape")) { printer->setPageOrientation(QPageLayout::Landscape); } else if (orientation == QLatin1String("portrait") || orientation == QLatin1String("reverse_portrait")) { printer->setPageOrientation(QPageLayout::Portrait); } } printer->setPageMargins(pageMargins, QPageLayout::Millimeter); QPrintDialog *printDialog = new QPrintDialog(printer); // Process options if (options.contains(QLatin1String("modal"))) { printDialog->setModal(options.value(QLatin1String("modal")).toBool()); } // Pass back what we configured if (printDialog->exec() == QDialog::Accepted) { QVariantMap resultingSettings; QVariantMap resultingPageSetup; // Process back printer settings resultingSettings.insert(QLatin1String("n-copies"), QString::number(printer->copyCount())); resultingSettings.insert(QLatin1String("resolution"), QString::number(printer->resolution())); resultingSettings.insert(QLatin1String("use-color"), printer->colorMode() == QPrinter::Color ? QLatin1String("yes") : QLatin1String("no")); if (printer->duplex() == QPrinter::DuplexNone) { resultingSettings.insert(QLatin1String("duplex"), QLatin1String("simplex")); } else if (printer->duplex() == QPrinter::DuplexShortSide) { resultingSettings.insert(QLatin1String("duplex"), QLatin1String("horizontal")); } else if (printer->duplex() == QPrinter::DuplexLongSide) { resultingSettings.insert(QLatin1String("duplex"), QLatin1String("vertical")); } resultingSettings.insert(QLatin1String("collate"), printer->collateCopies() ? QLatin1String("yes") : QLatin1String("no")); resultingSettings.insert(QLatin1String("reverse"), printer->pageOrder() == QPrinter::LastPageFirst ? QLatin1String("yes") : QLatin1String("no")); if (printer->printRange() == QPrinter::AllPages) { resultingSettings.insert(QLatin1String("print-pages"), QLatin1String("all")); } else if (printer->printRange() == QPrinter::Selection) { resultingSettings.insert(QLatin1String("print-pages"), QLatin1String("selection")); } else if (printer->printRange() == QPrinter::CurrentPage) { resultingSettings.insert(QLatin1String("print-pages"), QLatin1String("current")); } else if (printer->printRange() == QPrinter::PageRange) { resultingSettings.insert(QLatin1String("print-pages"), QLatin1String("ranges")); resultingSettings.insert(QLatin1String("page-ranges"), QString("%1-%2").arg(printer->fromPage()).arg(printer->toPage())); } // Set cups specific properties const QStringList cupsOptions = printer->printEngine()->property(PPK_CupsOptions).toStringList(); qCDebug(XdgDesktopPortalKdePrint) << cupsOptions; if (cupsOptions.contains(QLatin1String("page-set"))) { resultingSettings.insert(QLatin1String("page-set"), cupsOptions.at(cupsOptions.indexOf(QLatin1String("page-set")) + 1)); } if (cupsOptions.contains(QLatin1String("number-up"))) { resultingSettings.insert(QLatin1String("number-up"), cupsOptions.at(cupsOptions.indexOf(QLatin1String("number-up")) + 1)); } if (cupsOptions.contains(QLatin1String("number-up-layout"))) { resultingSettings.insert(QLatin1String("number-up-layout"), cupsOptions.at(cupsOptions.indexOf(QLatin1String("number-up-layout")) + 1)); } if (printer->outputFormat() == QPrinter::PdfFormat) { resultingSettings.insert(QLatin1String("output-file-format"), QLatin1String("pdf")); } if (!printer->outputFileName().isEmpty()) { resultingSettings.insert(QLatin1String("output-uri"), QUrl::fromLocalFile(printer->outputFileName()).toDisplayString()); } // Process back page setup resultingPageSetup.insert(QLatin1String("PPDName"), qt_keyForPageSizeId(printer->pageLayout().pageSize().id())); // TODO: verify if this make sense resultingPageSetup.insert(QLatin1String("Name"), qt_keyForPageSizeId(printer->pageLayout().pageSize().id())); // TODO: verify if this make sense resultingPageSetup.insert(QLatin1String("DisplayName"), qt_keyForPageSizeId(printer->pageLayout().pageSize().id())); resultingPageSetup.insert(QLatin1String("Width"), printer->pageLayout().pageSize().size(QPageSize::Millimeter).width()); resultingPageSetup.insert(QLatin1String("Height"), printer->pageLayout().pageSize().size(QPageSize::Millimeter).height()); resultingPageSetup.insert(QLatin1String("MarginTop"), printer->pageLayout().margins(QPageLayout::Millimeter).top()); resultingPageSetup.insert(QLatin1String("MarginBottom"), printer->pageLayout().margins(QPageLayout::Millimeter).bottom()); resultingPageSetup.insert(QLatin1String("MarginLeft"), printer->pageLayout().margins(QPageLayout::Millimeter).left()); resultingPageSetup.insert(QLatin1String("MarginRight"), printer->pageLayout().margins(QPageLayout::Millimeter).right()); resultingPageSetup.insert(QLatin1String("Orientation"), printer->pageLayout().orientation() == QPageLayout::Landscape ? QLatin1String("landscape") : QLatin1String("portrait")); qCDebug(XdgDesktopPortalKdePrint) << "Settings: "; qCDebug(XdgDesktopPortalKdePrint) << "---------------------------"; qCDebug(XdgDesktopPortalKdePrint) << resultingSettings; qCDebug(XdgDesktopPortalKdePrint) << "Page setup: "; qCDebug(XdgDesktopPortalKdePrint) << "---------------------------"; qCDebug(XdgDesktopPortalKdePrint) << resultingPageSetup; uint token = QDateTime::currentDateTime().toTime_t(); results.insert(QLatin1String("settings"), resultingSettings); results.insert(QLatin1String("page-setup"), resultingPageSetup); results.insert(QLatin1String("token"), token); m_printers.insert(token, printer); printDialog->deleteLater(); return 0; } else { printDialog->deleteLater(); return 1; } return 0; } QStringList PrintPortal::destination(const QPrinter *printer, const QString &version) { if (version == QLatin1String("lp")) { return QStringList(QStringLiteral("-d")) << printer->printerName(); } if (version.startsWith(QLatin1String("lpr"))) { return QStringList(QStringLiteral("-P")) << printer->printerName(); } return QStringList(); } QStringList PrintPortal::copies(const QPrinter *printer, const QString &version) { int cp = printer->actualNumCopies(); if (version == QLatin1String("lp")) { return QStringList(QStringLiteral("-n")) << QStringLiteral("%1").arg(cp); } if (version.startsWith(QLatin1String("lpr"))) { return QStringList() << QStringLiteral("-#%1").arg(cp); } return QStringList(); } QStringList PrintPortal::jobname(const QPrinter *printer, const QString &version) { if (!printer->docName().isEmpty()) { if (version == QLatin1String("lp")) { return QStringList(QStringLiteral("-t")) << printer->docName(); } if (version.startsWith(QLatin1String("lpr"))) { const QString shortenedDocName = QString::fromUtf8(printer->docName().toUtf8().left(255)); return QStringList(QStringLiteral("-J")) << shortenedDocName; } } return QStringList(); } // What about Upper and MultiPurpose? And others in PPD??? QString PrintPortal::mediaPaperSource(const QPrinter *printer) { switch (printer->paperSource()) { case QPrinter::Auto: return QString(); case QPrinter::Cassette: return QStringLiteral("Cassette"); case QPrinter::Envelope: return QStringLiteral("Envelope"); case QPrinter::EnvelopeManual: return QStringLiteral("EnvelopeManual"); case QPrinter::FormSource: return QStringLiteral("FormSource"); case QPrinter::LargeCapacity: return QStringLiteral("LargeCapacity"); case QPrinter::LargeFormat: return QStringLiteral("LargeFormat"); case QPrinter::Lower: return QStringLiteral("Lower"); case QPrinter::MaxPageSource: return QStringLiteral("MaxPageSource"); case QPrinter::Middle: return QStringLiteral("Middle"); case QPrinter::Manual: return QStringLiteral("Manual"); case QPrinter::OnlyOne: return QStringLiteral("OnlyOne"); case QPrinter::Tractor: return QStringLiteral("Tractor"); case QPrinter::SmallFormat: return QStringLiteral("SmallFormat"); default: return QString(); } } QStringList PrintPortal::optionOrientation(const QPrinter *printer, QPrinter::Orientation documentOrientation) { // portrait and landscape options rotate the document according to the document orientation // If we want to print a landscape document as one would expect it, we have to pass the // portrait option so that the document is not rotated additionally if (printer->orientation() == documentOrientation) { // the user wants the document printed as is return QStringList(QStringLiteral("-o")) << QStringLiteral("portrait"); } else { // the user expects the document being rotated by 90 degrees return QStringList(QStringLiteral("-o")) << QStringLiteral("landscape"); } } QStringList PrintPortal::optionDoubleSidedPrinting(const QPrinter *printer) { switch (printer->duplex()) { case QPrinter::DuplexNone: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=one-sided"); case QPrinter::DuplexAuto: if (printer->orientation() == QPrinter::Landscape) { return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-short-edge"); } else { return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-long-edge"); } case QPrinter::DuplexLongSide: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-long-edge"); case QPrinter::DuplexShortSide: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-short-edge"); default: return QStringList(); //Use printer default } } QStringList PrintPortal::optionPageOrder(const QPrinter *printer) { if (printer->pageOrder() == QPrinter::LastPageFirst) { return QStringList(QStringLiteral("-o")) << QStringLiteral("outputorder=reverse"); } return QStringList(QStringLiteral("-o")) << QStringLiteral("outputorder=normal"); } QStringList PrintPortal::optionCollateCopies(const QPrinter *printer) { if (printer->collateCopies()) { return QStringList(QStringLiteral("-o")) << QStringLiteral("Collate=True"); } return QStringList(QStringLiteral("-o")) << QStringLiteral("Collate=False"); } QStringList PrintPortal::optionPageMargins(const QPrinter *printer) { if (printer->printEngine()->property(QPrintEngine::PPK_PageMargins).isNull()) { return QStringList(); } else { qreal l, t, r, b; printer->getPageMargins(&l, &t, &r, &b, QPrinter::Point); return QStringList(QStringLiteral("-o")) << QStringLiteral("page-left=%1").arg(l) << QStringLiteral("-o") << QStringLiteral("page-top=%1").arg(t) << QStringLiteral("-o") << QStringLiteral("page-right=%1").arg(r) << QStringLiteral("-o") << QStringLiteral("page-bottom=%1").arg(b) << QStringLiteral("-o") << QStringLiteral("fit-to-page"); } } QStringList PrintPortal::optionCupsProperties(const QPrinter *printer) { QStringList dialogOptions = printer->printEngine()->property(QPrintEngine::PrintEnginePropertyKey(0xfe00)).toStringList(); QStringList cupsOptions; for (int i = 0; i < dialogOptions.count(); i = i + 2) { // Ignore some cups properties as the pdf we get is already formatted using these if (dialogOptions[i] == QLatin1String("number-up") || dialogOptions[i] == QLatin1String("number-up-layout")) { continue; } if (dialogOptions[i + 1].isEmpty()) { cupsOptions << QStringLiteral("-o") << dialogOptions[i]; } else { cupsOptions << QStringLiteral("-o") << dialogOptions[i] + QLatin1Char('=') + dialogOptions[i + 1]; } } return cupsOptions; } QStringList PrintPortal::optionMedia(const QPrinter *printer) { if (!qt_keyForPageSizeId(printer->pageLayout().pageSize().id()).isEmpty() && !mediaPaperSource(printer).isEmpty()) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1,%2").arg(qt_keyForPageSizeId(printer->pageLayout().pageSize().id()), mediaPaperSource(printer)); } if (!qt_keyForPageSizeId(printer->pageLayout().pageSize().id()).isEmpty()) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1").arg(qt_keyForPageSizeId(printer->pageLayout().pageSize().id())); } if (!mediaPaperSource(printer).isEmpty()) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1").arg(mediaPaperSource(printer)); } return QStringList(); } QStringList PrintPortal::pages(const QPrinter *printer, bool useCupsOptions, const QString &version) { if (printer->printRange() == QPrinter::PageRange) { if (version == QLatin1String("lp")) { return QStringList(QStringLiteral("-P")) << QStringLiteral("%1-%2").arg(printer->fromPage()) .arg(printer->toPage()); } if (version.startsWith(QLatin1String("lpr")) && useCupsOptions) { return QStringList(QStringLiteral("-o")) << QStringLiteral("page-ranges=%1-%2").arg(printer->fromPage()) .arg(printer->toPage()); } } return QStringList(); // AllPages } QStringList PrintPortal::cupsOptions(const QPrinter *printer, QPrinter::Orientation documentOrientation) { QStringList optionList; // if (!optionMedia(printer).isEmpty()) { // optionList << optionMedia(printer); // } // if (!optionOrientation(printer, documentOrientation).isEmpty()) { // optionList << optionOrientation(printer, documentOrientation); // } if (!optionDoubleSidedPrinting(printer).isEmpty()) { optionList << optionDoubleSidedPrinting(printer); } if (!optionPageOrder(printer).isEmpty()) { optionList << optionPageOrder(printer); } if (!optionCollateCopies(printer).isEmpty()) { optionList << optionCollateCopies(printer); } // if (!optionPageMargins(printer).isEmpty()) { // optionList << optionPageMargins(printer); // } optionList << optionCupsProperties(printer); return optionList; } QStringList PrintPortal::printArguments(const QPrinter *printer, bool useCupsOptions, const QString &version, QPrinter::Orientation documentOrientation) { QStringList argList; if (!destination(printer, version).isEmpty()) { argList << destination(printer, version); } // if (!copies(printer, version).isEmpty()) { // argList << copies(printer, version); // } if (!jobname(printer, version).isEmpty()) { argList << jobname(printer, version); } // if (!pages(printer, useCupsOptions, version).isEmpty()) { // argList << pages(printer, useCupsOptions, version); // } if (useCupsOptions && !cupsOptions(printer, documentOrientation).isEmpty()) { argList << cupsOptions(printer, documentOrientation); } if (version == QLatin1String("lp")) { argList << QStringLiteral("--"); } return argList; } bool PrintPortal::cupsAvailable() { // Ideally we would have access to the private Qt method // QCUPSSupport::cupsAvailable() to do this as it is very complex routine. // However, if CUPS is available then QPrinter::numCopies() will always return 1 // whereas if CUPS is not available it will return the real number of copies. // This behaviour is guaranteed never to change, so we can use it as a reliable substitute. QPrinter testPrinter; testPrinter.setNumCopies(2); return (testPrinter.numCopies() == 1); } diff --git a/src/request.cpp b/src/request.cpp index dc88c73..054bc87 100644 --- a/src/request.cpp +++ b/src/request.cpp @@ -1,101 +1,101 @@ /* * Copyright © 2016 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include "desktopportal.h" #include #include #include #include #include #include #include -Q_LOGGING_CATEGORY(XdgRequestKdeRequest, "xdg-desktop-portal-kde-request") +Q_LOGGING_CATEGORY(XdgRequestKdeRequest, "xdp-kde-request") Request::Request(QObject *parent, const QString &portalName, const QVariant &data) : QDBusVirtualObject(parent) , m_data(data) , m_portalName(portalName) { } Request::~Request() { } bool Request::handleMessage(const QDBusMessage &message, const QDBusConnection &connection) { Q_UNUSED(connection); /* Check to make sure we're getting properties on our interface */ if (message.type() != QDBusMessage::MessageType::MethodCallMessage) { return false; } qCDebug(XdgRequestKdeRequest) << message.interface(); qCDebug(XdgRequestKdeRequest) << message.member(); qCDebug(XdgRequestKdeRequest) << message.path(); QList arguments; if (message.interface() == QLatin1String("org.freedesktop.impl.portal.Request")) { if (message.member() == QLatin1String("Close")) { if (m_portalName == QLatin1String("org.freedesktop.impl.portal.Inhibit")) { QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.kde.Solid.PowerManagement"), QLatin1String("/org/kde/Solid/PowerManagement/PolicyAgent"), QLatin1String("org.kde.Solid.PowerManagement.PolicyAgent"), QLatin1String("ReleaseInhibition")); message << m_data.toUInt(); QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); connect(watcher, &QDBusPendingCallWatcher::finished, [this] (QDBusPendingCallWatcher *watcher) { QDBusPendingReply<> reply = *watcher; if (reply.isError()) { qCDebug(XdgRequestKdeRequest) << "Uninhibit error: " << reply.error().message(); } else { Q_EMIT closeRequested(); } }); } } } return true; } QString Request::introspect(const QString &path) const { QString nodes; if (path.startsWith(QLatin1String("/org/freedesktop/portal/desktop/request/"))) { nodes = QStringLiteral( "" " " " " ""); } qCDebug(XdgRequestKdeRequest) << nodes; return nodes; } diff --git a/src/screencast.cpp b/src/screencast.cpp new file mode 100644 index 0000000..012d801 --- /dev/null +++ b/src/screencast.cpp @@ -0,0 +1,573 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * 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 library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#include "screencast.h" +#include "session.h" +#include "screencaststream.h" +#include "screenchooserdialog.h" + +#include +#include +#include +#include + +#include +#include +#include + +// KWayland +#include +#include +#include +#include +#include + +// system +#include +#include + +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCast, "xdp-kde-screencast") + +Q_DECLARE_METATYPE(ScreenCastPortal::Stream); +Q_DECLARE_METATYPE(ScreenCastPortal::Streams); + +const QDBusArgument &operator >> (const QDBusArgument &arg, ScreenCastPortal::Stream &stream) +{ + arg.beginStructure(); + arg >> stream.nodeId; + + arg.beginMap(); + while (!arg.atEnd()) { + QString key; + QVariant map; + arg.beginMapEntry(); + arg >> key >> map; + arg.endMapEntry(); + stream.map.insert(key, map); + } + arg.endMap(); + arg.endStructure(); + + return arg; +} + +const QDBusArgument &operator << (QDBusArgument &arg, const ScreenCastPortal::Stream &stream) +{ + arg.beginStructure(); + arg << stream.nodeId; + arg << stream.map; + arg.endStructure(); + + return arg; +} + +static const char * formatGLError(GLenum err) +{ + switch(err) { + case GL_NO_ERROR: + return "GL_NO_ERROR"; + case GL_INVALID_ENUM: + return "GL_INVALID_ENUM"; + case GL_INVALID_VALUE: + return "GL_INVALID_VALUE"; + case GL_INVALID_OPERATION: + return "GL_INVALID_OPERATION"; + case GL_STACK_OVERFLOW: + return "GL_STACK_OVERFLOW"; + case GL_STACK_UNDERFLOW: + return "GL_STACK_UNDERFLOW"; + case GL_OUT_OF_MEMORY: + return "GL_OUT_OF_MEMORY"; + default: + return (QLatin1String("0x") + QString::number(err, 16)).toLocal8Bit().constData(); + } +} + +// Thank you kscreen +void ScreenCastPortalOutput::setOutputType(const QString &type) +{ + const auto embedded = { QLatin1String("LVDS"), + QLatin1String("IDP"), + QLatin1String("EDP"), + QLatin1String("LCD") }; + + for (const QLatin1String &pre : embedded) { + if (type.toUpper().startsWith(pre)) { + outputType = OutputType::Laptop; + return; + } + } + + if (type.contains("VGA") || type.contains("DVI") || type.contains("HDMI") || type.contains("Panel") || + type.contains("DisplayPort") || type.startsWith("DP") || type.contains("unknown")) { + outputType = OutputType::Monitor; + } else if (type.contains("TV")) { + outputType = OutputType::Television; + } else { + outputType = OutputType::Monitor; + } +} + +ScreenCastPortal::ScreenCastPortal(QObject *parent) + : QDBusAbstractAdaptor(parent) + , m_registryInitialized(false) + , m_streamingEnabled(false) + , m_connection(nullptr) + , m_queue(nullptr) + , m_registry(nullptr) + , m_remoteAccessManager(nullptr) +{ + initDrm(); + initEGL(); + initPipewire(); + initWayland(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); +} + +ScreenCastPortal::~ScreenCastPortal() +{ + if (m_remoteAccessManager) { + m_remoteAccessManager->destroy(); + } + + if (m_drmFd) { + gbm_device_destroy(m_gbmDevice); + } + + m_stream->deleteLater(); +} + +void ScreenCastPortal::initDrm() +{ + m_drmFd = open("/dev/dri/renderD128", O_RDWR); + m_gbmDevice = gbm_create_device(m_drmFd); + + if (!m_gbmDevice) { + qFatal("Cannot create GBM device: %s", strerror(errno)); + } +} + +void ScreenCastPortal::initEGL() +{ + // Get the list of client extensions + const char* clientExtensionsCString = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + const QByteArray clientExtensionsString = QByteArray::fromRawData(clientExtensionsCString, qstrlen(clientExtensionsCString)); + if (clientExtensionsString.isEmpty()) { + // If eglQueryString() returned NULL, the implementation doesn't support + // EGL_EXT_client_extensions. Expect an EGL_BAD_DISPLAY error. + qFatal("No client extensions defined! %s", formatGLError(eglGetError())); + } + + m_egl.extensions = clientExtensionsString.split(' '); + + // Use eglGetPlatformDisplayEXT() to get the display pointer + // if the implementation supports it. + if (!m_egl.extensions.contains(QByteArrayLiteral("EGL_EXT_platform_base")) || + !m_egl.extensions.contains(QByteArrayLiteral("EGL_MESA_platform_gbm"))) { + qFatal("One of required EGL extensions is missing"); + } + + m_egl.display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_MESA, m_gbmDevice, nullptr); + + if (m_egl.display == EGL_NO_DISPLAY) { + qFatal("Error during obtaining EGL display: %s", formatGLError(eglGetError())); + } + + EGLint major, minor; + if (eglInitialize(m_egl.display, &major, &minor) == EGL_FALSE) { + qFatal("Error during eglInitialize: %s", formatGLError(eglGetError())); + } + + if (eglBindAPI(EGL_OPENGL_API) == EGL_FALSE) { + qFatal("bind OpenGL API failed"); + } + + m_egl.context = eglCreateContext(m_egl.display, nullptr, EGL_NO_CONTEXT, nullptr); + + if (m_egl.context == EGL_NO_CONTEXT) { + qFatal("Couldn't create EGL context: %s", formatGLError(eglGetError())); + } + + qCDebug(XdgDesktopPortalKdeScreenCast) << "Egl initialization succeeded"; + qCDebug(XdgDesktopPortalKdeScreenCast) << QString("EGL version: %1.%2").arg(major).arg(minor); +} + +void ScreenCastPortal::initPipewire() +{ + m_stream = new ScreenCastStream; + m_stream->init(); + + connect(m_stream, &ScreenCastStream::streamReady, this, [] (uint nodeId) { + qCDebug(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is ready: " << nodeId; + }); + + connect(m_stream, &ScreenCastStream::startStreaming, this, [this] { + qCDebug(XdgDesktopPortalKdeScreenCast) << "Start streaming"; + m_streamingEnabled = true; + + if (!m_registryInitialized) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Cannot start stream because registry is not initialized yet"; + return; + } + if (m_registry->hasInterface(KWayland::Client::Registry::Interface::RemoteAccessManager)) { + KWayland::Client::Registry::AnnouncedInterface interface = m_registry->interface(KWayland::Client::Registry::Interface::RemoteAccessManager); + if (!interface.name && !interface.version) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Cannot start stream because remote access interface is not initialized yet"; + return; + } + m_remoteAccessManager = m_registry->createRemoteAccessManager(interface.name, interface.version); + connect(m_remoteAccessManager, &KWayland::Client::RemoteAccessManager::bufferReady, this, [this] (const void *output, const KWayland::Client::RemoteBuffer * rbuf) { + Q_UNUSED(output); + connect(rbuf, &KWayland::Client::RemoteBuffer::parametersObtained, this, [this, rbuf] { + processBuffer(rbuf); + }); + }); + } + }); + + connect(m_stream, &ScreenCastStream::stopStreaming, this, [this] { + if (m_streamingEnabled) { + qCDebug(XdgDesktopPortalKdeScreenCast) << "Stop streaming"; + m_remoteAccessManager->release(); + m_remoteAccessManager->destroy(); + + m_streamingEnabled = false; + m_stream->removeStream(); + + qDeleteAll(m_bindOutputs); + m_bindOutputs.clear(); + } + }); +} + +void ScreenCastPortal::initWayland() +{ + m_thread = new QThread(this); + m_connection = new KWayland::Client::ConnectionThread; + + connect(m_connection, &KWayland::Client::ConnectionThread::connected, this, &ScreenCastPortal::setupRegistry, Qt::QueuedConnection); + connect(m_connection, &KWayland::Client::ConnectionThread::connectionDied, this, [this] { + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + + m_connection->deleteLater(); + m_connection = nullptr; + + if (m_thread) { + m_thread->quit(); + if (!m_thread->wait(3000)) { + m_thread->terminate(); + m_thread->wait(); + } + delete m_thread; + m_thread = nullptr; + } + }); + connect(m_connection, &KWayland::Client::ConnectionThread::failed, this, [this] { + m_thread->quit(); + m_thread->wait(); + }); + + m_thread->start(); + m_connection->moveToThread(m_thread); + m_connection->initConnection(); +} + +uint ScreenCastPortal::CreateSession(const QDBusObjectPath &handle, + const QDBusObjectPath &session_handle, + const QString &app_id, + const QVariantMap &options, + QVariantMap &results) +{ + Q_UNUSED(results) + + qCDebug(XdgDesktopPortalKdeScreenCast) << "CreateSession called with parameters:"; + qCDebug(XdgDesktopPortalKdeScreenCast) << " handle: " << handle.path(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " session_handle: " << session_handle.path(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " app_id: " << app_id; + qCDebug(XdgDesktopPortalKdeScreenCast) << " options: " << options; + + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + Session *session = new Session(this, app_id, session_handle.path()); + if (sessionBus.registerVirtualObject(session_handle.path(), session, QDBusConnection::VirtualObjectRegisterOption::SubPath)) { + connect(session, &Session::closed, [this, session, session_handle] () { + m_sessionList.remove(session_handle.path()); + QDBusConnection::sessionBus().unregisterObject(session_handle.path()); + session->deleteLater(); + }); + m_sessionList.insert(session_handle.path(), session); + return 0; + } else { + qCDebug(XdgDesktopPortalKdeScreenCast) << sessionBus.lastError().message(); + qCDebug(XdgDesktopPortalKdeScreenCast) << "Failed to register session object: " << session_handle.path(); + session->deleteLater(); + return 2; + } +} + +uint ScreenCastPortal::SelectSources(const QDBusObjectPath &handle, + const QDBusObjectPath &session_handle, + const QString &app_id, + const QVariantMap &options, + QVariantMap &results) +{ + Q_UNUSED(results) + + qCDebug(XdgDesktopPortalKdeScreenCast) << "SelectSource called with parameters:"; + qCDebug(XdgDesktopPortalKdeScreenCast) << " handle: " << handle.path(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " session_handle: " << session_handle.path(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " app_id: " << app_id; + qCDebug(XdgDesktopPortalKdeScreenCast) << " options: " << options; + + uint types = Monitor; + Session *session = nullptr; + + session = m_sessionList.value(session_handle.path()); + + if (!session) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Tried to select sources on non-existing session " << session_handle.path(); + return 2; + } + + if (options.contains(QLatin1String("multiple"))) { + session->setMultipleSources(options.value(QLatin1String("multiple")).toBool()); + } + + if (options.contains(QLatin1String("types"))) { + types = (SourceType)(options.value(QLatin1String("types")).toUInt()); + } + + if (types == Window) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Screen cast of a window is not implemented"; + return 2; + } + + return 0; +} + +uint ScreenCastPortal::Start(const QDBusObjectPath &handle, + const QDBusObjectPath &session_handle, + const QString &app_id, + const QString &parent_window, + const QVariantMap &options, + QVariantMap &results) +{ + Q_UNUSED(results) + + qCDebug(XdgDesktopPortalKdeScreenCast) << "Start called with parameters:"; + qCDebug(XdgDesktopPortalKdeScreenCast) << " handle: " << handle.path(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " session_handle: " << session_handle.path(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " app_id: " << app_id; + qCDebug(XdgDesktopPortalKdeScreenCast) << " parent_window: " << parent_window; + qCDebug(XdgDesktopPortalKdeScreenCast) << " options: " << options; + + Session *session = nullptr; + session = m_sessionList.value(session_handle.path()); + + if (!session) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Tried to select sources on non-existing session " << session_handle.path(); + return 2; + } + + // TODO check whether we got some outputs? + if (m_outputMap.isEmpty()) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Failed to show dialog as there is no screen to select"; + return 2; + } + + QScopedPointer screenDialog(new ScreenChooserDialog(m_outputMap, session->multipleSources())); + + if (screenDialog->exec()) { + ScreenCastPortalOutput selectedOutput = m_outputMap.value(screenDialog->selectedScreens().first()); + + // HACK wait for stream to be ready + bool streamReady = false; + QEventLoop loop; + connect(m_stream, &ScreenCastStream::streamReady, this, [&loop, &streamReady] { + loop.quit(); + streamReady = true; + }); + + if (!m_stream->createStream(selectedOutput.resolution)) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Failed to create pipewire stream"; + return 2; + } + + QTimer::singleShot(3000, &loop, &QEventLoop::quit); + loop.exec(); + + if (!streamReady) { + qCWarning(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is not ready to be streamed"; + return 2; + } + + // TODO support multiple outputs + + qCDebug(XdgDesktopPortalKdeScreenCast) << "Pipewire node id: " << m_stream->nodeId(); + + KWayland::Client::Output *output = new KWayland::Client::Output(this); + output->setup(m_registry->bindOutput(selectedOutput.waylandOutputName, selectedOutput.waylandOutputVersion)); + m_bindOutputs << output; + + Stream stream; + stream.nodeId = m_stream->nodeId(); + stream.map = QVariantMap({{QLatin1String("size"), selectedOutput.resolution}}); + results.insert(QLatin1String("streams"), QVariant::fromValue({stream})); + + return 0; + } + + return 0; +} + +void ScreenCastPortal::addOutput(quint32 name, quint32 version) +{ + KWayland::Client::Output *output = new KWayland::Client::Output(this); + output->setup(m_registry->bindOutput(name, version)); + + connect(output, &KWayland::Client::Output::changed, this, [this, name, version, output] () { + qCDebug(XdgDesktopPortalKdeScreenCast) << "Adding output:"; + qCDebug(XdgDesktopPortalKdeScreenCast) << " manufacturer: " << output->manufacturer(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " model: " << output->model(); + qCDebug(XdgDesktopPortalKdeScreenCast) << " resolution: " << output->pixelSize(); + + ScreenCastPortalOutput portalOutput; + portalOutput.manufacturer = output->manufacturer(); + portalOutput.model = output->model(); + portalOutput.resolution = output->pixelSize(); + portalOutput.waylandOutputName = name; + portalOutput.waylandOutputVersion = version; + portalOutput.setOutputType(output->model()); + + m_outputMap.insert(name, portalOutput); + + delete output; + }); +} + +void ScreenCastPortal::removeOutput(quint32 name) +{ + ScreenCastPortalOutput output = m_outputMap.take(name); + qCDebug(XdgDesktopPortalKdeScreenCast) << "Removing output:"; + qCDebug(XdgDesktopPortalKdeScreenCast) << " manufacturer: " << output.manufacturer; + qCDebug(XdgDesktopPortalKdeScreenCast) << " model: " << output.model; +} + +void ScreenCastPortal::processBuffer(const KWayland::Client::RemoteBuffer* rbuf) +{ + QScopedPointer guard(rbuf); + + auto gbmHandle = rbuf->fd(); + auto width = rbuf->width(); + auto height = rbuf->height(); + auto stride = rbuf->stride(); + auto format = rbuf->format(); + + qCDebug(XdgDesktopPortalKdeScreenCast) << QString("Incoming GBM fd %1, %2x%3, stride %4, fourcc 0x%5").arg(gbmHandle).arg(width).arg(height).arg(stride).arg(QString::number(format, 16)); + + if (!m_streamingEnabled) { + qCDebug(XdgDesktopPortalKdeScreenCast) << "Streaming is disabled"; + close(gbmHandle); + return; + } + + if (!gbm_device_is_format_supported(m_gbmDevice, format, GBM_BO_USE_SCANOUT)) { + qCritical() << "GBM format is not supported by device!"; + } + + // import GBM buffer that was passed from KWin + gbm_import_fd_data importInfo = {gbmHandle, width, height, stride, format}; + gbm_bo *imported = gbm_bo_import(m_gbmDevice, GBM_BO_IMPORT_FD, &importInfo, GBM_BO_USE_SCANOUT); + if (!imported) { + qCritical() << "Cannot import passed GBM fd:" << strerror(errno); + } + + // bind context to render thread + eglMakeCurrent(m_egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, m_egl.context); + + // create EGL image from imported BO + EGLImageKHR image = eglCreateImageKHR(m_egl.display, NULL, EGL_NATIVE_PIXMAP_KHR, imported, NULL); + if (image == EGL_NO_IMAGE_KHR) { + qCritical() << "Error creating EGLImageKHR" << formatGLError(glGetError()); + return; + } + // create GL 2D texture for framebuffer + GLuint texture; + glGenTextures(1, &texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, texture); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); + + // bind framebuffer to copy pixels from + GLuint framebuffer; + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + qCritical() << "glCheckFramebufferStatus failed:" << formatGLError(glGetError()); + glDeleteTextures(1, &texture); + glDeleteFramebuffers(1, &framebuffer); + eglDestroyImageKHR(m_egl.display, image); + return; + } + + auto capture = new QImage(QSize(width, height), QImage::Format_RGBA8888); + glViewport(0, 0, width, height); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, capture->bits()); + + m_stream->recordFrame(capture->bits()); + + gbm_bo_destroy(imported); + glDeleteTextures(1, &texture); + glDeleteFramebuffers(1, &framebuffer); + eglDestroyImageKHR(m_egl.display, image); + + delete capture; + + close(gbmHandle); +} + +void ScreenCastPortal::setupRegistry() +{ + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + m_registry = new KWayland::Client::Registry(this); + + connect(m_registry, &KWayland::Client::Registry::outputAnnounced, this, &ScreenCastPortal::addOutput); + connect(m_registry, &KWayland::Client::Registry::outputRemoved, this, &ScreenCastPortal::removeOutput); + + connect(m_registry, &KWayland::Client::Registry::interfacesAnnounced, this, [this] { + m_registryInitialized = true; + qCDebug(XdgDesktopPortalKdeScreenCast) << "Registry initialized"; + }); + + m_registry->create(m_connection); + m_registry->setEventQueue(m_queue); + m_registry->setup(); +} diff --git a/src/screencast.h b/src/screencast.h new file mode 100644 index 0000000..25aef12 --- /dev/null +++ b/src/screencast.h @@ -0,0 +1,156 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * 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 library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#ifndef XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H +#define XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H + +#include +#include +#include + +#include + +#include +#include + +namespace KWayland { + namespace Client { + class ConnectionThread; + class EventQueue; + class OutputDevice; + class Registry; + class RemoteAccessManager; + class RemoteBuffer; + class Output; + } +} + +class Session; +class ScreenChooserDialog; +class ScreenCastStream; + +class ScreenCastPortalOutput +{ + enum OutputType { + Laptop, + Monitor, + Television + }; + + void setOutputType(const QString &type); + + QString manufacturer; + QString model; + QSize resolution; + OutputType outputType; + + // Needed for later output binding + int waylandOutputName; + int waylandOutputVersion; + + friend class ScreenCastPortal; + friend class ScreenChooserDialog; +}; + +class ScreenCastPortal : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.impl.portal.ScreenCast") + Q_PROPERTY(uint version READ version) + Q_PROPERTY(uint AvailableSourceTypes READ AvailableSourceTypes) + +public: + typedef struct { + uint nodeId; + QVariantMap map; + } Stream; + typedef QList Streams; + + enum SourceType { + Any = 0, + Monitor, + Window + }; + + ScreenCastPortal(QObject *parent); + ~ScreenCastPortal(); + + uint version() const { return 1; } + uint AvailableSourceTypes() const { return Monitor; }; + +public Q_SLOTS: + uint CreateSession(const QDBusObjectPath &handle, + const QDBusObjectPath &session_handle, + const QString &app_id, + const QVariantMap &options, + QVariantMap &results); + + uint SelectSources(const QDBusObjectPath &handle, + const QDBusObjectPath &session_handle, + const QString &app_id, + const QVariantMap &options, + QVariantMap &results); + + uint Start(const QDBusObjectPath &handle, + const QDBusObjectPath &session_handle, + const QString &app_id, + const QString &parent_window, + const QVariantMap &options, + QVariantMap &results); + +private Q_SLOTS: + void addOutput(quint32 name, quint32 version); + void removeOutput(quint32 name); + void processBuffer(const KWayland::Client::RemoteBuffer *rbuf); + void setupRegistry(); + +private: + void initDrm(); + void initEGL(); + void initPipewire(); + void initWayland(); + + bool m_registryInitialized; + bool m_streamingEnabled; + + QMap m_sessionList; + QMap m_outputMap; + QList m_bindOutputs; + + QThread *m_thread; + + ScreenCastStream *m_stream; + + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Registry *m_registry; + KWayland::Client::RemoteAccessManager *m_remoteAccessManager; + + qint32 m_drmFd = 0; // for GBM buffer mmap + gbm_device *m_gbmDevice = nullptr; // for passed GBM buffer retrieval + struct { + QList extensions; + EGLDisplay display = EGL_NO_DISPLAY; + EGLContext context = EGL_NO_CONTEXT; + } m_egl; +}; + +#endif // XDG_DESKTOP_PORTAL_KDE_SCREENCAST_H + diff --git a/src/screencaststream.cpp b/src/screencaststream.cpp new file mode 100644 index 0000000..66f0032 --- /dev/null +++ b/src/screencaststream.cpp @@ -0,0 +1,419 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * 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 library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#include "screencaststream.h" + +#include +#include +#include + +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCastStream, "xdp-kde-screencast-stream") + +class PwFraction { +public: + int num; + int denom; +}; + +// Stolen from mutter + +#define MAX_TERMS 30 +#define MIN_DIVISOR 1.0e-10 +#define MAX_ERROR 1.0e-20 + +#define PROP_RANGE(min, max) 2, (min), (max) + +#define BITS_PER_PIXEL 4 + +static int greatestCommonDivisor(int a, int b) +{ + while (b != 0) { + int temp = a; + + a = b; + b = temp % b; + } + + return ABS(a); +} + +static PwFraction pipewireFractionFromDouble(double src) +{ + double V, F; /* double being converted */ + int N, D; /* will contain the result */ + int A; /* current term in continued fraction */ + int64_t N1, D1; /* numerator, denominator of last approx */ + int64_t N2, D2; /* numerator, denominator of previous approx */ + int i; + int gcd; + gboolean negative = FALSE; + + /* initialize fraction being converted */ + F = src; + if (F < 0.0) { + F = -F; + negative = TRUE; + } + + V = F; + /* initialize fractions with 1/0, 0/1 */ + N1 = 1; + D1 = 0; + N2 = 0; + D2 = 1; + N = 1; + D = 1; + + for (i = 0; i < MAX_TERMS; i++) { + /* get next term */ + A = (gint) F; /* no floor() needed, F is always >= 0 */ + /* get new divisor */ + F = F - A; + + /* calculate new fraction in temp */ + N2 = N1 * A + N2; + D2 = D1 * A + D2; + + /* guard against overflow */ + if (N2 > G_MAXINT || D2 > G_MAXINT) + break; + + N = N2; + D = D2; + + /* save last two fractions */ + N2 = N1; + D2 = D1; + N1 = N; + D1 = D; + + /* quit if dividing by zero or close enough to target */ + if (F < MIN_DIVISOR || fabs (V - ((gdouble) N) / D) < MAX_ERROR) + break; + + /* Take reciprocal */ + F = 1 / F; + } + + /* fix for overflow */ + if (D == 0) { + N = G_MAXINT; + D = 1; + } + + /* fix for negative */ + if (negative) + N = -N; + + /* simplify */ + gcd = greatestCommonDivisor(N, D); + if (gcd) { + N /= gcd; + D /= gcd; + } + + PwFraction fraction; + fraction.num = N; + fraction.denom = D; + + return fraction; +} + +static void onStateChanged(void *_data, pw_remote_state old, pw_remote_state state, const char *error) +{ + Q_UNUSED(old); + Q_UNUSED(_data); + + switch (state) { + case PW_REMOTE_STATE_ERROR: + // TODO notify error + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Remote error: " << error; + break; + case PW_REMOTE_STATE_CONNECTED: + // TODO notify error + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); + break; + default: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); + break; + } +} + +static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message) +{ + Q_UNUSED(old) + + ScreenCastStream *pw = static_cast(data); + + switch (state) { + case PW_STREAM_STATE_ERROR: + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Stream error: " << error_message; + break; + case PW_STREAM_STATE_CONFIGURE: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->streamReady((uint)pw_stream_get_node_id(pw->pwStream)); + break; + case PW_STREAM_STATE_UNCONNECTED: + case PW_STREAM_STATE_CONNECTING: + case PW_STREAM_STATE_READY: + case PW_STREAM_STATE_PAUSED: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->stopStreaming(); + break; + case PW_STREAM_STATE_STREAMING: + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); + Q_EMIT pw->startStreaming(); + break; + } +} + +static void onStreamFormatChanged(void *data, struct spa_pod *format) +{ + qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream format changed"; + + ScreenCastStream *pw = static_cast(data); + + uint8_t paramsBuffer[1024]; + int32_t width, height, stride, size; + struct spa_pod_builder pod_builder; + struct spa_pod *params[1]; + const int bpp = 4; + + if (!format) { + pw_stream_finish_format(pw->pwStream, 0, NULL, 0); + return; + } + + spa_format_video_raw_parse (format, &pw->videoFormat, &pw->pwType->format_video); + + width = pw->videoFormat.size.width; + height =pw->videoFormat.size.height; + stride = SPA_ROUND_UP_N (width * bpp, 4); + size = height * stride; + + pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer)); + + params[0] = (spa_pod*) spa_pod_builder_object (&pod_builder, + pw->pwCoreType->param.idBuffers, pw->pwCoreType->param_buffers.Buffers, + ":", pw->pwCoreType->param_buffers.size, "i", size, + ":", pw->pwCoreType->param_buffers.stride, "i", stride, + ":", pw->pwCoreType->param_buffers.buffers, "iru", 16, PROP_RANGE (2, 16), + ":", pw->pwCoreType->param_buffers.align, "i", 16); + + pw_stream_finish_format (pw->pwStream, 0, + params, G_N_ELEMENTS (params)); +} + +static const struct pw_remote_events pwRemoteEvents = { + .version = PW_VERSION_REMOTE_EVENTS, + .destroy = nullptr, + .info_changed = nullptr, + .sync_reply = nullptr, + .state_changed = onStateChanged, +}; + +static const struct pw_stream_events pwStreamEvents = { + .version = PW_VERSION_STREAM_EVENTS, + .destroy = nullptr, + .state_changed = onStreamStateChanged, + .format_changed = onStreamFormatChanged, + .add_buffer = nullptr, + .remove_buffer = nullptr, + .new_buffer = nullptr, + .need_buffer = nullptr, +}; + +ScreenCastStream::ScreenCastStream(QObject *parent) + : QObject(parent) +{ +} + +ScreenCastStream::~ScreenCastStream() +{ + if (pwType) { + delete pwType; + } + + if (pwCore) { + pw_core_destroy(pwCore); + } + + if (pwLoop) { + pw_loop_leave(pwLoop); + pw_loop_destroy(pwLoop); + } +} + +void ScreenCastStream::init() +{ + pw_init(nullptr, nullptr); + + pwLoop = pw_loop_new(nullptr); + socketNotifier.reset(new QSocketNotifier(pw_loop_get_fd(pwLoop), QSocketNotifier::Read)); + connect(socketNotifier.data(), &QSocketNotifier::activated, this, &ScreenCastStream::processPipewireEvents); + + pwCore = pw_core_new(pwLoop, nullptr); + pwCoreType = pw_core_get_type(pwCore); + pwRemote = pw_remote_new(pwCore, nullptr, 0); + + spa_debug_set_type_map(pwCoreType->map); + + initializePwTypes(); + + pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); + + pw_remote_connect(pwRemote); +} + +uint ScreenCastStream::nodeId() +{ + if (pwStream) { + return (uint)pw_stream_get_node_id(pwStream); + } + + return 0; +} + +bool ScreenCastStream::createStream(const QSize &resolution) +{ + if (pw_remote_get_state(pwRemote, nullptr) != PW_REMOTE_STATE_CONNECTED) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Cannot create pipewire stream"; + return false; + } + + uint8_t buffer[1024]; + spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + const float frameRate = 25; + + spa_fraction maxFramerate; + spa_fraction minFramerate; + const spa_pod *params[1]; + + pwStream = pw_stream_new(pwRemote, "kwin-screen-cast", nullptr); + + PwFraction fraction = pipewireFractionFromDouble(frameRate); + + minFramerate = SPA_FRACTION(1, 1); + maxFramerate = SPA_FRACTION((uint32_t)fraction.num, (uint32_t)fraction.denom); + + spa_fraction paramFraction = SPA_FRACTION(0, 1); + spa_rectangle paramRectangle = SPA_RECTANGLE((uint32_t)resolution.width(), (uint32_t)resolution.height()); + + params[0] = (spa_pod*)spa_pod_builder_object(&podBuilder, + pwCoreType->param.idEnumFormat, pwCoreType->spa_format, + "I", pwType->media_type.video, + "I", pwType->media_subtype.raw, + ":", pwType->format_video.format, "I", pwType->video_format.RGBx, + ":", pwType->format_video.size, "R", ¶mRectangle, + ":", pwType->format_video.framerate, "F", ¶mFraction, + ":", pwType->format_video.max_framerate, "Fr", &maxFramerate, PROP_RANGE (&minFramerate, &maxFramerate)); + + pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); + + if (pw_stream_connect(pwStream, PW_DIRECTION_OUTPUT, nullptr, PW_STREAM_FLAG_NONE, params, G_N_ELEMENTS(¶ms)) != 0) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Could not connect to stream"; + return false; + } + + return true; +} + +bool ScreenCastStream::recordFrame(uint8_t *screenData) +{ + uint32_t bufferId; + struct spa_buffer *buffer; + uint8_t *map = nullptr; + uint8_t *data = nullptr; + + // TODO check timestamp like mutter does? + + if (!pwStream) { + return false; + } + + bufferId = pw_stream_get_empty_buffer(pwStream); + + if (bufferId == SPA_ID_INVALID) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to get empty stream buffer: " << strerror(errno); + return false; + } + + buffer = pw_stream_peek_buffer(pwStream, bufferId); + + if (buffer->datas[0].type == pwCoreType->data.MemFd) { + map = (uint8_t *)mmap(nullptr, buffer->datas[0].maxsize + buffer->datas[0].mapoffset, PROT_READ | PROT_WRITE, MAP_SHARED, buffer->datas[0].fd, 0); + + if (map == MAP_FAILED) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to mmap pipewire stream buffer: " << strerror(errno); + return false; + } + + data = SPA_MEMBER(map, buffer->datas[0].mapoffset, uint8_t); + } else if (buffer->datas[0].type == pwCoreType->data.MemPtr) { + data = (uint8_t *) buffer->datas[0].data; + } else { + return false; + } + + memcpy(data, screenData, BITS_PER_PIXEL * videoFormat.size.height * videoFormat.size.width * sizeof(uint8_t)); + + if (map) { + munmap(map, buffer->datas[0].maxsize + buffer->datas[0].mapoffset); + } + + buffer->datas[0].chunk->size = buffer->datas[0].maxsize; + + pw_stream_send_buffer(pwStream, bufferId); + + return true; +} + +void ScreenCastStream::removeStream() +{ + pw_stream_destroy(pwStream); + pwStream = nullptr; +} + +void ScreenCastStream::initializePwTypes() +{ + // raw C-like ScreenCastStream type map + auto map = pwCoreType->map; + + pwType = new PwType(); + + spa_type_media_type_map(map, &pwType->media_type); + spa_type_media_subtype_map(map, &pwType->media_subtype); + spa_type_format_video_map (map, &pwType->format_video); + spa_type_video_format_map (map, &pwType->video_format); +} + +void ScreenCastStream::processPipewireEvents() +{ + int result = pw_loop_iterate(pwLoop, 0); + if (result < 0) { + qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to iterate over pipewire loop: " << spa_strerror(result); + } +} diff --git a/src/screencaststream.h b/src/screencaststream.h new file mode 100644 index 0000000..0ea4a9b --- /dev/null +++ b/src/screencaststream.h @@ -0,0 +1,99 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * 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 library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#ifndef SCREEN_CAST_STREAM_H +#define SCREEN_CAST_STREAM_H + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +class PwType { +public: + spa_type_media_type media_type; + spa_type_media_subtype media_subtype; + spa_type_format_video format_video; + spa_type_video_format video_format; +}; + +class QSocketNotifier; + +class ScreenCastStream : public QObject +{ + Q_OBJECT +public: + explicit ScreenCastStream(QObject *parent = nullptr); + ~ScreenCastStream(); + + // Public + void init(); + uint nodeId(); + + // Public because we need access from static functions + bool createStream(const QSize &resolution); + bool recordFrame(uint8_t *screenData); + + void removeStream(); +Q_SIGNALS: + void streamReady(uint nodeId); + void startStreaming(); + void stopStreaming(); + +private: + void initializePwTypes(); + +private Q_SLOTS: + void processPipewireEvents(); + +public: + pw_core *pwCore = nullptr; + pw_loop *pwLoop = nullptr; + pw_node *pwNode = nullptr; + pw_stream *pwStream = nullptr; + pw_type *pwCoreType = nullptr; + pw_remote *pwRemote = nullptr; + PwType *pwType = nullptr; + + spa_hook remoteListener; + spa_hook streamListener; + + QScopedPointer socketNotifier; + + spa_video_info_raw videoFormat; + +}; + +#endif // SCREEN_CAST_STREAM_H + + diff --git a/src/screenchooserdialog.cpp b/src/screenchooserdialog.cpp new file mode 100644 index 0000000..8db69e9 --- /dev/null +++ b/src/screenchooserdialog.cpp @@ -0,0 +1,83 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * 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 library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#include "screenchooserdialog.h" +#include "ui_screenchooserdialog.h" +#include "screencast.h" + +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenChooserDialog, "xdp-kde-screen-chooser-dialog") + +ScreenChooserDialog::ScreenChooserDialog(const QMap &screens, bool multiple, QDialog *parent, Qt::WindowFlags flags) + : QDialog(parent, flags) + , m_dialog(new Ui::ScreenChooserDialog) +{ + m_dialog->setupUi(this); + + QMapIterator it(screens); + while (it.hasNext()) { + it.next(); + QListWidgetItem *widgetItem = new QListWidgetItem(m_dialog->screenView); + widgetItem->setData(Qt::UserRole, it.key()); + if (it.value().outputType == ScreenCastPortalOutput::Laptop) { + widgetItem->setIcon(QIcon::fromTheme("computer-laptop")); + widgetItem->setText(i18n("Laptop screen\nModel: %1").arg(it.value().model)); + } else if (it.value().outputType == ScreenCastPortalOutput::Monitor) { + widgetItem->setIcon(QIcon::fromTheme("video-display")); + widgetItem->setText(i18n("Manufacturer: %1\nModel: %2").arg(it.value().manufacturer).arg(it.value().model)); + } else { + widgetItem->setIcon(QIcon::fromTheme("video-television")); + widgetItem->setText(i18n("Manufacturer: %1\nModel: %2").arg(it.value().manufacturer).arg(it.value().model)); + } + } + + m_dialog->screenView->setItemSelected(m_dialog->screenView->itemAt(0, 0), true); + + connect(m_dialog->buttonBox, &QDialogButtonBox::accepted, this, &ScreenChooserDialog::accept); + connect(m_dialog->buttonBox, &QDialogButtonBox::rejected, this, &ScreenChooserDialog::reject); + connect(m_dialog->screenView, &QListWidget::itemDoubleClicked, this, &ScreenChooserDialog::accept); + + if (multiple) { + m_dialog->screenView->setSelectionMode(QAbstractItemView::ExtendedSelection); + } + + m_dialog->buttonBox->button(QDialogButtonBox::Ok)->setText(i18n("Share")); + setWindowTitle(i18n("Select screen to share")); +} + +ScreenChooserDialog::~ScreenChooserDialog() +{ + delete m_dialog; +} + +QList ScreenChooserDialog::selectedScreens() const +{ + QList selectedScreens; + + foreach (QListWidgetItem *item, m_dialog->screenView->selectedItems()) { + selectedScreens << item->data(Qt::UserRole).toUInt(); + } + + return selectedScreens; +} diff --git a/src/desktopportal.cpp b/src/screenchooserdialog.h similarity index 53% copy from src/desktopportal.cpp copy to src/screenchooserdialog.h index 49881c9..4a3fef5 100644 --- a/src/desktopportal.cpp +++ b/src/screenchooserdialog.h @@ -1,45 +1,47 @@ /* - * Copyright © 2016 Red Hat, Inc + * Copyright © 2018 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ -#include "desktopportal.h" +#ifndef XDG_DESKTOP_PORTAL_KDE_SCREENCHOOSER_DIALOG_H +#define XDG_DESKTOP_PORTAL_KDE_SCREENCHOOSER_DIALOG_H +#include #include -#include -#include -#include -#include - -Q_LOGGING_CATEGORY(XdgDesktopPortalKdeDesktopPortal, "xdg-desktop-portal-kde-desktop-portal") - -DesktopPortal::DesktopPortal(QObject *parent) - : QObject(parent) - , m_access(new AccessPortal(this)) - , m_appChooser(new AppChooserPortal(this)) - , m_email(new EmailPortal(this)) - , m_fileChooser(new FileChooserPortal(this)) - , m_inhibit(new InhibitPortal(this)) - , m_notification(new NotificationPortal(this)) - , m_print(new PrintPortal(this)) + +namespace Ui { +class ScreenChooserDialog; } -DesktopPortal::~DesktopPortal() +class ScreenCastPortalOutput; + +class ScreenChooserDialog : public QDialog { -} + Q_OBJECT +public: + ScreenChooserDialog(const QMap &screens, bool multiple = false, QDialog *parent = nullptr, Qt::WindowFlags flags = 0); + ~ScreenChooserDialog(); + + QList selectedScreens() const; + +private: + Ui::ScreenChooserDialog * m_dialog; +}; + +#endif // XDG_DESKTOP_PORTAL_KDE_SCREENCHOOSER_DIALOG_H diff --git a/src/screenchooserdialog.ui b/src/screenchooserdialog.ui new file mode 100644 index 0000000..17322a8 --- /dev/null +++ b/src/screenchooserdialog.ui @@ -0,0 +1,47 @@ + + + ScreenChooserDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + + + + QAbstractItemView::SingleSelection + + + + 48 + 48 + + + + false + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/src/session.cpp b/src/session.cpp new file mode 100644 index 0000000..fbddcfb --- /dev/null +++ b/src/session.cpp @@ -0,0 +1,126 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * 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 library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#include "desktopportal.h" +#include "session.h" + +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(XdgSessionKdeSession, "xdp-kde-session") + +Session::Session(QObject *parent, const QString &appId, const QString &path) + : QDBusVirtualObject(parent) + , m_multipleSources(false) + , m_appId(appId) + , m_path(path) +{ +} + +Session::~Session() +{ +} + +bool Session::handleMessage(const QDBusMessage &message, const QDBusConnection &connection) +{ + Q_UNUSED(connection); + + if (message.path() != m_path) { + return false; + } + + /* Check to make sure we're getting properties on our interface */ + if (message.type() != QDBusMessage::MessageType::MethodCallMessage) { + return false; + } + + qCDebug(XdgSessionKdeSession) << message.interface(); + qCDebug(XdgSessionKdeSession) << message.member(); + qCDebug(XdgSessionKdeSession) << message.path(); + + if (message.interface() == QLatin1String("org.freedesktop.impl.portal.Session")) { + if (message.member() == QLatin1String("Close")) { + Q_EMIT closed(); + QDBusMessage reply = message.createReply(); + return connection.send(reply); + } + } else if (message.interface() == QLatin1String("org.freedesktop.DBus.Properties")) { + if (message.member() == QLatin1String("Get")) { + if (message.arguments().count() == 2) { + const QString interface = message.arguments().at(0).toString(); + const QString property = message.arguments().at(1).toString(); + + if (interface == QLatin1String("org.freedesktop.impl.portal.Session") && + property == QLatin1String("version")) { + QList arguments; + arguments << 1; + + QDBusMessage reply = message.createReply(); + reply.setArguments(arguments); + return connection.send(reply); + } + } + } + } + + return false; +} + +QString Session::introspect(const QString &path) const +{ + QString nodes; + + if (path.startsWith(QLatin1String("/org/freedesktop/portal/desktop/session/"))) { + nodes = QStringLiteral( + "" + " " + " " + "" + "" + "" + ""); + } + + qCDebug(XdgSessionKdeSession) << nodes; + + return nodes; +} + +bool Session::multipleSources() const +{ + return m_multipleSources; +} + +void Session::setMultipleSources(bool multipleSources) +{ + m_multipleSources = multipleSources; +} + +bool Session::close() +{ + QDBusMessage reply = QDBusMessage::createSignal(m_path, QLatin1String("org.freedesktop.impl.portal.Session"), QLatin1String("Closed")); + return QDBusConnection::sessionBus().send(reply); +} + diff --git a/src/desktopportal.h b/src/session.h similarity index 53% copy from src/desktopportal.h copy to src/session.h index 0fe1b4c..3ede66b 100644 --- a/src/desktopportal.h +++ b/src/session.h @@ -1,53 +1,52 @@ /* - * Copyright © 2016 Red Hat, Inc + * Copyright © 2018 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ -#ifndef XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H -#define XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H +#ifndef XDG_DESKTOP_PORTAL_KDE_SESSION_H +#define XDG_DESKTOP_PORTAL_KDE_SESSION_H #include #include -#include "access.h" -#include "appchooser.h" -#include "email.h" -#include "filechooser.h" -#include "inhibit.h" -#include "notification.h" -#include "print.h" - -class DesktopPortal : public QObject +class Session : public QDBusVirtualObject { Q_OBJECT public: - explicit DesktopPortal(QObject *parent = nullptr); - ~DesktopPortal(); + explicit Session(QObject *parent = nullptr, const QString &appId = QString(), const QString &path = QString()); + ~Session(); + + bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) Q_DECL_OVERRIDE; + QString introspect(const QString &path) const Q_DECL_OVERRIDE; + + bool close(); + + bool multipleSources() const; + void setMultipleSources(bool multipleSources); + +Q_SIGNALS: + void closed(); private: - AccessPortal *m_access; - AppChooserPortal *m_appChooser; - EmailPortal *m_email; - FileChooserPortal *m_fileChooser; - InhibitPortal *m_inhibit; - NotificationPortal *m_notification; - PrintPortal *m_print; + bool m_multipleSources; + const QString m_appId; + const QString m_path; }; -#endif // XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H +#endif // XDG_DESKTOP_PORTAL_KDE_SESSION_H diff --git a/src/xdg-desktop-portal-kde.cpp b/src/xdg-desktop-portal-kde.cpp index 98f3039..9a52e15 100644 --- a/src/xdg-desktop-portal-kde.cpp +++ b/src/xdg-desktop-portal-kde.cpp @@ -1,48 +1,48 @@ /* * Copyright © 2016 Red Hat, Inc * * 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 library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Jan Grulich */ #include #include #include #include "desktopportal.h" -Q_LOGGING_CATEGORY(XdgDesktopPortalKde, "xdg-desktop-portal-kde") +Q_LOGGING_CATEGORY(XdgDesktopPortalKde, "xdp-kde") int main(int argc, char *argv[]) { QApplication a(argc, argv); a.setQuitOnLastWindowClosed(false); QDBusConnection sessionBus = QDBusConnection::sessionBus(); if (sessionBus.registerService(QLatin1String("org.freedesktop.impl.portal.desktop.kde"))) { DesktopPortal *desktopPortal = new DesktopPortal(&a); if (sessionBus.registerObject(QLatin1String("/org/freedesktop/portal/desktop"), desktopPortal, QDBusConnection::ExportAdaptors)) { qCDebug(XdgDesktopPortalKde) << "Desktop portal registered successfuly"; } else { qCDebug(XdgDesktopPortalKde) << "Failed to register desktop portal"; } } else { qCDebug(XdgDesktopPortalKde) << "Failed to register org.freedesktop.impl.portal.desktop.kde service"; } return a.exec(); }