workspace: move kscreen integration into separate files

Putting all the code into workspace.cpp makes the file harder to manage,
and kscreen integration is really its own topic.
This commit is contained in:
Xaver Hugl 2023-02-28 12:41:36 +01:00
parent ea66fad153
commit cc535a1ecd
4 changed files with 272 additions and 214 deletions

View file

@ -97,6 +97,7 @@ target_sources(kwin PRIVATE
keyboard_layout_switching.cpp
keyboard_repeat.cpp
killwindow.cpp
kscreenintegration.cpp
layers.cpp
layershellv1integration.cpp
layershellv1window.cpp

236
src/kscreenintegration.cpp Normal file
View file

@ -0,0 +1,236 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kscreenintegration.h"
#include "utils/common.h"
#include <QCryptographicHash>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <algorithm>
#include <cmath>
namespace KWin
{
namespace KScreenIntegration
{
/// See KScreen::Output::hashMd5
static QString outputHash(Output *output)
{
if (!output->edid().isEmpty()) {
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(output->edid());
return QString::fromLatin1(hash.result().toHex());
} else {
return output->name();
}
}
/// See KScreen::Config::connectedOutputsHash in libkscreen
QString connectedOutputsHash(const QVector<Output *> &outputs)
{
QStringList hashedOutputs;
hashedOutputs.reserve(outputs.count());
for (auto output : std::as_const(outputs)) {
if (!output->isPlaceholder() && !output->isNonDesktop()) {
hashedOutputs << outputHash(output);
}
}
std::sort(hashedOutputs.begin(), hashedOutputs.end());
const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
return QString::fromLatin1(hash.toHex());
}
static QMap<Output *, QJsonObject> outputsConfig(const QVector<Output *> &outputs, const QString &hash)
{
const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
if (kscreenJsonPath.isEmpty()) {
return {};
}
QFile f(kscreenJsonPath);
if (!f.open(QIODevice::ReadOnly)) {
qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
return {};
}
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
return {};
}
QHash<Output *, bool> duplicate;
QHash<Output *, QString> outputHashes;
for (Output *output : outputs) {
const QString hash = outputHash(output);
const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) {
return value == hash;
});
if (it == outputHashes.cend()) {
duplicate[output] = false;
} else {
duplicate[output] = true;
duplicate[it.key()] = true;
}
outputHashes[output] = hash;
}
QMap<Output *, QJsonObject> ret;
const auto outputsJson = doc.array();
for (const auto &outputJson : outputsJson) {
const auto outputObject = outputJson.toObject();
const auto id = outputObject["id"];
const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) {
if (outputHash(output) != id.toString()) {
return false;
}
if (duplicate[output]) {
// can't distinguish between outputs by hash alone, need to look at connector names
const auto metadata = outputObject[QStringLiteral("metadata")];
const auto outputName = metadata[QStringLiteral("name")].toString();
return outputName == output->name();
} else {
return true;
}
});
if (output != outputs.end()) {
ret[*output] = outputObject;
}
}
return ret;
}
/// See KScreen::Output::Rotation
enum Rotation {
None = 1,
Left = 2,
Inverted = 4,
Right = 8,
};
Output::Transform toDrmTransform(int rotation)
{
switch (Rotation(rotation)) {
case None:
return Output::Transform::Normal;
case Left:
return Output::Transform::Rotated90;
case Inverted:
return Output::Transform::Rotated180;
case Right:
return Output::Transform::Rotated270;
default:
Q_UNREACHABLE();
}
}
std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
{
const QJsonObject size = modeInfo["size"].toObject();
const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
const auto modes = output->modes();
auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
return mode->size() == modeSize && mode->refreshRate() == refreshRate;
});
return (it != modes.end()) ? *it : nullptr;
}
std::optional<std::pair<OutputConfiguration, QVector<Output *>>> readOutputConfig(const QVector<Output *> &outputs, const QString &hash)
{
const auto outputsInfo = outputsConfig(outputs, hash);
std::vector<std::pair<uint32_t, Output *>> outputOrder;
OutputConfiguration cfg;
// default position goes from left to right
QPoint pos(0, 0);
for (const auto &output : std::as_const(outputs)) {
if (output->isPlaceholder() || output->isNonDesktop()) {
continue;
}
auto props = cfg.changeSet(output);
const QJsonObject outputInfo = outputsInfo[output];
qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
if (!outputInfo.isEmpty()) {
props->enabled = outputInfo["enabled"].toBool(true);
if (outputInfo["primary"].toBool()) {
outputOrder.push_back(std::make_pair(1, output));
if (!props->enabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
return std::nullopt;
}
} else if (int prio = outputInfo["priority"].toInt(); prio > 0) {
outputOrder.push_back(std::make_pair(prio, output));
if (!props->enabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!";
return std::nullopt;
}
} else {
outputOrder.push_back(std::make_pair(0, output));
}
const QJsonObject pos = outputInfo["pos"].toObject();
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
if (const QJsonValue scale = outputInfo["scale"]; !scale.isUndefined()) {
props->scale = scale.toDouble(1.);
}
props->transform = KScreenIntegration::toDrmTransform(outputInfo["rotation"].toInt());
props->overscan = static_cast<uint32_t>(outputInfo["overscan"].toInt(props->overscan));
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(outputInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy)));
props->rgbRange = static_cast<Output::RgbRange>(outputInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange)));
if (const QJsonObject modeInfo = outputInfo["mode"].toObject(); !modeInfo.isEmpty()) {
if (auto mode = KScreenIntegration::parseMode(output, modeInfo)) {
props->mode = mode;
}
}
} else {
props->enabled = true;
props->pos = pos;
props->transform = output->panelOrientation();
outputOrder.push_back(std::make_pair(0, output));
}
pos.setX(pos.x() + output->geometry().width());
}
bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
return !cfg.changeSet(output)->enabled;
});
if (allDisabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
return std::nullopt;
}
std::erase_if(outputOrder, [&cfg](const auto &pair) {
return !cfg.constChangeSet(pair.second)->enabled;
});
std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) {
if (left.first == right.first) {
// sort alphabetically as a fallback
return left.second->name() < right.second->name();
} else if (left.first == 0) {
return false;
} else {
return left.first < right.first;
}
});
QVector<Output *> order;
order.reserve(outputOrder.size());
std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) {
return pair.second;
});
return std::make_pair(cfg, order);
}
}
}

