File indexing completed on 2024-11-10 04:57:49
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 }