feat(screen): use KScreen Wayland backend

main
reionwong 3 weeks ago
parent 118a3f9cec
commit 29307fd9c9

4
debian/control vendored

@ -5,6 +5,7 @@ Maintainer: CutefishOS <support@cutefishos.com>
Build-Depends: cmake, Build-Depends: cmake,
debhelper (>= 9), debhelper (>= 9),
extra-cmake-modules, extra-cmake-modules,
libkscreen-dev,
libkf6networkmanagerqt-dev, libkf6networkmanagerqt-dev,
libkf6bluezqt-dev, libkf6bluezqt-dev,
libkf6modemmanagerqt-dev, libkf6modemmanagerqt-dev,
@ -22,5 +23,6 @@ Homepage: https://cutefishos.com
Package: libcutefish Package: libcutefish
Architecture: any Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
${shlibs:Depends} ${shlibs:Depends},
libkscreen-bin
Description: CutefishOS Library Description: CutefishOS Library

@ -13,6 +13,7 @@ set(SCREEN_SRCS
) )
find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick) find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick)
find_package(KF6Screen REQUIRED)
add_library(cutefishscreen_qmlplugins SHARED ${SCREEN_SRCS}) add_library(cutefishscreen_qmlplugins SHARED ${SCREEN_SRCS})
@ -20,6 +21,7 @@ target_link_libraries (cutefishscreen_qmlplugins
Qt6::Core Qt6::Core
Qt6::Quick Qt6::Quick
Qt6::Gui Qt6::Gui
KF6::Screen
) )
install(TARGETS cutefishscreen_qmlplugins DESTINATION ${INSTALL_QMLDIR}/Cutefish/Screen) install(TARGETS cutefishscreen_qmlplugins DESTINATION ${INSTALL_QMLDIR}/Cutefish/Screen)

