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
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(); |
|
} |
|
}
|
|
|