KWin
Loading...
Searching...
No Matches
outputconfigurationstore.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: 2023 Xaver Hugl <xaver.hugl@gmail.com>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
10#include "core/iccprofile.h"
11#include "core/inputdevice.h"
12#include "core/output.h"
13#include "core/outputbackend.h"
15#include "input.h"
16#include "input_event.h"
17#include "kscreenintegration.h"
18#include "workspace.h"
19
20#include <QFile>
21#include <QJsonArray>
22#include <QJsonDocument>
23#include <QJsonObject>
24#include <QOrientationReading>
25
26namespace KWin
27{
28
33
38
39std::optional<std::tuple<OutputConfiguration, QList<Output *>, OutputConfigurationStore::ConfigType>> OutputConfigurationStore::queryConfig(const QList<Output *> &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode)
40{
41 QList<Output *> relevantOutputs;
42 std::copy_if(outputs.begin(), outputs.end(), std::back_inserter(relevantOutputs), [](Output *output) {
43 return !output->isNonDesktop() && !output->isPlaceholder();
44 });
45 if (relevantOutputs.isEmpty()) {
46 return std::nullopt;
47 }
48 if (const auto opt = findSetup(relevantOutputs, isLidClosed)) {
49 const auto &[setup, outputStates] = *opt;
50 auto [config, order] = setupToConfig(setup, outputStates);
51 applyOrientationReading(config, relevantOutputs, orientation, isTabletMode);
52 storeConfig(relevantOutputs, isLidClosed, config, order);
53 return std::make_tuple(config, order, ConfigType::Preexisting);
54 }
55 if (auto kscreenConfig = KScreenIntegration::readOutputConfig(relevantOutputs, KScreenIntegration::connectedOutputsHash(relevantOutputs, isLidClosed))) {
56 auto &[config, order] = *kscreenConfig;
57 applyOrientationReading(config, relevantOutputs, orientation, isTabletMode);
58 storeConfig(relevantOutputs, isLidClosed, config, order);
59 return std::make_tuple(config, order, ConfigType::Preexisting);
60 }
61 auto [config, order] = generateConfig(relevantOutputs, isLidClosed);
62 applyOrientationReading(config, relevantOutputs, orientation, isTabletMode);
63 storeConfig(relevantOutputs, isLidClosed, config, order);
64 return std::make_tuple(config, order, ConfigType::Generated);
65}
66
67void OutputConfigurationStore::applyOrientationReading(OutputConfiguration &config, const QList<Output *> &outputs, QOrientationReading *orientation, bool isTabletMode)
68{
69 const auto output = std::find_if(outputs.begin(), outputs.end(), [&config](Output *output) {
70 return output->isInternal() && config.changeSet(output)->enabled.value_or(output->isEnabled());
71 });
72 if (output == outputs.end()) {
73 return;
74 }
75 // TODO move other outputs to matching positions
76 const auto changeset = config.changeSet(*output);
77 if (!isAutoRotateActive(outputs, isTabletMode)) {
78 changeset->transform = changeset->manualTransform;
79 return;
80 }
81 const auto panelOrientation = (*output)->panelOrientation();
82 switch (orientation->orientation()) {
83 case QOrientationReading::Orientation::TopUp:
84 changeset->transform = panelOrientation;
85 return;
86 case QOrientationReading::Orientation::TopDown:
87 changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate180);
88 return;
89 case QOrientationReading::Orientation::LeftUp:
90 changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate90);
91 return;
92 case QOrientationReading::Orientation::RightUp:
93 changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate270);
94 return;
95 case QOrientationReading::Orientation::FaceUp:
96 case QOrientationReading::Orientation::FaceDown:
97 return;
98 case QOrientationReading::Orientation::Undefined:
99 changeset->transform = changeset->manualTransform;
100 return;
101 }
102}
103
104std::optional<std::pair<OutputConfigurationStore::Setup *, std::unordered_map<Output *, size_t>>> OutputConfigurationStore::findSetup(const QList<Output *> &outputs, bool lidClosed)
105{
106 std::unordered_map<Output *, size_t> outputStates;
107 for (Output *output : outputs) {
108 if (auto opt = findOutput(output, outputs)) {
109 outputStates[output] = *opt;
110 } else {
111 return std::nullopt;
112 }
113 }
114 const auto setup = std::find_if(m_setups.begin(), m_setups.end(), [lidClosed, &outputStates](const auto &setup) {
115 if (setup.lidClosed != lidClosed || size_t(setup.outputs.size()) != outputStates.size()) {
116 return false;
117 }
118 return std::all_of(outputStates.begin(), outputStates.end(), [&setup](const auto &outputIt) {
119 return std::any_of(setup.outputs.begin(), setup.outputs.end(), [&outputIt](const auto &outputInfo) {
120 return outputInfo.outputIndex == outputIt.second;
121 });
122 });
123 });
124 if (setup == m_setups.end()) {
125 return std::nullopt;
126 } else {
127 return std::make_pair(&(*setup), outputStates);
128 }
129}
130
131std::optional<size_t> OutputConfigurationStore::findOutput(Output *output, const QList<Output *> &allOutputs) const
132{
133 const bool uniqueEdid = !output->edid().identifier().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) {
134 return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier();
135 });
136 const bool uniqueEdidHash = !output->edid().hash().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) {
137 return otherOutput != output && otherOutput->edid().hash() == output->edid().hash();
138 });
139 const bool uniqueMst = !output->mstPath().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) {
140 return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier() && otherOutput->mstPath() == output->mstPath();
141 });
142 auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) {
143 if (output->edid().isValid()) {
144 if (outputState.edidIdentifier != output->edid().identifier()) {
145 return false;
146 } else if (uniqueEdid) {
147 return true;
148 }
149 }
150 if (!output->edid().hash().isEmpty()) {
151 if (outputState.edidHash != output->edid().hash()) {
152 return false;
153 } else if (uniqueEdidHash) {
154 return true;
155 }
156 }
157 if (outputState.mstPath != output->mstPath()) {
158 return false;
159 } else if (uniqueMst) {
160 return true;
161 }
162 return outputState.connectorName == output->name();
163 });
164 if (it == m_outputs.end() && uniqueEdidHash) {
165 // handle the edge case of EDID parsing failing in the past but not failing anymore
166 it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) {
167 return outputState.edidHash == output->edid().hash();
168 });
169 }
170 if (it != m_outputs.end()) {
171 return std::distance(m_outputs.begin(), it);
172 } else {
173 return std::nullopt;
174 }
175}
176
177void OutputConfigurationStore::storeConfig(const QList<Output *> &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QList<Output *> &outputOrder)
178{
179 QList<Output *> relevantOutputs;
180 std::copy_if(allOutputs.begin(), allOutputs.end(), std::back_inserter(relevantOutputs), [](Output *output) {
181 return !output->isNonDesktop() && !output->isPlaceholder();
182 });
183 if (relevantOutputs.isEmpty()) {
184 return;
185 }
186 const auto opt = findSetup(relevantOutputs, isLidClosed);
187 Setup *setup = nullptr;
188 if (opt) {
189 setup = opt->first;
190 } else {
191 m_setups.push_back(Setup{});
192 setup = &m_setups.back();
193 setup->lidClosed = isLidClosed;
194 }
195 for (Output *output : relevantOutputs) {
196 auto outputIndex = findOutput(output, outputOrder);
197 if (!outputIndex) {
198 m_outputs.push_back(OutputState{});
199 outputIndex = m_outputs.size() - 1;
200 }
201 auto outputIt = std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex](const auto &output) {
202 return output.outputIndex == outputIndex;
203 });
204 if (outputIt == setup->outputs.end()) {
205 setup->outputs.push_back(SetupState{});
206 outputIt = setup->outputs.end() - 1;
207 }
208 if (const auto changeSet = config.constChangeSet(output)) {
209 std::shared_ptr<OutputMode> mode = changeSet->mode.value_or(output->currentMode()).lock();
210 if (!mode) {
211 mode = output->currentMode();
212 }
213 m_outputs[*outputIndex] = OutputState{
214 .edidIdentifier = output->edid().identifier(),
215 .connectorName = output->name(),
216 .edidHash = output->edid().isValid() ? output->edid().hash() : QString{},
217 .mstPath = output->mstPath(),
218 .mode = ModeData{
219 .size = mode->size(),
220 .refreshRate = mode->refreshRate(),
221 },
222 .scale = changeSet->scale.value_or(output->scale()),
223 .transform = changeSet->transform.value_or(output->transform()),
224 .manualTransform = changeSet->manualTransform.value_or(output->manualTransform()),
225 .overscan = changeSet->overscan.value_or(output->overscan()),
226 .rgbRange = changeSet->rgbRange.value_or(output->rgbRange()),
227 .vrrPolicy = changeSet->vrrPolicy.value_or(output->vrrPolicy()),
228 .highDynamicRange = changeSet->highDynamicRange.value_or(output->highDynamicRange()),
229 .sdrBrightness = changeSet->sdrBrightness.value_or(output->sdrBrightness()),
230 .wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()),
231 .autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()),
232 .iccProfilePath = changeSet->iccProfilePath.value_or(output->iccProfilePath()),
233 .maxPeakBrightnessOverride = changeSet->maxPeakBrightnessOverride.value_or(output->maxPeakBrightnessOverride()),
234 .maxAverageBrightnessOverride = changeSet->maxAverageBrightnessOverride.value_or(output->maxAverageBrightnessOverride()),
235 .minBrightnessOverride = changeSet->minBrightnessOverride.value_or(output->minBrightnessOverride()),
236 .sdrGamutWideness = changeSet->sdrGamutWideness.value_or(output->sdrGamutWideness()),
237 };
238 *outputIt = SetupState{
239 .outputIndex = *outputIndex,
240 .position = changeSet->pos.value_or(output->geometry().topLeft()),
241 .enabled = changeSet->enabled.value_or(output->isEnabled()),
242 .priority = int(outputOrder.indexOf(output)),
243 };
244 } else {
245 const auto mode = output->currentMode();
246 m_outputs[*outputIndex] = OutputState{
247 .edidIdentifier = output->edid().identifier(),
248 .connectorName = output->name(),
249 .edidHash = output->edid().isValid() ? output->edid().hash() : QString{},
250 .mstPath = output->mstPath(),
251 .mode = ModeData{
252 .size = mode->size(),
253 .refreshRate = mode->refreshRate(),
254 },
255 .scale = output->scale(),
256 .transform = output->transform(),
257 .manualTransform = output->manualTransform(),
258 .overscan = output->overscan(),
259 .rgbRange = output->rgbRange(),
260 .vrrPolicy = output->vrrPolicy(),
261 .highDynamicRange = output->highDynamicRange(),
262 .sdrBrightness = output->sdrBrightness(),
263 .wideColorGamut = output->wideColorGamut(),
264 .autoRotation = output->autoRotationPolicy(),
265 .iccProfilePath = output->iccProfilePath(),
266 .maxPeakBrightnessOverride = output->maxPeakBrightnessOverride(),
267 .maxAverageBrightnessOverride = output->maxAverageBrightnessOverride(),
268 .minBrightnessOverride = output->minBrightnessOverride(),
269 .sdrGamutWideness = output->sdrGamutWideness(),
270 };
271 *outputIt = SetupState{
272 .outputIndex = *outputIndex,
273 .position = output->geometry().topLeft(),
274 .enabled = output->isEnabled(),
275 .priority = int(outputOrder.indexOf(output)),
276 };
277 }
278 }
279 save();
280}
281
282std::pair<OutputConfiguration, QList<Output *>> OutputConfigurationStore::setupToConfig(Setup *setup, const std::unordered_map<Output *, size_t> &outputMap) const
283{
285 QList<std::pair<Output *, size_t>> priorities;
286 for (const auto &[output, outputIndex] : outputMap) {
287 const OutputState &state = m_outputs[outputIndex];
288 const auto &setupState = *std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex = outputIndex](const auto &state) {
289 return state.outputIndex == outputIndex;
290 });
291 const auto modes = output->modes();
292 const auto mode = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) {
293 return state.mode
294 && mode->size() == state.mode->size
295 && mode->refreshRate() == state.mode->refreshRate;
296 });
297 *ret.changeSet(output) = OutputChangeSet{
298 .mode = mode == modes.end() ? std::nullopt : std::optional(*mode),
299 .enabled = setupState.enabled,
300 .pos = setupState.position,
301 .scale = state.scale,
302 .transform = state.transform,
303 .manualTransform = state.manualTransform,
304 .overscan = state.overscan,
305 .rgbRange = state.rgbRange,
306 .vrrPolicy = state.vrrPolicy,
307 .highDynamicRange = state.highDynamicRange,
308 .sdrBrightness = state.sdrBrightness,
309 .wideColorGamut = state.wideColorGamut,
310 .autoRotationPolicy = state.autoRotation,
311 .iccProfilePath = state.iccProfilePath,
312 .iccProfile = state.iccProfilePath ? IccProfile::load(*state.iccProfilePath) : nullptr,
313 .maxPeakBrightnessOverride = state.maxPeakBrightnessOverride,
314 .maxAverageBrightnessOverride = state.maxAverageBrightnessOverride,
315 .minBrightnessOverride = state.minBrightnessOverride,
316 .sdrGamutWideness = state.sdrGamutWideness,
317 };
318 if (setupState.enabled) {
319 priorities.push_back(std::make_pair(output, setupState.priority));
320 }
321 }
322 std::sort(priorities.begin(), priorities.end(), [](const auto &left, const auto &right) {
323 return left.second < right.second;
324 });
325 QList<Output *> order;
326 std::transform(priorities.begin(), priorities.end(), std::back_inserter(order), [](const auto &pair) {
327 return pair.first;
328 });
329 return std::make_pair(ret, order);
330}
331
332std::optional<std::pair<OutputConfiguration, QList<Output *>>> OutputConfigurationStore::generateLidClosedConfig(const QList<Output *> &outputs)
333{
334 const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) {
335 return output->isInternal();
336 });
337 if (internalIt == outputs.end()) {
338 return std::nullopt;
339 }
340 const auto setup = findSetup(outputs, false);
341 if (!setup) {
342 return std::nullopt;
343 }
344 Output *const internalOutput = *internalIt;
345 auto [config, order] = setupToConfig(setup->first, setup->second);
346 auto internalChangeset = config.changeSet(internalOutput);
347 internalChangeset->enabled = false;
348 order.removeOne(internalOutput);
349
350 const bool anyEnabled = std::any_of(outputs.begin(), outputs.end(), [&config = config](Output *output) {
351 return config.changeSet(output)->enabled.value_or(output->isEnabled());
352 });
353 if (!anyEnabled) {
354 return std::nullopt;
355 }
356
357 const auto getSize = [](OutputChangeSet *changeset, Output *output) {
358 auto mode = changeset->mode ? changeset->mode->lock() : nullptr;
359 if (!mode) {
360 mode = output->currentMode();
361 }
362 const auto scale = changeset->scale.value_or(output->scale());
363 return QSize(std::ceil(mode->size().width() / scale), std::ceil(mode->size().height() / scale));
364 };
365 const QPoint internalPos = internalChangeset->pos.value_or(internalOutput->geometry().topLeft());
366 const QSize internalSize = getSize(internalChangeset.get(), internalOutput);
367 for (Output *otherOutput : outputs) {
368 auto changeset = config.changeSet(otherOutput);
369 QPoint otherPos = changeset->pos.value_or(otherOutput->geometry().topLeft());
370 if (otherPos.x() >= internalPos.x() + internalSize.width()) {
371 otherPos.rx() -= std::floor(internalSize.width());
372 }
373 if (otherPos.y() >= internalPos.y() + internalSize.height()) {
374 otherPos.ry() -= std::floor(internalSize.height());
375 }
376 // make sure this doesn't make outputs overlap, which is neither supported nor expected by users
377 const QSize otherSize = getSize(changeset.get(), otherOutput);
378 const bool overlap = std::any_of(outputs.begin(), outputs.end(), [&, &config = config](Output *output) {
379 if (otherOutput == output) {
380 return false;
381 }
382 const auto changeset = config.changeSet(output);
383 const QPoint pos = changeset->pos.value_or(output->geometry().topLeft());
384 return QRect(pos, otherSize).intersects(QRect(otherPos, getSize(changeset.get(), output)));
385 });
386 if (!overlap) {
387 changeset->pos = otherPos;
388 }
389 }
390 return std::make_pair(config, order);
391}
392
393std::pair<OutputConfiguration, QList<Output *>> OutputConfigurationStore::generateConfig(const QList<Output *> &outputs, bool isLidClosed)
394{
395 if (isLidClosed) {
396 if (const auto closedConfig = generateLidClosedConfig(outputs)) {
397 return *closedConfig;
398 }
399 }
400 OutputConfiguration ret;
401 QList<Output *> outputOrder;
402 QPoint pos(0, 0);
403 for (const auto output : outputs) {
404 const auto outputIndex = findOutput(output, outputs);
405 const bool enable = !isLidClosed || !output->isInternal() || outputs.size() == 1;
406 const OutputState existingData = outputIndex ? m_outputs[*outputIndex] : OutputState{};
407
408 const auto modes = output->modes();
409 const auto modeIt = std::find_if(modes.begin(), modes.end(), [&existingData](const auto &mode) {
410 return existingData.mode
411 && mode->size() == existingData.mode->size
412 && mode->refreshRate() == existingData.mode->refreshRate;
413 });
414 const auto mode = modeIt == modes.end() ? output->currentMode() : *modeIt;
415
416 const auto changeset = ret.changeSet(output);
417 *changeset = {
418 .mode = mode,
419 .enabled = enable,
420 .pos = pos,
421 .scale = existingData.scale.value_or(chooseScale(output, mode.get())),
422 .transform = existingData.transform.value_or(output->panelOrientation()),
423 .manualTransform = existingData.manualTransform.value_or(output->panelOrientation()),
424 .overscan = existingData.overscan.value_or(0),
425 .rgbRange = existingData.rgbRange.value_or(Output::RgbRange::Automatic),
426 .vrrPolicy = existingData.vrrPolicy.value_or(VrrPolicy::Automatic),
427 .highDynamicRange = existingData.highDynamicRange.value_or(false),
428 .sdrBrightness = existingData.sdrBrightness.value_or(200),
429 .wideColorGamut = existingData.wideColorGamut.value_or(false),
430 .autoRotationPolicy = existingData.autoRotation.value_or(Output::AutoRotationPolicy::InTabletMode),
431 };
432 if (enable) {
433 const auto modeSize = changeset->transform->map(mode->size());
434 pos.setX(std::ceil(pos.x() + modeSize.width() / *changeset->scale));
435 outputOrder.push_back(output);
436 }
437 }
438 return std::make_pair(ret, outputs);
439}
440
441std::shared_ptr<OutputMode> OutputConfigurationStore::chooseMode(Output *output) const
442{
443 const auto modes = output->modes();
444
445 // some displays advertise bigger modes than their native resolution
446 // to avoid that, take the preferred mode into account, which is usually the native one
447 const auto preferred = std::find_if(modes.begin(), modes.end(), [](const auto &mode) {
448 return mode->flags() & OutputMode::Flag::Preferred;
449 });
450 if (preferred != modes.end()) {
451 // some high refresh rate displays advertise a 60Hz mode as preferred for compatibility reasons
452 // ignore that and choose the highest possible refresh rate by default instead
453 std::shared_ptr<OutputMode> highestRefresh = *preferred;
454 for (const auto &mode : modes) {
455 if (mode->size() == highestRefresh->size() && mode->refreshRate() > highestRefresh->refreshRate()) {
456 highestRefresh = mode;
457 }
458 }
459 // if the preferred mode size has a refresh rate that's too low for PCs,
460 // allow falling back to a mode with lower resolution and a more usable refresh rate
461 if (highestRefresh->refreshRate() >= 50000) {
462 return highestRefresh;
463 }
464 }
465
466 std::shared_ptr<OutputMode> ret;
467 for (auto mode : modes) {
468 if (mode->flags() & OutputMode::Flag::Generated) {
469 // generated modes aren't guaranteed to work, so don't choose one as the default
470 continue;
471 }
472 if (!ret) {
473 ret = mode;
474 continue;
475 }
476 const bool retUsableRefreshRate = ret->refreshRate() >= 50000;
477 const bool usableRefreshRate = mode->refreshRate() >= 50000;
478 if (retUsableRefreshRate && !usableRefreshRate) {
479 ret = mode;
480 continue;
481 }
482 if ((usableRefreshRate && !retUsableRefreshRate)
483 || mode->size().width() > ret->size().width()
484 || mode->size().height() > ret->size().height()
485 || (mode->size() == ret->size() && mode->refreshRate() > ret->refreshRate())) {
486 ret = mode;
487 }
488 }
489 return ret;
490}
491
492double OutputConfigurationStore::chooseScale(Output *output, OutputMode *mode) const
493{
494 if (output->physicalSize().height() <= 0) {
495 // invalid size, can't do anything with this
496 return 1.0;
497 }
498 const double outputDpi = mode->size().height() / (output->physicalSize().height() / 25.4);
499 const double desiredScale = outputDpi / targetDpi(output);
500 // round to 25% steps
501 return std::clamp(std::round(100.0 * desiredScale / 25.0) * 25.0 / 100.0, 1.0, 3.0);
502}
503
504double OutputConfigurationStore::targetDpi(Output *output) const
505{
506 // The eye's ability to perceive detail diminishes with distance, so objects
507 // that are closer can be smaller and their details remain equally
508 // distinguishable. As a result, each device type has its own ideal physical
509 // size of items on its screen based on how close the user's eyes are
510 // expected to be from it on average, and its target DPI value needs to be
511 // changed accordingly.
512 const auto devices = input()->devices();
513 const bool hasLaptopLid = std::any_of(devices.begin(), devices.end(), [](const auto &device) {
514 return device->isLidSwitch();
515 });
516 if (output->isInternal()) {
517 if (hasLaptopLid) {
518 // laptop screens: usually closer to the face than desktop monitors
519 return 125;
520 } else {
521 // phone screens: even closer than laptops
522 return 136;
523 }
524 } else {
525 // "normal" 1x scale desktop monitor dpi
526 return 96;
527 }
528}
529
530void OutputConfigurationStore::load()
531{
532 const QString jsonPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json"));
533 if (jsonPath.isEmpty()) {
534 return;
535 }
536
537 QFile f(jsonPath);
538 if (!f.open(QIODevice::ReadOnly)) {
539 qCWarning(KWIN_CORE) << "Could not open file" << jsonPath;
540 return;
541 }
542 QJsonParseError error;
543 const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
544 if (error.error != QJsonParseError::NoError) {
545 qCWarning(KWIN_CORE) << "Failed to parse" << jsonPath << error.errorString();
546 return;
547 }
548 const auto array = doc.array();
549 std::vector<QJsonObject> objects;
550 std::transform(array.begin(), array.end(), std::back_inserter(objects), [](const auto &json) {
551 return json.toObject();
552 });
553 const auto outputsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) {
554 return obj["name"].toString() == "outputs" && obj["data"].isArray();
555 });
556 const auto setupsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) {
557 return obj["name"].toString() == "setups" && obj["data"].isArray();
558 });
559 if (outputsIt == objects.end() || setupsIt == objects.end()) {
560 return;
561 }
562 const auto outputs = (*outputsIt)["data"].toArray();
563
564 std::vector<std::optional<OutputState>> outputDatas;
565 for (const auto &output : outputs) {
566 const auto data = output.toObject();
567 OutputState state;
568 bool hasIdentifier = false;
569 if (const auto it = data.find("edidIdentifier"); it != data.end()) {
570 if (const auto str = it->toString(); !str.isEmpty()) {
571 state.edidIdentifier = str;
572 hasIdentifier = true;
573 }
574 }
575 if (const auto it = data.find("edidHash"); it != data.end()) {
576 if (const auto str = it->toString(); !str.isEmpty()) {
577 state.edidHash = str;
578 hasIdentifier = true;
579 }
580 }
581 if (const auto it = data.find("connectorName"); it != data.end()) {
582 if (const auto str = it->toString(); !str.isEmpty()) {
583 state.connectorName = str;
584 hasIdentifier = true;
585 }
586 }
587 if (const auto it = data.find("mstPath"); it != data.end()) {
588 if (const auto str = it->toString(); !str.isEmpty()) {
589 state.mstPath = str;
590 hasIdentifier = true;
591 }
592 }
593 if (!hasIdentifier) {
594 // without an identifier the settings are useless
595 // we still have to push something into the list so that the indices stay correct
596 outputDatas.push_back(std::nullopt);
597 qCWarning(KWIN_CORE, "Output in config is missing identifiers");
598 continue;
599 }
600 const bool hasDuplicate = std::any_of(outputDatas.begin(), outputDatas.end(), [&state](const auto &data) {
601 return data
602 && data->edidIdentifier == state.edidIdentifier
603 && data->edidHash == state.edidHash
604 && data->mstPath == state.mstPath
605 && data->connectorName == state.connectorName;
606 });
607 if (hasDuplicate) {
608 qCWarning(KWIN_CORE) << "Duplicate output found in config for edidIdentifier:" << state.edidIdentifier.value_or("<empty>") << "; connectorName:" << state.connectorName.value_or("<empty>") << "; mstPath:" << state.mstPath;
609 outputDatas.push_back(std::nullopt);
610 continue;
611 }
612 if (const auto it = data.find("mode"); it != data.end()) {
613 const auto obj = it->toObject();
614 const int width = obj["width"].toInt(0);
615 const int height = obj["height"].toInt(0);
616 const int refreshRate = obj["refreshRate"].toInt(0);
617 if (width > 0 && height > 0 && refreshRate > 0) {
618 state.mode = ModeData{
619 .size = QSize(width, height),
620 .refreshRate = uint32_t(refreshRate),
621 };
622 }
623 }
624 if (const auto it = data.find("scale"); it != data.end()) {
625 const double scale = it->toDouble(0);
626 if (scale > 0 && scale <= 3) {
627 state.scale = scale;
628 }
629 }
630 if (const auto it = data.find("transform"); it != data.end()) {
631 const auto str = it->toString();
632 if (str == "Normal") {
633 state.transform = state.manualTransform = OutputTransform::Kind::Normal;
634 } else if (str == "Rotated90") {
635 state.transform = state.manualTransform = OutputTransform::Kind::Rotate90;
636 } else if (str == "Rotated180") {
637 state.transform = state.manualTransform = OutputTransform::Kind::Rotate180;
638 } else if (str == "Rotated270") {
639 state.transform = state.manualTransform = OutputTransform::Kind::Rotate270;
640 } else if (str == "Flipped") {
641 state.transform = state.manualTransform = OutputTransform::Kind::FlipX;
642 } else if (str == "Flipped90") {
643 state.transform = state.manualTransform = OutputTransform::Kind::FlipX90;
644 } else if (str == "Flipped180") {
645 state.transform = state.manualTransform = OutputTransform::Kind::FlipX180;
646 } else if (str == "Flipped270") {
647 state.transform = state.manualTransform = OutputTransform::Kind::FlipX270;
648 }
649 }
650 if (const auto it = data.find("overscan"); it != data.end()) {
651 const int overscan = it->toInt(-1);
652 if (overscan >= 0 && overscan <= 100) {
653 state.overscan = overscan;
654 }
655 }
656 if (const auto it = data.find("rgbRange"); it != data.end()) {
657 const auto str = it->toString();
658 if (str == "Automatic") {
659 state.rgbRange = Output::RgbRange::Automatic;
660 } else if (str == "Limited") {
661 state.rgbRange = Output::RgbRange::Limited;
662 } else if (str == "Full") {
663 state.rgbRange = Output::RgbRange::Full;
664 }
665 }
666 if (const auto it = data.find("vrrPolicy"); it != data.end()) {
667 const auto str = it->toString();
668 if (str == "Never") {
669 state.vrrPolicy = VrrPolicy::Never;
670 } else if (str == "Automatic") {
671 state.vrrPolicy = VrrPolicy::Automatic;
672 } else if (str == "Always") {
673 state.vrrPolicy = VrrPolicy::Always;
674 }
675 }
676 if (const auto it = data.find("highDynamicRange"); it != data.end() && it->isBool()) {
677 state.highDynamicRange = it->toBool();
678 }
679 if (const auto it = data.find("sdrBrightness"); it != data.end() && it->isDouble()) {
680 state.sdrBrightness = it->toInt(200);
681 }
682 if (const auto it = data.find("wideColorGamut"); it != data.end() && it->isBool()) {
683 state.wideColorGamut = it->toBool();
684 }
685 if (const auto it = data.find("autoRotation"); it != data.end()) {
686 const auto str = it->toString();
687 if (str == "Never") {
688 state.autoRotation = Output::AutoRotationPolicy::Never;
689 } else if (str == "InTabletMode") {
690 state.autoRotation = Output::AutoRotationPolicy::InTabletMode;
691 } else if (str == "Always") {
692 state.autoRotation = Output::AutoRotationPolicy::Always;
693 }
694 }
695 if (const auto it = data.find("iccProfilePath"); it != data.end()) {
696 state.iccProfilePath = it->toString();
697 }
698 if (const auto it = data.find("maxPeakBrightnessOverride"); it != data.end() && it->isDouble()) {
699 state.maxPeakBrightnessOverride = it->toDouble();
700 }
701 if (const auto it = data.find("maxAverageBrightnessOverride"); it != data.end() && it->isDouble()) {
702 state.maxAverageBrightnessOverride = it->toDouble();
703 }
704 if (const auto it = data.find("minBrightnessOverride"); it != data.end() && it->isDouble()) {
705 state.minBrightnessOverride = it->toDouble();
706 }
707 if (const auto it = data.find("sdrGamutWideness"); it != data.end() && it->isDouble()) {
708 state.sdrGamutWideness = it->toDouble();
709 }
710 outputDatas.push_back(state);
711 }
712
713 const auto setups = (*setupsIt)["data"].toArray();
714 for (const auto &s : setups) {
715 const auto data = s.toObject();
716 const auto outputs = data["outputs"].toArray();
717 Setup setup;
718 bool fail = false;
719 for (const auto &output : outputs) {
720 const auto outputData = output.toObject();
721 SetupState state;
722 if (const auto it = outputData.find("enabled"); it != outputData.end() && it->isBool()) {
723 state.enabled = it->toBool();
724 } else {
725 fail = true;
726 break;
727 }
728 if (const auto it = outputData.find("outputIndex"); it != outputData.end()) {
729 const int index = it->toInt(-1);
730 if (index <= -1 || size_t(index) >= outputDatas.size()) {
731 fail = true;
732 break;
733 }
734 // the outputs must be unique
735 const bool unique = std::none_of(setup.outputs.begin(), setup.outputs.end(), [&index](const auto &output) {
736 return output.outputIndex == size_t(index);
737 });
738 if (!unique) {
739 fail = true;
740 break;
741 }
742 state.outputIndex = index;
743 }
744 if (const auto it = outputData.find("position"); it != outputData.end()) {
745 const auto obj = it->toObject();
746 const auto x = obj.find("x");
747 const auto y = obj.find("y");
748 if (x == obj.end() || !x->isDouble() || y == obj.end() || !y->isDouble()) {
749 fail = true;
750 break;
751 }
752 state.position = QPoint(x->toInt(0), y->toInt(0));
753 } else {
754 fail = true;
755 break;
756 }
757 if (const auto it = outputData.find("priority"); it != outputData.end()) {
758 state.priority = it->toInt(-1);
759 if (state.priority < 0 && state.enabled) {
760 fail = true;
761 break;
762 }
763 }
764 setup.outputs.push_back(state);
765 }
766 if (fail || setup.outputs.empty()) {
767 continue;
768 }
769 // one of the outputs must be enabled
770 const bool noneEnabled = std::none_of(setup.outputs.begin(), setup.outputs.end(), [](const auto &output) {
771 return output.enabled;
772 });
773 if (noneEnabled) {
774 continue;
775 }
776 setup.lidClosed = data["lidClosed"].toBool(false);
777 // there must be only one setup that refers to a given set of outputs
778 const bool alreadyExists = std::any_of(m_setups.begin(), m_setups.end(), [&setup](const auto &other) {
779 if (setup.lidClosed != other.lidClosed || setup.outputs.size() != other.outputs.size()) {
780 return false;
781 }
782 return std::all_of(setup.outputs.begin(), setup.outputs.end(), [&other](const auto &output) {
783 return std::any_of(other.outputs.begin(), other.outputs.end(), [&output](const auto &otherOutput) {
784 return output.outputIndex == otherOutput.outputIndex;
785 });
786 });
787 });
788 if (alreadyExists) {
789 continue;
790 }
791 m_setups.push_back(setup);
792 }
793
794 // repair the outputs list in case it's broken
795 for (size_t i = 0; i < outputDatas.size();) {
796 if (!outputDatas[i]) {
797 outputDatas.erase(outputDatas.begin() + i);
798 for (auto setupIt = m_setups.begin(); setupIt != m_setups.end();) {
799 const bool broken = std::any_of(setupIt->outputs.begin(), setupIt->outputs.end(), [i](const auto &output) {
800 return output.outputIndex == i;
801 });
802 if (broken) {
803 setupIt = m_setups.erase(setupIt);
804 continue;
805 }
806 for (auto &output : setupIt->outputs) {
807 if (output.outputIndex > i) {
808 output.outputIndex--;
809 }
810 }
811 setupIt++;
812 }
813 } else {
814 i++;
815 }
816 }
817
818 for (const auto &o : outputDatas) {
819 Q_ASSERT(o);
820 m_outputs.push_back(*o);
821 }
822}
823
824void OutputConfigurationStore::save()
825{
826 QJsonDocument document;
827 QJsonArray array;
828 QJsonObject outputs;
829 outputs["name"] = "outputs";
830 QJsonArray outputsData;
831 for (const auto &output : m_outputs) {
832 QJsonObject o;
833 if (output.edidIdentifier) {
834 o["edidIdentifier"] = *output.edidIdentifier;
835 }
836 if (!output.edidHash.isEmpty()) {
837 o["edidHash"] = output.edidHash;
838 }
839 if (output.connectorName) {
840 o["connectorName"] = *output.connectorName;
841 }
842 if (!output.mstPath.isEmpty()) {
843 o["mstPath"] = output.mstPath;
844 }
845 if (output.mode) {
846 QJsonObject mode;
847 mode["width"] = output.mode->size.width();
848 mode["height"] = output.mode->size.height();
849 mode["refreshRate"] = int(output.mode->refreshRate);
850 o["mode"] = mode;
851 }
852 if (output.scale) {
853 o["scale"] = *output.scale;
854 }
855 if (output.manualTransform == OutputTransform::Kind::Normal) {
856 o["transform"] = "Normal";
857 } else if (output.manualTransform == OutputTransform::Kind::Rotate90) {
858 o["transform"] = "Rotated90";
859 } else if (output.manualTransform == OutputTransform::Kind::Rotate180) {
860 o["transform"] = "Rotated180";
861 } else if (output.manualTransform == OutputTransform::Kind::Rotate270) {
862 o["transform"] = "Rotated270";
863 } else if (output.manualTransform == OutputTransform::Kind::FlipX) {
864 o["transform"] = "Flipped";
865 } else if (output.manualTransform == OutputTransform::Kind::FlipX90) {
866 o["transform"] = "Flipped90";
867 } else if (output.manualTransform == OutputTransform::Kind::FlipX180) {
868 o["transform"] = "Flipped180";
869 } else if (output.manualTransform == OutputTransform::Kind::FlipX270) {
870 o["transform"] = "Flipped270";
871 }
872 if (output.overscan) {
873 o["overscan"] = int(*output.overscan);
874 }
875 if (output.rgbRange == Output::RgbRange::Automatic) {
876 o["rgbRange"] = "Automatic";
877 } else if (output.rgbRange == Output::RgbRange::Limited) {
878 o["rgbRange"] = "Limited";
879 } else if (output.rgbRange == Output::RgbRange::Full) {
880 o["rgbRange"] = "Full";
881 }
882 if (output.vrrPolicy == VrrPolicy::Never) {
883 o["vrrPolicy"] = "Never";
884 } else if (output.vrrPolicy == VrrPolicy::Automatic) {
885 o["vrrPolicy"] = "Automatic";
886 } else if (output.vrrPolicy == VrrPolicy::Always) {
887 o["vrrPolicy"] = "Always";
888 }
889 if (output.highDynamicRange) {
890 o["highDynamicRange"] = *output.highDynamicRange;
891 }
892 if (output.sdrBrightness) {
893 o["sdrBrightness"] = int(*output.sdrBrightness);
894 }
895 if (output.wideColorGamut) {
896 o["wideColorGamut"] = *output.wideColorGamut;
897 }
898 if (output.autoRotation) {
899 switch (*output.autoRotation) {
900 case Output::AutoRotationPolicy::Never:
901 o["autoRotation"] = "Never";
902 break;
903 case Output::AutoRotationPolicy::InTabletMode:
904 o["autoRotation"] = "InTabletMode";
905 break;
906 case Output::AutoRotationPolicy::Always:
907 o["autoRotation"] = "Always";
908 break;
909 }
910 }
911 if (output.iccProfilePath) {
912 o["iccProfilePath"] = *output.iccProfilePath;
913 }
914 if (output.maxPeakBrightnessOverride) {
915 o["maxPeakBrightnessOverride"] = *output.maxPeakBrightnessOverride;
916 }
917 if (output.maxAverageBrightnessOverride) {
918 o["maxAverageBrightnessOverride"] = *output.maxAverageBrightnessOverride;
919 }
920 if (output.minBrightnessOverride) {
921 o["minBrightnessOverride"] = *output.minBrightnessOverride;
922 }
923 if (output.sdrGamutWideness) {
924 o["sdrGamutWideness"] = *output.sdrGamutWideness;
925 }
926 outputsData.append(o);
927 }
928 outputs["data"] = outputsData;
929 array.append(outputs);
930
931 QJsonObject setups;
932 setups["name"] = "setups";
933 QJsonArray setupData;
934 for (const auto &setup : m_setups) {
935 QJsonObject o;
936 o["lidClosed"] = setup.lidClosed;
937 QJsonArray outputs;
938 for (ssize_t i = 0; i < setup.outputs.size(); i++) {
939 const auto &output = setup.outputs[i];
940 QJsonObject o;
941 o["enabled"] = output.enabled;
942 o["outputIndex"] = int(output.outputIndex);
943 o["priority"] = output.priority;
944 QJsonObject pos;
945 pos["x"] = output.position.x();
946 pos["y"] = output.position.y();
947 o["position"] = pos;
948
949 outputs.append(o);
950 }
951 o["outputs"] = outputs;
952
953 setupData.append(o);
954 }
955 setups["data"] = setupData;
956 array.append(setups);
957
958 const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/kwinoutputconfig.json";
959 QFile f(path);
960 if (!f.open(QIODevice::WriteOnly)) {
961 qCWarning(KWIN_CORE, "Couldn't open output config file %s", qPrintable(path));
962 return;
963 }
964 document.setArray(array);
965 f.write(document.toJson());
966 f.flush();
967}
968
969bool OutputConfigurationStore::isAutoRotateActive(const QList<Output *> &outputs, bool isTabletMode) const
970{
971 const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) {
972 return output->isInternal() && output->isEnabled();
973 });
974 if (internalIt == outputs.end()) {
975 return false;
976 }
977 Output *internal = *internalIt;
978 switch (internal->autoRotationPolicy()) {
979 case Output::AutoRotationPolicy::Never:
980 return false;
981 case Output::AutoRotationPolicy::InTabletMode:
982 return isTabletMode;
983 case Output::AutoRotationPolicy::Always:
984 return true;
985 }
986 Q_UNREACHABLE();
987}
988}
QString hash() const
Definition edid.cpp:237
QByteArray identifier() const
Definition edid.cpp:272
bool isValid() const
Definition edid.cpp:175
QList< InputDevice * > devices() const
Definition input.h:554
std::optional< std::weak_ptr< OutputMode > > mode
std::shared_ptr< OutputChangeSet > constChangeSet(Output *output) const
std::shared_ptr< OutputChangeSet > changeSet(Output *output)
void storeConfig(const QList< Output * > &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QList< Output * > &outputOrder)
std::optional< std::tuple< OutputConfiguration, QList< Output * >, ConfigType > > queryConfig(const QList< Output * > &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode)
bool isAutoRotateActive(const QList< Output * > &outputs, bool isTabletMode) const
VrrPolicy vrrPolicy() const
Definition output.cpp:657
std::shared_ptr< OutputMode > currentMode() const
Definition output.cpp:500
double sdrGamutWideness() const
Definition output.cpp:767
qreal scale() const
Definition output.cpp:455
uint32_t overscan() const
Definition output.cpp:652
bool highDynamicRange() const
Definition output.cpp:697
std::optional< double > maxAverageBrightnessOverride() const
Definition output.cpp:757
RgbRange rgbRange() const
Definition output.cpp:672
OutputTransform transform() const
Definition output.cpp:369
AutoRotationPolicy autoRotationPolicy() const
Definition output.cpp:707
QString iccProfilePath() const
Definition output.cpp:717
QByteArray mstPath() const
Definition output.cpp:722
uint32_t sdrBrightness() const
Definition output.cpp:702
const Edid & edid() const
Definition output.cpp:490
OutputTransform manualTransform() const
Definition output.cpp:374
bool wideColorGamut() const
Definition output.cpp:692
std::optional< double > minBrightnessOverride() const
Definition output.cpp:762
QRect geometry
Definition output.h:134
QString name
Definition output.h:136
bool isEnabled() const
Definition output.cpp:536
std::optional< double > maxPeakBrightnessOverride() const
Definition output.cpp:752
QString connectedOutputsHash(const QList< Output * > &outputs, bool isLidClosed)
See KScreen::Config::connectedOutputsHash in libkscreen.
std::optional< std::pair< OutputConfiguration, QList< Output * > > > readOutputConfig(const QList< Output * > &outputs, const QString &hash)
QList< KWayland::Client::Output * > outputs
InputRedirection * input()
Definition input.h:549