@ -1,89 +1,199 @@
#include "outputmodel.h" #include "outputmodel.h"
#include <QGuiApplication> #include <KScreen/Mode>
#include <KScreen/Output>
#include <algorithm>
#include <limits>
#include <QtMath> #include <QtMath>
Qt5OutputModel::Qt5OutputModel(QObject *parent) KScreenOutputModel::KScreenOutputModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
} }
int Qt5OutputModel::rowCount(const QModelIndex &parent) const int KScreenOutputModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_outputs.size();
}
KScreen::OutputPtr KScreenOutputModel::outputAt(int row) const
{
if (row < 0 || row >= m_outputs.size()) {
return {};
}
return m_outputs.at(row);
}
QList<KScreenOutputModel::ModeChoice> KScreenOutputModel::modesForOutput(const KScreen::OutputPtr &output) const
{
QList<ModeChoice> modes;
if (!output) {
return modes;
}
const KScreen::ModeList outputModes = output->modes();
for (auto it = outputModes.cbegin(); it != outputModes.cend(); ++it) {
if (!it.value()) {
continue;
}
modes.append({it.key(), it.value()->size(), it.value()->refreshRate()});
}
return modes;
}
QList<QSize> KScreenOutputModel::resolutionsForOutput(const KScreen::OutputPtr &output) const
{
QList<QSize> resolutions;
for (const ModeChoice &mode : modesForOutput(output)) {
if (mode.size.isValid() && !resolutions.contains(mode.size)) {
resolutions.append(mode.size);
}
}
std::sort(resolutions.begin(), resolutions.end(), [](const QSize &first, const QSize &second) {
const qint64 firstArea = qint64(first.width()) * first.height();
const qint64 secondArea = qint64(second.width()) * second.height();
if (firstArea != secondArea) {
return firstArea > secondArea;
}
return first.width() > second.width();
});
return resolutions;
}
QList<float> KScreenOutputModel::refreshRatesForOutput(const KScreen::OutputPtr &output,
const QSize &size) const
{ {
return parent.isValid() ? 0 : m_screens.size(); QList<float> refreshRates;
for (const ModeChoice &mode : modesForOutput(output)) {
if (mode.size != size || mode.refreshRate <= 0) {
continue;
}
bool alreadyListed = false;
for (const float refreshRate : refreshRates) {
if (sameRefreshRate(refreshRate, mode.refreshRate)) {
alreadyListed = true;
break;
}
}
if (!alreadyListed) {
refreshRates.append(mode.refreshRate);
}
}
std::sort(refreshRates.begin(), refreshRates.end());
return refreshRates;
} }
QString Qt5OutputModel::screenName(const QScreen *screen) QString KScreenOutputModel::outputName(const KScreen::OutputPtr &output)
{ {
if (!screen) { if (!output) {
return QString(); return QString();
} }
const QString name = screen->name(); if (!output->name().isEmpty()) {
return name.isEmpty() ? QStringLiteral("Display") : name; return output->name();
}
if (!output->model().isEmpty()) {
return output->model();
}
if (!output->vendor().isEmpty()) {
return output->vendor();
}
return QStringLiteral("Display");
} }
Output::Rotation Qt5OutputModel::rotationForScreen(const QScreen *screen) QString KScreenOutputModel::formatResolution(const QSize &size)
{ {
if (!screen) { return QStringLiteral("%1 × %2").arg(size.width()).arg(size.height());
return Output::None; }
}
QString KScreenOutputModel::formatRefreshRate(float refreshRate)
switch (screen->orientation()) { {
case Qt::PortraitOrientation: const int roundedRefreshRate = qRound(refreshRate);
return Output::Right; if (qAbs(refreshRate - roundedRefreshRate) < 0.01) {
case Qt::InvertedPortraitOrientation: return QString::number(roundedRefreshRate) + QStringLiteral(" Hz");
return Output::Left;
case Qt::InvertedLandscapeOrientation:
return Output::Inverted;
case Qt::LandscapeOrientation:
case Qt::PrimaryOrientation:
default:
return Output::None;
} }
return QString::number(refreshRate, 'f', 2) + QStringLiteral(" Hz");
} }
QVariant Qt5OutputModel::data(const QModelIndex &index, int role) const bool KScreenOutputModel::sameRefreshRate(float first, float second)
{ {
if (!index.isValid() || index.row() < 0 || index.row() >= m_screens.size()) { return qAbs(first - second) < 0.01;
}
QVariant KScreenOutputModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant(); return QVariant();
} }
const QScreen *screen = m_screens.at(index.row()); const KScreen::OutputPtr output = outputAt(index.row());
if (!screen) { if (!output) {
return QVariant(); return QVariant();
} }
const KScreen::ModePtr currentMode = output->currentMode();
const QSize currentModeSize = currentMode ? currentMode->size() : output->enforcedModeSize();
const QList<QSize> resolutions = resolutionsForOutput(output);
const QList<float> refreshRates = refreshRatesForOutput(output, currentModeSize);
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole:
return screenName(screen); return outputName(output);
case EnabledRole: case EnabledRole:
return true; return output->isEnabled();
case InternalRole: case InternalRole:
return screenName(screen).startsWith(QStringLiteral("eDP"), Qt::CaseInsensitive) return output->type() == KScreen::Output::Panel
|| screenName(screen).startsWith(QStringLiteral("LVDS"), Qt::CaseInsensitive); || outputName(output).startsWith(QStringLiteral("eDP"), Qt::CaseInsensitive)
|| outputName(output).startsWith(QStringLiteral("LVDS"), Qt::CaseInsensitive);
case PrimaryRole: case PrimaryRole:
return screen == QGuiApplication::primaryScreen(); return output->isPrimary();
case SizeRole: case SizeRole:
return screen->size(); return output->size();
case PositionRole: case PositionRole:
case NormalizedPositionRole: case NormalizedPositionRole:
return screen->geometry().topLeft(); return output->pos();
case AutoRotateRole: case AutoRotateRole:
return output->autoRotatePolicy() != KScreen::Output::AutoRotatePolicy::Never;
case AutoRotateOnlyInTabletModeRole: case AutoRotateOnlyInTabletModeRole:
return false; return output->autoRotatePolicy() == KScreen::Output::AutoRotatePolicy::InTabletMode;
case RotationRole: case RotationRole:
return static_cast<int>(rotationForScreen(screen)); return static_cast<int>(output->rotation());
case ScaleRole: case ScaleRole:
return screen->devicePixelRatio(); return output->scale();
case ResolutionIndexRole: case ResolutionIndexRole: {
return 0; const int resolutionIndex = resolutions.indexOf(currentModeSize);
case ResolutionsRole: return resolutionIndex >= 0 ? resolutionIndex : 0;
return QStringList(QStringLiteral("%1 × %2").arg(screen->size().width()).arg(screen->size().height())); }
case RefreshRateIndexRole: case ResolutionsRole: {
QStringList result;
for (const QSize &resolution : resolutions) {
result.append(formatResolution(resolution));
}
return result;
}
case RefreshRateIndexRole: {
if (!currentMode) {
return 0;
}
for (int i = 0; i < refreshRates.size(); ++i) {
if (sameRefreshRate(refreshRates.at(i), currentMode->refreshRate())) {
return i;
}
}
return 0; return 0;
case RefreshRatesRole: }
return QStringList(QStringLiteral("%1 Hz").arg(qRound(screen->refreshRate()))); case RefreshRatesRole: {
QStringList result;
for (const float refreshRate : refreshRates) {
result.append(formatRefreshRate(refreshRate));
}
return result;
}
case ReplicationSourceModelRole: case ReplicationSourceModelRole:
return QStringList(); return QStringList();
case ReplicationSourceIndexRole: case ReplicationSourceIndexRole:
@ -95,30 +205,129 @@ QVariant Qt5OutputModel::data(const QModelIndex &index, int role) const
} }
} }
bool Qt5OutputModel::setData(const QModelIndex &index, const QVariant &value, int role) bool KScreenOutputModel::setData(const QModelIndex &index, const QVariant &value, int role)
{ {
Q_UNUSED(value) if (!index.isValid()) {
return false;
}
if (!index.isValid() || index.row() < 0 || index.row() >= m_screens.size()) { const KScreen::OutputPtr output = outputAt(index.row());
if (!output) {
return false; return false;
} }
// QScreen exposes the active mode as read-only. Resolution, rotation and bool changed = false;
// output enablement are intentionally left to the desktop's display
// service on modern KDE systems.
switch (role) { switch (role) {
case EnabledRole: case EnabledRole: {
case RotationRole: const bool enabled = value.toBool();
case ResolutionIndexRole: if (!enabled) {
case RefreshRateIndexRole: int enabledOutputs = 0;
case ScaleRole: for (const KScreen::OutputPtr &candidate : m_outputs) {
return false; enabledOutputs += candidate && candidate->isEnabled() ? 1 : 0;
}
if (enabledOutputs <= 1) {
return false;
}
}
if (output->isEnabled() != enabled) {
output->setEnabled(enabled);
changed = true;
}
break;
}
case RotationRole: {
const auto rotation = static_cast<KScreen::Output::Rotation>(value.toInt());
if (rotation != KScreen::Output::None
&& rotation != KScreen::Output::Left
&& rotation != KScreen::Output::Inverted
&& rotation != KScreen::Output::Right) {
return false;
}
if (output->rotation() != rotation) {
output->setRotation(rotation);
changed = true;
}
break;
}
case ResolutionIndexRole: {
const QList<QSize> resolutions = resolutionsForOutput(output);
const int resolutionIndex = value.toInt();
if (resolutionIndex < 0 || resolutionIndex >= resolutions.size()) {
return false;
}
const QSize selectedSize = resolutions.at(resolutionIndex);
const KScreen::ModePtr currentMode = output->currentMode();
const float currentRefreshRate = currentMode ? currentMode->refreshRate() : 0;
QString selectedModeId;
float closestRefreshRateDifference = std::numeric_limits<float>::max();
for (const ModeChoice &mode : modesForOutput(output)) {
if (mode.size != selectedSize) {
continue;
}
const float difference = qAbs(mode.refreshRate - currentRefreshRate);
if (selectedModeId.isEmpty() || difference < closestRefreshRateDifference) {
selectedModeId = mode.id;
closestRefreshRateDifference = difference;
}
}
if (!selectedModeId.isEmpty() && selectedModeId != output->currentModeId()) {
output->setCurrentModeId(selectedModeId);
changed = true;
}
break;
}
case RefreshRateIndexRole: {
const KScreen::ModePtr currentMode = output->currentMode();
const QSize currentSize = currentMode ? currentMode->size() : output->enforcedModeSize();
const QList<float> refreshRates = refreshRatesForOutput(output, currentSize);
const int refreshRateIndex = value.toInt();
if (refreshRateIndex < 0 || refreshRateIndex >= refreshRates.size()) {
return false;
}
const float selectedRefreshRate = refreshRates.at(refreshRateIndex);
QString selectedModeId;
for (const ModeChoice &mode : modesForOutput(output)) {
if (mode.size == currentSize && sameRefreshRate(mode.refreshRate, selectedRefreshRate)) {
selectedModeId = mode.id;
break;
}
}
if (!selectedModeId.isEmpty() && selectedModeId != output->currentModeId()) {
output->setCurrentModeId(selectedModeId);
changed = true;
}
break;
}
case ScaleRole: {
const qreal scale = value.toReal();
if (scale <= 0 || qFuzzyCompare(output->scale(), scale)) {
return false;
}
output->setScale(scale);
changed = true;
break;
}
default: default:
return false; return false;
} }
if (changed) {
emit dataChanged(index, index, {role});
}
return changed;
} }
QHash<int, QByteArray> Qt5OutputModel::roleNames() const Qt::ItemFlags KScreenOutputModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QHash<int, QByteArray> KScreenOutputModel::roleNames() const
{ {
QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
roles[Qt::DisplayRole] = "display"; roles[Qt::DisplayRole] = "display";
@ -142,9 +351,18 @@ QHash<int, QByteArray> Qt5OutputModel::roleNames() const
return roles; return roles;
} }
void Qt5OutputModel::setScreens(const QList<QScreen *> &screens) void KScreenOutputModel::setConfig(const KScreen::ConfigPtr &config)
{ {
beginResetModel(); beginResetModel();
m_screens = screens; m_config = config;
m_outputs.clear();
if (m_config) {
const KScreen::OutputList outputs = m_config->outputs();
for (auto it = outputs.cbegin(); it != outputs.cend(); ++it) {
if (it.value() && it.value()->isConnected()) {
m_outputs.append(it.value());
}
}
}
endResetModel(); endResetModel();
} }

