You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
10 KiB
281 lines
10 KiB
/* |
|
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, |
|
}; |
|
|
|
Output::Transform toKWinTransform(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); |
|
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); |
|
} |
|
} |
|
}
|
|
|