kwin/src/outputconfigurationstore.cpp
Nate Graham 7ff51ba47c Raise target mobile device DPI
A target mobile DPI of 135 is rather low and results in excessively
large scale factors, so everything is too big.. Let's raise it a
little bit to improve the default level of information density on
these devices that are designed to be held close to the face.

This has the effect of changing the calculated default scale factor for
the following devices:

| Device     | Device DPI | Old scale factor | New scale factor |
| ---------- | ---------- | ---------------- | ---------------- |
| Steam Deck | 204        | 1.5              | 1.25             |
| PinePhone  | 268        | 2.0              | 1.75             |
| OnePlus    | 388        | 3.0              | 2.5              |

Devices are taken from the autotest data found at
https://invent.kde.org/plasma/kscreen/-/blob/master/tests/kded/configs/AutogeneratedMobileScreenScales.json
2024-03-08 17:14:16 +00:00

988 lines
42 KiB
C++

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "outputconfigurationstore.h"
#include "core/iccprofile.h"
#include "core/inputdevice.h"
#include "core/output.h"
#include "core/outputbackend.h"
#include "core/outputconfiguration.h"
#include "input.h"
#include "input_event.h"
#include "kscreenintegration.h"
#include "workspace.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QOrientationReading>
namespace KWin
{
OutputConfigurationStore::OutputConfigurationStore()
{
load();
}
OutputConfigurationStore::~OutputConfigurationStore()
{
save();
}
std::optional<std::tuple<OutputConfiguration, QList<Output *>, OutputConfigurationStore::ConfigType>> OutputConfigurationStore::queryConfig(const QList<Output *> &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode)
{
QList<Output *> relevantOutputs;
std::copy_if(outputs.begin(), outputs.end(), std::back_inserter(relevantOutputs), [](Output *output) {
return !output->isNonDesktop() && !output->isPlaceholder();
});
if (relevantOutputs.isEmpty()) {
return std::nullopt;
}
if (const auto opt = findSetup(relevantOutputs, isLidClosed)) {
const auto &[setup, outputStates] = *opt;
auto [config, order] = setupToConfig(setup, outputStates);
applyOrientationReading(config, relevantOutputs, orientation, isTabletMode);
storeConfig(relevantOutputs, isLidClosed, config, order);
return std::make_tuple(config, order, ConfigType::Preexisting);
}
if (auto kscreenConfig = KScreenIntegration::readOutputConfig(relevantOutputs, KScreenIntegration::connectedOutputsHash(relevantOutputs, isLidClosed))) {
auto &[config, order] = *kscreenConfig;
applyOrientationReading(config, relevantOutputs, orientation, isTabletMode);
storeConfig(relevantOutputs, isLidClosed, config, order);
return std::make_tuple(config, order, ConfigType::Preexisting);
}
auto [config, order] = generateConfig(relevantOutputs, isLidClosed);
applyOrientationReading(config, relevantOutputs, orientation, isTabletMode);
storeConfig(relevantOutputs, isLidClosed, config, order);
return std::make_tuple(config, order, ConfigType::Generated);
}
void OutputConfigurationStore::applyOrientationReading(OutputConfiguration &config, const QList<Output *> &outputs, QOrientationReading *orientation, bool isTabletMode)
{
const auto output = std::find_if(outputs.begin(), outputs.end(), [&config](Output *output) {
return output->isInternal() && config.changeSet(output)->enabled.value_or(output->isEnabled());
});
if (output == outputs.end()) {
return;
}
// TODO move other outputs to matching positions
const auto changeset = config.changeSet(*output);
if (!isAutoRotateActive(outputs, isTabletMode)) {
changeset->transform = changeset->manualTransform;
return;
}
const auto panelOrientation = (*output)->panelOrientation();
switch (orientation->orientation()) {
case QOrientationReading::Orientation::TopUp:
changeset->transform = panelOrientation;
return;
case QOrientationReading::Orientation::TopDown:
changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate180);
return;
case QOrientationReading::Orientation::LeftUp:
changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate90);
return;
case QOrientationReading::Orientation::RightUp:
changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate270);
return;
case QOrientationReading::Orientation::FaceUp:
case QOrientationReading::Orientation::FaceDown:
return;
case QOrientationReading::Orientation::Undefined:
changeset->transform = changeset->manualTransform;
return;
}
}
std::optional<std::pair<OutputConfigurationStore::Setup *, std::unordered_map<Output *, size_t>>> OutputConfigurationStore::findSetup(const QList<Output *> &outputs, bool lidClosed)
{
std::unordered_map<Output *, size_t> outputStates;
for (Output *output : outputs) {
if (auto opt = findOutput(output, outputs)) {
outputStates[output] = *opt;
} else {
return std::nullopt;
}
}
const auto setup = std::find_if(m_setups.begin(), m_setups.end(), [lidClosed, &outputStates](const auto &setup) {
if (setup.lidClosed != lidClosed || size_t(setup.outputs.size()) != outputStates.size()) {
return false;
}
return std::all_of(outputStates.begin(), outputStates.end(), [&setup](const auto &outputIt) {
return std::any_of(setup.outputs.begin(), setup.outputs.end(), [&outputIt](const auto &outputInfo) {
return outputInfo.outputIndex == outputIt.second;
});
});
});
if (setup == m_setups.end()) {
return std::nullopt;
} else {
return std::make_pair(&(*setup), outputStates);
}
}
std::optional<size_t> OutputConfigurationStore::findOutput(Output *output, const QList<Output *> &allOutputs) const
{
const bool uniqueEdid = !output->edid().identifier().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) {
return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier();
});
const bool uniqueEdidHash = !output->edid().hash().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) {
return otherOutput != output && otherOutput->edid().hash() == output->edid().hash();
});
const bool uniqueMst = !output->mstPath().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) {
return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier() && otherOutput->mstPath() == output->mstPath();
});
auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) {
if (output->edid().isValid()) {
if (outputState.edidIdentifier != output->edid().identifier()) {
return false;
} else if (uniqueEdid) {
return true;
}
}
if (!output->edid().hash().isEmpty()) {
if (outputState.edidHash != output->edid().hash()) {
return false;
} else if (uniqueEdidHash) {
return true;
}
}
if (outputState.mstPath != output->mstPath()) {
return false;
} else if (uniqueMst) {
return true;
}
return outputState.connectorName == output->name();
});
if (it == m_outputs.end() && uniqueEdidHash) {
// handle the edge case of EDID parsing failing in the past but not failing anymore
it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) {
return outputState.edidHash == output->edid().hash();
});
}
if (it != m_outputs.end()) {
return std::distance(m_outputs.begin(), it);
} else {
return std::nullopt;
}
}
void OutputConfigurationStore::storeConfig(const QList<Output *> &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QList<Output *> &outputOrder)
{
QList<Output *> relevantOutputs;
std::copy_if(allOutputs.begin(), allOutputs.end(), std::back_inserter(relevantOutputs), [](Output *output) {
return !output->isNonDesktop() && !output->isPlaceholder();
});
if (relevantOutputs.isEmpty()) {
return;
}
const auto opt = findSetup(relevantOutputs, isLidClosed);
Setup *setup = nullptr;
if (opt) {
setup = opt->first;
} else {
m_setups.push_back(Setup{});
setup = &m_setups.back();
setup->lidClosed = isLidClosed;
}
for (Output *output : relevantOutputs) {
auto outputIndex = findOutput(output, outputOrder);
if (!outputIndex) {
m_outputs.push_back(OutputState{});
outputIndex = m_outputs.size() - 1;
}
auto outputIt = std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex](const auto &output) {
return output.outputIndex == outputIndex;
});
if (outputIt == setup->outputs.end()) {
setup->outputs.push_back(SetupState{});
outputIt = setup->outputs.end() - 1;
}
if (const auto changeSet = config.constChangeSet(output)) {
std::shared_ptr<OutputMode> mode = changeSet->mode.value_or(output->currentMode()).lock();
if (!mode) {
mode = output->currentMode();
}
m_outputs[*outputIndex] = OutputState{
.edidIdentifier = output->edid().identifier(),
.connectorName = output->name(),
.edidHash = output->edid().isValid() ? output->edid().hash() : QString{},
.mstPath = output->mstPath(),
.mode = ModeData{
.size = mode->size(),
.refreshRate = mode->refreshRate(),
},
.scale = changeSet->scale.value_or(output->scale()),
.transform = changeSet->transform.value_or(output->transform()),
.manualTransform = changeSet->manualTransform.value_or(output->manualTransform()),
.overscan = changeSet->overscan.value_or(output->overscan()),
.rgbRange = changeSet->rgbRange.value_or(output->rgbRange()),
.vrrPolicy = changeSet->vrrPolicy.value_or(output->vrrPolicy()),
.highDynamicRange = changeSet->highDynamicRange.value_or(output->highDynamicRange()),
.sdrBrightness = changeSet->sdrBrightness.value_or(output->sdrBrightness()),
.wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()),
.autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()),
.iccProfilePath = changeSet->iccProfilePath.value_or(output->iccProfilePath()),
.maxPeakBrightnessOverride = changeSet->maxPeakBrightnessOverride.value_or(output->maxPeakBrightnessOverride()),
.maxAverageBrightnessOverride = changeSet->maxAverageBrightnessOverride.value_or(output->maxAverageBrightnessOverride()),
.minBrightnessOverride = changeSet->minBrightnessOverride.value_or(output->minBrightnessOverride()),
.sdrGamutWideness = changeSet->sdrGamutWideness.value_or(output->sdrGamutWideness()),
};
*outputIt = SetupState{
.outputIndex = *outputIndex,
.position = changeSet->pos.value_or(output->geometry().topLeft()),
.enabled = changeSet->enabled.value_or(output->isEnabled()),
.priority = int(outputOrder.indexOf(output)),
};
} else {
const auto mode = output->currentMode();
m_outputs[*outputIndex] = OutputState{
.edidIdentifier = output->edid().identifier(),
.connectorName = output->name(),
.edidHash = output->edid().isValid() ? output->edid().hash() : QString{},
.mstPath = output->mstPath(),
.mode = ModeData{
.size = mode->size(),
.refreshRate = mode->refreshRate(),
},
.scale = output->scale(),
.transform = output->transform(),
.manualTransform = output->manualTransform(),
.overscan = output->overscan(),
.rgbRange = output->rgbRange(),
.vrrPolicy = output->vrrPolicy(),
.highDynamicRange = output->highDynamicRange(),
.sdrBrightness = output->sdrBrightness(),
.wideColorGamut = output->wideColorGamut(),
.autoRotation = output->autoRotationPolicy(),
.iccProfilePath = output->iccProfilePath(),
.maxPeakBrightnessOverride = output->maxPeakBrightnessOverride(),
.maxAverageBrightnessOverride = output->maxAverageBrightnessOverride(),
.minBrightnessOverride = output->minBrightnessOverride(),
.sdrGamutWideness = output->sdrGamutWideness(),
};
*outputIt = SetupState{
.outputIndex = *outputIndex,
.position = output->geometry().topLeft(),
.enabled = output->isEnabled(),
.priority = int(outputOrder.indexOf(output)),
};
}
}
save();
}
std::pair<OutputConfiguration, QList<Output *>> OutputConfigurationStore::setupToConfig(Setup *setup, const std::unordered_map<Output *, size_t> &outputMap) const
{
OutputConfiguration ret;
QList<std::pair<Output *, size_t>> priorities;
for (const auto &[output, outputIndex] : outputMap) {
const OutputState &state = m_outputs[outputIndex];
const auto &setupState = *std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex = outputIndex](const auto &state) {
return state.outputIndex == outputIndex;
});
const auto modes = output->modes();
const auto mode = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) {
return state.mode
&& mode->size() == state.mode->size
&& mode->refreshRate() == state.mode->refreshRate;
});
*ret.changeSet(output) = OutputChangeSet{
.mode = mode == modes.end() ? std::nullopt : std::optional(*mode),
.enabled = setupState.enabled,
.pos = setupState.position,
.scale = state.scale,
.transform = state.transform,
.manualTransform = state.manualTransform,
.overscan = state.overscan,
.rgbRange = state.rgbRange,
.vrrPolicy = state.vrrPolicy,
.highDynamicRange = state.highDynamicRange,
.sdrBrightness = state.sdrBrightness,
.wideColorGamut = state.wideColorGamut,
.autoRotationPolicy = state.autoRotation,
.iccProfilePath = state.iccProfilePath,
.iccProfile = state.iccProfilePath ? IccProfile::load(*state.iccProfilePath) : nullptr,
.maxPeakBrightnessOverride = state.maxPeakBrightnessOverride,
.maxAverageBrightnessOverride = state.maxAverageBrightnessOverride,
.minBrightnessOverride = state.minBrightnessOverride,
.sdrGamutWideness = state.sdrGamutWideness,
};
if (setupState.enabled) {
priorities.push_back(std::make_pair(output, setupState.priority));
}
}
std::sort(priorities.begin(), priorities.end(), [](const auto &left, const auto &right) {
return left.second < right.second;
});
QList<Output *> order;
std::transform(priorities.begin(), priorities.end(), std::back_inserter(order), [](const auto &pair) {
return pair.first;
});
return std::make_pair(ret, order);
}
std::optional<std::pair<OutputConfiguration, QList<Output *>>> OutputConfigurationStore::generateLidClosedConfig(const QList<Output *> &outputs)
{
const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) {
return output->isInternal();
});
if (internalIt == outputs.end()) {
return std::nullopt;
}
const auto setup = findSetup(outputs, false);
if (!setup) {
return std::nullopt;
}
Output *const internalOutput = *internalIt;
auto [config, order] = setupToConfig(setup->first, setup->second);
auto internalChangeset = config.changeSet(internalOutput);
internalChangeset->enabled = false;
order.removeOne(internalOutput);
const bool anyEnabled = std::any_of(outputs.begin(), outputs.end(), [&config = config](Output *output) {
return config.changeSet(output)->enabled.value_or(output->isEnabled());
});
if (!anyEnabled) {
return std::nullopt;
}
const auto getSize = [](OutputChangeSet *changeset, Output *output) {
auto mode = changeset->mode ? changeset->mode->lock() : nullptr;
if (!mode) {
mode = output->currentMode();
}
const auto scale = changeset->scale.value_or(output->scale());
return QSize(std::ceil(mode->size().width() / scale), std::ceil(mode->size().height() / scale));
};
const QPoint internalPos = internalChangeset->pos.value_or(internalOutput->geometry().topLeft());
const QSize internalSize = getSize(internalChangeset.get(), internalOutput);
for (Output *otherOutput : outputs) {
auto changeset = config.changeSet(otherOutput);
QPoint otherPos = changeset->pos.value_or(otherOutput->geometry().topLeft());
if (otherPos.x() >= internalPos.x() + internalSize.width()) {
otherPos.rx() -= std::floor(internalSize.width());
}
if (otherPos.y() >= internalPos.y() + internalSize.height()) {
otherPos.ry() -= std::floor(internalSize.height());
}
// make sure this doesn't make outputs overlap, which is neither supported nor expected by users
const QSize otherSize = getSize(changeset.get(), otherOutput);
const bool overlap = std::any_of(outputs.begin(), outputs.end(), [&, &config = config](Output *output) {
if (otherOutput == output) {
return false;
}
const auto changeset = config.changeSet(output);
const QPoint pos = changeset->pos.value_or(output->geometry().topLeft());
return QRect(pos, otherSize).intersects(QRect(otherPos, getSize(changeset.get(), output)));
});
if (!overlap) {
changeset->pos = otherPos;
}
}
return std::make_pair(config, order);
}
std::pair<OutputConfiguration, QList<Output *>> OutputConfigurationStore::generateConfig(const QList<Output *> &outputs, bool isLidClosed)
{
if (isLidClosed) {
if (const auto closedConfig = generateLidClosedConfig(outputs)) {
return *closedConfig;
}
}
OutputConfiguration ret;
QList<Output *> outputOrder;
QPoint pos(0, 0);
for (const auto output : outputs) {
const auto outputIndex = findOutput(output, outputs);
const bool enable = !isLidClosed || !output->isInternal() || outputs.size() == 1;
const OutputState existingData = outputIndex ? m_outputs[*outputIndex] : OutputState{};
const auto modes = output->modes();
const auto modeIt = std::find_if(modes.begin(), modes.end(), [&existingData](const auto &mode) {
return existingData.mode
&& mode->size() == existingData.mode->size
&& mode->refreshRate() == existingData.mode->refreshRate;
});
const auto mode = modeIt == modes.end() ? output->currentMode() : *modeIt;
const auto changeset = ret.changeSet(output);
*changeset = {
.mode = mode,
.enabled = enable,
.pos = pos,
.scale = existingData.scale.value_or(chooseScale(output, mode.get())),
.transform = existingData.transform.value_or(output->panelOrientation()),
.manualTransform = existingData.manualTransform.value_or(output->panelOrientation()),
.overscan = existingData.overscan.value_or(0),
.rgbRange = existingData.rgbRange.value_or(Output::RgbRange::Automatic),
.vrrPolicy = existingData.vrrPolicy.value_or(VrrPolicy::Automatic),
.highDynamicRange = existingData.highDynamicRange.value_or(false),
.sdrBrightness = existingData.sdrBrightness.value_or(200),
.wideColorGamut = existingData.wideColorGamut.value_or(false),
.autoRotationPolicy = existingData.autoRotation.value_or(Output::AutoRotationPolicy::InTabletMode),
};
if (enable) {
const auto modeSize = changeset->transform->map(mode->size());
pos.setX(std::ceil(pos.x() + modeSize.width() / *changeset->scale));
outputOrder.push_back(output);
}
}
return std::make_pair(ret, outputs);
}
std::shared_ptr<OutputMode> OutputConfigurationStore::chooseMode(Output *output) const
{
const auto modes = output->modes();
// some displays advertise bigger modes than their native resolution
// to avoid that, take the preferred mode into account, which is usually the native one
const auto preferred = std::find_if(modes.begin(), modes.end(), [](const auto &mode) {
return mode->flags() & OutputMode::Flag::Preferred;
});
if (preferred != modes.end()) {
// some high refresh rate displays advertise a 60Hz mode as preferred for compatibility reasons
// ignore that and choose the highest possible refresh rate by default instead
std::shared_ptr<OutputMode> highestRefresh = *preferred;
for (const auto &mode : modes) {
if (mode->size() == highestRefresh->size() && mode->refreshRate() > highestRefresh->refreshRate()) {
highestRefresh = mode;
}
}
// if the preferred mode size has a refresh rate that's too low for PCs,
// allow falling back to a mode with lower resolution and a more usable refresh rate
if (highestRefresh->refreshRate() >= 50000) {
return highestRefresh;
}
}
std::shared_ptr<OutputMode> ret;
for (auto mode : modes) {
if (mode->flags() & OutputMode::Flag::Generated) {
// generated modes aren't guaranteed to work, so don't choose one as the default
continue;
}
if (!ret) {
ret = mode;
continue;
}
const bool retUsableRefreshRate = ret->refreshRate() >= 50000;
const bool usableRefreshRate = mode->refreshRate() >= 50000;
if (retUsableRefreshRate && !usableRefreshRate) {
ret = mode;
continue;
}
if ((usableRefreshRate && !retUsableRefreshRate)
|| mode->size().width() > ret->size().width()
|| mode->size().height() > ret->size().height()
|| (mode->size() == ret->size() && mode->refreshRate() > ret->refreshRate())) {
ret = mode;
}
}
return ret;
}
double OutputConfigurationStore::chooseScale(Output *output, OutputMode *mode) const
{
if (output->physicalSize().height() <= 0) {
// invalid size, can't do anything with this
return 1.0;
}
const double outputDpi = mode->size().height() / (output->physicalSize().height() / 25.4);
const double desiredScale = outputDpi / targetDpi(output);
// round to 25% steps
return std::clamp(std::round(100.0 * desiredScale / 25.0) * 25.0 / 100.0, 1.0, 3.0);
}
double OutputConfigurationStore::targetDpi(Output *output) const
{
// The eye's ability to perceive detail diminishes with distance, so objects
// that are closer can be smaller and their details remain equally
// distinguishable. As a result, each device type has its own ideal physical
// size of items on its screen based on how close the user's eyes are
// expected to be from it on average, and its target DPI value needs to be
// changed accordingly.
const auto devices = input()->devices();
const bool hasLaptopLid = std::any_of(devices.begin(), devices.end(), [](const auto &device) {
return device->isLidSwitch();
});
if (output->isInternal()) {
if (hasLaptopLid) {
// laptop screens: usually closer to the face than desktop monitors
return 125;
} else {
// phone screens: even closer than laptops
return 150;
}
} else {
// "normal" 1x scale desktop monitor dpi
return 96;
}
}
void OutputConfigurationStore::load()
{
const QString jsonPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json"));
if (jsonPath.isEmpty()) {
return;
}
QFile f(jsonPath);
if (!f.open(QIODevice::ReadOnly)) {
qCWarning(KWIN_CORE) << "Could not open file" << jsonPath;
return;
}
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(KWIN_CORE) << "Failed to parse" << jsonPath << error.errorString();
return;
}
const auto array = doc.array();
std::vector<QJsonObject> objects;
std::transform(array.begin(), array.end(), std::back_inserter(objects), [](const auto &json) {
return json.toObject();
});
const auto outputsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) {
return obj["name"].toString() == "outputs" && obj["data"].isArray();
});
const auto setupsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) {
return obj["name"].toString() == "setups" && obj["data"].isArray();
});
if (outputsIt == objects.end() || setupsIt == objects.end()) {
return;
}
const auto outputs = (*outputsIt)["data"].toArray();
std::vector<std::optional<OutputState>> outputDatas;
for (const auto &output : outputs) {
const auto data = output.toObject();
OutputState state;
bool hasIdentifier = false;
if (const auto it = data.find("edidIdentifier"); it != data.end()) {
if (const auto str = it->toString(); !str.isEmpty()) {
state.edidIdentifier = str;
hasIdentifier = true;
}
}
if (const auto it = data.find("edidHash"); it != data.end()) {
if (const auto str = it->toString(); !str.isEmpty()) {
state.edidHash = str;
hasIdentifier = true;
}
}
if (const auto it = data.find("connectorName"); it != data.end()) {
if (const auto str = it->toString(); !str.isEmpty()) {
state.connectorName = str;
hasIdentifier = true;
}
}
if (const auto it = data.find("mstPath"); it != data.end()) {
if (const auto str = it->toString(); !str.isEmpty()) {
state.mstPath = str;
hasIdentifier = true;
}
}
if (!hasIdentifier) {
// without an identifier the settings are useless
// we still have to push something into the list so that the indices stay correct
outputDatas.push_back(std::nullopt);
qCWarning(KWIN_CORE, "Output in config is missing identifiers");
continue;
}
const bool hasDuplicate = std::any_of(outputDatas.begin(), outputDatas.end(), [&state](const auto &data) {
return data
&& data->edidIdentifier == state.edidIdentifier
&& data->edidHash == state.edidHash
&& data->mstPath == state.mstPath
&& data->connectorName == state.connectorName;
});
if (hasDuplicate) {
qCWarning(KWIN_CORE) << "Duplicate output found in config for edidIdentifier:" << state.edidIdentifier.value_or("<empty>") << "; connectorName:" << state.connectorName.value_or("<empty>") << "; mstPath:" << state.mstPath;
outputDatas.push_back(std::nullopt);
continue;
}
if (const auto it = data.find("mode"); it != data.end()) {
const auto obj = it->toObject();
const int width = obj["width"].toInt(0);
const int height = obj["height"].toInt(0);
const int refreshRate = obj["refreshRate"].toInt(0);
if (width > 0 && height > 0 && refreshRate > 0) {
state.mode = ModeData{
.size = QSize(width, height),
.refreshRate = uint32_t(refreshRate),
};
}
}
if (const auto it = data.find("scale"); it != data.end()) {
const double scale = it->toDouble(0);
if (scale > 0 && scale <= 3) {
state.scale = scale;
}
}
if (const auto it = data.find("transform"); it != data.end()) {
const auto str = it->toString();
if (str == "Normal") {
state.transform = state.manualTransform = OutputTransform::Kind::Normal;
} else if (str == "Rotated90") {
state.transform = state.manualTransform = OutputTransform::Kind::Rotate90;
} else if (str == "Rotated180") {
state.transform = state.manualTransform = OutputTransform::Kind::Rotate180;
} else if (str == "Rotated270") {
state.transform = state.manualTransform = OutputTransform::Kind::Rotate270;
} else if (str == "Flipped") {
state.transform = state.manualTransform = OutputTransform::Kind::FlipX;
} else if (str == "Flipped90") {
state.transform = state.manualTransform = OutputTransform::Kind::FlipX90;
} else if (str == "Flipped180") {
state.transform = state.manualTransform = OutputTransform::Kind::FlipX180;
} else if (str == "Flipped270") {
state.transform = state.manualTransform = OutputTransform::Kind::FlipX270;
}
}
if (const auto it = data.find("overscan"); it != data.end()) {
const int overscan = it->toInt(-1);
if (overscan >= 0 && overscan <= 100) {
state.overscan = overscan;
}
}
if (const auto it = data.find("rgbRange"); it != data.end()) {
const auto str = it->toString();
if (str == "Automatic") {
state.rgbRange = Output::RgbRange::Automatic;
} else if (str == "Limited") {
state.rgbRange = Output::RgbRange::Limited;
} else if (str == "Full") {
state.rgbRange = Output::RgbRange::Full;
}
}
if (const auto it = data.find("vrrPolicy"); it != data.end()) {
const auto str = it->toString();
if (str == "Never") {
state.vrrPolicy = VrrPolicy::Never;
} else if (str == "Automatic") {
state.vrrPolicy = VrrPolicy::Automatic;
} else if (str == "Always") {
state.vrrPolicy = VrrPolicy::Always;
}
}
if (const auto it = data.find("highDynamicRange"); it != data.end() && it->isBool()) {
state.highDynamicRange = it->toBool();
}
if (const auto it = data.find("sdrBrightness"); it != data.end() && it->isDouble()) {
state.sdrBrightness = it->toInt(200);
}
if (const auto it = data.find("wideColorGamut"); it != data.end() && it->isBool()) {
state.wideColorGamut = it->toBool();
}
if (const auto it = data.find("autoRotation"); it != data.end()) {
const auto str = it->toString();
if (str == "Never") {
state.autoRotation = Output::AutoRotationPolicy::Never;
} else if (str == "InTabletMode") {
state.autoRotation = Output::AutoRotationPolicy::InTabletMode;
} else if (str == "Always") {
state.autoRotation = Output::AutoRotationPolicy::Always;
}
}
if (const auto it = data.find("iccProfilePath"); it != data.end()) {
state.iccProfilePath = it->toString();
}
if (const auto it = data.find("maxPeakBrightnessOverride"); it != data.end() && it->isDouble()) {
state.maxPeakBrightnessOverride = it->toDouble();
}
if (const auto it = data.find("maxAverageBrightnessOverride"); it != data.end() && it->isDouble()) {
state.maxAverageBrightnessOverride = it->toDouble();
}
if (const auto it = data.find("minBrightnessOverride"); it != data.end() && it->isDouble()) {
state.minBrightnessOverride = it->toDouble();
}
if (const auto it = data.find("sdrGamutWideness"); it != data.end() && it->isDouble()) {
state.sdrGamutWideness = it->toDouble();
}
outputDatas.push_back(state);
}
const auto setups = (*setupsIt)["data"].toArray();
for (const auto &s : setups) {
const auto data = s.toObject();
const auto outputs = data["outputs"].toArray();
Setup setup;
bool fail = false;
for (const auto &output : outputs) {
const auto outputData = output.toObject();
SetupState state;
if (const auto it = outputData.find("enabled"); it != outputData.end() && it->isBool()) {
state.enabled = it->toBool();
} else {
fail = true;
break;
}
if (const auto it = outputData.find("outputIndex"); it != outputData.end()) {
const int index = it->toInt(-1);
if (index <= -1 || size_t(index) >= outputDatas.size()) {
fail = true;
break;
}
// the outputs must be unique
const bool unique = std::none_of(setup.outputs.begin(), setup.outputs.end(), [&index](const auto &output) {
return output.outputIndex == size_t(index);
});
if (!unique) {
fail = true;
break;
}
state.outputIndex = index;
}
if (const auto it = outputData.find("position"); it != outputData.end()) {
const auto obj = it->toObject();
const auto x = obj.find("x");
const auto y = obj.find("y");
if (x == obj.end() || !x->isDouble() || y == obj.end() || !y->isDouble()) {
fail = true;
break;
}
state.position = QPoint(x->toInt(0), y->toInt(0));
} else {
fail = true;
break;
}
if (const auto it = outputData.find("priority"); it != outputData.end()) {
state.priority = it->toInt(-1);
if (state.priority < 0 && state.enabled) {
fail = true;
break;
}
}
setup.outputs.push_back(state);
}
if (fail || setup.outputs.empty()) {
continue;
}
// one of the outputs must be enabled
const bool noneEnabled = std::none_of(setup.outputs.begin(), setup.outputs.end(), [](const auto &output) {
return output.enabled;
});
if (noneEnabled) {
continue;
}
setup.lidClosed = data["lidClosed"].toBool(false);
// there must be only one setup that refers to a given set of outputs
const bool alreadyExists = std::any_of(m_setups.begin(), m_setups.end(), [&setup](const auto &other) {
if (setup.lidClosed != other.lidClosed || setup.outputs.size() != other.outputs.size()) {
return false;
}
return std::all_of(setup.outputs.begin(), setup.outputs.end(), [&other](const auto &output) {
return std::any_of(other.outputs.begin(), other.outputs.end(), [&output](const auto &otherOutput) {
return output.outputIndex == otherOutput.outputIndex;
});
});
});
if (alreadyExists) {
continue;
}
m_setups.push_back(setup);
}
// repair the outputs list in case it's broken
for (size_t i = 0; i < outputDatas.size();) {
if (!outputDatas[i]) {
outputDatas.erase(outputDatas.begin() + i);
for (auto setupIt = m_setups.begin(); setupIt != m_setups.end();) {
const bool broken = std::any_of(setupIt->outputs.begin(), setupIt->outputs.end(), [i](const auto &output) {
return output.outputIndex == i;
});
if (broken) {
setupIt = m_setups.erase(setupIt);
continue;
}
for (auto &output : setupIt->outputs) {
if (output.outputIndex > i) {
output.outputIndex--;
}
}
setupIt++;
}
} else {
i++;
}
}
for (const auto &o : outputDatas) {
Q_ASSERT(o);
m_outputs.push_back(*o);
}
}
void OutputConfigurationStore::save()
{
QJsonDocument document;
QJsonArray array;
QJsonObject outputs;
outputs["name"] = "outputs";
QJsonArray outputsData;
for (const auto &output : m_outputs) {
QJsonObject o;
if (output.edidIdentifier) {
o["edidIdentifier"] = *output.edidIdentifier;
}
if (!output.edidHash.isEmpty()) {
o["edidHash"] = output.edidHash;
}
if (output.connectorName) {
o["connectorName"] = *output.connectorName;
}
if (!output.mstPath.isEmpty()) {
o["mstPath"] = output.mstPath;
}
if (output.mode) {
QJsonObject mode;
mode["width"] = output.mode->size.width();
mode["height"] = output.mode->size.height();
mode["refreshRate"] = int(output.mode->refreshRate);
o["mode"] = mode;
}
if (output.scale) {
o["scale"] = *output.scale;
}
if (output.manualTransform == OutputTransform::Kind::Normal) {
o["transform"] = "Normal";
} else if (output.manualTransform == OutputTransform::Kind::Rotate90) {
o["transform"] = "Rotated90";
} else if (output.manualTransform == OutputTransform::Kind::Rotate180) {
o["transform"] = "Rotated180";
} else if (output.manualTransform == OutputTransform::Kind::Rotate270) {
o["transform"] = "Rotated270";
} else if (output.manualTransform == OutputTransform::Kind::FlipX) {
o["transform"] = "Flipped";
} else if (output.manualTransform == OutputTransform::Kind::FlipX90) {
o["transform"] = "Flipped90";
} else if (output.manualTransform == OutputTransform::Kind::FlipX180) {
o["transform"] = "Flipped180";
} else if (output.manualTransform == OutputTransform::Kind::FlipX270) {
o["transform"] = "Flipped270";
}
if (output.overscan) {
o["overscan"] = int(*output.overscan);
}
if (output.rgbRange == Output::RgbRange::Automatic) {
o["rgbRange"] = "Automatic";
} else if (output.rgbRange == Output::RgbRange::Limited) {
o["rgbRange"] = "Limited";
} else if (output.rgbRange == Output::RgbRange::Full) {
o["rgbRange"] = "Full";
}
if (output.vrrPolicy == VrrPolicy::Never) {
o["vrrPolicy"] = "Never";
} else if (output.vrrPolicy == VrrPolicy::Automatic) {
o["vrrPolicy"] = "Automatic";
} else if (output.vrrPolicy == VrrPolicy::Always) {
o["vrrPolicy"] = "Always";
}
if (output.highDynamicRange) {
o["highDynamicRange"] = *output.highDynamicRange;
}
if (output.sdrBrightness) {
o["sdrBrightness"] = int(*output.sdrBrightness);
}
if (output.wideColorGamut) {
o["wideColorGamut"] = *output.wideColorGamut;
}
if (output.autoRotation) {
switch (*output.autoRotation) {
case Output::AutoRotationPolicy::Never:
o["autoRotation"] = "Never";
break;
case Output::AutoRotationPolicy::InTabletMode:
o["autoRotation"] = "InTabletMode";
break;
case Output::AutoRotationPolicy::Always:
o["autoRotation"] = "Always";
break;
}
}
if (output.iccProfilePath) {
o["iccProfilePath"] = *output.iccProfilePath;
}
if (output.maxPeakBrightnessOverride) {
o["maxPeakBrightnessOverride"] = *output.maxPeakBrightnessOverride;
}
if (output.maxAverageBrightnessOverride) {
o["maxAverageBrightnessOverride"] = *output.maxAverageBrightnessOverride;
}
if (output.minBrightnessOverride) {
o["minBrightnessOverride"] = *output.minBrightnessOverride;
}
if (output.sdrGamutWideness) {
o["sdrGamutWideness"] = *output.sdrGamutWideness;
}
outputsData.append(o);
}
outputs["data"] = outputsData;
array.append(outputs);
QJsonObject setups;
setups["name"] = "setups";
QJsonArray setupData;
for (const auto &setup : m_setups) {
QJsonObject o;
o["lidClosed"] = setup.lidClosed;
QJsonArray outputs;
for (ssize_t i = 0; i < setup.outputs.size(); i++) {
const auto &output = setup.outputs[i];
QJsonObject o;
o["enabled"] = output.enabled;
o["outputIndex"] = int(output.outputIndex);
o["priority"] = output.priority;
QJsonObject pos;
pos["x"] = output.position.x();
pos["y"] = output.position.y();
o["position"] = pos;
outputs.append(o);
}
o["outputs"] = outputs;
setupData.append(o);
}
setups["data"] = setupData;
array.append(setups);
const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/kwinoutputconfig.json";
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
qCWarning(KWIN_CORE, "Couldn't open output config file %s", qPrintable(path));
return;
}
document.setArray(array);
f.write(document.toJson());
f.flush();
}
bool OutputConfigurationStore::isAutoRotateActive(const QList<Output *> &outputs, bool isTabletMode) const
{
const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) {
return output->isInternal() && output->isEnabled();
});
if (internalIt == outputs.end()) {
return false;
}
Output *internal = *internalIt;
switch (internal->autoRotationPolicy()) {
case Output::AutoRotationPolicy::Never:
return false;
case Output::AutoRotationPolicy::InTabletMode:
return isTabletMode;
case Output::AutoRotationPolicy::Always:
return true;
}
Q_UNREACHABLE();
}
}