From b24d6298027fa0b94c61f2c27ecf955a5123d3fc Mon Sep 17 00:00:00 2001 From: Fabian Arndt Date: Thu, 30 May 2024 23:13:54 +0000 Subject: [PATCH] virtualmonitor: implemented capabilities check BUG: 485829 ## Summary Currently, the plugin just fails silently if the local device is missing the `krfb` package or if the remote device misses an `vnc://` protocol/scheme handler. You click the button and nothing happens. One issue is, that the plugin is considered `virtualmonitor.available` in the `DeviceDelegate.qml`, even if the check for `krfb-virtualmonitor` fails and no dbus-path is provided. I investigated the behavior a bit, but ignored it in the end as this MR benefits from being shown for device constellations that _could_ provide this feature. A warning is shown with brief instructions, how to get the plugin working correctly. - Check if krfb-virtualmonitor is available locally - Check default scheme handler for vnc:// on device (Linux) - Show warnings / reasons, if no connection could be established ## Test Plan Regarding if the devices have mentioned packages installed, we should see different behaviors. If the remote device has no VNC client, it can not connect to out server. _A warning should be shown._ If the local device hasn't the `krfb-virtualmonitor` available, the remote device couldn't connect. _A warning should be shown._ If both problems are present, _both warnings should be shown._ If none of these are present, no warning should be shown and we should try to establish a connection. The connection attempts failed? _A warning should be shown._ --- declarativeplugin/qml/PluginChecker.qml | 1 - .../package/contents/ui/DeviceDelegate.qml | 36 ++++++++--- .../kdeconnect_virtualmonitor.json | 4 ++ .../virtualmonitor/virtualmonitorplugin.cpp | 62 ++++++++++++++++++- plugins/virtualmonitor/virtualmonitorplugin.h | 25 ++++++++ 5 files changed, 119 insertions(+), 9 deletions(-) diff --git a/declarativeplugin/qml/PluginChecker.qml b/declarativeplugin/qml/PluginChecker.qml index 64d5be716..632e42334 100644 --- a/declarativeplugin/qml/PluginChecker.qml +++ b/declarativeplugin/qml/PluginChecker.qml @@ -36,7 +36,6 @@ QtObject { function pluginsChanged() { availableResponse.setPendingCall(device.hasPlugin("kdeconnect_" + pluginName)) iconResponse.setPendingCall(device.pluginIconName("kdeconnect_" + pluginName)) - } readonly property var vv: DBusAsyncResponse { diff --git a/plasmoid/package/contents/ui/DeviceDelegate.qml b/plasmoid/package/contents/ui/DeviceDelegate.qml index 05307b0f5..04fb52b97 100644 --- a/plasmoid/package/contents/ui/DeviceDelegate.qml +++ b/plasmoid/package/contents/ui/DeviceDelegate.qml @@ -23,6 +23,14 @@ PlasmaComponents.ItemDelegate hoverEnabled: false down: false + Kirigami.PromptDialog { + id: prompt + visible: false + showCloseButton: true + standardButtons: Kirigami.Dialog.NoButton + title: i18n("Virtual Monitor is not available") + } + DropArea { id: fileDropArea anchors.fill: parent @@ -76,6 +84,11 @@ PlasmaComponents.ItemDelegate device: root.device } + VirtualMonitor { + id: virtualmonitor + device: root.device + } + PlasmaComponents.Label { id: deviceName elide: Text.ElideRight @@ -85,16 +98,25 @@ PlasmaComponents.ItemDelegate } PlasmaComponents.ToolButton { - VirtualMonitor { - id: vd - device: root.device - } icon.name: "video-monitor" + visible: virtualmonitor.available text: i18n("Virtual Display") - visible: vd.available onClicked: { - if (!vd.plugin.requestVirtualMonitor()) { - console.warn("Failed to create the virtual monitor") + let err = ""; + if (virtualmonitor?.plugin?.hasRemoteVncClient === false) { + err = i18n("Remote device does not have a VNC client (eg. krdc) installed."); + } + if (virtualmonitor?.plugin?.isVirtualMonitorAvailable === false) { + err = (err ? err + "\n\n" : "") + + i18n("The krfb package is required on the local device."); + } + + if (err) { + prompt.subtitle = err; + prompt.visible = true; + } else if (!virtualmonitor.plugin.requestVirtualMonitor()) { + prompt.subtitle = i18n("Failed to create the virtual monitor."); + prompt.visible = true; } } } diff --git a/plugins/virtualmonitor/kdeconnect_virtualmonitor.json b/plugins/virtualmonitor/kdeconnect_virtualmonitor.json index f19757c90..7733971de 100644 --- a/plugins/virtualmonitor/kdeconnect_virtualmonitor.json +++ b/plugins/virtualmonitor/kdeconnect_virtualmonitor.json @@ -46,6 +46,10 @@ "Name[x-test]": "xxAleix Pol i Gonzalezxx", "Name[zh_CN]": "Aleix Pol i Gonzalez", "Name[zh_TW]": "Aleix Pol i Gonzalez" + }, + { + "Email": "fabian.arndt@root-core.net", + "Name": "Fabian Arndt" } ], "Description": "Use your devices as virtual monitors", diff --git a/plugins/virtualmonitor/virtualmonitorplugin.cpp b/plugins/virtualmonitor/virtualmonitorplugin.cpp index 7e9898fbd..a4c616bd4 100644 --- a/plugins/virtualmonitor/virtualmonitorplugin.cpp +++ b/plugins/virtualmonitor/virtualmonitorplugin.cpp @@ -1,5 +1,6 @@ /** * SPDX-FileCopyrightText: 2021 Aleix Pol i Gonzalez + * SPDX-FileCopyrightText: 2024 Fabian Arndt * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -16,6 +17,12 @@ #include #include +#if defined(Q_OS_WIN) +#include +#else +#include +#endif + K_PLUGIN_CLASS_WITH_JSON(VirtualMonitorPlugin, "kdeconnect_virtualmonitor.json") #define QS QLatin1String static const int DEFAULT_PORT = 5901; @@ -41,6 +48,13 @@ void VirtualMonitorPlugin::stop() void VirtualMonitorPlugin::connected() { + // Get local capabilities + // We test again, since the user may have installed the necessary packages in the meantime + m_capabilitiesLocal.vncClient = checkVncClient(); + m_capabilitiesLocal.virtualMonitor = !QStandardPaths::findExecutable(QS("krfb-virtualmonitor")).isEmpty(); + qCDebug(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Local device supports VNC:" << m_capabilitiesLocal.vncClient; + qCDebug(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Local device supports Virtual Monitor:" << m_capabilitiesLocal.virtualMonitor; + auto screen = QGuiApplication::primaryScreen(); auto resolution = screen->size(); QString resolutionString = QString::number(resolution.width()) + QLatin1Char('x') + QString::number(resolution.height()); @@ -51,6 +65,8 @@ void VirtualMonitorPlugin::connected() {QS("resolution"), resolutionString}, {QS("scale"), screen->devicePixelRatio()}, }}}, + {QS("supports_vnc"), m_capabilitiesLocal.vncClient}, + {QS("supports_virt_mon"), m_capabilitiesLocal.virtualMonitor}, }); sendPacket(np); } @@ -92,13 +108,19 @@ void VirtualMonitorPlugin::receivePacket(const NetworkPacket &received) if (received.has(QS("failed"))) { stop(); } + + // Get device's capabilities + m_capabilitiesRemote.vncClient = received.get(QS("supports_vnc"), false); + m_capabilitiesRemote.virtualMonitor = received.get(QS("supports_virt_mon"), false); + qCDebug(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Remote device supports VNC:" << m_capabilitiesRemote.vncClient; + qCDebug(KDECONNECT_PLUGIN_VIRTUALMONITOR) << "Remote device supports Virtual Monitor:" << m_capabilitiesRemote.virtualMonitor; } } QString VirtualMonitorPlugin::dbusPath() const { // Don't offer the feature if krfb-virtualmonitor is not around - if (QStandardPaths::findExecutable(QS("krfb-virtualmonitor")).isEmpty()) + if (!m_capabilitiesLocal.virtualMonitor) return {}; return QLatin1String("/modules/kdeconnect/devices/%1/virtualmonitor").arg(device()->id()); @@ -159,5 +181,43 @@ bool VirtualMonitorPlugin::requestVirtualMonitor() return true; } +bool VirtualMonitorPlugin::checkVncClient() const +{ +#if defined(Q_OS_MAC) + // TODO: macOS has some VNC client preinstalled.. does it work for us? + return true; +#else + // krdc is default on KDE + if (!QStandardPaths::findExecutable(QS("krdc")).isEmpty()) + return true; + + // Check if another client is available + return checkDefaultSchemeHandler(QS("vnc")); +#endif +} + +bool VirtualMonitorPlugin::checkDefaultSchemeHandler(const QString &scheme) const +{ + // TODO: macOS built-in tool: defaults read com.apple.LaunchServices/com.apple.launchservices.secure + // A function to use such a program was implemented and later removed in MR !670 (Commit e72f688ae1fad8846c006a3f87d57e507f6dbcb6) +#if defined(Q_OS_WIN) + // Scheme handlers are stored in the registry on Windows + // https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85) + + // Check if url handler is present + QSettings handler(QS("HKEY_CLASSES_ROOT\\") + scheme, QSettings::Registry64Format); + if (handler.value("URL Protocol").isNull()) + return false; + + // Check if a command is actually present + QSettings command(QS("HKEY_CLASSES_ROOT\\") + scheme + QS("\\shell\\open\\command"), QSettings::Registry64Format); + return !command.value("Default").isNull(); +#else + KService::Ptr service = KApplicationTrader::preferredService(QS("x-scheme-handler/") + scheme); + const QString defaultApp = service ? service->desktopEntryName() : QString(); + return !defaultApp.isEmpty(); +#endif +} + #include "moc_virtualmonitorplugin.cpp" #include "virtualmonitorplugin.moc" diff --git a/plugins/virtualmonitor/virtualmonitorplugin.h b/plugins/virtualmonitor/virtualmonitorplugin.h index da47be79b..205940eab 100644 --- a/plugins/virtualmonitor/virtualmonitorplugin.h +++ b/plugins/virtualmonitor/virtualmonitorplugin.h @@ -1,5 +1,6 @@ /** * SPDX-FileCopyrightText: 2021 Aleix Pol i Gonzalez + * SPDX-FileCopyrightText: 2024 Fabian Arndt * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -19,6 +20,8 @@ class VirtualMonitorPlugin : public KdeConnectPlugin { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.virtualmonitor") + Q_PROPERTY(bool hasRemoteVncClient READ hasRemoteVncClient CONSTANT) + Q_PROPERTY(bool isVirtualMonitorAvailable READ isVirtualMonitorAvailable CONSTANT) public: using KdeConnectPlugin::KdeConnectPlugin; @@ -30,8 +33,30 @@ public: QString dbusPath() const override; void receivePacket(const NetworkPacket &np) override; + // The remote device has a VNC client installed + bool hasRemoteVncClient() const + { + return m_capabilitiesRemote.vncClient; + } + + // krfb is installed on local device, virtual monitors is supported + bool isVirtualMonitorAvailable() const + { + return m_capabilitiesLocal.virtualMonitor; + } + private: void stop(); + bool checkVncClient() const; + bool checkDefaultSchemeHandler(const QString &scheme) const; + + struct Capabilities { + bool virtualMonitor = false; + bool vncClient = false; + }; + + Capabilities m_capabilitiesLocal; + Capabilities m_capabilitiesRemote; QProcess *m_process = nullptr; QJsonObject m_remoteResolution;