diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f74d04ed25..5fcf1754f5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/kscreenintegration.cpp b/src/kscreenintegration.cpp new file mode 100644 index 0000000000..91ba96aefa --- /dev/null +++ b/src/kscreenintegration.cpp @@ -0,0 +1,236 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kscreenintegration.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include + +#include +#include + +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 &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 outputsConfig(const QVector &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 duplicate; + QHash 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 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 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>> readOutputConfig(const QVector &outputs, const QString &hash) +{ + const auto outputsInfo = outputsConfig(outputs, hash); + std::vector> 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(outputInfo["overscan"].toInt(props->overscan)); + props->vrrPolicy = static_cast(outputInfo["vrrpolicy"].toInt(static_cast(props->vrrPolicy))); + props->rgbRange = static_cast(outputInfo["rgbrange"].toInt(static_cast(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 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); +} +} +} diff --git a/src/kscreenintegration.h b/src/kscreenintegration.h new file mode 100644 index 0000000000..701a8f8258 --- /dev/null +++ b/src/kscreenintegration.h @@ -0,0 +1,24 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/output.h" +#include "core/outputconfiguration.h" + +#include +#include + +namespace KWin +{ +namespace KScreenIntegration +{ + +QString connectedOutputsHash(const QVector &outputs); +std::optional>> readOutputConfig(const QVector &outputs, const QString &hash); +} +} diff --git a/src/workspace.cpp b/src/workspace.cpp index 52692797f0..fe3e659f49 100644 --- a/src/workspace.cpp +++ b/src/workspace.cpp @@ -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 &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 outputsConfig(const QVector &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 duplicate; - QHash 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 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 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 &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> 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(outputInfo["overscan"].toInt(props->overscan)); - props->vrrPolicy = static_cast(outputInfo["vrrpolicy"].toInt(static_cast(props->vrrPolicy))); - props->rgbRange = static_cast(outputInfo["rgbrange"].toInt(static_cast(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 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)