637e3a6389
Being in the KWin namespace has a couple of advantages: the enum can be forward declared, and the transform can be replaced with a slightly more complex but useful type.
281 lines
10 KiB
C++
281 lines
10 KiB
C++
/*
|
|
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 <QStandardPaths>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace KWin
|
|
{
|
|
namespace KScreenIntegration
|
|
{
|
|
/// See KScreen::Output::hashMd5
|
|
static QString outputHash(Output *output)
|
|
{
|
|
if (output->edid().isValid()) {
|
|
return output->edid().hash();
|
|
} else {
|
|
return output->name();
|
|
}
|
|
}
|
|
|
|
/// See KScreen::Config::connectedOutputsHash in libkscreen
|
|
QString connectedOutputsHash(const QVector<Output *> &outputs, bool isLidClosed)
|
|
{
|
|
QStringList hashedOutputs;
|
|
hashedOutputs.reserve(outputs.count());
|
|
for (auto output : std::as_const(outputs)) {
|
|
if (output->isPlaceholder() || output->isNonDesktop()) {
|
|
continue;
|
|
}
|
|
if (output->isInternal() && isLidClosed) {
|
|
continue;
|
|
}
|
|
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;
|
|
}
|
|
|
|
static std::optional<QJsonObject> globalOutputConfig(Output *output)
|
|
{
|
|
const QString kscreenPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/"));
|
|
if (kscreenPath.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
const auto hash = outputHash(output);
|
|
// use connector specific data if available, unspecific data if not
|
|
QFile f(kscreenPath % hash % output->name());
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
f.setFileName(kscreenPath % hash);
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
qCWarning(KWIN_CORE) << "Could not open file" << f.fileName();
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
QJsonParseError error;
|
|
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
|
|
if (error.error != QJsonParseError::NoError) {
|
|
qCWarning(KWIN_CORE) << "Failed to parse" << f.fileName() << error.errorString();
|
|
return std::nullopt;
|
|
}
|
|
return doc.object();
|
|
}
|
|
|
|
/// See KScreen::Output::Rotation
|
|
enum Rotation {
|
|
None = 1,
|
|
Left = 2,
|
|
Inverted = 4,
|
|
Right = 8,
|
|
};
|
|
|
|
OutputTransform toKWinTransform(int rotation)
|
|
{
|
|
switch (Rotation(rotation)) {
|
|
case None:
|
|
return OutputTransform::Normal;
|
|
case Left:
|
|
return OutputTransform::Rotated90;
|
|
case Inverted:
|
|
return OutputTransform::Rotated180;
|
|
case Right:
|
|
return OutputTransform::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);
|
|
if (outputsInfo.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
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];
|
|
const auto globalOutputInfo = globalOutputConfig(output);
|
|
qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
|
|
if (!outputInfo.isEmpty() || globalOutputInfo.has_value()) {
|
|
// settings that are per output setup:
|
|
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));
|
|
}
|
|
if (const QJsonObject pos = outputInfo["pos"].toObject(); !pos.isEmpty()) {
|
|
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
|
|
}
|
|
|
|
// settings that are independent of per output setups:
|
|
const auto &globalInfo = globalOutputInfo ? globalOutputInfo.value() : outputInfo;
|
|
if (const QJsonValue scale = globalInfo["scale"]; !scale.isUndefined()) {
|
|
props->scale = scale.toDouble(1.);
|
|
}
|
|
if (const QJsonValue rotation = globalInfo["rotation"]; !rotation.isUndefined()) {
|
|
props->transform = KScreenIntegration::toKWinTransform(rotation.toInt());
|
|
}
|
|
if (const QJsonValue overscan = globalInfo["overscan"]; !overscan.isUndefined()) {
|
|
props->overscan = globalInfo["overscan"].toInt();
|
|
}
|
|
if (const QJsonValue vrrpolicy = globalInfo["vrrpolicy"]; !vrrpolicy.isUndefined()) {
|
|
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(vrrpolicy.toInt());
|
|
}
|
|
if (const QJsonValue rgbrange = globalInfo["rgbrange"]; !rgbrange.isUndefined()) {
|
|
props->rgbRange = static_cast<Output::RgbRange>(rgbrange.toInt());
|
|
}
|
|
|
|
if (const QJsonObject modeInfo = globalInfo["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.value_or(output->isEnabled());
|
|
});
|
|
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.value_or(pair.second->isEnabled());
|
|
});
|
|
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);
|
|
}
|
|
}
|
|
}
|