feat(screen): use native display configuration with immediate updates

pull/18/head
reionwong 2 months ago
parent 7797d1f492
commit 2d25cb7fcc

@ -17,6 +17,7 @@ list(APPEND CMAKE_MODULE_PATH
)
include(GNUInstallDirs)
include(CTest)
include(GenerateExportHeader)
include(CMakePackageConfigHelpers)

@ -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
```

4
debian/control vendored

@ -5,7 +5,6 @@ Maintainer: CutefishOS <support@cutefishos.com>
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

@ -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_FILE:display-wayland-screen-probe>")
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 -- $<TARGET_FILE:display-screen-integration-test>)
set_tests_properties(display-screen-integration-test PROPERTIES
ENVIRONMENT "CUTEFISH_TEST_COMPOSITOR=${CUTEFISH_TEST_COMPOSITOR}")
endif()
endif()

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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: a versioned session D-Bus interface without Ash or Mojo.
-->
<node>
<interface name="com.cutefish.DisplayConfig1">
<property name="ProtocolVersion" type="u" access="read"/>
<method name="GetState">
<arg name="instanceId" type="s" direction="out"/><arg name="seatId" type="s" direction="out"/>
<arg name="topologyGeneration" type="t" direction="out"/><arg name="appliedRevision" type="t" direction="out"/>
<arg name="physicalDisplays" type="a(sssssssasssiiuuua(iiiiiiubbs))" direction="out"/>
<arg name="logicalDisplays" type="a(ssasiiiiiiiiuub)" direction="out"/><arg name="placements" type="a(ssui)" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out4" value="QList&lt;cutefish::display::ipc::PhysicalDisplay&gt;"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out5" value="QList&lt;cutefish::display::ipc::LogicalDisplay&gt;"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out6" value="QList&lt;cutefish::display::ipc::Placement&gt;"/>
<arg name="previewToken" type="s" direction="out"/><arg name="previewDeadlineMs" type="t" direction="out"/>
</method>
<method name="TestConfiguration">
<arg name="instanceId" type="s" direction="in"/><arg name="topologyGeneration" type="t" direction="in"/><arg name="appliedRevision" type="t" direction="in"/>
<arg name="displays" type="a(sb(iiiiiiubs)uu)" direction="in"/><arg name="primary" type="s" direction="in"/><arg name="placements" type="a(ssui)" direction="in"/><arg name="mirrorGroups" type="aas" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In3" value="QList&lt;cutefish::display::ipc::DisplayRequest&gt;"/><annotation name="org.qtproject.QtDBus.QtTypeName.In5" value="QList&lt;cutefish::display::ipc::Placement&gt;"/><annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="cutefish::display::ipc::MirrorGroups"/>
<arg name="operationToken" type="s" direction="out"/>
</method>
<method name="SetConfiguration">
<arg name="instanceId" type="s" direction="in"/><arg name="topologyGeneration" type="t" direction="in"/><arg name="appliedRevision" type="t" direction="in"/>
<arg name="displays" type="a(sb(iiiiiiubs)uu)" direction="in"/><arg name="primary" type="s" direction="in"/><arg name="placements" type="a(ssui)" direction="in"/><arg name="mirrorGroups" type="aas" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In3" value="QList&lt;cutefish::display::ipc::DisplayRequest&gt;"/><annotation name="org.qtproject.QtDBus.QtTypeName.In5" value="QList&lt;cutefish::display::ipc::Placement&gt;"/><annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="cutefish::display::ipc::MirrorGroups"/>
<arg name="operationToken" type="s" direction="out"/>
</method>
<method name="ConfirmConfiguration"><arg name="previewToken" type="s" direction="in"/></method>
<method name="RevertConfiguration"><arg name="previewToken" type="s" direction="in"/></method>
<method name="ResetConfiguration"><arg name="instanceId" type="s" direction="in"/><arg name="topologyGeneration" type="t" direction="in"/><arg name="appliedRevision" type="t" direction="in"/><arg name="operationToken" type="s" direction="out"/></method>
<method name="SetDisplayPower"><arg name="displayIds" type="as" direction="in"/><arg name="state" type="u" direction="in"/><arg name="operationToken" type="s" direction="out"/></method>
<method name="SetDockedLidClosed"><arg name="closed" type="b" direction="in"/><arg name="operationToken" type="s" direction="out"/></method>
<method name="IdentifyDisplays"><arg name="displayIds" type="as" direction="in"/></method>
<signal name="StateChanged"><arg name="topologyGeneration" type="t"/><arg name="appliedRevision" type="t"/></signal>
<signal name="OperationFinished"><arg name="operationToken" type="s"/><arg name="succeeded" type="b"/><arg name="degraded" type="b"/><arg name="errorCode" type="u"/><arg name="errorMessage" type="s"/><arg name="affectedDisplayIds" type="as"/><arg name="topologyGeneration" type="t"/><arg name="appliedRevision" type="t"/></signal>
<signal name="PreviewChanged"><arg name="previewToken" type="s"/><arg name="deadlineMs" type="t"/><arg name="topologyGeneration" type="t"/><arg name="appliedRevision" type="t"/></signal>
<signal name="IdentifyRequested"><arg name="displayIds" type="as"/></signal>
</interface>
</node>

@ -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 <QDBusConnectionInterface>
#include <QDBusPendingCallWatcher>
#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<QString, QString, qulonglong, qulonglong, QList<ipc::PhysicalDisplay>,
QList<ipc::LogicalDisplay>, QList<ipc::Placement>, 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<ipc::DisplayRequest> &displays, const QString &primary,
const QList<ipc::Placement> &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<QString> 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);
}

@ -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 <optional>
#include <QHash>
#include <QObject>
#include <QTimer>
#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<cutefish::display::ipc::PhysicalDisplay> physical;
QList<cutefish::display::ipc::LogicalDisplay> logical;
QList<cutefish::display::ipc::Placement> 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<cutefish::display::ipc::DisplayRequest> &displays,
const QString &primary,
const QList<cutefish::display::ipc::Placement> &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<QString, OperationResult> m_earlyResults;
std::optional<OperationResult> m_completion;
quint64 m_serviceEpoch = 0;
quint64 m_operationSerial = 0;
QTimer m_operationTimeout;
};

@ -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 <QDBusMetaType>
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<Mode>();
qDBusRegisterMetaType<RequestedMode>();
qDBusRegisterMetaType<PhysicalDisplay>();
qDBusRegisterMetaType<LogicalDisplay>();
qDBusRegisterMetaType<DisplayRequest>();
qDBusRegisterMetaType<Placement>();
qDBusRegisterMetaType<QList<Mode>>();
qDBusRegisterMetaType<QList<PhysicalDisplay>>();
qDBusRegisterMetaType<QList<LogicalDisplay>>();
qDBusRegisterMetaType<QList<DisplayRequest>>();
qDBusRegisterMetaType<QList<Placement>>();
qDBusRegisterMetaType<MirrorGroups>();
}
} // namespace cutefish::display::ipc

@ -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 <QDBusArgument>
#include <QList>
#include <QString>
#include <QStringList>
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<Mode> 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<QStringList>;
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<cutefish::display::ipc::Mode>)
Q_DECLARE_METATYPE(QList<cutefish::display::ipc::PhysicalDisplay>)
Q_DECLARE_METATYPE(QList<cutefish::display::ipc::LogicalDisplay>)
Q_DECLARE_METATYPE(QList<cutefish::display::ipc::DisplayRequest>)
Q_DECLARE_METATYPE(QList<cutefish::display::ipc::Placement>)
Q_DECLARE_METATYPE(cutefish::display::ipc::MirrorGroups)

@ -1,2 +1 @@
#include "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

@ -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 <KScreen/Mode>
#include <KScreen/Output>
#include "outputmodel.h"
#include <algorithm>
#include <limits>
#include <climits>
#include <QtMath>
#include <QRect>
#include <QSet>
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::ModeChoice> KScreenOutputModel::modesForOutput(const KScreen::OutputPtr &output) const
const ipc::Mode *DisplayOutputModel::selectedMode(const Entry &e) 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;
return e.modeIndex >= 0 && e.modeIndex < e.physical.modes.size()
? &e.physical.modes.at(e.modeIndex)
: nullptr;
}
QList<QSize> KScreenOutputModel::resolutionsForOutput(const KScreen::OutputPtr &output) const
QList<int> DisplayOutputModel::resolutionModes(const Entry &e) const
{
QList<QSize> resolutions;
for (const ModeChoice &mode : modesForOutput(output)) {
if (mode.size.isValid() && !resolutions.contains(mode.size)) {
resolutions.append(mode.size);
QList<int> result;
QList<QSize> 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<float> KScreenOutputModel::refreshRatesForOutput(const KScreen::OutputPtr &output,
const QSize &size) const
{
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 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<int> 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<int> 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<QSize> resolutions = resolutionsForOutput(output);
const QList<float> 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<int>(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<int> 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<KScreen::Output::Rotation>(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<QSize> 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<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;
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<float> 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<int, QByteArray> 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<QString> 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<ipc::DisplayRequest> DisplayOutputModel::requests() const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
QList<ipc::DisplayRequest> 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<int, QByteArray> KScreenOutputModel::roleNames() const
QString DisplayOutputModel::primary() const
{
QHash<int, QByteArray> 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<ipc::Placement> 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<ipc::Placement> 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<const Entry *> 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<QString, QRect> 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;
}

@ -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 <KScreen/Config>
#pragma once
#include <QAbstractListModel>
#include <QPoint>
#include <QSize>
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<int, QByteArray> 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<int, QByteArray> roleNames() const override;
QList<cutefish::display::ipc::DisplayRequest> requests() const;
QList<cutefish::display::ipc::Placement> 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<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;
const cutefish::display::ipc::Mode *selectedMode(const Entry &entry) const;
QList<int> resolutionModes(const Entry &entry) const;
QList<int> refreshModes(const Entry &entry) const;
void markDirty(const QModelIndex &index, int role);
DisplayClient::State m_applied;
QList<Entry> m_entries;
bool m_dirty = false;
bool m_layoutDirty = false;
};

@ -1,181 +1,115 @@
#include "screen.h"
#include "outputmodel.h"
#include <QDateTime>
#include <KScreen/ConfigMonitor>
#include <KScreen/GetConfigOperation>
#include <KScreen/Mode>
#include <KScreen/SetConfigOperation>
#include "outputmodel.h"
#include <QDebug>
#include <QGuiApplication>
#include <QSettings>
#include <QTimer>
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<int>(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);
}

@ -1,32 +1,65 @@
#pragma once
#include <KScreen/Config>
#include <QObject>
#include <QTimer>
#include <QtQml/qqmlregistration.h>
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;
};

@ -0,0 +1,97 @@
#include <QSignalSpy>
#include <QTest>
#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<QList<int>>();
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"

@ -0,0 +1,183 @@
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QTemporaryDir>
#include <QTest>
#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"

@ -0,0 +1,41 @@
#include <cstdio>
#include <QGuiApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QScreen>
#include <QTimer>
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();
}
Loading…
Cancel
Save