diff --git a/debian/control b/debian/control index 9a2b52f..53f45b4 100644 --- a/debian/control +++ b/debian/control @@ -5,6 +5,7 @@ Maintainer: CutefishOS 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 diff --git a/screen/CMakeLists.txt b/screen/CMakeLists.txt index d262830..95146ed 100644 --- a/screen/CMakeLists.txt +++ b/screen/CMakeLists.txt @@ -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) diff --git a/screen/qt5/outputmodel.cpp b/screen/qt5/outputmodel.cpp index 4c89122..cd097a1 100644 --- a/screen/qt5/outputmodel.cpp +++ b/screen/qt5/outputmodel.cpp @@ -1,89 +1,199 @@ #include "outputmodel.h" -#include +#include +#include + +#include +#include + #include -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::modesForOutput(const KScreen::OutputPtr &output) const +{ + QList 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 KScreenOutputModel::resolutionsForOutput(const KScreen::OutputPtr &output) const +{ + QList 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 KScreenOutputModel::refreshRatesForOutput(const KScreen::OutputPtr &output, + const QSize &size) const { - return parent.isValid() ? 0 : m_screens.size(); + QList 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 resolutions = resolutionsForOutput(output); + const QList 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(rotationForScreen(screen)); + return static_cast(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(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 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::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 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 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 KScreenOutputModel::roleNames() const { QHash roles = QAbstractListModel::roleNames(); roles[Qt::DisplayRole] = "display"; @@ -142,9 +351,18 @@ QHash Qt5OutputModel::roleNames() const return roles; } -void Qt5OutputModel::setScreens(const QList &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(); } diff --git a/screen/qt5/outputmodel.h b/screen/qt5/outputmodel.h index 9f09327..2046375 100644 --- a/screen/qt5/outputmodel.h +++ b/screen/qt5/outputmodel.h @@ -2,10 +2,11 @@ #include "output.h" +#include + #include -#include -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 &screens); + void setConfig(const KScreen::ConfigPtr &config); protected: QHash 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 m_screens; -}; + KScreen::OutputPtr outputAt(int row) const; + QList modesForOutput(const KScreen::OutputPtr &output) const; + QList resolutionsForOutput(const KScreen::OutputPtr &output) const; + QList 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 m_outputs; +}; diff --git a/screen/qt5/plugin.cpp b/screen/qt5/plugin.cpp index bb81ece..5d74baf 100644 --- a/screen/qt5/plugin.cpp +++ b/screen/qt5/plugin.cpp @@ -12,11 +12,10 @@ class QmlPlugins : public QQmlExtensionPlugin public: void registerTypes(const char *uri) override { - qmlRegisterType(uri, 1, 0, "Screen"); + qmlRegisterType(uri, 1, 0, "Screen"); qmlRegisterUncreatableMetaObject(Output::staticMetaObject, uri, 1, 0, "Output", QStringLiteral("Output is only a namespace for enums")); } }; #include "plugin.moc" - diff --git a/screen/qt5/screen.cpp b/screen/qt5/screen.cpp index e541afd..6771c38 100644 --- a/screen/qt5/screen.cpp +++ b/screen/qt5/screen.cpp @@ -2,48 +2,87 @@ #include "outputmodel.h" +#include +#include +#include + +#include #include +#include -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 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(); +} diff --git a/screen/qt5/screen.h b/screen/qt5/screen.h index ed0057a..82338f7 100644 --- a/screen/qt5/screen.h +++ b/screen/qt5/screen.h @@ -1,29 +1,30 @@ #pragma once +#include + #include -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; }; -