From 2d25cb7fccd305168f37e4f13b727957befaf89c Mon Sep 17 00:00:00 2001 From: reionwong Date: Wed, 9 Sep 2026 03:48:02 -0400 Subject: [PATCH] feat(screen): use native display configuration with immediate updates --- CMakeLists.txt | 1 + README.md | 4 +- debian/control | 4 +- screen/CMakeLists.txt | 37 +- screen/com.cutefish.DisplayConfig1.xml | 48 ++ screen/displayclient.cpp | 209 +++++++ screen/displayclient.h | 88 +++ screen/displaytypes.cpp | 78 +++ screen/displaytypes.h | 126 +++++ screen/output.cpp | 1 - screen/output.h | 5 +- screen/outputmodel.cpp | 667 ++++++++++++++--------- screen/outputmodel.h | 85 +-- screen/screen.cpp | 234 +++----- screen/screen.h | 59 +- screen/tests/outputmodel_test.cpp | 97 ++++ screen/tests/screen_integration_test.cpp | 183 +++++++ screen/tests/wayland_screen_probe.cpp | 41 ++ 18 files changed, 1486 insertions(+), 481 deletions(-) create mode 100644 screen/com.cutefish.DisplayConfig1.xml create mode 100644 screen/displayclient.cpp create mode 100644 screen/displayclient.h create mode 100644 screen/displaytypes.cpp create mode 100644 screen/displaytypes.h create mode 100644 screen/tests/outputmodel_test.cpp create mode 100644 screen/tests/screen_integration_test.cpp create mode 100644 screen/tests/wayland_screen_probe.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ddff4ed..0ee3d5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ list(APPEND CMAKE_MODULE_PATH ) include(GNUInstallDirs) +include(CTest) include(GenerateExportHeader) include(CMakePackageConfigHelpers) diff --git a/README.md b/README.md index b461953..5ffaf64 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The framework uses Qt 6, KDE Frameworks 6, and a C++20 compiler. ```shell sudo apt install cmake extra-cmake-modules pkg-config build-essential \ qt6-base-dev qt6-declarative-dev qt6-tools-dev qt6-tools-dev-tools \ - libkscreen-dev libkf6networkmanagerqt-dev libkf6bluezqt-dev \ + libkf6networkmanagerqt-dev libkf6bluezqt-dev \ libkf6modemmanagerqt-dev libcanberra-dev libpulse-dev \ sound-theme-freedesktop ``` @@ -42,7 +42,7 @@ sudo apt install cmake extra-cmake-modules pkg-config build-essential \ ```shell sudo pacman -S --needed base-devel cmake extra-cmake-modules pkgconf \ - qt6-base qt6-declarative kscreen bluez-qt networkmanager-qt \ + qt6-base qt6-declarative bluez-qt networkmanager-qt \ modemmanager-qt libcanberra libpulse sound-theme-freedesktop ``` diff --git a/debian/control b/debian/control index 53f45b4..9a2b52f 100644 --- a/debian/control +++ b/debian/control @@ -5,7 +5,6 @@ Maintainer: CutefishOS Build-Depends: cmake, debhelper (>= 9), extra-cmake-modules, - libkscreen-dev, libkf6networkmanagerqt-dev, libkf6bluezqt-dev, libkf6modemmanagerqt-dev, @@ -23,6 +22,5 @@ Homepage: https://cutefishos.com Package: libcutefish Architecture: any Depends: ${misc:Depends}, - ${shlibs:Depends}, - libkscreen-bin + ${shlibs:Depends} Description: CutefishOS Library diff --git a/screen/CMakeLists.txt b/screen/CMakeLists.txt index da555ce..a84cfd5 100644 --- a/screen/CMakeLists.txt +++ b/screen/CMakeLists.txt @@ -1,4 +1,8 @@ set(CUTEFISH_SCREEN_SOURCES + displayclient.cpp + displayclient.h + displaytypes.cpp + displaytypes.h output.cpp output.h outputmodel.cpp @@ -7,10 +11,13 @@ set(CUTEFISH_SCREEN_SOURCES screen.h ) -find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick) -find_package(KF6Screen REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS Core DBus Gui Quick) -add_library(cutefish-framework-screen SHARED ${CUTEFISH_SCREEN_SOURCES}) +set(DISPLAY_CONFIG_XML ${CMAKE_CURRENT_SOURCE_DIR}/com.cutefish.DisplayConfig1.xml) +set_source_files_properties(${DISPLAY_CONFIG_XML} PROPERTIES INCLUDE displaytypes.h) +qt_add_dbus_interface(DISPLAY_CONFIG_DBUS_SOURCES ${DISPLAY_CONFIG_XML} display_config_interface) + +add_library(cutefish-framework-screen SHARED ${CUTEFISH_SCREEN_SOURCES} ${DISPLAY_CONFIG_DBUS_SOURCES}) add_library(Cutefish::Screen ALIAS cutefish-framework-screen) qt_add_qml_module(cutefish-framework-screen @@ -24,9 +31,31 @@ qt_add_qml_module(cutefish-framework-screen target_link_libraries(cutefish-framework-screen PUBLIC Qt6::Core + Qt6::DBus Qt6::Quick Qt6::Gui - KF6::Screen ) cutefish_install_qml_module(cutefish-framework-screen) + +if(BUILD_TESTING) + find_package(Qt6 REQUIRED COMPONENTS Test) + add_executable(display-outputmodel-test tests/outputmodel_test.cpp) + target_link_libraries(display-outputmodel-test PRIVATE cutefish-framework-screen Qt6::Test) + target_include_directories(display-outputmodel-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + add_test(NAME display-outputmodel-test COMMAND display-outputmodel-test) + if(CUTEFISH_TEST_COMPOSITOR) + add_executable(display-wayland-screen-probe tests/wayland_screen_probe.cpp) + target_link_libraries(display-wayland-screen-probe PRIVATE Qt6::Gui) + add_executable(display-screen-integration-test tests/screen_integration_test.cpp) + add_dependencies(display-screen-integration-test display-wayland-screen-probe) + target_compile_definitions(display-screen-integration-test PRIVATE + SCREEN_PROBE_PATH="$") + target_link_libraries(display-screen-integration-test PRIVATE cutefish-framework-screen Qt6::Test) + target_include_directories(display-screen-integration-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + add_test(NAME display-screen-integration-test + COMMAND dbus-run-session -- $) + set_tests_properties(display-screen-integration-test PROPERTIES + ENVIRONMENT "CUTEFISH_TEST_COMPOSITOR=${CUTEFISH_TEST_COMPOSITOR}") + endif() +endif() diff --git a/screen/com.cutefish.DisplayConfig1.xml b/screen/com.cutefish.DisplayConfig1.xml new file mode 100644 index 0000000..8abfaf5 --- /dev/null +++ b/screen/com.cutefish.DisplayConfig1.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/screen/displayclient.cpp b/screen/displayclient.cpp new file mode 100644 index 0000000..db145b4 --- /dev/null +++ b/screen/displayclient.cpp @@ -0,0 +1,209 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Upstream: chromium/src/ash/display/cros_display_config.cc +// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a +// +// Adapted for Cutefish: service-owner watching and pending D-Bus replies +// replace synchronous KScreen operations and reconnect after compositor +// restarts. + +#include "displayclient.h" + +#include +#include + +#include "display_config_interface.h" + +using namespace cutefish::display; + +DisplayClient::DisplayClient(QObject *parent) + : QObject(parent) + , m_serviceName(qEnvironmentVariable("CUTEFISH_DISPLAY_CONFIG_SERVICE", + QStringLiteral("com.cutefish.DisplayConfig"))) +{ + ipc::registerTypes(); + m_operationTimeout.setSingleShot(true); + m_operationTimeout.setInterval(15000); + connect(&m_operationTimeout, &QTimer::timeout, this, [this] { + finishOperation(m_operationToken, + {false, false, 1, tr("The display operation timed out."), {}}); + refresh(); + }); + connect(QDBusConnection::sessionBus().interface(), + &QDBusConnectionInterface::serviceOwnerChanged, this, + [this](const QString &name, const QString &, const QString &) { + if (name != m_serviceName) + return; + const bool wasAvailable = available(); + connectInterface(); + if (wasAvailable != available()) + emit availabilityChanged(); + refresh(); + }); + connectInterface(); + refresh(); +} + +bool DisplayClient::available() const +{ + return m_interface && m_interface->isValid() && !m_state.instanceId.isEmpty(); +} + +void DisplayClient::connectInterface() +{ + ++m_refreshSerial; + ++m_serviceEpoch; + m_state = {}; + emit stateChanged(); + if (m_applyPending || !m_operationToken.isEmpty() || m_completion) + finishOperation(m_operationToken, + {false, false, 1, tr("The display service restarted."), {}}); + delete m_interface; + m_interface = new ComCutefishDisplayConfig1Interface( + m_serviceName, QStringLiteral("/com/cutefish/DisplayConfig1"), + QDBusConnection::sessionBus(), this); + connect(m_interface, &ComCutefishDisplayConfig1Interface::StateChanged, this, [this] { + refresh(); + }); + connect(m_interface, &ComCutefishDisplayConfig1Interface::PreviewChanged, this, + [this](const QString &, qulonglong, qulonglong, qulonglong) { + refresh(); + }); + connect(m_interface, &ComCutefishDisplayConfig1Interface::OperationFinished, this, + [this](const QString &token, bool succeeded, bool degraded, uint errorCode, + const QString &message, const QStringList &affected, qulonglong generation, + qulonglong revision) { + const OperationResult result {succeeded, degraded, errorCode, message, + affected, generation, revision}; + if (m_applyPending) + m_earlyResults.insert(token, result); + else if (token == m_operationToken) + queueCompletion(token, result); + refresh(); + }); +} + +void DisplayClient::refresh() +{ + const auto serial = ++m_refreshSerial; + if (!m_interface || !m_interface->isValid()) { + m_state = {}; + emit stateChanged(); + return; + } + auto pending = m_interface->GetState(); + auto *watcher = new QDBusPendingCallWatcher(pending, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, serial] { + QDBusPendingReply, + QList, QList, QString, qulonglong> + reply = *watcher; + watcher->deleteLater(); + if (serial != m_refreshSerial) + return; + if (reply.isError()) { + m_state = {}; + emit stateChanged(); + emit availabilityChanged(); + if (m_applyPending || !m_operationToken.isEmpty() || m_completion) + finishOperation(m_operationToken, {false, false, 1, reply.error().message(), {}}); + return; + } + const bool wasAvailable = available(); + m_state = {reply.argumentAt<0>(), reply.argumentAt<1>(), reply.argumentAt<2>(), + reply.argumentAt<3>(), reply.argumentAt<4>(), reply.argumentAt<5>(), + reply.argumentAt<6>(), reply.argumentAt<7>(), reply.argumentAt<8>()}; + emit stateChanged(); + if (wasAvailable != available()) + emit availabilityChanged(); + completeIfCurrent(); + }); +} + +QString DisplayClient::apply(const QList &displays, const QString &primary, + const QList &placements, + const ipc::MirrorGroups &mirrorGroups) +{ + if (!available() || m_applyPending || !m_operationToken.isEmpty() || m_completion) + return {}; + m_applyPending = true; + m_operationToken.clear(); + m_earlyResults.clear(); + m_operationTimeout.start(); + const auto epoch = m_serviceEpoch; + const auto operation = ++m_operationSerial; + auto pending = m_interface->SetConfiguration(m_state.instanceId, m_state.topologyGeneration, + m_state.appliedRevision, displays, primary, + placements, mirrorGroups); + auto *watcher = new QDBusPendingCallWatcher(pending, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, epoch, operation] { + QDBusPendingReply reply = *watcher; + watcher->deleteLater(); + if (epoch != m_serviceEpoch || operation != m_operationSerial || !m_applyPending) + return; + m_applyPending = false; + if (reply.isError()) { + queueCompletion({}, {false, false, 1, reply.error().message(), {}}); + } else { + m_operationToken = reply.value(); + const auto result = m_earlyResults.constFind(m_operationToken); + if (result != m_earlyResults.cend()) { + const auto completed = *result; + queueCompletion(m_operationToken, completed); + } + m_earlyResults.clear(); + } + }); + return QStringLiteral("pending"); +} + +void DisplayClient::finishOperation(const QString &token, const OperationResult &result) +{ + const QString completedToken = token; + m_applyPending = false; + m_operationToken.clear(); + m_earlyResults.clear(); + m_completion.reset(); + m_operationTimeout.stop(); + emit operationFinished(completedToken, result.succeeded, result.degraded, result.errorCode, + result.message, result.affected); +} + +void DisplayClient::queueCompletion(const QString &token, const OperationResult &result) +{ + m_operationToken = token; + m_completion = result; + refresh(); +} + +void DisplayClient::completeIfCurrent() +{ + if (!m_completion) + return; + if (m_completion->generation + && (m_state.topologyGeneration < m_completion->generation + || (m_state.topologyGeneration == m_completion->generation + && m_state.appliedRevision < m_completion->revision))) + return; + const auto result = *m_completion; + finishOperation(m_operationToken, result); +} + +void DisplayClient::confirm(const QString &token) +{ + if (available()) + m_interface->ConfirmConfiguration(token); +} + +void DisplayClient::revert(const QString &token) +{ + if (available()) + m_interface->RevertConfiguration(token); +} + +void DisplayClient::identify(const QStringList &displayIds) +{ + if (available()) + m_interface->IdentifyDisplays(displayIds); +} diff --git a/screen/displayclient.h b/screen/displayclient.h new file mode 100644 index 0000000..c10ae8d --- /dev/null +++ b/screen/displayclient.h @@ -0,0 +1,88 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Upstream: chromium/src/ash/display/cros_display_config.h +// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a +// +// Adapted for Cutefish: an asynchronous Qt D-Bus client watches compositor +// ownership and exposes coherent native display snapshots to the QML model. + +#pragma once + +#include + +#include +#include +#include + +#include "displaytypes.h" + +class ComCutefishDisplayConfig1Interface; + +class DisplayClient : public QObject +{ + Q_OBJECT + +public: + struct State { + QString instanceId; + QString seatId; + qulonglong topologyGeneration = 0; + qulonglong appliedRevision = 0; + QList physical; + QList logical; + QList placements; + QString previewToken; + qulonglong previewDeadlineMs = 0; + }; + + explicit DisplayClient(QObject *parent = nullptr); + bool available() const; + + const State &state() const { return m_state; } + + void refresh(); + QString apply(const QList &displays, + const QString &primary, + const QList &placements, + const cutefish::display::ipc::MirrorGroups &mirrorGroups); + void confirm(const QString &token); + void revert(const QString &token); + void identify(const QStringList &displayIds); + +signals: + void availabilityChanged(); + void stateChanged(); + void operationFinished(QString token, bool succeeded, bool degraded, uint errorCode, + QString message, QStringList affectedDisplays); + +private: + void connectInterface(); + + struct OperationResult { + bool succeeded; + bool degraded; + uint errorCode; + QString message; + QStringList affected; + qulonglong generation = 0; + qulonglong revision = 0; + }; + + void queueCompletion(const QString &token, const OperationResult &result); + void completeIfCurrent(); + void finishOperation(const QString &token, const OperationResult &result); + + QString m_serviceName; + ComCutefishDisplayConfig1Interface *m_interface = nullptr; + State m_state; + quint64 m_refreshSerial = 0; + bool m_applyPending = false; + QString m_operationToken; + QHash m_earlyResults; + std::optional m_completion; + quint64 m_serviceEpoch = 0; + quint64 m_operationSerial = 0; + QTimer m_operationTimeout; +}; diff --git a/screen/displaytypes.cpp b/screen/displaytypes.cpp new file mode 100644 index 0000000..7a9849a --- /dev/null +++ b/screen/displaytypes.cpp @@ -0,0 +1,78 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Upstream: chromium/src/ash/display/cros_display_config.cc +// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a +// +// Adapted for Cutefish: serializers implement the compositor's versioned +// session D-Bus tuples without Ash or Mojo. + +#include "displaytypes.h" + +#include + +namespace cutefish::display::ipc { +#define WRITE(Type, Fields) \ + QDBusArgument &operator<<(QDBusArgument &a, const Type &v) \ + { \ + a.beginStructure(); \ + a Fields; \ + a.endStructure(); \ + return a; \ + } +#define READ(Type, Fields) \ + const QDBusArgument &operator>>(const QDBusArgument &a, Type &v) \ + { \ + a.beginStructure(); \ + a Fields; \ + a.endStructure(); \ + return a; \ + } +WRITE(Mode, << v.width << v.height << v.refreshMilliHz << v.pixelClockKHz << v.horizontalTotal + << v.verticalTotal << v.flags << v.preferred << v.current << v.name) +READ(Mode, >> v.width >> v.height >> v.refreshMilliHz >> v.pixelClockKHz >> v.horizontalTotal + >> v.verticalTotal >> v.flags >> v.preferred >> v.current >> v.name) +WRITE(RequestedMode, << v.width << v.height << v.refreshMilliHz << v.pixelClockKHz + << v.horizontalTotal << v.verticalTotal << v.flags << v.preferred << v.name) +READ(RequestedMode, >> v.width >> v.height >> v.refreshMilliHz >> v.pixelClockKHz + >> v.horizontalTotal >> v.verticalTotal >> v.flags >> v.preferred >> v.name) +WRITE(PhysicalDisplay, << v.sessionId << v.persistentKey << v.portKey << v.edidKey + << v.connectorName << v.devicePath << v.baseConnectorKey << v.pathTopology + << v.make << v.model << v.serialNumber << v.physicalWidthMm + << v.physicalHeightMm << v.connectionType << v.panelOrientation + << v.capabilities << v.modes) +READ(PhysicalDisplay, >> v.sessionId >> v.persistentKey >> v.portKey >> v.edidKey >> v.connectorName + >> v.devicePath >> v.baseConnectorKey >> v.pathTopology >> v.make >> v.model + >> v.serialNumber >> v.physicalWidthMm >> v.physicalHeightMm >> v.connectionType + >> v.panelOrientation >> v.capabilities >> v.modes) +WRITE(LogicalDisplay, << v.id << v.waylandName << v.destinations << v.x << v.y << v.width + << v.height << v.workX << v.workY << v.workWidth << v.workHeight << v.scale120 + << v.rotation << v.primary) +READ(LogicalDisplay, >> v.id >> v.waylandName >> v.destinations >> v.x >> v.y >> v.width >> v.height + >> v.workX >> v.workY >> v.workWidth >> v.workHeight >> v.scale120 >> v.rotation + >> v.primary) +WRITE(DisplayRequest, << v.id << v.enabled << v.mode << v.scale120 << v.rotation) +READ(DisplayRequest, >> v.id >> v.enabled >> v.mode >> v.scale120 >> v.rotation) +WRITE(Placement, << v.displayId << v.parentId << v.edge << v.offset) +READ(Placement, >> v.displayId >> v.parentId >> v.edge >> v.offset) +#undef WRITE +#undef READ + +void registerTypes() +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); +} + +} // namespace cutefish::display::ipc diff --git a/screen/displaytypes.h b/screen/displaytypes.h new file mode 100644 index 0000000..d1a8016 --- /dev/null +++ b/screen/displaytypes.h @@ -0,0 +1,126 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Upstream: chromium/src/ash/display/cros_display_config.h +// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a +// +// Adapted for Cutefish: these fixed Qt D-Bus value types mirror the native +// compositor schema without linking the framework to compositor internals. + +#pragma once + +#include +#include +#include +#include + +namespace cutefish::display::ipc { + +struct Mode { + int width = 0; + int height = 0; + int refreshMilliHz = 0; + int pixelClockKHz = 0; + int horizontalTotal = 0; + int verticalTotal = 0; + uint flags = 0; + bool preferred = false; + bool current = false; + QString name; +}; + +struct RequestedMode { + int width = 0; + int height = 0; + int refreshMilliHz = 0; + int pixelClockKHz = 0; + int horizontalTotal = 0; + int verticalTotal = 0; + uint flags = 0; + bool preferred = false; + QString name; +}; + +struct PhysicalDisplay { + QString sessionId; + QString persistentKey; + QString portKey; + QString edidKey; + QString connectorName; + QString devicePath; + QString baseConnectorKey; + QStringList pathTopology; + QString make; + QString model; + QString serialNumber; + int physicalWidthMm = 0; + int physicalHeightMm = 0; + uint connectionType = 0; + uint panelOrientation = 0; + uint capabilities = 0; + QList modes; +}; + +struct LogicalDisplay { + QString id; + QString waylandName; + QStringList destinations; + int x = 0; + int y = 0; + int width = 0; + int height = 0; + int workX = 0; + int workY = 0; + int workWidth = 0; + int workHeight = 0; + uint scale120 = 120; + uint rotation = 0; + bool primary = false; +}; + +struct DisplayRequest { + QString id; + bool enabled = true; + RequestedMode mode; + uint scale120 = 120; + uint rotation = 0; +}; + +struct Placement { + QString displayId; + QString parentId; + uint edge = 0; + int offset = 0; +}; + +using MirrorGroups = QList; + +QDBusArgument &operator<<(QDBusArgument &, const Mode &); +const QDBusArgument &operator>>(const QDBusArgument &, Mode &); +QDBusArgument &operator<<(QDBusArgument &, const RequestedMode &); +const QDBusArgument &operator>>(const QDBusArgument &, RequestedMode &); +QDBusArgument &operator<<(QDBusArgument &, const PhysicalDisplay &); +const QDBusArgument &operator>>(const QDBusArgument &, PhysicalDisplay &); +QDBusArgument &operator<<(QDBusArgument &, const LogicalDisplay &); +const QDBusArgument &operator>>(const QDBusArgument &, LogicalDisplay &); +QDBusArgument &operator<<(QDBusArgument &, const DisplayRequest &); +const QDBusArgument &operator>>(const QDBusArgument &, DisplayRequest &); +QDBusArgument &operator<<(QDBusArgument &, const Placement &); +const QDBusArgument &operator>>(const QDBusArgument &, Placement &); +void registerTypes(); + +} // namespace cutefish::display::ipc + +Q_DECLARE_METATYPE(cutefish::display::ipc::Mode) +Q_DECLARE_METATYPE(cutefish::display::ipc::RequestedMode) +Q_DECLARE_METATYPE(cutefish::display::ipc::PhysicalDisplay) +Q_DECLARE_METATYPE(cutefish::display::ipc::LogicalDisplay) +Q_DECLARE_METATYPE(cutefish::display::ipc::DisplayRequest) +Q_DECLARE_METATYPE(cutefish::display::ipc::Placement) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(cutefish::display::ipc::MirrorGroups) diff --git a/screen/output.cpp b/screen/output.cpp index c5cf9c0..4c51781 100644 --- a/screen/output.cpp +++ b/screen/output.cpp @@ -1,2 +1 @@ #include "output.h" - diff --git a/screen/output.h b/screen/output.h index 41b0389..43fa98c 100644 --- a/screen/output.h +++ b/screen/output.h @@ -19,9 +19,8 @@ public: // A Q_GADGET registered directly would become a QML value type, and those have // to be named in lowercase. Exposing it as a namespace keeps `Output.Left`. -namespace OutputEnums -{ +namespace OutputEnums { Q_NAMESPACE QML_FOREIGN_NAMESPACE(Output) QML_NAMED_ELEMENT(Output) -} +} // namespace OutputEnums diff --git a/screen/outputmodel.cpp b/screen/outputmodel.cpp index e007778..a647829 100644 --- a/screen/outputmodel.cpp +++ b/screen/outputmodel.cpp @@ -1,368 +1,495 @@ -#include "outputmodel.h" +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Upstream: chromium/src/ash/display/cros_display_config.cc +// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a +// +// Adapted for Cutefish: stable native display requests replace KScreen state. -#include -#include +#include "outputmodel.h" #include -#include +#include -#include +#include +#include -KScreenOutputModel::KScreenOutputModel(QObject *parent) - : QAbstractListModel(parent) -{ -} +using namespace cutefish::display; -int KScreenOutputModel::rowCount(const QModelIndex &parent) const +DisplayOutputModel::DisplayOutputModel(QObject *parent) + : QAbstractListModel(parent) { - return parent.isValid() ? 0 : m_outputs.size(); } -KScreen::OutputPtr KScreenOutputModel::outputAt(int row) const +int DisplayOutputModel::rowCount(const QModelIndex &parent) const { - if (row < 0 || row >= m_outputs.size()) { - return {}; - } - - return m_outputs.at(row); + return parent.isValid() ? 0 : m_entries.size(); } -QList KScreenOutputModel::modesForOutput(const KScreen::OutputPtr &output) const +const ipc::Mode *DisplayOutputModel::selectedMode(const Entry &e) 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; + return e.modeIndex >= 0 && e.modeIndex < e.physical.modes.size() + ? &e.physical.modes.at(e.modeIndex) + : nullptr; } -QList KScreenOutputModel::resolutionsForOutput(const KScreen::OutputPtr &output) const +QList DisplayOutputModel::resolutionModes(const Entry &e) const { - QList resolutions; - for (const ModeChoice &mode : modesForOutput(output)) { - if (mode.size.isValid() && !resolutions.contains(mode.size)) { - resolutions.append(mode.size); + QList result; + QList seen; + for (int i = 0; i < e.physical.modes.size(); ++i) { + const auto &m = e.physical.modes.at(i); + const QSize s(m.width, m.height); + if (!seen.contains(s)) { + seen.append(s); + result.append(i); } } - 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(); + std::sort(result.begin(), result.end(), [&e](int a, int b) { + const auto &x = e.physical.modes.at(a); + const auto &y = e.physical.modes.at(b); + return qint64(x.width) * x.height > qint64(y.width) * y.height; }); - return resolutions; -} - -QList KScreenOutputModel::refreshRatesForOutput(const KScreen::OutputPtr &output, - const QSize &size) const -{ - 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 KScreenOutputModel::outputName(const KScreen::OutputPtr &output) -{ - if (!output) { - return QString(); - } - - if (!output->name().isEmpty()) { - return output->name(); - } - if (!output->model().isEmpty()) { - return output->model(); - } - if (!output->vendor().isEmpty()) { - return output->vendor(); - } - return QStringLiteral("Display"); + return result; } -QString KScreenOutputModel::formatResolution(const QSize &size) +QList DisplayOutputModel::refreshModes(const Entry &e) const { - 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"); + QList result; + const auto *selected = selectedMode(e); + if (!selected) + return result; + for (int i = 0; i < e.physical.modes.size(); ++i) { + const auto &m = e.physical.modes.at(i); + if (m.width == selected->width && m.height == selected->height) + result.append(i); } - return QString::number(refreshRate, 'f', 2) + QStringLiteral(" Hz"); -} - -bool KScreenOutputModel::sameRefreshRate(float first, float second) -{ - return qAbs(first - second) < 0.01; + std::sort(result.begin(), result.end(), [&e](int a, int b) { + return e.physical.modes.at(a).refreshMilliHz < e.physical.modes.at(b).refreshMilliHz; + }); + return result; } -QVariant KScreenOutputModel::data(const QModelIndex &index, int role) const +QVariant DisplayOutputModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) { - return QVariant(); - } - - 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); - + if (!index.isValid() || index.row() >= m_entries.size()) + return {}; + const Entry &e = m_entries.at(index.row()); + const auto *mode = selectedMode(e); + const auto resolutions = resolutionModes(e); + const auto refreshes = refreshModes(e); switch (role) { case Qt::DisplayRole: - return outputName(output); + return e.physical.connectorName.isEmpty() ? e.physical.model : e.physical.connectorName; + case IdRole: + return e.physical.sessionId; case EnabledRole: - return output->isEnabled(); + return e.enabled; case InternalRole: - return output->type() == KScreen::Output::Panel - || outputName(output).startsWith(QStringLiteral("eDP"), Qt::CaseInsensitive) - || outputName(output).startsWith(QStringLiteral("LVDS"), Qt::CaseInsensitive); + return e.physical.connectionType == 1; case PrimaryRole: - return m_config && m_config->primaryOutput() == output; + return e.primary; case SizeRole: - return output->size(); + return mode ? QSize(mode->width, mode->height) : QSize(); case PositionRole: case NormalizedPositionRole: - return output->pos(); + return e.position; case AutoRotateRole: - return output->autoRotatePolicy() != KScreen::Output::AutoRotatePolicy::Never; case AutoRotateOnlyInTabletModeRole: - return output->autoRotatePolicy() == KScreen::Output::AutoRotatePolicy::InTabletMode; + return false; case RotationRole: - return static_cast(output->rotation()); + return e.rotation == 0 ? 1 : e.rotation == 1 ? 2 : e.rotation == 2 ? 4 : 8; case ScaleRole: - 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 e.scale120 / 120.0; + case ResolutionIndexRole: + for (int i = 0; i < resolutions.size(); ++i) { + const auto &m = e.physical.modes.at(resolutions.at(i)); + if (mode && m.width == mode->width && m.height == mode->height) return i; - } } return 0; + case ResolutionsRole: { + QStringList v; + for (int i : resolutions) { + const auto &m = e.physical.modes.at(i); + v.append(QStringLiteral("%1 × %2").arg(m.width).arg(m.height)); + } + return v; } + case RefreshRateIndexRole: + return refreshes.indexOf(e.modeIndex); case RefreshRatesRole: { - QStringList result; - for (const float refreshRate : refreshRates) { - result.append(formatRefreshRate(refreshRate)); + QStringList v; + for (int i : refreshes) + v.append(QStringLiteral("%1 Hz").arg(e.physical.modes.at(i).refreshMilliHz / 1000.0, 0, + 'f', 2)); + return v; + } + case ReplicationSourceModelRole: { + QStringList v {QStringLiteral("Extend")}; + for (const auto &o : m_entries) + if (o.physical.sessionId != e.physical.sessionId) + v.append(o.physical.connectorName); + return v; + } + case ReplicationSourceIndexRole: { + if (e.mirrorSource.isEmpty()) + return 0; + int n = 1; + for (const auto &o : m_entries) { + if (o.physical.sessionId == e.physical.sessionId) + continue; + if (o.physical.sessionId == e.mirrorSource) + return n; + ++n; } - return result; + return 0; } - case ReplicationSourceModelRole: - return QStringList(); - case ReplicationSourceIndexRole: - return -1; case ReplicasModelRole: return QVariantList(); + case CapabilitiesRole: + return e.physical.capabilities; + case SupportedScalesRole: + return QVariantList {1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0}; default: - return QVariant(); + return {}; } } -bool KScreenOutputModel::setData(const QModelIndex &index, const QVariant &value, int role) +void DisplayOutputModel::markDirty(const QModelIndex &index, int role) { - if (!index.isValid()) { - return false; + QList roles {role}; + if (role == ResolutionIndexRole || role == RefreshRateIndexRole) + roles = {ResolutionIndexRole, RefreshRateIndexRole, RefreshRatesRole, SizeRole}; + emit dataChanged(index, index, roles); + if (!m_dirty) { + m_dirty = true; + emit dirtyChanged(); } +} - const KScreen::OutputPtr output = outputAt(index.row()); - if (!output) { +bool DisplayOutputModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.model() != this || index.row() >= m_entries.size()) return false; - } - - bool changed = false; + Entry &e = m_entries[index.row()]; switch (role) { 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) { + const bool v = value.toBool(); + if (!v) { + int n = 0; + for (const auto &i : m_entries) + n += i.enabled; + if (n <= 1) return false; - } - } - if (output->isEnabled() != enabled) { - output->setEnabled(enabled); - changed = true; } + if (e.enabled == v) + return false; + e.enabled = v; break; } + case PrimaryRole: + if (!value.toBool() || !e.enabled || e.primary) + return false; + for (auto &i : m_entries) + i.primary = false; + e.primary = true; + emit dataChanged(this->index(0), this->index(m_entries.size() - 1), {PrimaryRole}); + break; + case PositionRole: + case NormalizedPositionRole: + if (e.position == value.toPoint()) + return false; + e.position = value.toPoint(); + 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) { + const int raw = value.toInt(); + const uint r = raw == 1 ? 0 : raw == 2 ? 1 : raw == 4 ? 2 : raw == 8 ? 3 : 99; + if (r > 3 || e.rotation == r) return false; - } - if (output->rotation() != rotation) { - output->setRotation(rotation); - changed = true; - } + e.rotation = r; + break; + } + case ScaleRole: { + const int s = qRound(value.toDouble() * 120); + if (s < 120 || s > 480 || e.scale120 == uint(s)) + return false; + e.scale120 = s; break; } case ResolutionIndexRole: { - const QList resolutions = resolutionsForOutput(output); - const int resolutionIndex = value.toInt(); - if (resolutionIndex < 0 || resolutionIndex >= resolutions.size()) { + const auto choices = resolutionModes(e); + const int selected = value.toInt(); + if (selected < 0 || selected >= choices.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; + const auto &t = e.physical.modes.at(choices.at(selected)); + int best = choices.at(selected), difference = INT_MAX; + for (int i = 0; i < e.physical.modes.size(); ++i) { + const auto &c = e.physical.modes.at(i); + if (c.width == t.width && c.height == t.height) { + const int d = qAbs(c.refreshMilliHz + - (selectedMode(e) ? selectedMode(e)->refreshMilliHz : 0)); + if (d < difference) { + best = i; + difference = d; + } } } - if (!selectedModeId.isEmpty() && selectedModeId != output->currentModeId()) { - output->setCurrentModeId(selectedModeId); - changed = true; - } + if (e.modeIndex == best) + return false; + e.modeIndex = best; 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()) { + const auto choices = refreshModes(e); + const int selected = value.toInt(); + if (selected < 0 || selected >= choices.size() || e.modeIndex == choices.at(selected)) 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; + e.modeIndex = choices.at(selected); + break; + } + case ReplicationSourceIndexRole: { + const int selected = value.toInt(); + QString source; + int n = 1; + for (const auto &o : m_entries) { + if (o.physical.sessionId == e.physical.sessionId) + continue; + if (n++ == selected) { + source = o.physical.sessionId; 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)) { + if (e.mirrorSource == source) return false; - } - output->setScale(scale); - changed = true; + e.mirrorSource = source; break; } default: return false; } + m_layoutDirty |= role == PositionRole || role == NormalizedPositionRole || role == EnabledRole + || role == ReplicationSourceIndexRole; + if (role == EnabledRole && !e.enabled && e.primary) { + e.primary = false; + for (auto &candidate : m_entries) { + if (candidate.enabled && candidate.mirrorSource.isEmpty()) { + candidate.primary = true; + break; + } + } + emit dataChanged(this->index(0), this->index(m_entries.size() - 1), {PrimaryRole}); + } + markDirty(index, role); + return true; +} + +Qt::ItemFlags DisplayOutputModel::flags(const QModelIndex &i) const +{ + return i.isValid() ? Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable + : Qt::NoItemFlags; +} - if (changed) { - emit dataChanged(index, index, {role}); +QHash DisplayOutputModel::roleNames() const +{ + return {{Qt::DisplayRole, "display"}, + {EnabledRole, "enabled"}, + {InternalRole, "internal"}, + {PrimaryRole, "primary"}, + {SizeRole, "size"}, + {PositionRole, "position"}, + {NormalizedPositionRole, "normalizedPosition"}, + {AutoRotateRole, "autoRotate"}, + {AutoRotateOnlyInTabletModeRole, "autoRotateOnlyInTabletMode"}, + {RotationRole, "rotation"}, + {ScaleRole, "scale"}, + {ResolutionIndexRole, "resolutionIndex"}, + {ResolutionsRole, "resolutions"}, + {RefreshRateIndexRole, "refreshRateIndex"}, + {RefreshRatesRole, "refreshRates"}, + {ReplicationSourceModelRole, "replicationSourceModel"}, + {ReplicationSourceIndexRole, "replicationSourceIndex"}, + {ReplicasModelRole, "replicasModel"}, + {IdRole, "displayId"}, + {CapabilitiesRole, "capabilities"}, + {SupportedScalesRole, "supportedScales"}}; +} + +void DisplayOutputModel::setState(const DisplayClient::State &state) +{ + m_applied = state; + m_dirty = false; + m_layoutDirty = false; + QSet connected; + for (const auto &physical : state.physical) + connected.insert(physical.sessionId); + for (int row = m_entries.size() - 1; row >= 0; --row) { + if (!connected.contains(m_entries[row].physical.sessionId)) { + beginRemoveRows({}, row, row); + m_entries.removeAt(row); + endRemoveRows(); + } } - return changed; + for (const auto &physical : state.physical) { + Entry entry; + for (const auto &previous : m_entries) + if (previous.physical.sessionId == physical.sessionId) { + entry = previous; + break; + } + entry.physical = physical; + entry.enabled = false; + entry.primary = false; + entry.mirrorSource.clear(); + for (int i = 0; i < physical.modes.size(); ++i) { + if (physical.modes[i].current) { + entry.modeIndex = i; + break; + } + } + for (const auto &logical : state.logical) { + if (!logical.destinations.contains(physical.sessionId)) + continue; + entry.enabled = true; + entry.primary = logical.primary && logical.destinations.first() == physical.sessionId; + entry.position = {logical.x, logical.y}; + entry.scale120 = logical.scale120; + entry.rotation = logical.rotation; + if (logical.destinations.first() != physical.sessionId) + entry.mirrorSource = logical.destinations.first(); + break; + } + int row = 0; + while (row < m_entries.size() && m_entries[row].physical.sessionId != physical.sessionId) + ++row; + if (row == m_entries.size()) { + beginInsertRows({}, row, row); + m_entries.append(entry); + endInsertRows(); + } else { + m_entries[row] = entry; + emit dataChanged(index(row), index(row)); + } + } + emit dirtyChanged(); +} + +void DisplayOutputModel::discard() +{ + setState(m_applied); } -Qt::ItemFlags KScreenOutputModel::flags(const QModelIndex &index) const +QList DisplayOutputModel::requests() const { - if (!index.isValid()) { - return Qt::NoItemFlags; + QList r; + for (const auto &e : m_entries) { + const auto *m = selectedMode(e); + if (!m) + continue; + r.append({e.physical.sessionId, + e.enabled, + {m->width, m->height, m->refreshMilliHz, m->pixelClockKHz, m->horizontalTotal, + m->verticalTotal, m->flags, m->preferred, m->name}, + e.scale120, + e.rotation}); } - return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; + return r; } -QHash KScreenOutputModel::roleNames() const +QString DisplayOutputModel::primary() const { - QHash roles = QAbstractListModel::roleNames(); - roles[Qt::DisplayRole] = "display"; - roles[EnabledRole] = "enabled"; - roles[InternalRole] = "internal"; - roles[PrimaryRole] = "primary"; - roles[SizeRole] = "size"; - roles[PositionRole] = "position"; - roles[NormalizedPositionRole] = "normalizedPosition"; - roles[AutoRotateRole] = "autoRotate"; - roles[AutoRotateOnlyInTabletModeRole] = "autoRotateOnlyInTabletMode"; - roles[RotationRole] = "rotation"; - roles[ScaleRole] = "scale"; - roles[ResolutionIndexRole] = "resolutionIndex"; - roles[ResolutionsRole] = "resolutions"; - roles[RefreshRateIndexRole] = "refreshRateIndex"; - roles[RefreshRatesRole] = "refreshRates"; - roles[ReplicationSourceModelRole] = "replicationSourceModel"; - roles[ReplicationSourceIndexRole] = "replicationSourceIndex"; - roles[ReplicasModelRole] = "replicasModel"; - return roles; + for (const auto &e : m_entries) + if (e.primary) + return e.physical.sessionId; + return {}; } -void KScreenOutputModel::setConfig(const KScreen::ConfigPtr &config) +QList DisplayOutputModel::placements() const { - beginResetModel(); - 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()); + if (!m_layoutDirty) + return m_applied.placements; + QList result; + const QString root = primary(); + const Entry *parent = nullptr; + for (const auto &entry : m_entries) + if (entry.physical.sessionId == root) + parent = &entry; + if (!parent) + return result; + QList placed {parent}; + const auto logicalSize = [this](const Entry &entry) { + const auto *mode = selectedMode(entry); + if (!mode) + return QSize(1, 1); + QSize size(mode->width, mode->height); + if (entry.rotation % 2) + size.transpose(); + return QSize((size.width() * 120 + entry.scale120 - 1) / entry.scale120, + (size.height() * 120 + entry.scale120 - 1) / entry.scale120); + }; + QHash resolved; + resolved.insert(parent->physical.sessionId, QRect(parent->position, logicalSize(*parent))); + for (const auto &entry : m_entries) { + if (!entry.enabled || entry.physical.sessionId == root || !entry.mirrorSource.isEmpty()) + continue; + int bestDistance = INT_MAX; + ipc::Placement best; + QRect bestBounds; + const QSize childSize = logicalSize(entry); + for (const auto *candidate : placed) { + const QSize parentSize = logicalSize(*candidate); + const QPoint parentPosition = resolved.value(candidate->physical.sessionId).topLeft(); + const QPoint delta = entry.position - parentPosition; + for (uint edge = 0; edge < 4; ++edge) { + const bool horizontal = edge % 2; + const int childExtent = horizontal ? childSize.height() : childSize.width(); + const int parentExtent = horizontal ? parentSize.height() : parentSize.width(); + const int overlap = qMin(32, qMin(childExtent, parentExtent)); + const int cross = horizontal ? delta.y() : delta.x(); + const int offset = qBound(-childExtent + overlap, cross, parentExtent - overlap); + const int normal = edge == 0 ? -childSize.height() + : edge == 1 ? parentSize.width() + : edge == 2 ? parentSize.height() + : -childSize.width(); + const int distance = + qAbs((horizontal ? delta.x() : delta.y()) - normal) + qAbs(cross - offset); + const QRect bounds( + parentPosition + + (horizontal ? QPoint(normal, offset) : QPoint(offset, normal)), + childSize); + bool overlaps = false; + for (const auto &occupied : resolved) + overlaps |= occupied.intersects(bounds); + if (overlaps) + continue; + if (distance < bestDistance) { + bestDistance = distance; + best = {entry.physical.sessionId, candidate->physical.sessionId, edge, offset}; + bestBounds = bounds; + } } } + result.append(best); + placed.append(&entry); + resolved.insert(entry.physical.sessionId, bestBounds); + } + return result; +} + +ipc::MirrorGroups DisplayOutputModel::mirrorGroups() const +{ + ipc::MirrorGroups result; + for (const auto &entry : m_entries) { + if (!entry.enabled || entry.mirrorSource.isEmpty()) + continue; + auto group = std::find_if(result.begin(), result.end(), [&](const auto &members) { + return members.first() == entry.mirrorSource; + }); + if (group == result.end()) + result.append({entry.mirrorSource, entry.physical.sessionId}); + else + group->append(entry.physical.sessionId); } - endResetModel(); + return result; } diff --git a/screen/outputmodel.h b/screen/outputmodel.h index 2046375..478e4b6 100644 --- a/screen/outputmodel.h +++ b/screen/outputmodel.h @@ -1,15 +1,24 @@ -#pragma once - -#include "output.h" +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Upstream: chromium/src/ash/display/cros_display_config.h +// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a +// +// Adapted for Cutefish: stable-ID draft rows expose native modes, layout, +// primary selection, scale, rotation, and software mirror requests to QML. -#include +#pragma once #include +#include +#include -class KScreenOutputModel : public QAbstractListModel +#include "displayclient.h" + +class DisplayOutputModel : public QAbstractListModel { Q_OBJECT - public: enum OutputRoles { EnabledRole = Qt::UserRole + 1, @@ -28,42 +37,48 @@ public: RefreshRatesRole, ReplicationSourceModelRole, ReplicationSourceIndexRole, - ReplicasModelRole + ReplicasModelRole, + IdRole, + CapabilitiesRole, + SupportedScalesRole }; - explicit KScreenOutputModel(QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, - int role = Qt::DisplayRole) const override; - bool setData(const QModelIndex &index, - const QVariant &value, - int role = Qt::EditRole) override; + explicit DisplayOutputModel(QObject *parent = nullptr); + int rowCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; Qt::ItemFlags flags(const QModelIndex &index) const override; + QHash roleNames() const override; + void setState(const DisplayClient::State &state); + void discard(); - void setConfig(const KScreen::ConfigPtr &config); + bool dirty() const { return m_dirty; } -protected: - QHash roleNames() const override; + QList requests() const; + QList placements() const; + cutefish::display::ipc::MirrorGroups mirrorGroups() const; + QString primary() const; +signals: + void dirtyChanged(); private: - struct ModeChoice { - QString id; - QSize size; - float refreshRate = 0; + struct Entry { + cutefish::display::ipc::PhysicalDisplay physical; + bool enabled = false; + bool primary = false; + QPoint position; + uint scale120 = 120; + uint rotation = 0; + int modeIndex = 0; + QString mirrorSource; }; - 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; + const cutefish::display::ipc::Mode *selectedMode(const Entry &entry) const; + QList resolutionModes(const Entry &entry) const; + QList refreshModes(const Entry &entry) const; + void markDirty(const QModelIndex &index, int role); + DisplayClient::State m_applied; + QList m_entries; + bool m_dirty = false; + bool m_layoutDirty = false; }; diff --git a/screen/screen.cpp b/screen/screen.cpp index c0b29dc..1c5dc1a 100644 --- a/screen/screen.cpp +++ b/screen/screen.cpp @@ -1,181 +1,115 @@ #include "screen.h" -#include "outputmodel.h" +#include -#include -#include -#include -#include +#include "outputmodel.h" -#include -#include -#include -#include +KScreenScreen::KScreenScreen(QObject *parent) + : QObject(parent) + , m_client(this) + , m_outputModel(new DisplayOutputModel(this)) +{ + m_countdown.setInterval(250); + connect(&m_countdown, &QTimer::timeout, this, &KScreenScreen::stateChanged); + connect(&m_client, &DisplayClient::availabilityChanged, this, &KScreenScreen::stateChanged); + connect(&m_client, &DisplayClient::stateChanged, this, &KScreenScreen::updateState); + connect(m_outputModel, &DisplayOutputModel::dirtyChanged, this, &KScreenScreen::stateChanged); + connect(m_outputModel, &DisplayOutputModel::dirtyChanged, this, [this] { + if (dirty()) + apply(); + }); + connect(&m_client, &DisplayClient::operationFinished, this, + [this](const QString &, bool succeeded, bool degraded, uint, const QString &message, + const QStringList &) { + m_busy = false; + m_error = succeeded ? (degraded ? message : QString()) : message; + m_outputModel->setState(m_client.state()); + emit stateChanged(); + }); +} -static bool isDynamicDisplay(const KScreen::OutputPtr &output) +bool KScreenScreen::dirty() const { - if (!output) { - return false; - } + return m_outputModel->dirty(); +} - const QString identity = QStringLiteral("%1 %2 %3") - .arg(output->name(), output->vendor(), output->model()); - return identity.contains(QStringLiteral("virtual"), Qt::CaseInsensitive) - || identity.contains(QStringLiteral("qemu"), Qt::CaseInsensitive) - || identity.contains(QStringLiteral("spice"), Qt::CaseInsensitive) - || identity.contains(QStringLiteral("virtio"), Qt::CaseInsensitive); +bool KScreenScreen::canApply() const +{ + return available() && !m_busy && dirty() && !pendingConfirmation(); } -static QString displayOutputKey(const KScreen::OutputPtr &output) +int KScreenScreen::confirmationSeconds() const { - if (!output) { - return QString(); - } + if (!pendingConfirmation()) + return 0; + return qMax(0, + int((qint64(m_client.state().previewDeadlineMs) + - QDateTime::currentMSecsSinceEpoch() + 999) + / 1000)); +} - // Virtual outputs can change their EDID/mode list whenever the host - // resizes the guest window. Their hash is therefore not a stable - // identity; use the compositor's output name instead. - QString key = isDynamicDisplay(output) ? output->name() : output->hashMd5(); - if (key.isEmpty()) { - key = output->name(); - } - if (key.isEmpty()) { - key = QString::number(output->id()); +void KScreenScreen::updateState() +{ + const auto &state = m_client.state(); + if (!m_client.available()) { + m_busy = false; + m_error = tr("The display service is unavailable."); } - return key.replace(QLatin1Char('/'), QLatin1Char('_')); + const bool topologyChanged = !m_instanceId.isEmpty() + && (m_instanceId != state.instanceId + || m_topologyGeneration != state.topologyGeneration); + if (!m_outputModel->dirty() || topologyChanged || m_busy || pendingConfirmation()) + m_outputModel->setState(state); + if (topologyChanged) + m_error = tr("Displays changed. The previous draft was discarded."); + m_instanceId = state.instanceId; + m_topologyGeneration = state.topologyGeneration; + pendingConfirmation() ? m_countdown.start() : m_countdown.stop(); + emit stateChanged(); } -static void saveDisplayConfiguration(const KScreen::ConfigPtr &config) +void KScreenScreen::apply() { - if (!config) { + if (!canApply()) return; - } - - QSettings settings(QSettings::UserScope, QStringLiteral("cutefishos"), QStringLiteral("display")); - settings.setValue(QStringLiteral("Version"), 1); - settings.beginGroup(QStringLiteral("Outputs")); - const KScreen::OutputList outputs = config->outputs(); - for (auto it = outputs.cbegin(); it != outputs.cend(); ++it) { - const KScreen::OutputPtr output = it.value(); - if (!output || !output->isConnected()) { - continue; - } - - settings.beginGroup(displayOutputKey(output)); - settings.setValue(QStringLiteral("Name"), output->name()); - settings.setValue(QStringLiteral("Enabled"), output->isEnabled()); - settings.setValue(QStringLiteral("Priority"), output->priority()); - settings.setValue(QStringLiteral("PositionX"), output->pos().x()); - settings.setValue(QStringLiteral("PositionY"), output->pos().y()); - settings.setValue(QStringLiteral("Scale"), output->scale()); - settings.setValue(QStringLiteral("Rotation"), static_cast(output->rotation())); - - if (const KScreen::ModePtr mode = output->currentMode()) { - settings.setValue(QStringLiteral("ModeId"), mode->id()); - settings.setValue(QStringLiteral("ModeWidth"), mode->size().width()); - settings.setValue(QStringLiteral("ModeHeight"), mode->size().height()); - settings.setValue(QStringLiteral("ModeRefreshRate"), mode->refreshRate()); - } - settings.endGroup(); - } - settings.endGroup(); - settings.sync(); - - KScreen::OutputPtr primary = config->primaryOutput(); - if (!primary) { - for (const KScreen::OutputPtr &output : config->connectedOutputs()) { - if (output && output->isEnabled()) { - primary = output; - break; - } - } - } - if (primary) { - QSettings themeSettings(QSettings::UserScope, - QStringLiteral("cutefishos"), - QStringLiteral("theme")); - themeSettings.setValue(QStringLiteral("PixelRatio"), primary->scale()); - themeSettings.sync(); + m_busy = true; + m_error.clear(); + emit stateChanged(); + if (m_client.apply(m_outputModel->requests(), m_outputModel->primary(), + m_outputModel->placements(), m_outputModel->mirrorGroups()) + .isEmpty()) { + m_busy = false; + m_error = tr("The display service is unavailable."); + emit stateChanged(); } } -KScreenScreen::KScreenScreen(QObject *parent) - : QObject(parent) - , m_outputModel(new KScreenOutputModel(this)) +void KScreenScreen::discard() { - if (QGuiApplication::platformName().contains(QStringLiteral("wayland"), Qt::CaseInsensitive) - && qEnvironmentVariableIsEmpty("KSCREEN_BACKEND")) { - qputenv("KSCREEN_BACKEND", QByteArrayLiteral("kwayland")); + if (!m_busy) { + m_outputModel->discard(); + m_error.clear(); + emit stateChanged(); } - - connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, - this, [this] { - if (!m_applying) { - loadConfig(); - } - }); - - QTimer::singleShot(0, this, &KScreenScreen::loadConfig); } -KScreenOutputModel *KScreenScreen::outputModel() const +void KScreenScreen::keep() { - return m_outputModel; + if (pendingConfirmation()) + m_client.confirm(m_client.state().previewToken); } -void KScreenScreen::loadConfig() +void KScreenScreen::revert() { - if (m_applying) { - return; - } - - 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; - } - - if (m_config) { - KScreen::ConfigMonitor::instance()->removeConfig(m_config); - } - m_config = operation.config(); - KScreen::ConfigMonitor::instance()->addConfig(m_config); - m_outputModel->setConfig(m_config); - emit outputModelChanged(); + if (pendingConfirmation()) + m_client.revert(m_client.state().previewToken); } -void KScreenScreen::save() +void KScreenScreen::identify() { - 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; - // Cutefish owns the persistent display configuration. KWin is only the - // live display server to which this snapshot is applied. - saveDisplayConfiguration(m_config); - - 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(); + QStringList ids; + for (const auto &display : m_client.state().logical) + ids.append(display.id); + m_client.identify(ids); + emit identifyRequested(ids); } diff --git a/screen/screen.h b/screen/screen.h index 9a9e0b9..4178525 100644 --- a/screen/screen.h +++ b/screen/screen.h @@ -1,32 +1,65 @@ #pragma once -#include - #include +#include #include -class KScreenOutputModel; +#include "displayclient.h" + +class DisplayOutputModel; class KScreenScreen : public QObject { Q_OBJECT QML_NAMED_ELEMENT(Screen) - Q_PROPERTY(KScreenOutputModel *outputModel READ outputModel NOTIFY outputModelChanged) + Q_PROPERTY(DisplayOutputModel *outputModel READ outputModel CONSTANT) + Q_PROPERTY(bool available READ available NOTIFY stateChanged) + Q_PROPERTY(bool busy READ busy NOTIFY stateChanged) + Q_PROPERTY(bool dirty READ dirty NOTIFY stateChanged) + Q_PROPERTY(bool canApply READ canApply NOTIFY stateChanged) + Q_PROPERTY(QString error READ error NOTIFY stateChanged) + Q_PROPERTY(bool pendingConfirmation READ pendingConfirmation NOTIFY stateChanged) + Q_PROPERTY(int confirmationSeconds READ confirmationSeconds NOTIFY stateChanged) public: explicit KScreenScreen(QObject *parent = nullptr); - KScreenOutputModel *outputModel() const; - Q_INVOKABLE void save(); + DisplayOutputModel *outputModel() const { return m_outputModel; } -signals: - void outputModelChanged(); + bool available() const { return m_client.available(); } + + bool busy() const { return m_busy; } + + bool dirty() const; + bool canApply() const; + + QString error() const { return m_error; } + + bool pendingConfirmation() const { return !m_client.state().previewToken.isEmpty(); } -private slots: - void loadConfig(); + int confirmationSeconds() const; + + Q_INVOKABLE void apply(); + Q_INVOKABLE void discard(); + Q_INVOKABLE void keep(); + Q_INVOKABLE void revert(); + + Q_INVOKABLE void save() { apply(); } + + Q_INVOKABLE void identify(); + +signals: + void stateChanged(); + void identifyRequested(QStringList displayIds); private: - KScreen::ConfigPtr m_config; - KScreenOutputModel *m_outputModel; - bool m_applying = false; + void updateState(); + + DisplayClient m_client; + DisplayOutputModel *m_outputModel = nullptr; + QTimer m_countdown; + QString m_instanceId; + qulonglong m_topologyGeneration = 0; + QString m_error; + bool m_busy = false; }; diff --git a/screen/tests/outputmodel_test.cpp b/screen/tests/outputmodel_test.cpp new file mode 100644 index 0000000..a2a5557 --- /dev/null +++ b/screen/tests/outputmodel_test.cpp @@ -0,0 +1,97 @@ +#include +#include + +#include "outputmodel.h" + +class OutputModelTest : public QObject +{ + Q_OBJECT +private slots: + + void stateRefreshKeepsRowsAndLayout() + { + DisplayClient::State state; + for (int i = 0; i < 3; ++i) { + cutefish::display::ipc::PhysicalDisplay physical; + physical.sessionId = QString::number(i); + cutefish::display::ipc::Mode mode; + mode.width = 1920; + mode.height = 1080; + mode.refreshMilliHz = 60000; + mode.current = true; + physical.modes.append(mode); + state.physical.append(physical); + cutefish::display::ipc::LogicalDisplay logical; + logical.id = physical.sessionId; + logical.destinations = {physical.sessionId}; + logical.primary = i == 0; + logical.scale120 = 120; + logical.x = i == 1 ? -1920 : 0; + logical.y = i == 2 ? 1080 : 0; + state.logical.append(logical); + } + state.placements = {{"1", "0", 3, 0}, {"2", "0", 2, 0}}; + DisplayOutputModel model; + model.setState(state); + QSignalSpy reset(&model, &QAbstractItemModel::modelReset); + const QPersistentModelIndex selected(model.index(1)); + QVERIFY(model.setData(selected, 1.25, DisplayOutputModel::ScaleRole)); + const auto placements = model.placements(); + QCOMPARE(placements.size(), 2); + QCOMPARE(placements[1].parentId, QString("0")); + QCOMPARE(placements[1].edge, 2u); + std::reverse(state.physical.begin(), state.physical.end()); + model.setState(state); + QCOMPARE(reset.count(), 0); + QVERIFY(selected.isValid()); + QCOMPARE(model.data(selected, DisplayOutputModel::IdRole).toString(), QString("1")); + } + + void selectedResolutionAndRefreshStayConsistent() + { + DisplayClient::State state; + cutefish::display::ipc::PhysicalDisplay physical; + physical.sessionId = "display-1"; + cutefish::display::ipc::Mode mode; + mode.width = 1920; + mode.height = 1080; + mode.refreshMilliHz = 60000; + mode.current = true; + physical.modes.append(mode); + mode.current = false; + mode.refreshMilliHz = 144000; + physical.modes.append(mode); + mode.width = 1280; + mode.height = 720; + mode.refreshMilliHz = 60000; + physical.modes.append(mode); + state.physical.append(physical); + DisplayOutputModel model; + model.setState(state); + QSignalSpy changed(&model, &QAbstractItemModel::dataChanged); + const auto row = model.index(0); + QVERIFY(model.setData(row, 1, DisplayOutputModel::RefreshRateIndexRole)); + QCOMPARE(model.requests().first().mode.refreshMilliHz, 144000); + QVERIFY(model.setData(row, 1, DisplayOutputModel::ResolutionIndexRole)); + QCOMPARE(model.requests().first().mode.width, 1280); + QCOMPARE(model.data(row, DisplayOutputModel::RefreshRateIndexRole).toInt(), 0); + QCOMPARE(model.data(row, DisplayOutputModel::RefreshRatesRole).toStringList().size(), 1); + const auto roles = changed.last().at(2).value>(); + QVERIFY(roles.contains(DisplayOutputModel::RefreshRatesRole)); + QVERIFY(roles.contains(DisplayOutputModel::RefreshRateIndexRole)); + } + + void externalStateDoesNotBecomeAnEdit() + { + DisplayOutputModel model; + bool edit = false; + connect(&model, &DisplayOutputModel::dirtyChanged, [&] { + edit |= model.dirty(); + }); + model.setState({}); + QVERIFY(!edit); + } +}; + +QTEST_GUILESS_MAIN(OutputModelTest) +#include "outputmodel_test.moc" diff --git a/screen/tests/screen_integration_test.cpp b/screen/tests/screen_integration_test.cpp new file mode 100644 index 0000000..677fd14 --- /dev/null +++ b/screen/tests/screen_integration_test.cpp @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "outputmodel.h" +#include "screen.h" + +class ScreenIntegrationTest : public QObject +{ + Q_OBJECT +private slots: + + void multipleDisplaysStayConsistentWithQt() + { + QTemporaryDir directory; + QVERIFY(directory.isValid()); + qputenv("XDG_RUNTIME_DIR", directory.path().toUtf8()); + qputenv("XDG_CONFIG_HOME", directory.path().toUtf8()); + qputenv("CUTEFISH_DISPLAY_CONFIG_SERVICE", "com.cutefish.DisplayConfig.Test"); + QProcess compositor; + compositor.start(qEnvironmentVariable("CUTEFISH_TEST_COMPOSITOR"), + {"--no-drm", "--no-xwayland", "--headless-outputs", "3", "--socket", + "display-test"}); + QVERIFY(compositor.waitForStarted()); + QProcess probe; + + struct Stop { + QProcess &compositor; + QProcess &probe; + + ~Stop() + { + for (auto *process : {&probe, &compositor}) { + process->terminate(); + if (!process->waitForFinished(3000)) { + process->kill(); + process->waitForFinished(); + } + } + } + } stop {compositor, probe}; + + KScreenScreen screen; + auto *model = screen.outputModel(); + QTRY_COMPARE_WITH_TIMEOUT(model->rowCount(), 3, 5000); + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("QT_QPA_PLATFORM", "wayland"); + env.insert("WAYLAND_DISPLAY", "display-test"); + env.remove("WAYLAND_SOCKET"); + env.remove("QT_SCALE_FACTOR"); + env.remove("QT_SCREEN_SCALE_FACTORS"); + probe.setProcessEnvironment(env); + probe.start(QStringLiteral(SCREEN_PROBE_PATH)); + QVERIFY(probe.waitForStarted()); + QJsonArray screens; + const auto count = [&] { + while (probe.canReadLine()) + screens = QJsonDocument::fromJson(probe.readLine()).array(); + return screens.size(); + }; + QTRY_COMPARE_WITH_TIMEOUT(count(), 3, 5000); + const QPersistentModelIndex second(model->index(1)); + QVERIFY(model->setData(second, 1.25, DisplayOutputModel::ScaleRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + const auto width = [&] { + count(); + for (const auto &value : screens) { + if (value.toObject()["name"] == "Cutefish-2") + return value.toObject()["width"].toInt(); + } + return 0; + }; + QTRY_COMPARE(width(), 1536); + QCOMPARE(model->data(second, DisplayOutputModel::IdRole).toString(), QString("display-2")); + QVERIFY(model->setData(model->index(2), false, DisplayOutputModel::EnabledRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(count(), 2); + QVERIFY(model->setData(model->index(2), true, DisplayOutputModel::EnabledRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(count(), 3); + QVERIFY(model->setData(model->index(2), 2, DisplayOutputModel::ReplicationSourceIndexRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(count(), 2); + QTRY_COMPARE(width(), 1536); + QVERIFY(model->setData(model->index(2), 0, DisplayOutputModel::ReplicationSourceIndexRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(count(), 3); + QVERIFY(model->setData(second, 2, DisplayOutputModel::RotationRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(width(), 864); + QVERIFY(model->setData(second, true, DisplayOutputModel::PrimaryRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QVERIFY(model->data(second, DisplayOutputModel::PrimaryRole).toBool()); + } + + void clickAppliesAndPersistsWithoutConfirmation() + { + QTemporaryDir directory; + QVERIFY(directory.isValid()); + qputenv("XDG_RUNTIME_DIR", directory.path().toUtf8()); + qputenv("XDG_CONFIG_HOME", directory.path().toUtf8()); + qputenv("CUTEFISH_DISPLAY_CONFIG_SERVICE", "com.cutefish.DisplayConfig.Test"); + QProcess compositor; + compositor.start(qEnvironmentVariable("CUTEFISH_TEST_COMPOSITOR"), + {"--no-drm", "--no-xwayland", "--socket", "display-test"}); + QVERIFY(compositor.waitForStarted()); + + struct Stop { + QProcess &process; + + ~Stop() + { + process.terminate(); + if (!process.waitForFinished(3000)) { + process.kill(); + process.waitForFinished(); + } + } + } stop {compositor}; + + KScreenScreen screen; + QTRY_COMPARE_WITH_TIMEOUT(screen.outputModel()->rowCount(), 1, 5000); + auto *model = screen.outputModel(); + QVERIFY(model->setData(model->index(0), 1.25, DisplayOutputModel::ScaleRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QVERIFY(!screen.pendingConfirmation()); + QVERIFY(!screen.dirty()); + QTRY_COMPARE(model->data(model->index(0), DisplayOutputModel::ScaleRole).toDouble(), 1.25); + DisplayClient observer; + QTRY_COMPARE(observer.state().logical.size(), 1); + QCOMPARE(observer.state().logical.first().width, 1536); + QCOMPARE(observer.state().logical.first().height, 864); + QCOMPARE(observer.state().physical.size(), 1); + QVERIFY(observer.state().previewToken.isEmpty()); + QVERIFY(model->setData(model->index(0), 1.5, DisplayOutputModel::ScaleRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(observer.state().logical.first().width, 1280); + QTRY_COMPARE(observer.state().logical.first().height, 720); + QVERIFY(model->setData(model->index(0), 2, DisplayOutputModel::ResolutionIndexRole)); + QTRY_VERIFY_WITH_TIMEOUT(!screen.busy(), 5000); + QVERIFY2(screen.error().isEmpty(), qPrintable(screen.error())); + QTRY_COMPARE(observer.state().logical.first().width, 854); + QTRY_COMPARE(observer.state().logical.first().height, 480); + QCOMPARE(model->data(model->index(0), DisplayOutputModel::SizeRole).toSize(), + QSize(1280, 720)); + int currentModes = 0; + for (const auto &mode : observer.state().physical.first().modes) { + if (!mode.current) + continue; + ++currentModes; + QCOMPARE(mode.width, 1280); + QCOMPARE(mode.height, 720); + } + QCOMPARE(currentModes, 1); + compositor.terminate(); + QVERIFY(compositor.waitForFinished(3000)); + QTRY_COMPARE(model->rowCount(), 0); + compositor.start(); + QVERIFY(compositor.waitForStarted()); + QTRY_COMPARE_WITH_TIMEOUT(model->rowCount(), 1, 5000); + QCOMPARE(model->data(model->index(0), DisplayOutputModel::ScaleRole).toDouble(), 1.5); + QCOMPARE(model->data(model->index(0), DisplayOutputModel::SizeRole).toSize(), + QSize(1280, 720)); + QVERIFY(!screen.dirty()); + QVERIFY(!screen.pendingConfirmation()); + } +}; + +QTEST_GUILESS_MAIN(ScreenIntegrationTest) +#include "screen_integration_test.moc" diff --git a/screen/tests/wayland_screen_probe.cpp b/screen/tests/wayland_screen_probe.cpp new file mode 100644 index 0000000..452ae1a --- /dev/null +++ b/screen/tests/wayland_screen_probe.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + QTimer report; + report.setSingleShot(true); + const auto schedule = [&] { + report.start(0); + }; + const auto watch = [&](QScreen *screen) { + QObject::connect(screen, &QScreen::geometryChanged, &app, schedule); + schedule(); + }; + QObject::connect(&app, &QGuiApplication::screenAdded, &app, watch); + QObject::connect(&app, &QGuiApplication::screenRemoved, &app, schedule); + QObject::connect(&report, &QTimer::timeout, &app, [] { + QJsonArray screens; + for (auto *screen : QGuiApplication::screens()) { + const auto geometry = screen->geometry(); + screens.append(QJsonObject {{"name", screen->name()}, + {"x", geometry.x()}, + {"y", geometry.y()}, + {"width", geometry.width()}, + {"height", geometry.height()}}); + } + const auto data = QJsonDocument(screens).toJson(QJsonDocument::Compact); + std::puts(data.constData()); + std::fflush(stdout); + }); + for (auto *screen : QGuiApplication::screens()) + watch(screen); + return app.exec(); +}