File indexing completed on 2024-04-28 05:30:22

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
0006     SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include "kscreenintegration.h"
0011 #include "utils/common.h"
0012 
0013 #include <QCryptographicHash>
0014 #include <QFile>
0015 #include <QJsonArray>
0016 #include <QJsonDocument>
0017 #include <QJsonObject>
0018 #include <QStandardPaths>
0019 
0020 #include <algorithm>
0021 #include <cmath>
0022 
0023 namespace KWin
0024 {
0025 namespace KScreenIntegration
0026 {
0027 /// See KScreen::Output::hashMd5
0028 static QString outputHash(Output *output)
0029 {
0030     if (output->edid().isValid()) {
0031         return output->edid().hash();
0032     } else {
0033         return output->name();
0034     }
0035 }
0036 
0037 /// See KScreen::Config::connectedOutputsHash in libkscreen
0038 QString connectedOutputsHash(const QList<Output *> &outputs, bool isLidClosed)
0039 {
0040     QStringList hashedOutputs;
0041     hashedOutputs.reserve(outputs.count());
0042     for (auto output : std::as_const(outputs)) {
0043         if (output->isPlaceholder() || output->isNonDesktop()) {
0044             continue;
0045         }
0046         if (output->isInternal() && isLidClosed) {
0047             continue;
0048         }
0049         hashedOutputs << outputHash(output);
0050     }
0051     std::sort(hashedOutputs.begin(), hashedOutputs.end());
0052     const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
0053     return QString::fromLatin1(hash.toHex());
0054 }
0055 
0056 static QHash<Output *, QJsonObject> outputsConfig(const QList<Output *> &outputs, const QString &hash)
0057 {
0058     const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
0059     if (kscreenJsonPath.isEmpty()) {
0060         return {};
0061     }
0062 
0063     QFile f(kscreenJsonPath);
0064     if (!f.open(QIODevice::ReadOnly)) {
0065         qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
0066         return {};
0067     }
0068 
0069     QJsonParseError error;
0070     const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
0071     if (error.error != QJsonParseError::NoError) {
0072         qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
0073         return {};
0074     }
0075 
0076     QHash<Output *, bool> duplicate;
0077     QHash<Output *, QString> outputHashes;
0078     for (Output *output : outputs) {
0079         const QString hash = outputHash(output);
0080         const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) {
0081             return value == hash;
0082         });
0083         if (it == outputHashes.cend()) {
0084             duplicate[output] = false;
0085         } else {
0086             duplicate[output] = true;
0087             duplicate[it.key()] = true;
0088         }
0089         outputHashes[output] = hash;
0090     }
0091 
0092     QHash<Output *, QJsonObject> ret;
0093     const auto outputsJson = doc.array();
0094     for (const auto &outputJson : outputsJson) {
0095         const auto outputObject = outputJson.toObject();
0096         const auto id = outputObject[QLatin1String("id")];
0097         const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) {
0098             if (outputHash(output) != id.toString()) {
0099                 return false;
0100             }
0101             if (duplicate[output]) {
0102                 // can't distinguish between outputs by hash alone, need to look at connector names
0103                 const auto metadata = outputObject[QLatin1String("metadata")];
0104                 const auto outputName = metadata[QLatin1String("name")].toString();
0105                 return outputName == output->name();
0106             } else {
0107                 return true;
0108             }
0109         });
0110         if (output != outputs.end()) {
0111             ret[*output] = outputObject;
0112         }
0113     }
0114     return ret;
0115 }
0116 
0117 static std::optional<QJsonObject> globalOutputConfig(Output *output)
0118 {
0119     const QString kscreenPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/"));
0120     if (kscreenPath.isEmpty()) {
0121         return std::nullopt;
0122     }
0123     const auto hash = outputHash(output);
0124     // use connector specific data if available, unspecific data if not
0125     QFile f(kscreenPath % hash % output->name());
0126     if (!f.open(QIODevice::ReadOnly)) {
0127         f.setFileName(kscreenPath % hash);
0128         if (!f.open(QIODevice::ReadOnly)) {
0129             qCWarning(KWIN_CORE) << "Could not open file" << f.fileName();
0130             return std::nullopt;
0131         }
0132     }
0133 
0134     QJsonParseError error;
0135     const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
0136     if (error.error != QJsonParseError::NoError) {
0137         qCWarning(KWIN_CORE) << "Failed to parse" << f.fileName() << error.errorString();
0138         return std::nullopt;
0139     }
0140     return doc.object();
0141 }
0142 
0143 /// See KScreen::Output::Rotation
0144 enum Rotation {
0145     None = 1,
0146     Left = 2,
0147     Inverted = 4,
0148     Right = 8,
0149 };
0150 
0151 OutputTransform toKWinTransform(int rotation)
0152 {
0153     switch (Rotation(rotation)) {
0154     case None:
0155         return OutputTransform::Normal;
0156     case Left:
0157         return OutputTransform::Rotate90;
0158     case Inverted:
0159         return OutputTransform::Rotate180;
0160     case Right:
0161         return OutputTransform::Rotate270;
0162     default:
0163         Q_UNREACHABLE();
0164     }
0165 }
0166 
0167 std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
0168 {
0169     const QJsonObject size = modeInfo["size"].toObject();
0170     const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
0171     const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
0172 
0173     const auto modes = output->modes();
0174     auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
0175         return mode->size() == modeSize && mode->refreshRate() == refreshRate;
0176     });
0177     return (it != modes.end()) ? *it : nullptr;
0178 }
0179 
0180 std::optional<std::pair<OutputConfiguration, QList<Output *>>> readOutputConfig(const QList<Output *> &outputs, const QString &hash)
0181 {
0182     const auto outputsInfo = outputsConfig(outputs, hash);
0183     if (outputsInfo.isEmpty()) {
0184         return std::nullopt;
0185     }
0186     std::vector<std::pair<uint32_t, Output *>> outputOrder;
0187     OutputConfiguration cfg;
0188     // default position goes from left to right
0189     QPoint pos(0, 0);
0190     for (const auto &output : std::as_const(outputs)) {
0191         if (output->isPlaceholder() || output->isNonDesktop()) {
0192             continue;
0193         }
0194         auto props = cfg.changeSet(output);
0195         const QJsonObject outputInfo = outputsInfo[output];
0196         const auto globalOutputInfo = globalOutputConfig(output);
0197         qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
0198         if (!outputInfo.isEmpty() || globalOutputInfo.has_value()) {
0199             // settings that are per output setup:
0200             props->enabled = outputInfo["enabled"].toBool(true);
0201             if (outputInfo["primary"].toBool()) {
0202                 outputOrder.push_back(std::make_pair(1, output));
0203                 if (!props->enabled) {
0204                     qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
0205                     return std::nullopt;
0206                 }
0207             } else if (int prio = outputInfo["priority"].toInt(); prio > 0) {
0208                 outputOrder.push_back(std::make_pair(prio, output));
0209                 if (!props->enabled) {
0210                     qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!";
0211                     return std::nullopt;
0212                 }
0213             } else {
0214                 outputOrder.push_back(std::make_pair(0, output));
0215             }
0216             if (const QJsonObject pos = outputInfo["pos"].toObject(); !pos.isEmpty()) {
0217                 props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
0218             }
0219 
0220             // settings that are independent of per output setups:
0221             const auto &globalInfo = globalOutputInfo ? globalOutputInfo.value() : outputInfo;
0222             if (const QJsonValue scale = globalInfo["scale"]; !scale.isUndefined()) {
0223                 props->scale = scale.toDouble(1.);
0224             }
0225             if (const QJsonValue rotation = globalInfo["rotation"]; !rotation.isUndefined()) {
0226                 props->transform = KScreenIntegration::toKWinTransform(rotation.toInt());
0227             }
0228             if (const QJsonValue overscan = globalInfo["overscan"]; !overscan.isUndefined()) {
0229                 props->overscan = globalInfo["overscan"].toInt();
0230             }
0231             if (const QJsonValue vrrpolicy = globalInfo["vrrpolicy"]; !vrrpolicy.isUndefined()) {
0232                 props->vrrPolicy = static_cast<VrrPolicy>(vrrpolicy.toInt());
0233             }
0234             if (const QJsonValue rgbrange = globalInfo["rgbrange"]; !rgbrange.isUndefined()) {
0235                 props->rgbRange = static_cast<Output::RgbRange>(rgbrange.toInt());
0236             }
0237 
0238             if (const QJsonObject modeInfo = globalInfo["mode"].toObject(); !modeInfo.isEmpty()) {
0239                 if (auto mode = KScreenIntegration::parseMode(output, modeInfo)) {
0240                     props->mode = mode;
0241                 }
0242             }
0243         } else {
0244             props->enabled = true;
0245             props->pos = pos;
0246             props->transform = output->panelOrientation();
0247             outputOrder.push_back(std::make_pair(0, output));
0248         }
0249         pos.setX(pos.x() + output->geometry().width());
0250     }
0251 
0252     bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
0253         return !cfg.changeSet(output)->enabled.value_or(output->isEnabled());
0254     });
0255     if (allDisabled) {
0256         qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
0257         return std::nullopt;
0258     }
0259     std::erase_if(outputOrder, [&cfg](const auto &pair) {
0260         return !cfg.constChangeSet(pair.second)->enabled.value_or(pair.second->isEnabled());
0261     });
0262     std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) {
0263         if (left.first == right.first) {
0264             // sort alphabetically as a fallback
0265             return left.second->name() < right.second->name();
0266         } else if (left.first == 0) {
0267             return false;
0268         } else {
0269             return left.first < right.first;
0270         }
0271     });
0272 
0273     QList<Output *> order;
0274     order.reserve(outputOrder.size());
0275     std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) {
0276         return pair.second;
0277     });
0278     return std::make_pair(cfg, order);
0279 }
0280 }
0281 }