File indexing completed on 2024-11-10 04:57:53
0001 /* 0002 KWin - the KDE window manager 0003 This file is part of the KDE project. 0004 0005 SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com> 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 #include "outputconfigurationstore.h" 0010 #include "core/iccprofile.h" 0011 #include "core/inputdevice.h" 0012 #include "core/output.h" 0013 #include "core/outputbackend.h" 0014 #include "core/outputconfiguration.h" 0015 #include "input.h" 0016 #include "input_event.h" 0017 #include "kscreenintegration.h" 0018 #include "workspace.h" 0019 0020 #include <QFile> 0021 #include <QJsonArray> 0022 #include <QJsonDocument> 0023 #include <QJsonObject> 0024 #include <QOrientationReading> 0025 0026 namespace KWin 0027 { 0028 0029 OutputConfigurationStore::OutputConfigurationStore() 0030 { 0031 load(); 0032 } 0033 0034 OutputConfigurationStore::~OutputConfigurationStore() 0035 { 0036 save(); 0037 } 0038 0039 std::optional<std::tuple<OutputConfiguration, QList<Output *>, OutputConfigurationStore::ConfigType>> OutputConfigurationStore::queryConfig(const QList<Output *> &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode) 0040 { 0041 QList<Output *> relevantOutputs; 0042 std::copy_if(outputs.begin(), outputs.end(), std::back_inserter(relevantOutputs), [](Output *output) { 0043 return !output->isNonDesktop() && !output->isPlaceholder(); 0044 }); 0045 if (relevantOutputs.isEmpty()) { 0046 return std::nullopt; 0047 } 0048 if (const auto opt = findSetup(relevantOutputs, isLidClosed)) { 0049 const auto &[setup, outputStates] = *opt; 0050 auto [config, order] = setupToConfig(setup, outputStates); 0051 applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); 0052 storeConfig(relevantOutputs, isLidClosed, config, order); 0053 return std::make_tuple(config, order, ConfigType::Preexisting); 0054 } 0055 if (auto kscreenConfig = KScreenIntegration::readOutputConfig(relevantOutputs, KScreenIntegration::connectedOutputsHash(relevantOutputs, isLidClosed))) { 0056 auto &[config, order] = *kscreenConfig; 0057 applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); 0058 storeConfig(relevantOutputs, isLidClosed, config, order); 0059 return std::make_tuple(config, order, ConfigType::Preexisting); 0060 } 0061 auto [config, order] = generateConfig(relevantOutputs, isLidClosed); 0062 applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); 0063 storeConfig(relevantOutputs, isLidClosed, config, order); 0064 return std::make_tuple(config, order, ConfigType::Generated); 0065 } 0066 0067 void OutputConfigurationStore::applyOrientationReading(OutputConfiguration &config, const QList<Output *> &outputs, QOrientationReading *orientation, bool isTabletMode) 0068 { 0069 const auto output = std::find_if(outputs.begin(), outputs.end(), [&config](Output *output) { 0070 return output->isInternal() && config.changeSet(output)->enabled.value_or(output->isEnabled()); 0071 }); 0072 if (output == outputs.end()) { 0073 return; 0074 } 0075 // TODO move other outputs to matching positions 0076 const auto changeset = config.changeSet(*output); 0077 if (!isAutoRotateActive(outputs, isTabletMode)) { 0078 changeset->transform = changeset->manualTransform; 0079 return; 0080 } 0081 const auto panelOrientation = (*output)->panelOrientation(); 0082 switch (orientation->orientation()) { 0083 case QOrientationReading::Orientation::TopUp: 0084 changeset->transform = panelOrientation; 0085 return; 0086 case QOrientationReading::Orientation::TopDown: 0087 changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate180); 0088 return; 0089 case QOrientationReading::Orientation::LeftUp: 0090 changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate90); 0091 return; 0092 case QOrientationReading::Orientation::RightUp: 0093 changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate270); 0094 return; 0095 case QOrientationReading::Orientation::FaceUp: 0096 case QOrientationReading::Orientation::FaceDown: 0097 return; 0098 case QOrientationReading::Orientation::Undefined: 0099 changeset->transform = changeset->manualTransform; 0100 return; 0101 } 0102 } 0103 0104 std::optional<std::pair<OutputConfigurationStore::Setup *, std::unordered_map<Output *, size_t>>> OutputConfigurationStore::findSetup(const QList<Output *> &outputs, bool lidClosed) 0105 { 0106 std::unordered_map<Output *, size_t> outputStates; 0107 for (Output *output : outputs) { 0108 if (auto opt = findOutput(output, outputs)) { 0109 outputStates[output] = *opt; 0110 } else { 0111 return std::nullopt; 0112 } 0113 } 0114 const auto setup = std::find_if(m_setups.begin(), m_setups.end(), [lidClosed, &outputStates](const auto &setup) { 0115 if (setup.lidClosed != lidClosed || size_t(setup.outputs.size()) != outputStates.size()) { 0116 return false; 0117 } 0118 return std::all_of(outputStates.begin(), outputStates.end(), [&setup](const auto &outputIt) { 0119 return std::any_of(setup.outputs.begin(), setup.outputs.end(), [&outputIt](const auto &outputInfo) { 0120 return outputInfo.outputIndex == outputIt.second; 0121 }); 0122 }); 0123 }); 0124 if (setup == m_setups.end()) { 0125 return std::nullopt; 0126 } else { 0127 return std::make_pair(&(*setup), outputStates); 0128 } 0129 } 0130 0131 std::optional<size_t> OutputConfigurationStore::findOutput(Output *output, const QList<Output *> &allOutputs) const 0132 { 0133 const bool uniqueEdid = !output->edid().identifier().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { 0134 return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier(); 0135 }); 0136 const bool uniqueEdidHash = !output->edid().hash().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { 0137 return otherOutput != output && otherOutput->edid().hash() == output->edid().hash(); 0138 }); 0139 const bool uniqueMst = !output->mstPath().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { 0140 return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier() && otherOutput->mstPath() == output->mstPath(); 0141 }); 0142 auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) { 0143 if (output->edid().isValid()) { 0144 if (outputState.edidIdentifier != output->edid().identifier()) { 0145 return false; 0146 } else if (uniqueEdid) { 0147 return true; 0148 } 0149 } 0150 if (!output->edid().hash().isEmpty()) { 0151 if (outputState.edidHash != output->edid().hash()) { 0152 return false; 0153 } else if (uniqueEdidHash) { 0154 return true; 0155 } 0156 } 0157 if (outputState.mstPath != output->mstPath()) { 0158 return false; 0159 } else if (uniqueMst) { 0160 return true; 0161 } 0162 return outputState.connectorName == output->name(); 0163 }); 0164 if (it == m_outputs.end() && uniqueEdidHash) { 0165 // handle the edge case of EDID parsing failing in the past but not failing anymore 0166 it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) { 0167 return outputState.edidHash == output->edid().hash(); 0168 }); 0169 } 0170 if (it != m_outputs.end()) { 0171 return std::distance(m_outputs.begin(), it); 0172 } else { 0173 return std::nullopt; 0174 } 0175 } 0176 0177 void OutputConfigurationStore::storeConfig(const QList<Output *> &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QList<Output *> &outputOrder) 0178 { 0179 QList<Output *> relevantOutputs; 0180 std::copy_if(allOutputs.begin(), allOutputs.end(), std::back_inserter(relevantOutputs), [](Output *output) { 0181 return !output->isNonDesktop() && !output->isPlaceholder(); 0182 }); 0183 if (relevantOutputs.isEmpty()) { 0184 return; 0185 } 0186 const auto opt = findSetup(relevantOutputs, isLidClosed); 0187 Setup *setup = nullptr; 0188 if (opt) { 0189 setup = opt->first; 0190 } else { 0191 m_setups.push_back(Setup{}); 0192 setup = &m_setups.back(); 0193 setup->lidClosed = isLidClosed; 0194 } 0195 for (Output *output : relevantOutputs) { 0196 auto outputIndex = findOutput(output, outputOrder); 0197 if (!outputIndex) { 0198 m_outputs.push_back(OutputState{}); 0199 outputIndex = m_outputs.size() - 1; 0200 } 0201 auto outputIt = std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex](const auto &output) { 0202 return output.outputIndex == outputIndex; 0203 }); 0204 if (outputIt == setup->outputs.end()) { 0205 setup->outputs.push_back(SetupState{}); 0206 outputIt = setup->outputs.end() - 1; 0207 } 0208 if (const auto changeSet = config.constChangeSet(output)) { 0209 std::shared_ptr<OutputMode> mode = changeSet->mode.value_or(output->currentMode()).lock(); 0210 if (!mode) { 0211 mode = output->currentMode(); 0212 } 0213 m_outputs[*outputIndex] = OutputState{ 0214 .edidIdentifier = output->edid().identifier(), 0215 .connectorName = output->name(), 0216 .edidHash = output->edid().isValid() ? output->edid().hash() : QString{}, 0217 .mstPath = output->mstPath(), 0218 .mode = ModeData{ 0219 .size = mode->size(), 0220 .refreshRate = mode->refreshRate(), 0221 }, 0222 .scale = changeSet->scale.value_or(output->scale()), 0223 .transform = changeSet->transform.value_or(output->transform()), 0224 .manualTransform = changeSet->manualTransform.value_or(output->manualTransform()), 0225 .overscan = changeSet->overscan.value_or(output->overscan()), 0226 .rgbRange = changeSet->rgbRange.value_or(output->rgbRange()), 0227 .vrrPolicy = changeSet->vrrPolicy.value_or(output->vrrPolicy()), 0228 .highDynamicRange = changeSet->highDynamicRange.value_or(output->highDynamicRange()), 0229 .sdrBrightness = changeSet->sdrBrightness.value_or(output->sdrBrightness()), 0230 .wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()), 0231 .autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()), 0232 .iccProfilePath = changeSet->iccProfilePath.value_or(output->iccProfilePath()), 0233 .maxPeakBrightnessOverride = changeSet->maxPeakBrightnessOverride.value_or(output->maxPeakBrightnessOverride()), 0234 .maxAverageBrightnessOverride = changeSet->maxAverageBrightnessOverride.value_or(output->maxAverageBrightnessOverride()), 0235 .minBrightnessOverride = changeSet->minBrightnessOverride.value_or(output->minBrightnessOverride()), 0236 .sdrGamutWideness = changeSet->sdrGamutWideness.value_or(output->sdrGamutWideness()), 0237 }; 0238 *outputIt = SetupState{ 0239 .outputIndex = *outputIndex, 0240 .position = changeSet->pos.value_or(output->geometry().topLeft()), 0241 .enabled = changeSet->enabled.value_or(output->isEnabled()), 0242 .priority = int(outputOrder.indexOf(output)), 0243 }; 0244 } else { 0245 const auto mode = output->currentMode(); 0246 m_outputs[*outputIndex] = OutputState{ 0247 .edidIdentifier = output->edid().identifier(), 0248 .connectorName = output->name(), 0249 .edidHash = output->edid().isValid() ? output->edid().hash() : QString{}, 0250 .mstPath = output->mstPath(), 0251 .mode = ModeData{ 0252 .size = mode->size(), 0253 .refreshRate = mode->refreshRate(), 0254 }, 0255 .scale = output->scale(), 0256 .transform = output->transform(), 0257 .manualTransform = output->manualTransform(), 0258 .overscan = output->overscan(), 0259 .rgbRange = output->rgbRange(), 0260 .vrrPolicy = output->vrrPolicy(), 0261 .highDynamicRange = output->highDynamicRange(), 0262 .sdrBrightness = output->sdrBrightness(), 0263 .wideColorGamut = output->wideColorGamut(), 0264 .autoRotation = output->autoRotationPolicy(), 0265 .iccProfilePath = output->iccProfilePath(), 0266 .maxPeakBrightnessOverride = output->maxPeakBrightnessOverride(), 0267 .maxAverageBrightnessOverride = output->maxAverageBrightnessOverride(), 0268 .minBrightnessOverride = output->minBrightnessOverride(), 0269 .sdrGamutWideness = output->sdrGamutWideness(), 0270 }; 0271 *outputIt = SetupState{ 0272 .outputIndex = *outputIndex, 0273 .position = output->geometry().topLeft(), 0274 .enabled = output->isEnabled(), 0275 .priority = int(outputOrder.indexOf(output)), 0276 }; 0277 } 0278 } 0279 save(); 0280 } 0281 0282 std::pair<OutputConfiguration, QList<Output *>> OutputConfigurationStore::setupToConfig(Setup *setup, const std::unordered_map<Output *, size_t> &outputMap) const 0283 { 0284 OutputConfiguration ret; 0285 QList<std::pair<Output *, size_t>> priorities; 0286 for (const auto &[output, outputIndex] : outputMap) { 0287 const OutputState &state = m_outputs[outputIndex]; 0288 const auto &setupState = *std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex = outputIndex](const auto &state) { 0289 return state.outputIndex == outputIndex; 0290 }); 0291 const auto modes = output->modes(); 0292 const auto mode = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) { 0293 return state.mode 0294 && mode->size() == state.mode->size 0295 && mode->refreshRate() == state.mode->refreshRate; 0296 }); 0297 *ret.changeSet(output) = OutputChangeSet{ 0298 .mode = mode == modes.end() ? std::nullopt : std::optional(*mode), 0299 .enabled = setupState.enabled, 0300 .pos = setupState.position, 0301 .scale = state.scale, 0302 .transform = state.transform, 0303 .manualTransform = state.manualTransform, 0304 .overscan = state.overscan, 0305 .rgbRange = state.rgbRange, 0306 .vrrPolicy = state.vrrPolicy, 0307 .highDynamicRange = state.highDynamicRange, 0308 .sdrBrightness = state.sdrBrightness, 0309 .wideColorGamut = state.wideColorGamut, 0310 .autoRotationPolicy = state.autoRotation, 0311 .iccProfilePath = state.iccProfilePath, 0312 .iccProfile = state.iccProfilePath ? IccProfile::load(*state.iccProfilePath) : nullptr, 0313 .maxPeakBrightnessOverride = state.maxPeakBrightnessOverride, 0314 .maxAverageBrightnessOverride = state.maxAverageBrightnessOverride, 0315 .minBrightnessOverride = state.minBrightnessOverride, 0316 .sdrGamutWideness = state.sdrGamutWideness, 0317 }; 0318 if (setupState.enabled) { 0319 priorities.push_back(std::make_pair(output, setupState.priority)); 0320 } 0321 } 0322 std::sort(priorities.begin(), priorities.end(), [](const auto &left, const auto &right) { 0323 return left.second < right.second; 0324 }); 0325 QList<Output *> order; 0326 std::transform(priorities.begin(), priorities.end(), std::back_inserter(order), [](const auto &pair) { 0327 return pair.first; 0328 }); 0329 return std::make_pair(ret, order); 0330 } 0331 0332 std::optional<std::pair<OutputConfiguration, QList<Output *>>> OutputConfigurationStore::generateLidClosedConfig(const QList<Output *> &outputs) 0333 { 0334 const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) { 0335 return output->isInternal(); 0336 }); 0337 if (internalIt == outputs.end()) { 0338 return std::nullopt; 0339 } 0340 const auto setup = findSetup(outputs, false); 0341 if (!setup) { 0342 return std::nullopt; 0343 } 0344 Output *const internalOutput = *internalIt; 0345 auto [config, order] = setupToConfig(setup->first, setup->second); 0346 auto internalChangeset = config.changeSet(internalOutput); 0347 internalChangeset->enabled = false; 0348 order.removeOne(internalOutput); 0349 0350 const bool anyEnabled = std::any_of(outputs.begin(), outputs.end(), [&config = config](Output *output) { 0351 return config.changeSet(output)->enabled.value_or(output->isEnabled()); 0352 }); 0353 if (!anyEnabled) { 0354 return std::nullopt; 0355 } 0356 0357 const auto getSize = [](OutputChangeSet *changeset, Output *output) { 0358 auto mode = changeset->mode ? changeset->mode->lock() : nullptr; 0359 if (!mode) { 0360 mode = output->currentMode(); 0361 } 0362 const auto scale = changeset->scale.value_or(output->scale()); 0363 return QSize(std::ceil(mode->size().width() / scale), std::ceil(mode->size().height() / scale)); 0364 }; 0365 const QPoint internalPos = internalChangeset->pos.value_or(internalOutput->geometry().topLeft()); 0366 const QSize internalSize = getSize(internalChangeset.get(), internalOutput); 0367 for (Output *otherOutput : outputs) { 0368 auto changeset = config.changeSet(otherOutput); 0369 QPoint otherPos = changeset->pos.value_or(otherOutput->geometry().topLeft()); 0370 if (otherPos.x() >= internalPos.x() + internalSize.width()) { 0371 otherPos.rx() -= std::floor(internalSize.width()); 0372 } 0373 if (otherPos.y() >= internalPos.y() + internalSize.height()) { 0374 otherPos.ry() -= std::floor(internalSize.height()); 0375 } 0376 // make sure this doesn't make outputs overlap, which is neither supported nor expected by users 0377 const QSize otherSize = getSize(changeset.get(), otherOutput); 0378 const bool overlap = std::any_of(outputs.begin(), outputs.end(), [&, &config = config](Output *output) { 0379 if (otherOutput == output) { 0380 return false; 0381 } 0382 const auto changeset = config.changeSet(output); 0383 const QPoint pos = changeset->pos.value_or(output->geometry().topLeft()); 0384 return QRect(pos, otherSize).intersects(QRect(otherPos, getSize(changeset.get(), output))); 0385 }); 0386 if (!overlap) { 0387 changeset->pos = otherPos; 0388 } 0389 } 0390 return std::make_pair(config, order); 0391 } 0392 0393 std::pair<OutputConfiguration, QList<Output *>> OutputConfigurationStore::generateConfig(const QList<Output *> &outputs, bool isLidClosed) 0394 { 0395 if (isLidClosed) { 0396 if (const auto closedConfig = generateLidClosedConfig(outputs)) { 0397 return *closedConfig; 0398 } 0399 } 0400 OutputConfiguration ret; 0401 QList<Output *> outputOrder; 0402 QPoint pos(0, 0); 0403 for (const auto output : outputs) { 0404 const auto outputIndex = findOutput(output, outputs); 0405 const bool enable = !isLidClosed || !output->isInternal() || outputs.size() == 1; 0406 const OutputState existingData = outputIndex ? m_outputs[*outputIndex] : OutputState{}; 0407 0408 const auto modes = output->modes(); 0409 const auto modeIt = std::find_if(modes.begin(), modes.end(), [&existingData](const auto &mode) { 0410 return existingData.mode 0411 && mode->size() == existingData.mode->size 0412 && mode->refreshRate() == existingData.mode->refreshRate; 0413 }); 0414 const auto mode = modeIt == modes.end() ? output->currentMode() : *modeIt; 0415 0416 const auto changeset = ret.changeSet(output); 0417 *changeset = { 0418 .mode = mode, 0419 .enabled = enable, 0420 .pos = pos, 0421 .scale = existingData.scale.value_or(chooseScale(output, mode.get())), 0422 .transform = existingData.transform.value_or(output->panelOrientation()), 0423 .manualTransform = existingData.manualTransform.value_or(output->panelOrientation()), 0424 .overscan = existingData.overscan.value_or(0), 0425 .rgbRange = existingData.rgbRange.value_or(Output::RgbRange::Automatic), 0426 .vrrPolicy = existingData.vrrPolicy.value_or(VrrPolicy::Automatic), 0427 .highDynamicRange = existingData.highDynamicRange.value_or(false), 0428 .sdrBrightness = existingData.sdrBrightness.value_or(200), 0429 .wideColorGamut = existingData.wideColorGamut.value_or(false), 0430 .autoRotationPolicy = existingData.autoRotation.value_or(Output::AutoRotationPolicy::InTabletMode), 0431 }; 0432 if (enable) { 0433 const auto modeSize = changeset->transform->map(mode->size()); 0434 pos.setX(std::ceil(pos.x() + modeSize.width() / *changeset->scale)); 0435 outputOrder.push_back(output); 0436 } 0437 } 0438 return std::make_pair(ret, outputs); 0439 } 0440 0441 std::shared_ptr<OutputMode> OutputConfigurationStore::chooseMode(Output *output) const 0442 { 0443 const auto modes = output->modes(); 0444 0445 // some displays advertise bigger modes than their native resolution 0446 // to avoid that, take the preferred mode into account, which is usually the native one 0447 const auto preferred = std::find_if(modes.begin(), modes.end(), [](const auto &mode) { 0448 return mode->flags() & OutputMode::Flag::Preferred; 0449 }); 0450 if (preferred != modes.end()) { 0451 // some high refresh rate displays advertise a 60Hz mode as preferred for compatibility reasons 0452 // ignore that and choose the highest possible refresh rate by default instead 0453 std::shared_ptr<OutputMode> highestRefresh = *preferred; 0454 for (const auto &mode : modes) { 0455 if (mode->size() == highestRefresh->size() && mode->refreshRate() > highestRefresh->refreshRate()) { 0456 highestRefresh = mode; 0457 } 0458 } 0459 // if the preferred mode size has a refresh rate that's too low for PCs, 0460 // allow falling back to a mode with lower resolution and a more usable refresh rate 0461 if (highestRefresh->refreshRate() >= 50000) { 0462 return highestRefresh; 0463 } 0464 } 0465 0466 std::shared_ptr<OutputMode> ret; 0467 for (auto mode : modes) { 0468 if (mode->flags() & OutputMode::Flag::Generated) { 0469 // generated modes aren't guaranteed to work, so don't choose one as the default 0470 continue; 0471 } 0472 if (!ret) { 0473 ret = mode; 0474 continue; 0475 } 0476 const bool retUsableRefreshRate = ret->refreshRate() >= 50000; 0477 const bool usableRefreshRate = mode->refreshRate() >= 50000; 0478 if (retUsableRefreshRate && !usableRefreshRate) { 0479 ret = mode; 0480 continue; 0481 } 0482 if ((usableRefreshRate && !retUsableRefreshRate) 0483 || mode->size().width() > ret->size().width() 0484 || mode->size().height() > ret->size().height() 0485 || (mode->size() == ret->size() && mode->refreshRate() > ret->refreshRate())) { 0486 ret = mode; 0487 } 0488 } 0489 return ret; 0490 } 0491 0492 double OutputConfigurationStore::chooseScale(Output *output, OutputMode *mode) const 0493 { 0494 if (output->physicalSize().height() <= 0) { 0495 // invalid size, can't do anything with this 0496 return 1.0; 0497 } 0498 const double outputDpi = mode->size().height() / (output->physicalSize().height() / 25.4); 0499 const double desiredScale = outputDpi / targetDpi(output); 0500 // round to 25% steps 0501 return std::clamp(std::round(100.0 * desiredScale / 25.0) * 25.0 / 100.0, 1.0, 3.0); 0502 } 0503 0504 double OutputConfigurationStore::targetDpi(Output *output) const 0505 { 0506 // The eye's ability to perceive detail diminishes with distance, so objects 0507 // that are closer can be smaller and their details remain equally 0508 // distinguishable. As a result, each device type has its own ideal physical 0509 // size of items on its screen based on how close the user's eyes are 0510 // expected to be from it on average, and its target DPI value needs to be 0511 // changed accordingly. 0512 const auto devices = input()->devices(); 0513 const bool hasLaptopLid = std::any_of(devices.begin(), devices.end(), [](const auto &device) { 0514 return device->isLidSwitch(); 0515 }); 0516 if (output->isInternal()) { 0517 if (hasLaptopLid) { 0518 // laptop screens: usually closer to the face than desktop monitors 0519 return 125; 0520 } else { 0521 // phone screens: even closer than laptops 0522 return 136; 0523 } 0524 } else { 0525 // "normal" 1x scale desktop monitor dpi 0526 return 96; 0527 } 0528 } 0529 0530 void OutputConfigurationStore::load() 0531 { 0532 const QString jsonPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json")); 0533 if (jsonPath.isEmpty()) { 0534 return; 0535 } 0536 0537 QFile f(jsonPath); 0538 if (!f.open(QIODevice::ReadOnly)) { 0539 qCWarning(KWIN_CORE) << "Could not open file" << jsonPath; 0540 return; 0541 } 0542 QJsonParseError error; 0543 const auto doc = QJsonDocument::fromJson(f.readAll(), &error); 0544 if (error.error != QJsonParseError::NoError) { 0545 qCWarning(KWIN_CORE) << "Failed to parse" << jsonPath << error.errorString(); 0546 return; 0547 } 0548 const auto array = doc.array(); 0549 std::vector<QJsonObject> objects; 0550 std::transform(array.begin(), array.end(), std::back_inserter(objects), [](const auto &json) { 0551 return json.toObject(); 0552 }); 0553 const auto outputsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { 0554 return obj["name"].toString() == "outputs" && obj["data"].isArray(); 0555 }); 0556 const auto setupsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { 0557 return obj["name"].toString() == "setups" && obj["data"].isArray(); 0558 }); 0559 if (outputsIt == objects.end() || setupsIt == objects.end()) { 0560 return; 0561 } 0562 const auto outputs = (*outputsIt)["data"].toArray(); 0563 0564 std::vector<std::optional<OutputState>> outputDatas; 0565 for (const auto &output : outputs) { 0566 const auto data = output.toObject(); 0567 OutputState state; 0568 bool hasIdentifier = false; 0569 if (const auto it = data.find("edidIdentifier"); it != data.end()) { 0570 if (const auto str = it->toString(); !str.isEmpty()) { 0571 state.edidIdentifier = str; 0572 hasIdentifier = true; 0573 } 0574 } 0575 if (const auto it = data.find("edidHash"); it != data.end()) { 0576 if (const auto str = it->toString(); !str.isEmpty()) { 0577 state.edidHash = str; 0578 hasIdentifier = true; 0579 } 0580 } 0581 if (const auto it = data.find("connectorName"); it != data.end()) { 0582 if (const auto str = it->toString(); !str.isEmpty()) { 0583 state.connectorName = str; 0584 hasIdentifier = true; 0585 } 0586 } 0587 if (const auto it = data.find("mstPath"); it != data.end()) { 0588 if (const auto str = it->toString(); !str.isEmpty()) { 0589 state.mstPath = str; 0590 hasIdentifier = true; 0591 } 0592 } 0593 if (!hasIdentifier) { 0594 // without an identifier the settings are useless 0595 // we still have to push something into the list so that the indices stay correct 0596 outputDatas.push_back(std::nullopt); 0597 qCWarning(KWIN_CORE, "Output in config is missing identifiers"); 0598 continue; 0599 } 0600 const bool hasDuplicate = std::any_of(outputDatas.begin(), outputDatas.end(), [&state](const auto &data) { 0601 return data 0602 && data->edidIdentifier == state.edidIdentifier 0603 && data->edidHash == state.edidHash 0604 && data->mstPath == state.mstPath 0605 && data->connectorName == state.connectorName; 0606 }); 0607 if (hasDuplicate) { 0608 qCWarning(KWIN_CORE) << "Duplicate output found in config for edidIdentifier:" << state.edidIdentifier.value_or("<empty>") << "; connectorName:" << state.connectorName.value_or("<empty>") << "; mstPath:" << state.mstPath; 0609 outputDatas.push_back(std::nullopt); 0610 continue; 0611 } 0612 if (const auto it = data.find("mode"); it != data.end()) { 0613 const auto obj = it->toObject(); 0614 const int width = obj["width"].toInt(0); 0615 const int height = obj["height"].toInt(0); 0616 const int refreshRate = obj["refreshRate"].toInt(0); 0617 if (width > 0 && height > 0 && refreshRate > 0) { 0618 state.mode = ModeData{ 0619 .size = QSize(width, height), 0620 .refreshRate = uint32_t(refreshRate), 0621 }; 0622 } 0623 } 0624 if (const auto it = data.find("scale"); it != data.end()) { 0625 const double scale = it->toDouble(0); 0626 if (scale > 0 && scale <= 3) { 0627 state.scale = scale; 0628 } 0629 } 0630 if (const auto it = data.find("transform"); it != data.end()) { 0631 const auto str = it->toString(); 0632 if (str == "Normal") { 0633 state.transform = state.manualTransform = OutputTransform::Kind::Normal; 0634 } else if (str == "Rotated90") { 0635 state.transform = state.manualTransform = OutputTransform::Kind::Rotate90; 0636 } else if (str == "Rotated180") { 0637 state.transform = state.manualTransform = OutputTransform::Kind::Rotate180; 0638 } else if (str == "Rotated270") { 0639 state.transform = state.manualTransform = OutputTransform::Kind::Rotate270; 0640 } else if (str == "Flipped") { 0641 state.transform = state.manualTransform = OutputTransform::Kind::FlipX; 0642 } else if (str == "Flipped90") { 0643 state.transform = state.manualTransform = OutputTransform::Kind::FlipX90; 0644 } else if (str == "Flipped180") { 0645 state.transform = state.manualTransform = OutputTransform::Kind::FlipX180; 0646 } else if (str == "Flipped270") { 0647 state.transform = state.manualTransform = OutputTransform::Kind::FlipX270; 0648 } 0649 } 0650 if (const auto it = data.find("overscan"); it != data.end()) { 0651 const int overscan = it->toInt(-1); 0652 if (overscan >= 0 && overscan <= 100) { 0653 state.overscan = overscan; 0654 } 0655 } 0656 if (const auto it = data.find("rgbRange"); it != data.end()) { 0657 const auto str = it->toString(); 0658 if (str == "Automatic") { 0659 state.rgbRange = Output::RgbRange::Automatic; 0660 } else if (str == "Limited") { 0661 state.rgbRange = Output::RgbRange::Limited; 0662 } else if (str == "Full") { 0663 state.rgbRange = Output::RgbRange::Full; 0664 } 0665 } 0666 if (const auto it = data.find("vrrPolicy"); it != data.end()) { 0667 const auto str = it->toString(); 0668 if (str == "Never") { 0669 state.vrrPolicy = VrrPolicy::Never; 0670 } else if (str == "Automatic") { 0671 state.vrrPolicy = VrrPolicy::Automatic; 0672 } else if (str == "Always") { 0673 state.vrrPolicy = VrrPolicy::Always; 0674 } 0675 } 0676 if (const auto it = data.find("highDynamicRange"); it != data.end() && it->isBool()) { 0677 state.highDynamicRange = it->toBool(); 0678 } 0679 if (const auto it = data.find("sdrBrightness"); it != data.end() && it->isDouble()) { 0680 state.sdrBrightness = it->toInt(200); 0681 } 0682 if (const auto it = data.find("wideColorGamut"); it != data.end() && it->isBool()) { 0683 state.wideColorGamut = it->toBool(); 0684 } 0685 if (const auto it = data.find("autoRotation"); it != data.end()) { 0686 const auto str = it->toString(); 0687 if (str == "Never") { 0688 state.autoRotation = Output::AutoRotationPolicy::Never; 0689 } else if (str == "InTabletMode") { 0690 state.autoRotation = Output::AutoRotationPolicy::InTabletMode; 0691 } else if (str == "Always") { 0692 state.autoRotation = Output::AutoRotationPolicy::Always; 0693 } 0694 } 0695 if (const auto it = data.find("iccProfilePath"); it != data.end()) { 0696 state.iccProfilePath = it->toString(); 0697 } 0698 if (const auto it = data.find("maxPeakBrightnessOverride"); it != data.end() && it->isDouble()) { 0699 state.maxPeakBrightnessOverride = it->toDouble(); 0700 } 0701 if (const auto it = data.find("maxAverageBrightnessOverride"); it != data.end() && it->isDouble()) { 0702 state.maxAverageBrightnessOverride = it->toDouble(); 0703 } 0704 if (const auto it = data.find("minBrightnessOverride"); it != data.end() && it->isDouble()) { 0705 state.minBrightnessOverride = it->toDouble(); 0706 } 0707 if (const auto it = data.find("sdrGamutWideness"); it != data.end() && it->isDouble()) { 0708 state.sdrGamutWideness = it->toDouble(); 0709 } 0710 outputDatas.push_back(state); 0711 } 0712 0713 const auto setups = (*setupsIt)["data"].toArray(); 0714 for (const auto &s : setups) { 0715 const auto data = s.toObject(); 0716 const auto outputs = data["outputs"].toArray(); 0717 Setup setup; 0718 bool fail = false; 0719 for (const auto &output : outputs) { 0720 const auto outputData = output.toObject(); 0721 SetupState state; 0722 if (const auto it = outputData.find("enabled"); it != outputData.end() && it->isBool()) { 0723 state.enabled = it->toBool(); 0724 } else { 0725 fail = true; 0726 break; 0727 } 0728 if (const auto it = outputData.find("outputIndex"); it != outputData.end()) { 0729 const int index = it->toInt(-1); 0730 if (index <= -1 || size_t(index) >= outputDatas.size()) { 0731 fail = true; 0732 break; 0733 } 0734 // the outputs must be unique 0735 const bool unique = std::none_of(setup.outputs.begin(), setup.outputs.end(), [&index](const auto &output) { 0736 return output.outputIndex == size_t(index); 0737 }); 0738 if (!unique) { 0739 fail = true; 0740 break; 0741 } 0742 state.outputIndex = index; 0743 } 0744 if (const auto it = outputData.find("position"); it != outputData.end()) { 0745 const auto obj = it->toObject(); 0746 const auto x = obj.find("x"); 0747 const auto y = obj.find("y"); 0748 if (x == obj.end() || !x->isDouble() || y == obj.end() || !y->isDouble()) { 0749 fail = true; 0750 break; 0751 } 0752 state.position = QPoint(x->toInt(0), y->toInt(0)); 0753 } else { 0754 fail = true; 0755 break; 0756 } 0757 if (const auto it = outputData.find("priority"); it != outputData.end()) { 0758 state.priority = it->toInt(-1); 0759 if (state.priority < 0 && state.enabled) { 0760 fail = true; 0761 break; 0762 } 0763 } 0764 setup.outputs.push_back(state); 0765 } 0766 if (fail || setup.outputs.empty()) { 0767 continue; 0768 } 0769 // one of the outputs must be enabled 0770 const bool noneEnabled = std::none_of(setup.outputs.begin(), setup.outputs.end(), [](const auto &output) { 0771 return output.enabled; 0772 }); 0773 if (noneEnabled) { 0774 continue; 0775 } 0776 setup.lidClosed = data["lidClosed"].toBool(false); 0777 // there must be only one setup that refers to a given set of outputs 0778 const bool alreadyExists = std::any_of(m_setups.begin(), m_setups.end(), [&setup](const auto &other) { 0779 if (setup.lidClosed != other.lidClosed || setup.outputs.size() != other.outputs.size()) { 0780 return false; 0781 } 0782 return std::all_of(setup.outputs.begin(), setup.outputs.end(), [&other](const auto &output) { 0783 return std::any_of(other.outputs.begin(), other.outputs.end(), [&output](const auto &otherOutput) { 0784 return output.outputIndex == otherOutput.outputIndex; 0785 }); 0786 }); 0787 }); 0788 if (alreadyExists) { 0789 continue; 0790 } 0791 m_setups.push_back(setup); 0792 } 0793 0794 // repair the outputs list in case it's broken 0795 for (size_t i = 0; i < outputDatas.size();) { 0796 if (!outputDatas[i]) { 0797 outputDatas.erase(outputDatas.begin() + i); 0798 for (auto setupIt = m_setups.begin(); setupIt != m_setups.end();) { 0799 const bool broken = std::any_of(setupIt->outputs.begin(), setupIt->outputs.end(), [i](const auto &output) { 0800 return output.outputIndex == i; 0801 }); 0802 if (broken) { 0803 setupIt = m_setups.erase(setupIt); 0804 continue; 0805 } 0806 for (auto &output : setupIt->outputs) { 0807 if (output.outputIndex > i) { 0808 output.outputIndex--; 0809 } 0810 } 0811 setupIt++; 0812 } 0813 } else { 0814 i++; 0815 } 0816 } 0817 0818 for (const auto &o : outputDatas) { 0819 Q_ASSERT(o); 0820 m_outputs.push_back(*o); 0821 } 0822 } 0823 0824 void OutputConfigurationStore::save() 0825 { 0826 QJsonDocument document; 0827 QJsonArray array; 0828 QJsonObject outputs; 0829 outputs["name"] = "outputs"; 0830 QJsonArray outputsData; 0831 for (const auto &output : m_outputs) { 0832 QJsonObject o; 0833 if (output.edidIdentifier) { 0834 o["edidIdentifier"] = *output.edidIdentifier; 0835 } 0836 if (!output.edidHash.isEmpty()) { 0837 o["edidHash"] = output.edidHash; 0838 } 0839 if (output.connectorName) { 0840 o["connectorName"] = *output.connectorName; 0841 } 0842 if (!output.mstPath.isEmpty()) { 0843 o["mstPath"] = output.mstPath; 0844 } 0845 if (output.mode) { 0846 QJsonObject mode; 0847 mode["width"] = output.mode->size.width(); 0848 mode["height"] = output.mode->size.height(); 0849 mode["refreshRate"] = int(output.mode->refreshRate); 0850 o["mode"] = mode; 0851 } 0852 if (output.scale) { 0853 o["scale"] = *output.scale; 0854 } 0855 if (output.manualTransform == OutputTransform::Kind::Normal) { 0856 o["transform"] = "Normal"; 0857 } else if (output.manualTransform == OutputTransform::Kind::Rotate90) { 0858 o["transform"] = "Rotated90"; 0859 } else if (output.manualTransform == OutputTransform::Kind::Rotate180) { 0860 o["transform"] = "Rotated180"; 0861 } else if (output.manualTransform == OutputTransform::Kind::Rotate270) { 0862 o["transform"] = "Rotated270"; 0863 } else if (output.manualTransform == OutputTransform::Kind::FlipX) { 0864 o["transform"] = "Flipped"; 0865 } else if (output.manualTransform == OutputTransform::Kind::FlipX90) { 0866 o["transform"] = "Flipped90"; 0867 } else if (output.manualTransform == OutputTransform::Kind::FlipX180) { 0868 o["transform"] = "Flipped180"; 0869 } else if (output.manualTransform == OutputTransform::Kind::FlipX270) { 0870 o["transform"] = "Flipped270"; 0871 } 0872 if (output.overscan) { 0873 o["overscan"] = int(*output.overscan); 0874 } 0875 if (output.rgbRange == Output::RgbRange::Automatic) { 0876 o["rgbRange"] = "Automatic"; 0877 } else if (output.rgbRange == Output::RgbRange::Limited) { 0878 o["rgbRange"] = "Limited"; 0879 } else if (output.rgbRange == Output::RgbRange::Full) { 0880 o["rgbRange"] = "Full"; 0881 } 0882 if (output.vrrPolicy == VrrPolicy::Never) { 0883 o["vrrPolicy"] = "Never"; 0884 } else if (output.vrrPolicy == VrrPolicy::Automatic) { 0885 o["vrrPolicy"] = "Automatic"; 0886 } else if (output.vrrPolicy == VrrPolicy::Always) { 0887 o["vrrPolicy"] = "Always"; 0888 } 0889 if (output.highDynamicRange) { 0890 o["highDynamicRange"] = *output.highDynamicRange; 0891 } 0892 if (output.sdrBrightness) { 0893 o["sdrBrightness"] = int(*output.sdrBrightness); 0894 } 0895 if (output.wideColorGamut) { 0896 o["wideColorGamut"] = *output.wideColorGamut; 0897 } 0898 if (output.autoRotation) { 0899 switch (*output.autoRotation) { 0900 case Output::AutoRotationPolicy::Never: 0901 o["autoRotation"] = "Never"; 0902 break; 0903 case Output::AutoRotationPolicy::InTabletMode: 0904 o["autoRotation"] = "InTabletMode"; 0905 break; 0906 case Output::AutoRotationPolicy::Always: 0907 o["autoRotation"] = "Always"; 0908 break; 0909 } 0910 } 0911 if (output.iccProfilePath) { 0912 o["iccProfilePath"] = *output.iccProfilePath; 0913 } 0914 if (output.maxPeakBrightnessOverride) { 0915 o["maxPeakBrightnessOverride"] = *output.maxPeakBrightnessOverride; 0916 } 0917 if (output.maxAverageBrightnessOverride) { 0918 o["maxAverageBrightnessOverride"] = *output.maxAverageBrightnessOverride; 0919 } 0920 if (output.minBrightnessOverride) { 0921 o["minBrightnessOverride"] = *output.minBrightnessOverride; 0922 } 0923 if (output.sdrGamutWideness) { 0924 o["sdrGamutWideness"] = *output.sdrGamutWideness; 0925 } 0926 outputsData.append(o); 0927 } 0928 outputs["data"] = outputsData; 0929 array.append(outputs); 0930 0931 QJsonObject setups; 0932 setups["name"] = "setups"; 0933 QJsonArray setupData; 0934 for (const auto &setup : m_setups) { 0935 QJsonObject o; 0936 o["lidClosed"] = setup.lidClosed; 0937 QJsonArray outputs; 0938 for (ssize_t i = 0; i < setup.outputs.size(); i++) { 0939 const auto &output = setup.outputs[i]; 0940 QJsonObject o; 0941 o["enabled"] = output.enabled; 0942 o["outputIndex"] = int(output.outputIndex); 0943 o["priority"] = output.priority; 0944 QJsonObject pos; 0945 pos["x"] = output.position.x(); 0946 pos["y"] = output.position.y(); 0947 o["position"] = pos; 0948 0949 outputs.append(o); 0950 } 0951 o["outputs"] = outputs; 0952 0953 setupData.append(o); 0954 } 0955 setups["data"] = setupData; 0956 array.append(setups); 0957 0958 const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/kwinoutputconfig.json"; 0959 QFile f(path); 0960 if (!f.open(QIODevice::WriteOnly)) { 0961 qCWarning(KWIN_CORE, "Couldn't open output config file %s", qPrintable(path)); 0962 return; 0963 } 0964 document.setArray(array); 0965 f.write(document.toJson()); 0966 f.flush(); 0967 } 0968 0969 bool OutputConfigurationStore::isAutoRotateActive(const QList<Output *> &outputs, bool isTabletMode) const 0970 { 0971 const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) { 0972 return output->isInternal() && output->isEnabled(); 0973 }); 0974 if (internalIt == outputs.end()) { 0975 return false; 0976 } 0977 Output *internal = *internalIt; 0978 switch (internal->autoRotationPolicy()) { 0979 case Output::AutoRotationPolicy::Never: 0980 return false; 0981 case Output::AutoRotationPolicy::InTabletMode: 0982 return isTabletMode; 0983 case Output::AutoRotationPolicy::Always: 0984 return true; 0985 } 0986 Q_UNREACHABLE(); 0987 } 0988 }