@ -2,10 +2,11 @@
#include "output.h" #include "output.h"
#include <KScreen/Config>
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QScreen>
class Qt5OutputModel : public QAbstractListModel class KScreenOutputModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
@ -30,7 +31,7 @@ public:
ReplicasModelRole ReplicasModelRole
}; };
explicit Qt5OutputModel(QObject *parent = nullptr); explicit KScreenOutputModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, QVariant data(const QModelIndex &index,
@ -38,16 +39,31 @@ public:
bool setData(const QModelIndex &index, bool setData(const QModelIndex &index,
const QVariant &value, const QVariant &value,
int role = Qt::EditRole) override; int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void setScreens(const QList<QScreen *> &screens); void setConfig(const KScreen::ConfigPtr &config);
protected: protected:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
private: private:
static Output::Rotation rotationForScreen(const QScreen *screen); struct ModeChoice {
static QString screenName(const QScreen *screen); QString id;
QSize size;
float refreshRate = 0;
};
QList<QScreen *> m_screens; KScreen::OutputPtr outputAt(int row) const;
}; QList<ModeChoice> modesForOutput(const KScreen::OutputPtr &output) const;
QList<QSize> resolutionsForOutput(const KScreen::OutputPtr &output) const;
QList<float> refreshRatesForOutput(const KScreen::OutputPtr &output,
const QSize &size) const;
static QString outputName(const KScreen::OutputPtr &output);
static QString formatResolution(const QSize &size);
static QString formatRefreshRate(float refreshRate);
static bool sameRefreshRate(float first, float second);
KScreen::ConfigPtr m_config;
QList<KScreen::OutputPtr> m_outputs;
};

