feat(screen): use native display configuration with immediate updates
parent
7797d1f492
commit
2d25cb7fcc
@ -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<cutefish::display::ipc::PhysicalDisplay>"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out5" value="QList<cutefish::display::ipc::LogicalDisplay>"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out6" value="QList<cutefish::display::ipc::Placement>"/>
|
||||
<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<cutefish::display::ipc::DisplayRequest>"/><annotation name="org.qtproject.QtDBus.QtTypeName.In5" value="QList<cutefish::display::ipc::Placement>"/><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<cutefish::display::ipc::DisplayRequest>"/><annotation name="org.qtproject.QtDBus.QtTypeName.In5" value="QList<cutefish::display::ipc::Placement>"/><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,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…
Reference in New Issue