KWin
Loading...
Searching...
No Matches
kscreenintegration.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
6 SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10#include "kscreenintegration.h"
11#include "utils/common.h"
12
13#include <QCryptographicHash>
14#include <QFile>
15#include <QJsonArray>
16#include <QJsonDocument>
17#include <QJsonObject>
18#include <QStandardPaths>
19
20#include <algorithm>
21#include <cmath>
22
23namespace KWin
24{
25namespace KScreenIntegration
26{
28static QString outputHash(Output *output)
29{
30 if (output->edid().isValid()) {
31 return output->edid().hash();
32 } else {
33 return output->name();
34 }
35}
36
38QString connectedOutputsHash(const QList<Output *> &outputs, bool isLidClosed)
39{
40 QStringList hashedOutputs;
41 hashedOutputs.reserve(outputs.count());
42 for (auto output : std::as_const(outputs)) {
43 if (output->isPlaceholder() || output->isNonDesktop()) {
44 continue;
45 }
46 if (output->isInternal() && isLidClosed) {
47 continue;
48 }
49 hashedOutputs << outputHash(output);
50 }
51 std::sort(hashedOutputs.begin(), hashedOutputs.end());
52 const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
53 return QString::fromLatin1(hash.toHex());
54}
55
56static QHash<Output *, QJsonObject> outputsConfig(const QList<Output *> &outputs, const QString &hash)
57{
58 const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
59 if (kscreenJsonPath.isEmpty()) {
60 return {};
61 }
62
63 QFile f(kscreenJsonPath);
64 if (!f.open(QIODevice::ReadOnly)) {
65 qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
66 return {};
67 }
68
69 QJsonParseError error;
70 const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
71 if (error.error != QJsonParseError::NoError) {
72 qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
73 return {};
74 }
75
76 QHash<Output *, bool> duplicate;
77 QHash<Output *, QString> outputHashes;
78 for (Output *output : outputs) {
79 const QString hash = outputHash(output);
80 const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) {
81 return value == hash;
82 });
83 if (it == outputHashes.cend()) {
84 duplicate[output] = false;
85 } else {
86 duplicate[output] = true;
87 duplicate[it.key()] = true;
88 }
89 outputHashes[output] = hash;
90 }
91
92 QHash<Output *, QJsonObject> ret;
93 const auto outputsJson = doc.array();
94 for (const auto &outputJson : outputsJson) {
95 const auto outputObject = outputJson.toObject();
96 const auto id = outputObject[QLatin1String("id")];
97 const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) {
98 if (outputHash(output) != id.toString()) {
99 return false;
100 }
101 if (duplicate[output]) {
102 // can't distinguish between outputs by hash alone, need to look at connector names
103 const auto metadata = outputObject[QLatin1String("metadata")];
104 const auto outputName = metadata[QLatin1String("name")].toString();
105 return outputName == output->name();
106 } else {
107 return true;
108 }
109 });
110 if (output != outputs.end()) {
111 ret[*output] = outputObject;
112 }
113 }
114 return ret;
115}
116
117static std::optional<QJsonObject> globalOutputConfig(Output *output)
118{
119 const QString kscreenPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/"));
120 if (kscreenPath.isEmpty()) {
121 return std::nullopt;
122 }
123 const auto hash = outputHash(output);
124 // use connector specific data if available, unspecific data if not
125 QFile f(kscreenPath % hash % output->name());
126 if (!f.open(QIODevice::ReadOnly)) {
127 f.setFileName(kscreenPath % hash);
128 if (!f.open(QIODevice::ReadOnly)) {
129 qCWarning(KWIN_CORE) << "Could not open file" << f.fileName();
130 return std::nullopt;
131 }
132 }
133
134 QJsonParseError error;
135 const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
136 if (error.error != QJsonParseError::NoError) {
137 qCWarning(KWIN_CORE) << "Failed to parse" << f.fileName() << error.errorString();
138 return std::nullopt;
139 }
140 return doc.object();
141}
142
145 None = 1,
146 Left = 2,
148 Right = 8,
149};
150
152{
153 switch (Rotation(rotation)) {
154 case None:
156 case Left:
158 case Inverted:
160 case Right:
162 default:
163 Q_UNREACHABLE();
164 }
165}
166
167std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
168{
169 const QJsonObject size = modeInfo["size"].toObject();
170 const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
171 const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
172
173 const auto modes = output->modes();
174 auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
175 return mode->size() == modeSize && mode->refreshRate() == refreshRate;
176 });
177 return (it != modes.end()) ? *it : nullptr;
178}
179
180std::optional<std::pair<OutputConfiguration, QList<Output *>>> readOutputConfig(const QList<Output *> &outputs, const QString &hash)
181{
182 const auto outputsInfo = outputsConfig(outputs, hash);
183 if (outputsInfo.isEmpty()) {
184 return std::nullopt;
185 }
186 std::vector<std::pair<uint32_t, Output *>> outputOrder;
188 // default position goes from left to right
189 QPoint pos(0, 0);
190 for (const auto &output : std::as_const(outputs)) {
191 if (output->isPlaceholder() || output->isNonDesktop()) {
192 continue;
193 }
194 auto props = cfg.changeSet(output);
195 const QJsonObject outputInfo = outputsInfo[output];
196 const auto globalOutputInfo = globalOutputConfig(output);
197 qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
198 if (!outputInfo.isEmpty() || globalOutputInfo.has_value()) {
199 // settings that are per output setup:
200 props->enabled = outputInfo["enabled"].toBool(true);
201 if (outputInfo["primary"].toBool()) {
202 outputOrder.push_back(std::make_pair(1, output));
203 if (!props->enabled) {
204 qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
205 return std::nullopt;
206 }
207 } else if (int prio = outputInfo["priority"].toInt(); prio > 0) {
208 outputOrder.push_back(std::make_pair(prio, output));
209 if (!props->enabled) {
210 qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!";
211 return std::nullopt;
212 }
213 } else {
214 outputOrder.push_back(std::make_pair(0, output));
215 }
216 if (const QJsonObject pos = outputInfo["pos"].toObject(); !pos.isEmpty()) {
217 props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
218 }
219
220 // settings that are independent of per output setups:
221 const auto &globalInfo = globalOutputInfo ? globalOutputInfo.value() : outputInfo;
222 if (const QJsonValue scale = globalInfo["scale"]; !scale.isUndefined()) {
223 props->scale = scale.toDouble(1.);
224 }
225 if (const QJsonValue rotation = globalInfo["rotation"]; !rotation.isUndefined()) {
226 props->transform = KScreenIntegration::toKWinTransform(rotation.toInt());
227 props->manualTransform = props->transform;
228 }
229 if (const QJsonValue overscan = globalInfo["overscan"]; !overscan.isUndefined()) {
230 props->overscan = globalInfo["overscan"].toInt();
231 }
232 if (const QJsonValue vrrpolicy = globalInfo["vrrpolicy"]; !vrrpolicy.isUndefined()) {
233 props->vrrPolicy = static_cast<VrrPolicy>(vrrpolicy.toInt());
234 }
235 if (const QJsonValue rgbrange = globalInfo["rgbrange"]; !rgbrange.isUndefined()) {
236 props->rgbRange = static_cast<Output::RgbRange>(rgbrange.toInt());
237 }
238
239 if (const QJsonObject modeInfo = globalInfo["mode"].toObject(); !modeInfo.isEmpty()) {
240 if (auto mode = KScreenIntegration::parseMode(output, modeInfo)) {
241 props->mode = mode;
242 }
243 }
244 } else {
245 props->enabled = true;
246 props->pos = pos;
247 props->transform = output->panelOrientation();
248 outputOrder.push_back(std::make_pair(0, output));
249 }
250 pos.setX(pos.x() + output->geometry().width());
251 }
252
253 bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
254 return !cfg.changeSet(output)->enabled.value_or(output->isEnabled());
255 });
256 if (allDisabled) {
257 qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
258 return std::nullopt;
259 }
260 std::erase_if(outputOrder, [&cfg](const auto &pair) {
261 return !cfg.constChangeSet(pair.second)->enabled.value_or(pair.second->isEnabled());
262 });
263 std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) {
264 if (left.first == right.first) {
265 // sort alphabetically as a fallback
266 return left.second->name() < right.second->name();
267 } else if (left.first == 0) {
268 return false;
269 } else {
270 return left.first < right.first;
271 }
272 });
273
274 QList<Output *> order;
275 order.reserve(outputOrder.size());
276 std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) {
277 return pair.second;
278 });
279 return std::make_pair(cfg, order);
280}
281}
282}
QString hash() const
Definition edid.cpp:237
bool isValid() const
Definition edid.cpp:175
std::shared_ptr< OutputChangeSet > constChangeSet(Output *output) const
std::shared_ptr< OutputChangeSet > changeSet(Output *output)
bool isPlaceholder() const
Definition output.cpp:662
QList< std::shared_ptr< OutputMode > > modes() const
Definition output.cpp:495
const Edid & edid() const
Definition output.cpp:490
bool isInternal() const
Definition output.cpp:399
QString name
Definition output.h:136
bool isNonDesktop() const
Definition output.cpp:667
QString connectedOutputsHash(const QList< Output * > &outputs, bool isLidClosed)
See KScreen::Config::connectedOutputsHash in libkscreen.
Rotation
See KScreen::Output::Rotation.
std::shared_ptr< OutputMode > parseMode(Output *output, const QJsonObject &modeInfo)
OutputTransform toKWinTransform(int rotation)
std::optional< std::pair< OutputConfiguration, QList< Output * > > > readOutputConfig(const QList< Output * > &outputs, const QString &hash)
QList< KWayland::Client::Output * > outputs
@ None
Definition options.h:37
VrrPolicy
Definition globals.h:292