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

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

@ -1,89 +1,199 @@
#include "outputmodel.h"
#include <QGuiApplication>
#include <KScreen/Mode>
#include <KScreen/Output>
#include <algorithm>
#include <limits>
#include <QtMath>
Qt5OutputModel::Qt5OutputModel(QObject *parent)
KScreenOutputModel::KScreenOutputModel(QObject *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();
}
const QString name = screen->name();
return name.isEmpty() ? QStringLiteral("Display") : name;
if (!output->name().isEmpty()) {
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 Output::None;
}
switch (screen->orientation()) {
case Qt::PortraitOrientation:
return Output::Right;
case Qt::InvertedPortraitOrientation:
return Output::Left;
case Qt::InvertedLandscapeOrientation:
return Output::Inverted;
case Qt::LandscapeOrientation:
case Qt::PrimaryOrientation:
default:
return Output::None;
return QStringLiteral("%1 × %2").arg(size.width()).arg(size.height());
}
QString KScreenOutputModel::formatRefreshRate(float refreshRate)
{
const int roundedRefreshRate = qRound(refreshRate);
if (qAbs(refreshRate - roundedRefreshRate) < 0.01) {
return QString::number(roundedRefreshRate) + QStringLiteral(" Hz");
}
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();
}
const QScreen *screen = m_screens.at(index.row());
if (!screen) {
const KScreen::OutputPtr output = outputAt(index.row());
if (!output) {
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) {
case Qt::DisplayRole:
return screenName(screen);
return outputName(output);
case EnabledRole:
return true;
return output->isEnabled();
case InternalRole:
return screenName(screen).startsWith(QStringLiteral("eDP"), Qt::CaseInsensitive)
|| screenName(screen).startsWith(QStringLiteral("LVDS"), Qt::CaseInsensitive);
return output->type() == KScreen::Output::Panel
|| outputName(output).startsWith(QStringLiteral("eDP"), Qt::CaseInsensitive)
|| outputName(output).startsWith(QStringLiteral("LVDS"), Qt::CaseInsensitive);
case PrimaryRole:
return screen == QGuiApplication::primaryScreen();
return output->isPrimary();
case SizeRole:
return screen->size();
return output->size();
case PositionRole:
case NormalizedPositionRole:
return screen->geometry().topLeft();
return output->pos();
case AutoRotateRole:
return output->autoRotatePolicy() != KScreen::Output::AutoRotatePolicy::Never;
case AutoRotateOnlyInTabletModeRole:
return false;
return output->autoRotatePolicy() == KScreen::Output::AutoRotatePolicy::InTabletMode;
case RotationRole:
return static_cast<int>(rotationForScreen(screen));
return static_cast<int>(output->rotation());
case ScaleRole:
return screen->devicePixelRatio();
case ResolutionIndexRole:
return 0;
case ResolutionsRole:
return QStringList(QStringLiteral("%1 × %2").arg(screen->size().width()).arg(screen->size().height()));
case RefreshRateIndexRole:
return output->scale();
case ResolutionIndexRole: {
const int resolutionIndex = resolutions.indexOf(currentModeSize);
return resolutionIndex >= 0 ? resolutionIndex : 0;
}
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;
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:
return QStringList();
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;
}
// QScreen exposes the active mode as read-only. Resolution, rotation and
// output enablement are intentionally left to the desktop's display
// service on modern KDE systems.
bool changed = false;
switch (role) {
case EnabledRole:
case RotationRole:
case ResolutionIndexRole:
case RefreshRateIndexRole:
case ScaleRole:
return false;
case EnabledRole: {
const bool enabled = value.toBool();
if (!enabled) {
int enabledOutputs = 0;
for (const KScreen::OutputPtr &candidate : m_outputs) {
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:
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();
roles[Qt::DisplayRole] = "display";
@ -142,9 +351,18 @@ QHash<int, QByteArray> Qt5OutputModel::roleNames() const
return roles;
}
void Qt5OutputModel::setScreens(const QList<QScreen *> &screens)
void KScreenOutputModel::setConfig(const KScreen::ConfigPtr &config)
{
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();
}

@ -2,10 +2,11 @@
#include "output.h"
#include <KScreen/Config>
#include <QAbstractListModel>
#include <QScreen>
class Qt5OutputModel : public QAbstractListModel
class KScreenOutputModel : public QAbstractListModel
{
Q_OBJECT
@ -30,7 +31,7 @@ public:
ReplicasModelRole
};
explicit Qt5OutputModel(QObject *parent = nullptr);
explicit KScreenOutputModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index,
@ -38,16 +39,31 @@ public:
bool setData(const QModelIndex &index,
const QVariant &value,
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:
QHash<int, QByteArray> roleNames() const override;
private:
static Output::Rotation rotationForScreen(const QScreen *screen);
static QString screenName(const QScreen *screen);
struct ModeChoice {
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:
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",
QStringLiteral("Output is only a namespace for enums"));
}
};
#include "plugin.moc"

@ -2,48 +2,87 @@
#include "outputmodel.h"
#include <KScreen/ConfigMonitor>
#include <KScreen/GetConfigOperation>
#include <KScreen/SetConfigOperation>
#include <QDebug>
#include <QGuiApplication>
#include <QTimer>
Qt5Screen::Qt5Screen(QObject *parent)
KScreenScreen::KScreenScreen(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(qApp, &QGuiApplication::screenAdded, this, &Qt5Screen::refreshScreens);
connect(qApp, &QGuiApplication::screenRemoved, this, &Qt5Screen::refreshScreens);
connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged,
this, [this] {
if (!m_applying) {
loadConfig();
}
});
QTimer::singleShot(0, this, &KScreenScreen::loadConfig);
}
Qt5OutputModel *Qt5Screen::outputModel() const
KScreenOutputModel *KScreenScreen::outputModel() const
{
return m_outputModel;
}
void Qt5Screen::connectScreen(QScreen *screen)
void KScreenScreen::loadConfig()
{
if (!screen) {
if (m_applying) {
return;
}
connect(screen, &QScreen::geometryChanged, this, &Qt5Screen::refreshScreens, Qt::UniqueConnection);
connect(screen, &QScreen::orientationChanged, this, &Qt5Screen::refreshScreens, Qt::UniqueConnection);
connect(screen, &QScreen::physicalDotsPerInchChanged, this, &Qt5Screen::refreshScreens, Qt::UniqueConnection);
}
KScreen::GetConfigOperation operation(KScreen::ConfigOperation::NoOptions, this);
if (!operation.exec() || operation.hasError() || !operation.config()) {
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()
{
const QList<QScreen *> screens = QGuiApplication::screens();
for (QScreen *screen : screens) {
connectScreen(screen);
if (m_config) {
KScreen::ConfigMonitor::instance()->removeConfig(m_config);
}
m_outputModel->setScreens(screens);
m_config = operation.config();
KScreen::ConfigMonitor::instance()->addConfig(m_config);
m_outputModel->setConfig(m_config);
emit outputModelChanged();
}
void Qt5Screen::save()
void KScreenScreen::save()
{
// Display configuration is managed by KScreen/system settings on KDE6.
// QScreen is a read-only Qt API, so there is nothing to write here.
}
if (!m_config || m_applying) {
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
#include <KScreen/Config>
#include <QObject>
class Qt5OutputModel;
class QScreen;
class KScreenOutputModel;
class Qt5Screen : public QObject
class KScreenScreen : public QObject
{
Q_OBJECT
Q_PROPERTY(Qt5OutputModel *outputModel READ outputModel NOTIFY outputModelChanged)
Q_PROPERTY(KScreenOutputModel *outputModel READ outputModel NOTIFY outputModelChanged)
public:
explicit Qt5Screen(QObject *parent = nullptr);
explicit KScreenScreen(QObject *parent = nullptr);
Qt5OutputModel *outputModel() const;
KScreenOutputModel *outputModel() const;
Q_INVOKABLE void save();
private slots:
void refreshScreens();
void connectScreen(QScreen *screen);
signals:
void outputModelChanged();
private slots:
void loadConfig();
private:
Qt5OutputModel *m_outputModel;
KScreen::ConfigPtr m_config;
KScreenOutputModel *m_outputModel;
bool m_applying = false;
};

Loading…
Cancel
Save