Add virtual display management to the KWin UI

This commit is contained in:
wheaney 2025-09-12 12:54:03 -07:00
parent aa3d7d24c1
commit 85f9e9a9d6
6 changed files with 247 additions and 30 deletions

View File

@ -67,5 +67,12 @@
<label>Remove virtual displays on disable</label>
<description>Whether to remove any virtual displays when the effect is disabled</description>
</entry>
<entry name="LookAheadOverride" type="Int">
<default>-1</default>
<min>-1</min>
<max>40</max>
<label>Movement look-ahead (ms)</label>
<description>Override the default look ahead time in milliseconds (-1 to use default)</description>
</entry>
</group>
</kcfg>

View File

@ -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;
}

View File

@ -8,6 +8,9 @@
#include <QImage>
#include <QKeySequence>
#include <QQuaternion>
#include <QVariant>
#include <QVariantList>
#include <QHash>
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<Output *> m_virtualOutputs;
struct VirtualOutputInfo {
Output *output = nullptr;
QString id;
QSize size;
};
QHash<QString, VirtualOutputInfo> m_virtualDisplays;
};
} // namespace KWin

View File

@ -26,6 +26,14 @@
#include <QComboBox>
#include <QDBusInterface>
#include <QDBusConnection>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDBusArgument>
#include <QVariant>
#include <QVariantList>
#include <QHBoxLayout>
#include <QPushButton>
#include <QIcon>
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<QPushButton*>("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<QPushButton*>("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<QPushButton*>(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<QVariantList> 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<QVariantList> 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<QVariantList> list = iface.call(QStringLiteral("ListVirtualDisplays"));
return list.isValid() ? list.value() : QVariantList{};
}
void BreezyDesktopEffectConfig::renderVirtualDisplays(const QVariantList &rows) {
auto listContainer = widget()->findChild<QWidget*>(QStringLiteral("widgetVirtualDisplayList"));
auto listLayout = listContainer ? qobject_cast<QVBoxLayout*>(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<QDBusVariant>()) {
const QDBusVariant dv = v.value<QDBusVariant>();
if (dv.variant().metaType().id() == QMetaType::QVariantMap) {
return dv.variant().toMap();
}
}
if (v.metaType().id() == qMetaTypeId<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
QVariantMap map;
arg.beginMap();
while (!arg.atEnd()) {
arg.beginMapEntry();
QString key; QVariant val;
QDBusArgument &nonConst = const_cast<QDBusArgument&>(arg);
nonConst >> key >> val;
arg.endMapEntry();
map.insert(key, val);
}
arg.endMap();
return map;
}
return QVariantMap{};
};
auto unwrapValue = [](QVariant v) -> QVariant {
if (v.canConvert<QDBusVariant>()) {
const QDBusVariant dv = v.value<QDBusVariant>();
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();

View File

@ -5,6 +5,9 @@
#include <memory>
#include <QTimer>
#include <QVariant>
#include <QVariantList>
#include <QString>
#include "ui_breezydesktopeffectkcm.h"
@ -39,6 +42,12 @@ private:
void setRequestInProgress(std::initializer_list<QObject*> 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;
};

View File

@ -160,7 +160,7 @@
</property>
</widget>
</item>
<item row="5" column="1">
<item row="5" column="1">
<widget class="QWidget" name="widgetVirtualDisplayButtons">
<property name="visible">
<bool>false</bool>
@ -193,7 +193,20 @@
</layout>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="6" column="0" colspan="2">
<widget class="QWidget" name="widgetVirtualDisplayList">
<property name="visible"><bool>false</bool></property>
<property name="enabled"><bool>false</bool></property>
<layout class="QVBoxLayout" name="layoutVirtualDisplayList">
<property name="spacing"><number>6</number></property>
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
</layout>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="KShortcutsEditor" name="shortcutsEditor" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">