From 85f9e9a9d6afb837e78c27d979a00a93d9d1ea55 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:54:03 -0700 Subject: [PATCH] Add virtual display management to the KWin UI --- kwin/src/breezydesktopconfig.kcfg | 7 + kwin/src/breezydesktopeffect.cpp | 68 ++++++++-- kwin/src/breezydesktopeffect.h | 13 +- kwin/src/kcm/breezydesktopeffectkcm.cpp | 162 ++++++++++++++++++++++-- kwin/src/kcm/breezydesktopeffectkcm.h | 10 ++ kwin/src/kcm/breezydesktopeffectkcm.ui | 17 ++- 6 files changed, 247 insertions(+), 30 deletions(-) diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg index 1ba29a6..d65e0ea 100644 --- a/kwin/src/breezydesktopconfig.kcfg +++ b/kwin/src/breezydesktopconfig.kcfg @@ -67,5 +67,12 @@ Whether to remove any virtual displays when the effect is disabled + + -1 + -1 + 40 + + Override the default look ahead time in milliseconds (-1 to use default) + diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 724618e..61d03bd 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -1,4 +1,4 @@ - +#include "core/output.h" #include "core/rendertarget.h" #include "core/renderviewport.h" #include "kcm/shortcuts.h" @@ -39,15 +39,23 @@ public: : QObject(effect), m_effect(effect) {} public Q_SLOTS: - void AddVirtualDisplay(int width, int height) { - QMetaObject::invokeMethod(m_effect, [this, width, height]() { - m_effect->addVirtualDisplay(QSize(width, height)); - }, Qt::QueuedConnection); + QVariantList AddVirtualDisplay(int width, int height) { + m_effect->addVirtualDisplay(QSize(width, height)); + return m_effect->listVirtualDisplays(); } -private: - KWin::BreezyDesktopEffect *m_effect; -}; + QVariantList ListVirtualDisplays() const { + return m_effect->listVirtualDisplays(); + } + + QVariantList RemoveVirtualDisplay(const QString &id) { + m_effect->removeVirtualDisplay(id); + return m_effect->listVirtualDisplays(); + } + + private: + KWin::BreezyDesktopEffect *m_effect; + }; } // namespace namespace DataView @@ -263,10 +271,12 @@ void BreezyDesktopEffect::deactivate() showCursor(); if (m_removeVirtualDisplaysOnDisable) { - for (auto output : m_virtualOutputs) { - KWin::kwinApp()->outputBackend()->removeVirtualOutput(output); + for (auto it = m_virtualDisplays.begin(); it != m_virtualDisplays.end(); ++it) { + if (it->output) { + KWin::kwinApp()->outputBackend()->removeVirtualOutput(it->output); + } } - m_virtualOutputs.clear(); + m_virtualDisplays.clear(); } setRunning(false); @@ -302,7 +312,7 @@ void BreezyDesktopEffect::addVirtualDisplay(QSize size) { static int virtualDisplayCount = 0; ++virtualDisplayCount; - QString name = QStringLiteral("BreezyDesktop_VirtualDisplay_%1x%2_%3").arg(size.width()).arg(size.height()).arg(virtualDisplayCount); + QString name = QStringLiteral("BreezyDesktop_%1").arg(virtualDisplayCount); #if defined(KWIN_VERSION_ENCODED) && KWIN_VERSION_ENCODED >= 60290 QString description = QStringLiteral("Breezy Display %1x%2 (%3)").arg(size.width()).arg(size.height()).arg(virtualDisplayCount); auto output = KWin::kwinApp()->outputBackend()->createVirtualOutput(name, description, size, 1.0); @@ -310,10 +320,42 @@ void BreezyDesktopEffect::addVirtualDisplay(QSize size) auto output = KWin::kwinApp()->outputBackend()->createVirtualOutput(name, size, 1.0); #endif if (output) { - m_virtualOutputs.append(output); + VirtualOutputInfo info; + info.output = output; + info.id = name; + info.size = size; + m_virtualDisplays.insert(info.id, info); } } +QVariantList BreezyDesktopEffect::listVirtualDisplays() const { + QVariantList list; + for (auto it = m_virtualDisplays.constBegin(); it != m_virtualDisplays.constEnd(); ++it) { + const auto &info = it.value(); + if (!info.output) + continue; + QVariantMap entry; + entry.insert(QStringLiteral("id"), info.id); + entry.insert(QStringLiteral("width"), info.size.width()); + entry.insert(QStringLiteral("height"), info.size.height()); + list.push_back(entry); + } + return list; +} + +bool BreezyDesktopEffect::removeVirtualDisplay(const QString &id) { + auto it = m_virtualDisplays.find(id); + if (it != m_virtualDisplays.end()) { + Output *output = it->output; + if (output) { + KWin::kwinApp()->outputBackend()->removeVirtualOutput(output); + } + m_virtualDisplays.erase(it); + return true; + } + return false; +} + bool BreezyDesktopEffect::isEnabled() const { return m_enabled; } diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index bcbdd49..c731ab0 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include namespace KWin { @@ -91,6 +94,8 @@ namespace KWin void updateImuRotation(); void updateCursorImage(); void updateCursorPos(); + QVariantList listVirtualDisplays() const; + bool removeVirtualDisplay(const QString &id); Q_SIGNALS: void focusedDisplayDistanceChanged(); @@ -146,7 +151,13 @@ namespace KWin int m_antialiasingQuality = 3; // 0=None, 1=Medium, 2=High, 3=VeryHigh bool m_removeVirtualDisplaysOnDisable = true; bool m_mirrorPhysicalDisplays = false; - QList m_virtualOutputs; + + struct VirtualOutputInfo { + Output *output = nullptr; + QString id; + QSize size; + }; + QHash m_virtualDisplays; }; } // namespace KWin diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index cfbf6a9..471724f 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -26,6 +26,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") @@ -149,27 +157,28 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu } // Wire Add Virtual Display buttons via DBus to the effect - auto callAddVirtualDisplay = [](int w, int h) { - QDBusInterface iface( - QStringLiteral("org.kde.KWin"), - QStringLiteral("/com/xronlinux/BreezyDesktop"), - QStringLiteral("com.xronlinux.BreezyDesktop"), - QDBusConnection::sessionBus()); - if (iface.isValid()) { - iface.call(QDBus::NoBlock, QStringLiteral("AddVirtualDisplay"), w, h); - } - }; if (auto btn1080p = widget()->findChild("buttonAdd1080p")) { - connect(btn1080p, &QPushButton::clicked, this, [callAddVirtualDisplay]() { - callAddVirtualDisplay(1920, 1080); + connect(btn1080p, &QPushButton::clicked, this, [this]() { + auto list = dbusAddVirtualDisplay(1920, 1080); + renderVirtualDisplays(list); }); } if (auto btn1440p = widget()->findChild("buttonAdd1440p")) { - connect(btn1440p, &QPushButton::clicked, this, [callAddVirtualDisplay]() { - callAddVirtualDisplay(2560, 1440); + connect(btn1440p, &QPushButton::clicked, this, [this]() { + auto list = dbusAddVirtualDisplay(2560, 1440); + renderVirtualDisplays(list); }); } + renderVirtualDisplays(dbusListVirtualDisplays()); + + m_virtualDisplayPollTimer.setInterval(15000); + m_virtualDisplayPollTimer.setTimerType(Qt::CoarseTimer); + connect(&m_virtualDisplayPollTimer, &QTimer::timeout, this, [this]() { + renderVirtualDisplays(dbusListVirtualDisplays()); + }); + m_virtualDisplayPollTimer.start(); + // General tab: Open KDE Displays Settings if (auto btnDisplays = widget()->findChild(QStringLiteral("buttonOpenDisplaysSettings"))) { connect(btnDisplays, &QPushButton::clicked, this, [this]() { @@ -243,6 +252,131 @@ void BreezyDesktopEffectConfig::updateUnmanagedState() { } +static QDBusInterface makeVDInterface() { + return QDBusInterface( + QStringLiteral("org.kde.KWin"), + QStringLiteral("/com/xronlinux/BreezyDesktop"), + QStringLiteral("com.xronlinux.BreezyDesktop"), + QDBusConnection::sessionBus()); +} + +QVariantList BreezyDesktopEffectConfig::dbusListVirtualDisplays() const { + QDBusInterface iface = makeVDInterface(); + if (!iface.isValid()) return {}; + QDBusReply reply = iface.call(QStringLiteral("ListVirtualDisplays")); + return reply.isValid() ? reply.value() : QVariantList{}; +} + +QVariantList BreezyDesktopEffectConfig::dbusAddVirtualDisplay(int w, int h) const { + QDBusInterface iface = makeVDInterface(); + if (!iface.isValid()) return {}; + // Fire add, then fetch authoritative list to avoid marshalling quirks + iface.call(QStringLiteral("AddVirtualDisplay"), w, h); + QDBusReply list = iface.call(QStringLiteral("ListVirtualDisplays")); + return list.isValid() ? list.value() : QVariantList{}; +} + +QVariantList BreezyDesktopEffectConfig::dbusRemoveVirtualDisplay(const QString &id) const { + QDBusInterface iface = makeVDInterface(); + if (!iface.isValid()) return {}; + // Fire remove, then fetch authoritative list to avoid marshalling quirks + iface.call(QStringLiteral("RemoveVirtualDisplay"), id); + QDBusReply list = iface.call(QStringLiteral("ListVirtualDisplays")); + return list.isValid() ? list.value() : QVariantList{}; +} + +void BreezyDesktopEffectConfig::renderVirtualDisplays(const QVariantList &rows) { + auto listContainer = widget()->findChild(QStringLiteral("widgetVirtualDisplayList")); + auto listLayout = listContainer ? qobject_cast(listContainer->layout()) : nullptr; + if (!listContainer || !listLayout) return; + + while (QLayoutItem *child = listLayout->takeAt(0)) { + if (auto w = child->widget()) w->deleteLater(); + delete child; + } + + const bool hasRows = !rows.isEmpty(); + listContainer->setVisible(hasRows); + listContainer->setEnabled(hasRows); + + auto toMapCompat = [](const QVariant &v) -> QVariantMap { + if (v.metaType().id() == QMetaType::QVariantMap) { + return v.toMap(); + } + if (v.canConvert()) { + const QDBusVariant dv = v.value(); + if (dv.variant().metaType().id() == QMetaType::QVariantMap) { + return dv.variant().toMap(); + } + } + if (v.metaType().id() == qMetaTypeId()) { + const QDBusArgument arg = v.value(); + QVariantMap map; + arg.beginMap(); + while (!arg.atEnd()) { + arg.beginMapEntry(); + QString key; QVariant val; + QDBusArgument &nonConst = const_cast(arg); + nonConst >> key >> val; + arg.endMapEntry(); + map.insert(key, val); + } + arg.endMap(); + return map; + } + return QVariantMap{}; + }; + + auto unwrapValue = [](QVariant v) -> QVariant { + if (v.canConvert()) { + const QDBusVariant dv = v.value(); + return dv.variant(); + } + return v; + }; + + for (const QVariant &rowVar : rows) { + const QVariantMap row = toMapCompat(rowVar); + const QString id = unwrapValue(row.value(QStringLiteral("id"))).toString(); + const int w = unwrapValue(row.value(QStringLiteral("width"))).toInt(); + const int h = unwrapValue(row.value(QStringLiteral("height"))).toInt(); + + QWidget *rowWidget = new QWidget(listContainer); + auto *hl = new QHBoxLayout(rowWidget); + hl->setContentsMargins(0, 0, 0, 0); + + auto *spacer = new QWidget(rowWidget); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + hl->addWidget(spacer, 1); + + auto *icon = new QLabel(rowWidget); + icon->setPixmap(QIcon::fromTheme(QStringLiteral("video-display-symbolic")).pixmap(16, 16)); + icon->setContentsMargins(8, 0, 8, 0); + hl->addWidget(icon, 0); + + auto *idLabel = new QLabel(QStringLiteral("%1").arg(id), rowWidget); + idLabel->setContentsMargins(8, 0, 8, 0); + hl->addWidget(idLabel, 0); + + auto *resLabel = new QLabel(QStringLiteral("%1x%2").arg(w).arg(h), rowWidget); + resLabel->setContentsMargins(8, 0, 8, 0); + hl->addWidget(resLabel, 0); + + auto *removeBtn = new QPushButton(rowWidget); + removeBtn->setIcon(QIcon::fromTheme(QStringLiteral("user-trash-symbolic"))); + removeBtn->setToolTip(QStringLiteral("Remove virtual display")); + removeBtn->setObjectName(QStringLiteral("remove-virtual-display")); + hl->addWidget(removeBtn, 0); + + connect(removeBtn, &QPushButton::clicked, this, [this, id]() { + auto list = dbusRemoveVirtualDisplay(id); + renderVirtualDisplays(list); + }); + + listLayout->addWidget(rowWidget); + } +} + void BreezyDesktopEffectConfig::updateDriverEnabled() { auto configJsonOpt = XRDriverIPC::instance().retrieveConfig(); diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index 84f2765..253a5ae 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -5,6 +5,9 @@ #include #include +#include +#include +#include #include "ui_breezydesktopeffectkcm.h" @@ -39,6 +42,12 @@ private: void setRequestInProgress(std::initializer_list widgets, bool inProgress); bool eventFilter(QObject *watched, QEvent *event) override; + // Virtual display DBus helpers and UI rendering + QVariantList dbusListVirtualDisplays() const; + QVariantList dbusAddVirtualDisplay(int w, int h) const; + QVariantList dbusRemoveVirtualDisplay(const QString &id) const; + void renderVirtualDisplays(const QVariantList &rows); + ::Ui::BreezyDesktopEffectConfig ui; KConfigWatcher::Ptr m_configWatcher; @@ -47,5 +56,6 @@ private: QString m_connectedDeviceBrand; QString m_connectedDeviceModel; QTimer m_statePollTimer; // periodic driver state polling + QTimer m_virtualDisplayPollTimer; // periodic virtual display list polling bool m_licenseLoading = false; }; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index 9870652..bfa38c2 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -160,7 +160,7 @@ - + false @@ -193,7 +193,20 @@ - + + + false + false + + 6 + 0 + 0 + 0 + 0 + + + +