@ -12,11 +12,10 @@ class QmlPlugins : public QQmlExtensionPlugin
public: public:
void registerTypes(const char *uri) override void registerTypes(const char *uri) override
{ {
qmlRegisterType<Qt5Screen>(uri, 1, 0, "Screen"); qmlRegisterType<KScreenScreen>(uri, 1, 0, "Screen");
qmlRegisterUncreatableMetaObject(Output::staticMetaObject, uri, 1, 0, "Output", qmlRegisterUncreatableMetaObject(Output::staticMetaObject, uri, 1, 0, "Output",
QStringLiteral("Output is only a namespace for enums")); QStringLiteral("Output is only a namespace for enums"));
} }
}; };
#include "plugin.moc" #include "plugin.moc"

@ -2,48 +2,87 @@
#include "outputmodel.h" #include "outputmodel.h"
#include <KScreen/ConfigMonitor>
#include <KScreen/GetConfigOperation>
#include <KScreen/SetConfigOperation>
#include <QDebug>
#include <QGuiApplication> #include <QGuiApplication>
#include <QTimer>
Qt5Screen::Qt5Screen(QObject *parent) KScreenScreen::KScreenScreen(QObject *parent)
: QObject(parent) : QObject(parent)
, m_outputModel(new Qt5OutputModel(this)) , m_outputModel(new KScreenOutputModel(this))
{ {
refreshScreens(); if (QGuiApplication::platformName().contains(QStringLiteral("wayland"), Qt::CaseInsensitive)
&& qEnvironmentVariableIsEmpty("KSCREEN_BACKEND")) {
qputenv("KSCREEN_BACKEND", QByteArrayLiteral("kwayland"));
}
connect(qApp, &QGuiApplication::screenAdded, this, &Qt5Screen::connectScreen); connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged,
connect(qApp, &QGuiApplication::screenAdded, this, &Qt5Screen::refreshScreens); this, [this] {
connect(qApp, &QGuiApplication::screenRemoved, this, &Qt5Screen::refreshScreens); if (!m_applying) {
loadConfig();
}
});
QTimer::singleShot(0, this, &KScreenScreen::loadConfig);
} }
Qt5OutputModel *Qt5Screen::outputModel() const KScreenOutputModel *KScreenScreen::outputModel() const
{ {
return m_outputModel; return m_outputModel;
} }
void Qt5Screen::connectScreen(QScreen *screen) void KScreenScreen::loadConfig()
{ {
if (!screen) { if (m_applying) {
return; return;
} }
connect(screen, &QScreen::geometryChanged, this, &Qt5Screen::refreshScreens, Qt::UniqueConnection); KScreen::GetConfigOperation operation(KScreen::ConfigOperation::NoOptions, this);
connect(screen, &QScreen::orientationChanged, this, &Qt5Screen::refreshScreens, Qt::UniqueConnection); if (!operation.exec() || operation.hasError() || !operation.config()) {
connect(screen, &QScreen::physicalDotsPerInchChanged, this, &Qt5Screen::refreshScreens, Qt::UniqueConnection); qWarning() << "Unable to read the display configuration from KScreen:"
} << operation.errorString();
if (m_config) {
KScreen::ConfigMonitor::instance()->removeConfig(m_config);
}
m_config.clear();
m_outputModel->setConfig({});
emit outputModelChanged();
return;
}
void Qt5Screen::refreshScreens() if (m_config) {
{ KScreen::ConfigMonitor::instance()->removeConfig(m_config);
const QList<QScreen *> screens = QGuiApplication::screens();
for (QScreen *screen : screens) {
connectScreen(screen);
} }
m_outputModel->setScreens(screens); m_config = operation.config();
KScreen::ConfigMonitor::instance()->addConfig(m_config);
m_outputModel->setConfig(m_config);
emit outputModelChanged(); emit outputModelChanged();
} }
void Qt5Screen::save() void KScreenScreen::save()
{ {
// Display configuration is managed by KScreen/system settings on KDE6. if (!m_config || m_applying) {
// QScreen is a read-only Qt API, so there is nothing to write here. return;
} }
if (!KScreen::Config::canBeApplied(m_config,
KScreen::Config::ValidityFlag::RequireAtLeastOneEnabledScreen)) {
qWarning() << "The requested display configuration is not valid";
loadConfig();
return;
}
m_applying = true;
KScreen::SetConfigOperation operation(m_config, this);
const bool applied = operation.exec() && !operation.hasError();
if (!applied) {
qWarning() << "Unable to apply the display configuration through KWin Wayland:"
<< operation.errorString();
}
m_applying = false;
loadConfig();
}

@ -1,29 +1,30 @@
#pragma once #pragma once
#include <KScreen/Config>
#include <QObject> #include <QObject>
class Qt5OutputModel; class KScreenOutputModel;
class QScreen;
class Qt5Screen : public QObject class KScreenScreen : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(Qt5OutputModel *outputModel READ outputModel NOTIFY outputModelChanged) Q_PROPERTY(KScreenOutputModel *outputModel READ outputModel NOTIFY outputModelChanged)
public: public:
explicit Qt5Screen(QObject *parent = nullptr); explicit KScreenScreen(QObject *parent = nullptr);
Qt5OutputModel *outputModel() const; KScreenOutputModel *outputModel() const;
Q_INVOKABLE void save(); Q_INVOKABLE void save();
private slots:
void refreshScreens();
void connectScreen(QScreen *screen);
signals: signals:
void outputModelChanged(); void outputModelChanged();
private slots:
void loadConfig();
private: private:
Qt5OutputModel *m_outputModel; KScreen::ConfigPtr m_config;
KScreenOutputModel *m_outputModel;
bool m_applying = false;
}; };

Loading…
Cancel
Save