24
src/kscreenintegration.h Normal file
View file

@ -0,0 +1,24 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "core/output.h"
#include "core/outputconfiguration.h"
#include <QVector>
#include <optional>
namespace KWin
{
namespace KScreenIntegration
{
QString connectedOutputsHash(const QVector<Output *> &outputs);
std::optional<std::pair<OutputConfiguration, QVector<Output *>>> readOutputConfig(const QVector<Output *> &outputs, const QString &hash);
}
}

View file

@ -47,6 +47,7 @@
#include "tabbox.h"
#endif
#include "decorations/decorationbridge.h"
#include "kscreenintegration.h"
#include "main.h"
#include "placeholderinputeventfilter.h"
#include "placeholderoutput.h"
@ -508,134 +509,6 @@ Workspace::~Workspace()
_self = nullptr;
}
namespace KWinKScreenIntegration
{
/// See KScreen::Output::hashMd5
QString outputHash(Output *output)
{
if (!output->edid().isEmpty()) {
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(output->edid());
return QString::fromLatin1(hash.result().toHex());
} else {
return output->name();
}
}
/// See KScreen::Config::connectedOutputsHash in libkscreen
QString connectedOutputsHash(const QVector<Output *> &outputs)
{
QStringList hashedOutputs;
hashedOutputs.reserve(outputs.count());
for (auto output : std::as_const(outputs)) {
if (!output->isPlaceholder() && !output->isNonDesktop()) {
hashedOutputs << outputHash(output);
}
}
std::sort(hashedOutputs.begin(), hashedOutputs.end());
const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
return QString::fromLatin1(hash.toHex());
}
QMap<Output *, QJsonObject> outputsConfig(const QVector<Output *> &outputs, const QString &hash)
{
const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
if (kscreenJsonPath.isEmpty()) {
return {};
}
QFile f(kscreenJsonPath);
if (!f.open(QIODevice::ReadOnly)) {
qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
return {};
}
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
return {};
}
QHash<Output *, bool> duplicate;
QHash<Output *, QString> outputHashes;
for (Output *output : outputs) {
const QString hash = outputHash(output);
const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) {
return value == hash;
});
if (it == outputHashes.cend()) {
duplicate[output] = false;
} else {
duplicate[output] = true;
duplicate[it.key()] = true;
}
outputHashes[output] = hash;
}
QMap<Output *, QJsonObject> ret;
const auto outputsJson = doc.array();
for (const auto &outputJson : outputsJson) {
const auto outputObject = outputJson.toObject();
const auto id = outputObject["id"];
const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) {
if (outputHash(output) != id.toString()) {
return false;
}
if (duplicate[output]) {
// can't distinguish between outputs by hash alone, need to look at connector names
const auto metadata = outputObject[QStringLiteral("metadata")];
const auto outputName = metadata[QStringLiteral("name")].toString();
return outputName == output->name();
} else {
return true;
}
});
if (output != outputs.end()) {
ret[*output] = outputObject;
}
}
return ret;
}
/// See KScreen::Output::Rotation
enum Rotation {
None = 1,
Left = 2,
Inverted = 4,
Right = 8,
};
Output::Transform toDrmTransform(int rotation)
{
switch (Rotation(rotation)) {
case None:
return Output::Transform::Normal;
case Left:
return Output::Transform::Rotated90;
case Inverted:
return Output::Transform::Rotated180;
case Right:
return Output::Transform::Rotated270;
default:
Q_UNREACHABLE();
}
}
std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
{
const QJsonObject size = modeInfo["size"].toObject();
const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
const auto modes = output->modes();
auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
return mode->size() == modeSize && mode->refreshRate() == refreshRate;
});
return (it != modes.end()) ? *it : nullptr;
}
}
bool Workspace::applyOutputConfiguration(const OutputConfiguration &config, const QVector<Output *> &outputOrder)
{
if (!kwinApp()->outputBackend()->applyOutputChanges(config)) {
@ -655,11 +528,9 @@ void Workspace::updateOutputConfiguration()
const auto outputs = kwinApp()->outputBackend()->outputs();
if (outputs.empty()) {
// nothing to do
setOutputOrder({});
return;
}
const QString hash = KWinKScreenIntegration::connectedOutputsHash(outputs);
const auto outputsInfo = KWinKScreenIntegration::outputsConfig(outputs, hash);
m_outputsHash = hash;
// Update the output order to a fallback list, to avoid dangling pointers
const auto setFallbackOutputOrder = [this, &outputs]() {
@ -674,92 +545,18 @@ void Workspace::updateOutputConfiguration()
setOutputOrder(newOrder);
};
std::vector<std::pair<uint32_t, Output *>> outputOrder;
OutputConfiguration cfg;
// default position goes from left to right
QPoint pos(0, 0);
for (const auto &output : std::as_const(outputs)) {
if (output->isPlaceholder() || output->isNonDesktop()) {
continue;
m_outputsHash = KScreenIntegration::connectedOutputsHash(outputs);
if (const auto config = KScreenIntegration::readOutputConfig(outputs, m_outputsHash)) {
const auto &[cfg, order] = config.value();
if (!kwinApp()->outputBackend()->applyOutputChanges(cfg)) {
qCWarning(KWIN_CORE) << "Applying KScreen config failed!";
setFallbackOutputOrder();
return;
}
auto props = cfg.changeSet(output);
const QJsonObject outputInfo = outputsInfo[output];
qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
if (!outputInfo.isEmpty()) {
props->enabled = outputInfo["enabled"].toBool(true);
if (outputInfo["primary"].toBool()) {
outputOrder.push_back(std::make_pair(1, output));
if (!props->enabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
setFallbackOutputOrder();
return;
}
} else if (int prio = outputInfo["priority"].toInt(); prio > 0) {
outputOrder.push_back(std::make_pair(prio, output));
if (!props->enabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!";
setFallbackOutputOrder();
return;
}
} else {
outputOrder.push_back(std::make_pair(0, output));
}
const QJsonObject pos = outputInfo["pos"].toObject();
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
if (const QJsonValue scale = outputInfo["scale"]; !scale.isUndefined()) {
props->scale = scale.toDouble(1.);
}
props->transform = KWinKScreenIntegration::toDrmTransform(outputInfo["rotation"].toInt());
props->overscan = static_cast<uint32_t>(outputInfo["overscan"].toInt(props->overscan));
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(outputInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy)));
props->rgbRange = static_cast<Output::RgbRange>(outputInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange)));
if (const QJsonObject modeInfo = outputInfo["mode"].toObject(); !modeInfo.isEmpty()) {
if (auto mode = KWinKScreenIntegration::parseMode(output, modeInfo)) {
props->mode = mode;
}
}
} else {
props->enabled = true;
props->pos = pos;
props->transform = output->panelOrientation();
outputOrder.push_back(std::make_pair(0, output));
}
pos.setX(pos.x() + output->geometry().width());
}
bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
return !cfg.changeSet(output)->enabled;
});
if (allDisabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
setOutputOrder(order);
} else {
setFallbackOutputOrder();
return;
}
std::erase_if(outputOrder, [&cfg](const auto &pair) {
return !cfg.constChangeSet(pair.second)->enabled;
});
std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) {
if (left.first == right.first) {
// sort alphabetically as a fallback
return left.second->name() < right.second->name();
} else if (left.first == 0) {
return false;
} else {
return left.first < right.first;
}
});
if (!kwinApp()->outputBackend()->applyOutputChanges(cfg)) {
qCWarning(KWIN_CORE) << "Applying KScreen config failed!";
setFallbackOutputOrder();
return;
}
QVector<Output *> order;
order.reserve(outputOrder.size());
std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) {
return pair.second;
});
setOutputOrder(order);
}
void Workspace::setupWindowConnections(Window *window)