From 084bfebcc86e7a5f31c9e7690fab2aadb48b3d71 Mon Sep 17 00:00:00 2001 From: Aleix Pol Date: Tue, 19 Oct 2021 01:25:14 +0200 Subject: [PATCH] Introduce the VirtualMonitor plugin It allows to use other paired devices as external displays transparently. --- .../kdeconnectdeclarativeplugin.cpp | 1 + interfaces/CMakeLists.txt | 1 + interfaces/dbusinterfaces.cpp | 9 ++ interfaces/dbusinterfaces.h | 10 ++ .../package/contents/ui/DeviceDelegate.qml | 15 ++ .../package/contents/ui/VirtualMonitor.qml | 24 +++ plugins/CMakeLists.txt | 1 + plugins/virtualmonitor/CMakeLists.txt | 21 +++ plugins/virtualmonitor/README | 7 + .../kdeconnect_virtualmonitor.json | 29 ++++ .../virtualmonitor/virtualmonitorplugin.cpp | 138 ++++++++++++++++++ plugins/virtualmonitor/virtualmonitorplugin.h | 43 ++++++ 12 files changed, 299 insertions(+) create mode 100644 plasmoid/package/contents/ui/VirtualMonitor.qml create mode 100644 plugins/virtualmonitor/CMakeLists.txt create mode 100644 plugins/virtualmonitor/README create mode 100644 plugins/virtualmonitor/kdeconnect_virtualmonitor.json create mode 100644 plugins/virtualmonitor/virtualmonitorplugin.cpp create mode 100644 plugins/virtualmonitor/virtualmonitorplugin.h diff --git a/declarativeplugin/kdeconnectdeclarativeplugin.cpp b/declarativeplugin/kdeconnectdeclarativeplugin.cpp index b72e41eb7..188f2688c 100644 --- a/declarativeplugin/kdeconnectdeclarativeplugin.cpp +++ b/declarativeplugin/kdeconnectdeclarativeplugin.cpp @@ -99,6 +99,7 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri) registerFactory(uri, "ShareDbusInterfaceFactory"); registerFactory(uri, "RemoteSystemVolumeDbusInterfaceFactory"); registerFactory(uri, "BigscreenDbusInterfaceFactory"); + registerFactory(uri, "VirtualmonitorDbusInterfaceFactory"); } void KdeConnectDeclarativePlugin::initializeEngine(QQmlEngine* engine, const char* uri) diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt index e424dd717..d53bd3541 100644 --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -56,6 +56,7 @@ geninterface(${PROJECT_SOURCE_DIR}/plugins/sms/conversationsdbusinterface.h conv geninterface(${PROJECT_SOURCE_DIR}/plugins/share/shareplugin.h shareinterface) geninterface(${PROJECT_SOURCE_DIR}/plugins/remotesystemvolume/remotesystemvolumeplugin.h remotesystemvolumeinterface) geninterface(${PROJECT_SOURCE_DIR}/plugins/bigscreen/bigscreenplugin.h bigscreeninterface) +geninterface(${PROJECT_SOURCE_DIR}/plugins/virtualmonitor/virtualmonitorplugin.h virtualmonitorinterface) add_library(kdeconnectinterfaces ${libkdeconnect_SRC}) set_target_properties(kdeconnectinterfaces PROPERTIES diff --git a/interfaces/dbusinterfaces.cpp b/interfaces/dbusinterfaces.cpp index 5e76f061f..d861a9fc1 100644 --- a/interfaces/dbusinterfaces.cpp +++ b/interfaces/dbusinterfaces.cpp @@ -200,3 +200,12 @@ BigscreenDbusInterface::BigscreenDbusInterface(const QString& deviceId, QObject* BigscreenDbusInterface::~BigscreenDbusInterface() { } + +VirtualmonitorDbusInterface::VirtualmonitorDbusInterface(const QString& deviceId, QObject* parent): + OrgKdeKdeconnectDeviceVirtualmonitorInterface(DaemonDbusInterface::activatedService(), QStringLiteral("/modules/kdeconnect/devices/") + deviceId + QStringLiteral("/virtualmonitor"), QDBusConnection::sessionBus(), parent) +{ +} + +VirtualmonitorDbusInterface::~VirtualmonitorDbusInterface() +{ +} diff --git a/interfaces/dbusinterfaces.h b/interfaces/dbusinterfaces.h index 9e19fe5d8..c18dc0c3d 100644 --- a/interfaces/dbusinterfaces.h +++ b/interfaces/dbusinterfaces.h @@ -27,6 +27,7 @@ #include "shareinterface.h" #include "remotesystemvolumeinterface.h" #include "bigscreeninterface.h" +#include "virtualmonitorinterface.h" /** * Using these "proxy" classes just in case we need to rename the @@ -259,6 +260,15 @@ public: ~BigscreenDbusInterface() override; }; +class KDECONNECTINTERFACES_EXPORT VirtualmonitorDbusInterface + : public OrgKdeKdeconnectDeviceVirtualmonitorInterface +{ + Q_OBJECT +public: + explicit VirtualmonitorDbusInterface(const QString& deviceId, QObject* parent = nullptr); + ~VirtualmonitorDbusInterface() override; +}; + template static void setWhenAvailable(const QDBusPendingReply& pending, W func, QObject* parent) { diff --git a/plasmoid/package/contents/ui/DeviceDelegate.qml b/plasmoid/package/contents/ui/DeviceDelegate.qml index 73ced4526..76f0bdbea 100644 --- a/plasmoid/package/contents/ui/DeviceDelegate.qml +++ b/plasmoid/package/contents/ui/DeviceDelegate.qml @@ -58,6 +58,7 @@ PlasmaComponents.ListItem RowLayout { + width: parent.width Battery { id: battery @@ -77,6 +78,20 @@ PlasmaComponents.ListItem textFormat: Text.PlainText } + PlasmaComponents3.ToolButton { + VirtualMonitor { + id: vd + device: root.device + } + icon.name: "video-monitor" + text: i18n("Virtual Display") + visible: vd.available + onClicked: { + if (!vd.plugin.requestVirtualMonitor()) { + console.warn("Failed to create the virtual monitor") + } + } + } RowLayout { id: connectionInformation diff --git a/plasmoid/package/contents/ui/VirtualMonitor.qml b/plasmoid/package/contents/ui/VirtualMonitor.qml new file mode 100644 index 000000000..12a21eb3f --- /dev/null +++ b/plasmoid/package/contents/ui/VirtualMonitor.qml @@ -0,0 +1,24 @@ +/** + * SPDX-FileCopyrightText: 2021 Aleix Pol i Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.kdeconnect 1.0 + +QtObject +{ + property alias device: checker.device + readonly property alias available: checker.available + + readonly property PluginChecker pluginChecker: PluginChecker { + id: checker + pluginName: "virtualmonitor" + } + + readonly property QtObject plugin: available ? VirtualmonitorDbusInterfaceFactory.create(device.id()) : null +} + diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 5e6c22526..86cc19155 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -36,6 +36,7 @@ if(NOT SAILFISHOS) add_subdirectory(mousepad) add_subdirectory(sms) add_subdirectory(screensaver-inhibit) + add_subdirectory(virtualmonitor) if(NOT WIN32) add_subdirectory(sendnotifications) diff --git a/plugins/virtualmonitor/CMakeLists.txt b/plugins/virtualmonitor/CMakeLists.txt new file mode 100644 index 000000000..b12896f7c --- /dev/null +++ b/plugins/virtualmonitor/CMakeLists.txt @@ -0,0 +1,21 @@ +set(debug_file_SRCS) +ecm_qt_declare_logging_category( + debug_file_SRCS HEADER plugin_virtualmonitor_debug.h + IDENTIFIER KDECONNECT_PLUGIN_VIRTUALMONITOR CATEGORY_NAME kdeconnect.plugin.virtualmonitor + DEFAULT_SEVERITY Warning + EXPORT kdeconnect-kde DESCRIPTION "kdeconnect (plugin virtualmonitor)") + +set(kdeconnect_virtualmonitor_SRCS + virtualmonitorplugin.cpp + ${debug_file_SRCS} +) + +kdeconnect_add_plugin(kdeconnect_virtualmonitor + SOURCES ${kdeconnect_virtualmonitor_SRCS}) + +target_link_libraries(kdeconnect_virtualmonitor + kdeconnectcore + Qt5::Core + Qt5::Multimedia + Qt5::DBus +) diff --git a/plugins/virtualmonitor/README b/plugins/virtualmonitor/README new file mode 100644 index 000000000..29fcd68a2 --- /dev/null +++ b/plugins/virtualmonitor/README @@ -0,0 +1,7 @@ +This plugin will allow users to use other kde connect devices as external displays. + +It will use krfb-virtualmonitor to create it and feed the information and also request an URL to be opened on the other device that should be rendering the contents. + +Upon connection, we'll be notifying under kdeconnect.virtualmonitor about our "resolutions, which include objects with a "resolution" and a "scale". These will provide the information necessary to issue a remote feed. + +When a virtual monitor is requested, kdeconnect.virtualmonitor.request will include a "url" field that contains all information necessary to connect to the created feed. The remote client needs to support this url type. diff --git a/plugins/virtualmonitor/kdeconnect_virtualmonitor.json b/plugins/virtualmonitor/kdeconnect_virtualmonitor.json new file mode 100644 index 000000000..036fcc321 --- /dev/null +++ b/plugins/virtualmonitor/kdeconnect_virtualmonitor.json @@ -0,0 +1,29 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "aleixpol@kde.org", + "Name": "Aleix Pol i Gonzalez" + } + ], + "Name": "Virtual Monitor", + "Description": "Use your devices as virtual monitors", + "EnabledByDefault": true, + "Icon": "video-monitor", + "Id": "kdeconnect_virtualmonitor", + "License": "GPL", + "ServiceTypes": [ + "KdeConnect/Plugin" + ], + "Version": "0.1", + "Website": "https://kde.org" + }, + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.virtualmonitor", + "kdeconnect.virtualmonitor.request" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.virtualmonitor", + "kdeconnect.virtualmonitor.request" + ] +} diff --git a/plugins/virtualmonitor/virtualmonitorplugin.cpp b/plugins/virtualmonitor/virtualmonitorplugin.cpp new file mode 100644 index 000000000..8a7c1cf40 --- /dev/null +++ b/plugins/virtualmonitor/virtualmonitorplugin.cpp @@ -0,0 +1,138 @@ +/** + * SPDX-FileCopyrightText: 2021 Aleix Pol i Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "virtualmonitorplugin.h" + +#include + +#include +#include +#include +#include +#include +#include "plugin_virtualmonitor_debug.h" + +K_PLUGIN_CLASS_WITH_JSON(VirtualMonitorPlugin, "kdeconnect_virtualmonitor.json") +#define QS QLatin1String + +VirtualMonitorPlugin::VirtualMonitorPlugin(QObject* parent, const QVariantList& args) + : KdeConnectPlugin(parent, args) +{ +} + +VirtualMonitorPlugin::~VirtualMonitorPlugin() +{ + stop(); +} + +void VirtualMonitorPlugin::stop() +{ + if (!m_process) + return; + + m_process->terminate(); + if (!m_process->waitForFinished()) { + m_process->kill(); + m_process->waitForFinished(); + } + delete m_process; + m_process = nullptr; +} + +void VirtualMonitorPlugin::connected() +{ + auto screen = QGuiApplication::primaryScreen(); + auto resolution = screen->size(); + QString resolutionString = QString::number(resolution.width()) + QLatin1Char('x') + QString::number(resolution.height()); + NetworkPacket np(PACKET_TYPE_VIRTUALMONITOR, { + { QS("resolutions"), QJsonArray { + QJsonObject { + { QS("resolution"), resolutionString }, + { QS("scale"), screen->devicePixelRatio() }, + } + } }, + }); + sendPacket(np); +} + +bool VirtualMonitorPlugin::receivePacket(const NetworkPacket& received) +{ + if (received.type() == PACKET_TYPE_VIRTUALMONITOR_REQUEST && received.has(QS("url"))) { + QUrl url(received.get(QS("url"))); + if (!QDesktopServices::openUrl(url)) { + qCWarning(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Failed to open" << url.toDisplayString(); + NetworkPacket np(PACKET_TYPE_VIRTUALMONITOR, { { QS("failed"), 0 } }); + sendPacket(np); + } + } else if (received.type() == PACKET_TYPE_VIRTUALMONITOR) { + if (received.has(QS("resolutions"))) { + m_remoteResolution = received.get(QS("resolutions")).first().toObject(); + } + if (received.has(QS("failed"))) { + stop(); + } + } + return true; +} + +QString VirtualMonitorPlugin::dbusPath() const +{ + // Don't offer the feature if krfb-virtualmonitor is not around + if (QStandardPaths::findExecutable(QS("krfb-virtualmonitor")).isEmpty()) + return {}; + + return QS("/modules/kdeconnect/devices/") + device()->id() + QS("/virtualmonitor"); +} + +bool VirtualMonitorPlugin::requestVirtualMonitor() +{ + stop(); + if (m_remoteResolution.isEmpty()) { + qCWarning(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Cannot start a request without a resolution"; + return false; + } + + qCDebug(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Requesting virtual display " << device()->name(); + + QUuid uuid = QUuid::createUuid(); + static int s_port = 5901; + const QString port = QString::number(s_port++); + + m_process = new QProcess(this); + m_process->setProgram(QS("krfb-virtualmonitor")); + const double scale = m_remoteResolution.value(QLatin1String("scale")).toDouble(); + m_process->setArguments({QS("--name"), device()->name(), QS("--resolution"), m_remoteResolution.value(QLatin1String("resolution")).toString(), QS("--scale"), QString::number(scale), QS("--password"), uuid.toString(), QS("--port"), port}); + connect(m_process, QOverload::of(&QProcess::finished), this, [this] (int exitCode, QProcess::ExitStatus exitStatus) { + if (m_retries < 5 && (exitCode == 1 || exitStatus == QProcess::CrashExit)) { + m_retries++; + requestVirtualMonitor(); + } else { + m_process->deleteLater(); + } + qCWarning(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "virtual display finished with" << device()->name() << m_process->readAllStandardError(); + }); + + m_process->start(); + + if (!m_process->waitForStarted()) { + qCWarning(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Failed to start krfb-virtualmonitor" << m_process->error() << m_process->errorString(); + return false; + } + + QUrl url; + url.setScheme(QS("vnc")); + url.setUserName(QS("user")); + url.setPassword(uuid.toString()); + url.setHost(device()->getLocalIpAddress().toString()); + + NetworkPacket np(PACKET_TYPE_VIRTUALMONITOR_REQUEST, { + { QS("url"), url.toEncoded() } + }); + sendPacket(np); + return true; +} + +#include "virtualmonitorplugin.moc" diff --git a/plugins/virtualmonitor/virtualmonitorplugin.h b/plugins/virtualmonitor/virtualmonitorplugin.h new file mode 100644 index 000000000..9604ccd4f --- /dev/null +++ b/plugins/virtualmonitor/virtualmonitorplugin.h @@ -0,0 +1,43 @@ +/** + * SPDX-FileCopyrightText: 2021 Aleix Pol i Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef VIRTUALMONITORPLUGIN_H +#define VIRTUALMONITORPLUGIN_H + +#include +#include +#include "plugin_virtualmonitor_debug.h" + +#define PACKET_TYPE_VIRTUALMONITOR QStringLiteral("kdeconnect.virtualmonitor") +#define PACKET_TYPE_VIRTUALMONITOR_REQUEST QStringLiteral("kdeconnect.virtualmonitor.request") + +class QProcess; + +class VirtualMonitorPlugin + : public KdeConnectPlugin +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.virtualmonitor") + +public: + explicit VirtualMonitorPlugin(QObject* parent, const QVariantList& args); + ~VirtualMonitorPlugin() override; + + Q_SCRIPTABLE bool requestVirtualMonitor(); + + void connected() override; + QString dbusPath() const override; + bool receivePacket(const NetworkPacket& np) override; + +private: + void stop(); + + QProcess *m_process = nullptr; + QJsonObject m_remoteResolution; + uint m_retries = 0; +}; + +#endif