You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1072 lines
47 KiB

/*
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);
}
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)) {
QSize modeSize = changeSet->desiredModeSize.value_or(output->desiredModeSize());
if (modeSize.isEmpty()) {
modeSize = output->currentMode()->size();
}
uint32_t refreshRate = changeSet->desiredModeRefreshRate.value_or(output->desiredModeRefreshRate());
if (refreshRate == 0) {
refreshRate = output->currentMode()->refreshRate();
}
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 = modeSize,
.refreshRate = 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()),
.referenceLuminance = changeSet->referenceLuminance.value_or(output->referenceLuminance()),
.wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()),
.autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()),
.iccProfilePath = changeSet->iccProfilePath.value_or(output->iccProfilePath()),
.colorProfileSource = changeSet->colorProfileSource.value_or(output->colorProfileSource()),
.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()),
.brightness = changeSet->brightness.value_or(output->brightnessSetting()),
.allowSdrSoftwareBrightness = changeSet->allowSdrSoftwareBrightness.value_or(output->allowSdrSoftwareBrightness()),
};
*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 {
QSize modeSize = output->desiredModeSize();
if (modeSize.isEmpty()) {
modeSize = output->currentMode()->size();
}
uint32_t refreshRate = output->desiredModeRefreshRate();
if (refreshRate == 0) {
refreshRate = output->currentMode()->refreshRate();
}
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 = modeSize,
.refreshRate = refreshRate,
},
.scale = output->scale(),
.transform = output->transform(),
.manualTransform = output->manualTransform(),
.overscan = output->overscan(),
.rgbRange = output->rgbRange(),
.vrrPolicy = output->vrrPolicy(),
.highDynamicRange = output->highDynamicRange(),
.referenceLuminance = output->referenceLuminance(),
.wideColorGamut = output->wideColorGamut(),
.autoRotation = output->autoRotationPolicy(),
.iccProfilePath = output->iccProfilePath(),
.colorProfileSource = output->colorProfileSource(),
.maxPeakBrightnessOverride = output->maxPeakBrightnessOverride(),
.maxAverageBrightnessOverride = output->maxAverageBrightnessOverride(),
.minBrightnessOverride = output->minBrightnessOverride(),
.sdrGamutWideness = output->sdrGamutWideness(),
.brightness = output->brightnessSetting(),
.allowSdrSoftwareBrightness = output->allowSdrSoftwareBrightness(),
};
*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 modeIt = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) {
return state.mode
&& mode->size() == state.mode->size
&& mode->refreshRate() == state.mode->refreshRate;
});
std::optional<std::shared_ptr<OutputMode>> mode = modeIt == modes.end() ? std::nullopt : std::optional(*modeIt);
if (!mode.has_value() || !*mode || ((*mode)->flags() & OutputMode::Flag::Removed)) {
mode = chooseMode(output);
}
*ret.changeSet(output) = OutputChangeSet{
.mode = mode,
.desiredModeSize = state.mode.has_value() ? std::make_optional(state.mode->size) : std::nullopt,
.desiredModeRefreshRate = state.mode.has_value() ? std::make_optional(state.mode->refreshRate) : std::nullopt,
.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,
.referenceLuminance = state.referenceLuminance,
.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,
.colorProfileSource = state.colorProfileSource,
.brightness = state.brightness,
.allowSdrSoftwareBrightness = state.allowSdrSoftwareBrightness,
};
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;
}
}
const auto kscreenConfig = KScreenIntegration::readOutputConfig(outputs, KScreenIntegration::connectedOutputsHash(outputs, isLidClosed));
OutputConfiguration ret;
QList<Output *> outputOrder;
QPoint pos(0, 0);
for (const auto output : outputs) {
const auto kscreenChangeSetPtr = kscreenConfig ? kscreenConfig->first.constChangeSet(output) : nullptr;
const auto kscreenChangeSet = kscreenChangeSetPtr ? *kscreenChangeSetPtr : OutputChangeSet{};
const auto outputIndex = findOutput(output, outputs);
const bool enable = kscreenChangeSet.enabled.value_or(!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() ? kscreenChangeSet.mode.value_or(output->currentMode()).lock() : *modeIt;
const auto changeset = ret.changeSet(output);
*changeset = {
.mode = mode,
.desiredModeSize = mode->size(),
.desiredModeRefreshRate = mode->refreshRate(),
.enabled = kscreenChangeSet.enabled.value_or(enable),
.pos = pos,
// kscreen scale is unreliable because it gets overwritten with the value 1 on Xorg,
// and we don't know if it's from Xorg or the 5.27 Wayland session... so just ignore it
.scale = existingData.scale.value_or(chooseScale(output, mode.get())),
.transform = existingData.transform.value_or(kscreenChangeSet.transform.value_or(output->panelOrientation())),
.manualTransform = existingData.manualTransform.value_or(kscreenChangeSet.transform.value_or(output->panelOrientation())),
.overscan = existingData.overscan.value_or(kscreenChangeSet.overscan.value_or(0)),
.rgbRange = existingData.rgbRange.value_or(kscreenChangeSet.rgbRange.value_or(Output::RgbRange::Automatic)),
.vrrPolicy = existingData.vrrPolicy.value_or(kscreenChangeSet.vrrPolicy.value_or(VrrPolicy::Automatic)),
.highDynamicRange = existingData.highDynamicRange.value_or(false),
.referenceLuminance = existingData.referenceLuminance.value_or(std::clamp(output->maxAverageBrightness().value_or(200), 200.0, 500.0)),
.wideColorGamut = existingData.wideColorGamut.value_or(false),
.autoRotationPolicy = existingData.autoRotation.value_or(Output::AutoRotationPolicy::InTabletMode),
.colorProfileSource = existingData.colorProfileSource.value_or(Output::ColorProfileSource::sRGB),
.brightness = existingData.brightness.value_or(1.0),
.allowSdrSoftwareBrightness = existingData.allowSdrSoftwareBrightness.value_or(output->brightnessDevice() == nullptr),
};
if (enable) {
const auto modeSize = changeset->transform->map(mode->size());
pos.setX(std::ceil(pos.x() + modeSize.width() / *changeset->scale));
outputOrder.push_back(output);
}
}
if (kscreenConfig && kscreenConfig->second.size() == outputOrder.size()) {
// make sure the old output order is consistent with the enablement states of the outputs
const bool consistent = std::ranges::all_of(outputOrder, [&kscreenConfig](const auto output) {
return kscreenConfig->second.contains(output);
});
if (consistent) {
outputOrder = kscreenConfig->second;
}
}
return std::make_pair(ret, outputOrder);
}
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)
&& !(mode->flags() & OutputMode::Flag::Removed);
});
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() < 3 || output->physicalSize().width() < 3) {
// A screen less than 3mm wide or tall doesn't make any sense; these are
// all caused by the screen mis-reporting its size.
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.referenceLuminance = 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();
}
if (const auto it = data.find("colorProfileSource"); it != data.end()) {
const auto str = it->toString();
if (str == "sRGB") {
state.colorProfileSource = Output::ColorProfileSource::sRGB;
} else if (str == "ICC") {
state.colorProfileSource = Output::ColorProfileSource::ICC;
} else if (str == "EDID") {
state.colorProfileSource = Output::ColorProfileSource::EDID;
}
} else {
const bool icc = state.iccProfilePath && !state.iccProfilePath->isEmpty() && !state.highDynamicRange.value_or(false) && !state.wideColorGamut.value_or(false);
if (icc) {
state.colorProfileSource = Output::ColorProfileSource::ICC;
} else {
state.colorProfileSource = Output::ColorProfileSource::sRGB;
}
}
if (const auto it = data.find("brightness"); it != data.end() && it->isDouble()) {
state.brightness = std::clamp(it->toDouble(), 0.0, 1.0);
}
if (const auto it = data.find("allowSdrSoftwareBrightness"); it != data.end() && it->isBool()) {
state.allowSdrSoftwareBrightness = it->toBool();
}
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.referenceLuminance) {
o["sdrBrightness"] = int(*output.referenceLuminance);
}
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;
}
if (output.colorProfileSource) {
switch (*output.colorProfileSource) {
case Output::ColorProfileSource::sRGB:
o["colorProfileSource"] = "sRGB";
break;
case Output::ColorProfileSource::ICC:
o["colorProfileSource"] = "ICC";
break;
case Output::ColorProfileSource::EDID:
o["colorProfileSource"] = "EDID";
break;
}
}
if (output.brightness) {
o["brightness"] = *output.brightness;
}
if (output.allowSdrSoftwareBrightness) {
o["allowSdrSoftwareBrightness"] = *output.allowSdrSoftwareBrightness;
}
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();
}
}