File indexing completed on 2024-05-12 13:31:55
0001 /* 0002 SPDX-FileCopyrightText: 2012 Alejandro Fiestas Olivares <afiestas@kde.org> 0003 SPDX-FileCopyrightText: 2021 David Redondo <kde@david-redondo.de> 0004 SPDX-FileCopyrightText: 2022 Nate Graham <nate@kde.org> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include <cmath> 0010 0011 #include "generator.h" 0012 #include "device.h" 0013 #include "kscreen_daemon_debug.h" 0014 #include "output.h" 0015 #include <QRect> 0016 0017 #include <kscreen/screen.h> 0018 0019 #if defined(QT_NO_DEBUG) 0020 #define ASSERT_OUTPUTS(outputs) 0021 #else 0022 #define ASSERT_OUTPUTS(outputs) \ 0023 while (true) { \ 0024 Q_ASSERT(!outputs.isEmpty()); \ 0025 for (const KScreen::OutputPtr &output : qAsConst(outputs)) { \ 0026 Q_ASSERT(output); \ 0027 Q_ASSERT(output->isConnected()); \ 0028 } \ 0029 break; \ 0030 } 0031 #endif 0032 0033 // The industry-standard "normal" 1x scale desktop monitor DPI value since forever 0034 static const int targetDpiDesktop = 96; 0035 0036 // Higher because laptop screens are smaller and used closer to the face 0037 static const int targetDpiLaptop = 125; 0038 0039 // Because phone and tablet screens are even smaller and used even closer 0040 static const int targetDpiMobile = 136; 0041 0042 // Round calculated ideal scale factor to the nearest quarter 0043 static const int scaleRoundingness = 4; 0044 0045 Generator *Generator::instance = nullptr; 0046 0047 bool operator<(const QSize &s1, const QSize &s2) 0048 { 0049 return s1.width() * s1.height() < s2.width() * s2.height(); 0050 } 0051 0052 Generator *Generator::self() 0053 { 0054 if (!Generator::instance) { 0055 Generator::instance = new Generator(); 0056 } 0057 return Generator::instance; 0058 } 0059 0060 Generator::Generator() 0061 : QObject() 0062 , m_forceLaptop(false) 0063 , m_forceLidClosed(false) 0064 , m_forceNotLaptop(false) 0065 , m_forceDocked(false) 0066 { 0067 connect(Device::self(), &Device::ready, this, &Generator::ready); 0068 } 0069 0070 void Generator::destroy() 0071 { 0072 delete Generator::instance; 0073 Generator::instance = nullptr; 0074 } 0075 0076 Generator::~Generator() 0077 { 0078 } 0079 0080 void Generator::setCurrentConfig(const KScreen::ConfigPtr ¤tConfig) 0081 { 0082 m_currentConfig = currentConfig; 0083 } 0084 0085 KScreen::ConfigPtr Generator::idealConfig(const KScreen::ConfigPtr ¤tConfig) 0086 { 0087 Q_ASSERT(currentConfig); 0088 0089 KScreen::ConfigPtr config = currentConfig->clone(); 0090 0091 disableAllDisconnectedOutputs(config->outputs()); 0092 0093 KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0094 qCDebug(KSCREEN_KDED) << "Connected outputs: " << connectedOutputs.count(); 0095 0096 if (connectedOutputs.isEmpty()) { 0097 return config; 0098 } 0099 0100 for (const auto &output : connectedOutputs) { 0101 initializeOutput(output, config->supportedFeatures()); 0102 output->setExplicitLogicalSize(config->logicalSizeForOutput(*output)); 0103 } 0104 0105 if (connectedOutputs.count() == 1) { 0106 singleOutput(config); 0107 return config; 0108 } 0109 0110 if (isLaptop()) { 0111 laptop(config); 0112 return fallbackIfNeeded(config); 0113 } 0114 0115 qCDebug(KSCREEN_KDED) << "Extend to Right"; 0116 extendToRight(config, connectedOutputs); 0117 return fallbackIfNeeded(config); 0118 } 0119 0120 KScreen::ConfigPtr Generator::fallbackIfNeeded(const KScreen::ConfigPtr &config) 0121 { 0122 qCDebug(KSCREEN_KDED) << "fallbackIfNeeded()"; 0123 0124 KScreen::ConfigPtr newConfig; 0125 0126 // If the ideal config can't be applied, try clonning 0127 if (!KScreen::Config::canBeApplied(config)) { 0128 if (isLaptop()) { 0129 newConfig = displaySwitch(Generator::Clone); // Try to clone at our best 0130 } else { 0131 newConfig = config; 0132 KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0133 if (connectedOutputs.isEmpty()) { 0134 return config; 0135 } else { 0136 config->setPrimaryOutput(connectedOutputs.first()); 0137 cloneScreens(config); 0138 } 0139 } 0140 } else { 0141 newConfig = config; 0142 } 0143 0144 // If after trying to clone at our best, we fail... return current 0145 if (!KScreen::Config::canBeApplied(newConfig)) { 0146 qCDebug(KSCREEN_KDED) << "Config cannot be applied"; 0147 newConfig = config; 0148 } 0149 0150 return config; 0151 } 0152 0153 KScreen::ConfigPtr Generator::displaySwitch(DisplaySwitchAction action) 0154 { 0155 KScreen::ConfigPtr config = m_currentConfig; 0156 Q_ASSERT(config); 0157 0158 KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0159 0160 for (const auto &output : connectedOutputs) { 0161 initializeOutput(output, config->supportedFeatures()); 0162 } 0163 0164 // There's not much else we can do with only one output 0165 if (connectedOutputs.count() < 2) { 0166 singleOutput(config); 0167 return config; 0168 } 0169 0170 // We cannot try all possible combinations with two and more outputs 0171 if (connectedOutputs.count() > 2) { 0172 extendToRight(config, connectedOutputs); 0173 return config; 0174 } 0175 0176 KScreen::OutputPtr embedded, external; 0177 embedded = embeddedOutput(connectedOutputs); 0178 // If we don't have an embedded output (desktop with two external screens 0179 // for instance), then pretend the current primary one is embedded 0180 if (!embedded) { 0181 // Find primary screen 0182 for (auto &screen : connectedOutputs) { 0183 if (screen->isPrimary()) { 0184 embedded = screen; 0185 break; 0186 } 0187 } 0188 if (!embedded) { 0189 // If all else fail take the first screen 0190 embedded = connectedOutputs.first(); 0191 } 0192 } 0193 // Just to be sure 0194 if (embedded->modes().isEmpty()) { 0195 return config; 0196 } 0197 0198 if (action == Generator::Clone) { 0199 qCDebug(KSCREEN_KDED) << "Cloning"; 0200 config->setPrimaryOutput(embedded); 0201 cloneScreens(config); 0202 return config; 0203 } 0204 0205 connectedOutputs.remove(embedded->id()); 0206 external = connectedOutputs.constBegin().value(); 0207 // Just to be sure 0208 if (external->modes().isEmpty()) { 0209 return config; 0210 } 0211 0212 Q_ASSERT(embedded->currentMode()); 0213 Q_ASSERT(external->currentMode()); 0214 0215 // Change action to be relative to embedded screen 0216 if (!embedded->isPrimary()) { 0217 switch (action) { 0218 case Generator::ExtendToLeft: 0219 action = Generator::ExtendToRight; 0220 break; 0221 case Generator::ExtendToRight: 0222 action = Generator::ExtendToLeft; 0223 break; 0224 default: 0225 break; 0226 } 0227 } 0228 0229 switch (action) { 0230 case Generator::ExtendToLeft: { 0231 qCDebug(KSCREEN_KDED) << "Extend to left"; 0232 external->setPos(QPoint(0, 0)); 0233 external->setEnabled(true); 0234 0235 const QSize size = external->geometry().size(); 0236 embedded->setPos(QPoint(size.width(), 0)); 0237 embedded->setEnabled(true); 0238 0239 return config; 0240 } 0241 case Generator::TurnOffEmbedded: { 0242 qCDebug(KSCREEN_KDED) << "Turn off embedded (laptop)"; 0243 embedded->setEnabled(false); 0244 external->setEnabled(true); 0245 config->setPrimaryOutput(external); 0246 return config; 0247 } 0248 case Generator::TurnOffExternal: { 0249 qCDebug(KSCREEN_KDED) << "Turn off external screen"; 0250 embedded->setPos(QPoint(0, 0)); 0251 embedded->setEnabled(true); 0252 external->setEnabled(false); 0253 config->setPrimaryOutput(embedded); 0254 return config; 0255 } 0256 case Generator::ExtendToRight: { 0257 qCDebug(KSCREEN_KDED) << "Extend to the right"; 0258 embedded->setPos(QPoint(0, 0)); 0259 embedded->setEnabled(true); 0260 0261 Q_ASSERT(embedded->currentMode()); // we must have a mode now 0262 const QSize size = embedded->geometry().size(); 0263 external->setPos(QPoint(size.width(), 0)); 0264 external->setEnabled(true); 0265 0266 return config; 0267 } 0268 case Generator::None: // just return config 0269 case Generator::Clone: // handled above 0270 break; 0271 } // switch 0272 0273 return config; 0274 } 0275 0276 uint qHash(const QSize &size) 0277 { 0278 return size.width() * size.height(); 0279 } 0280 0281 void Generator::cloneScreens(const KScreen::ConfigPtr &config) 0282 { 0283 KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0284 0285 ASSERT_OUTPUTS(connectedOutputs); 0286 if (connectedOutputs.isEmpty()) { 0287 return; 0288 } 0289 0290 QSet<QSize> commonSizes; 0291 const QSize maxScreenSize = config->screen()->maxSize(); 0292 0293 Q_FOREACH (const KScreen::OutputPtr &output, connectedOutputs) { 0294 QSet<QSize> modeSizes; 0295 Q_FOREACH (const KScreen::ModePtr &mode, output->modes()) { 0296 const QSize size = mode->size(); 0297 if (size.width() > maxScreenSize.width() || size.height() > maxScreenSize.height()) { 0298 continue; 0299 } 0300 modeSizes.insert(mode->size()); 0301 } 0302 0303 // If we have nothing to compare against 0304 if (commonSizes.isEmpty()) { 0305 commonSizes = modeSizes; 0306 } else { 0307 commonSizes.intersect(modeSizes); 0308 } 0309 0310 // If there's already nothing in common, bail 0311 if (commonSizes.isEmpty()) { 0312 break; 0313 } 0314 } 0315 0316 qCDebug(KSCREEN_KDED) << "Common sizes: " << commonSizes; 0317 // fallback to biggestMode if no commonSizes have been found 0318 if (commonSizes.isEmpty()) { 0319 for (KScreen::OutputPtr &output : connectedOutputs) { 0320 if (output->modes().isEmpty()) { 0321 continue; 0322 } 0323 output->setEnabled(true); 0324 output->setPos(QPoint(0, 0)); 0325 const KScreen::ModePtr mode = biggestMode(output->modes()); 0326 Q_ASSERT(mode); 0327 output->setCurrentModeId(mode->id()); 0328 } 0329 return; 0330 } 0331 0332 // At this point, we know we have common sizes, let's get the biggest on 0333 QList<QSize> commonSizeList = commonSizes.values(); 0334 std::sort(commonSizeList.begin(), commonSizeList.end()); 0335 const QSize biggestSize = commonSizeList.last(); 0336 0337 // Finally, look for the mode with biggestSize and biggest refreshRate and set it 0338 qCDebug(KSCREEN_KDED) << "Biggest Size: " << biggestSize; 0339 KScreen::ModePtr bestMode; 0340 for (KScreen::OutputPtr &output : connectedOutputs) { 0341 if (output->modes().isEmpty()) { 0342 continue; 0343 } 0344 bestMode = bestModeForSize(output->modes(), biggestSize); 0345 Q_ASSERT(bestMode); // we resolved this mode previously, so it better works 0346 output->setEnabled(true); 0347 output->setPos(QPoint(0, 0)); 0348 output->setCurrentModeId(bestMode->id()); 0349 } 0350 } 0351 0352 void Generator::singleOutput(KScreen::ConfigPtr &config) 0353 { 0354 const KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0355 0356 ASSERT_OUTPUTS(connectedOutputs); 0357 if (connectedOutputs.isEmpty()) { 0358 return; 0359 } 0360 0361 KScreen::OutputPtr output = connectedOutputs.first(); 0362 if (output->modes().isEmpty()) { 0363 return; 0364 } 0365 0366 config->setPrimaryOutput(output); 0367 output->setPos(QPoint(0, 0)); 0368 } 0369 0370 void Generator::laptop(KScreen::ConfigPtr &config) 0371 { 0372 KScreen::OutputList usableOutputs = config->connectedOutputs(); 0373 0374 ASSERT_OUTPUTS(usableOutputs) 0375 if (usableOutputs.isEmpty()) { 0376 return; 0377 } 0378 0379 KScreen::OutputPtr embedded = embeddedOutput(usableOutputs); 0380 /* Apparently older laptops use "VGA-*" as embedded output ID, so embeddedOutput() 0381 * will fail, because it looks only for modern "LVDS", "EDP", etc. If we 0382 * fail to detect which output is embedded, just use the one with the lowest 0383 * ID. It's a wild guess, but I think it's highly probable that it will work. 0384 * See bug #318907 for further reference. -- dvratil 0385 */ 0386 if (!embedded) { 0387 QList<int> keys = usableOutputs.keys(); 0388 std::sort(keys.begin(), keys.end()); 0389 embedded = usableOutputs.value(keys.first()); 0390 } 0391 usableOutputs.remove(embedded->id()); 0392 0393 if (usableOutputs.isEmpty() || embedded->modes().isEmpty()) { 0394 qCWarning(KSCREEN_KDED) << "No external outputs found, going for singleOutput()"; 0395 return singleOutput(config); 0396 } 0397 0398 if (isLidClosed() && usableOutputs.count() == 1) { 0399 qCDebug(KSCREEN_KDED) << "With lid closed"; 0400 embedded->setEnabled(false); 0401 0402 KScreen::OutputPtr external = usableOutputs.first(); 0403 if (external->modes().isEmpty()) { 0404 return; 0405 } 0406 config->setPrimaryOutput(external); 0407 external->setPos(QPoint(0, 0)); 0408 return; 0409 } 0410 0411 if (isLidClosed() && usableOutputs.count() > 1) { 0412 qCDebug(KSCREEN_KDED) << "Lid is closed, and more than one output"; 0413 embedded->setEnabled(false); 0414 0415 extendToRight(config, usableOutputs); 0416 return; 0417 } 0418 0419 qCDebug(KSCREEN_KDED) << "Lid is open"; 0420 0421 // If lid is open, laptop screen should be primary 0422 embedded->setPos(QPoint(0, 0)); 0423 embedded->setEnabled(true); 0424 int globalWidth = embedded->geometry().width(); 0425 0426 KScreen::OutputPtr biggest = biggestOutput(usableOutputs); 0427 Q_ASSERT(biggest); 0428 usableOutputs.remove(biggest->id()); 0429 0430 biggest->setPos(QPoint(globalWidth, 0)); 0431 biggest->setEnabled(true); 0432 globalWidth += biggest->geometry().width(); 0433 0434 for (KScreen::OutputPtr output : qAsConst(usableOutputs)) { 0435 output->setEnabled(true); 0436 output->setPos(QPoint(globalWidth, 0)); 0437 globalWidth += output->geometry().width(); 0438 } 0439 0440 if (isDocked()) { 0441 qCDebug(KSCREEN_KDED) << "Docked"; 0442 config->setPrimaryOutput(biggest); 0443 } else { 0444 config->setPrimaryOutput(embedded); 0445 } 0446 } 0447 0448 void Generator::extendToRight(KScreen::ConfigPtr &config, KScreen::OutputList usableOutputs) 0449 { 0450 ASSERT_OUTPUTS(usableOutputs); 0451 if (usableOutputs.isEmpty()) { 0452 return; 0453 } 0454 0455 qCDebug(KSCREEN_KDED) << "Extending to the right"; 0456 0457 KScreen::OutputPtr biggest = biggestOutput(usableOutputs); 0458 Q_ASSERT(biggest); 0459 usableOutputs.remove(biggest->id()); 0460 0461 biggest->setEnabled(true); 0462 biggest->setPos(QPoint(0, 0)); 0463 int globalWidth = biggest->geometry().width(); 0464 0465 for (KScreen::OutputPtr output : qAsConst(usableOutputs)) { 0466 output->setEnabled(true); 0467 output->setPos(QPoint(globalWidth, 0)); 0468 globalWidth += output->geometry().width(); 0469 } 0470 0471 config->setPrimaryOutput(biggest); 0472 } 0473 0474 void Generator::initializeOutput(const KScreen::OutputPtr &output, KScreen::Config::Features features) 0475 { 0476 if (output->modes().empty()) { 0477 output->setEnabled(false); 0478 return; 0479 } 0480 Output::GlobalConfig config = Output::readGlobal(output); 0481 output->setCurrentModeId(config.modeId.value_or(bestModeForOutput(output)->id())); 0482 output->setRotation(config.rotation.value_or(output->rotation())); 0483 if (features & KScreen::Config::Feature::PerOutputScaling) { 0484 output->setScale(config.scale.value_or(bestScaleForOutput(output))); 0485 } 0486 } 0487 0488 KScreen::ModePtr Generator::biggestMode(const KScreen::ModeList &modes) 0489 { 0490 Q_ASSERT(!modes.isEmpty()); 0491 0492 int modeArea, biggestArea = 0; 0493 KScreen::ModePtr biggestMode; 0494 for (const KScreen::ModePtr &mode : modes) { 0495 modeArea = mode->size().width() * mode->size().height(); 0496 if (modeArea < biggestArea) { 0497 continue; 0498 } 0499 if (modeArea == biggestArea && mode->refreshRate() < biggestMode->refreshRate()) { 0500 continue; 0501 } 0502 if (modeArea == biggestArea && mode->refreshRate() > biggestMode->refreshRate()) { 0503 biggestMode = mode; 0504 continue; 0505 } 0506 0507 biggestArea = modeArea; 0508 biggestMode = mode; 0509 } 0510 0511 return biggestMode; 0512 } 0513 0514 KScreen::ModePtr Generator::bestModeForSize(const KScreen::ModeList &modes, const QSize &size) 0515 { 0516 KScreen::ModePtr bestMode; 0517 for (const KScreen::ModePtr &mode : modes) { 0518 if (mode->size() != size) { 0519 continue; 0520 } 0521 0522 if (!bestMode) { 0523 bestMode = mode; 0524 continue; 0525 } 0526 0527 if (mode->refreshRate() > bestMode->refreshRate()) { 0528 bestMode = mode; 0529 } 0530 } 0531 0532 return bestMode; 0533 } 0534 0535 qreal Generator::bestScaleForOutput(const KScreen::OutputPtr &output) 0536 { 0537 // Sanity check outputs that tell us they have no physical size 0538 if (output->sizeMm().height() <= 0) { 0539 return 1.0; 0540 } 0541 0542 /* The eye's ability to perceive detail diminishes with distance, so objects 0543 * that are closer can be smaller and their details remain equally 0544 * distinguishable. As a result, each device type has its own ideal physical 0545 * size of items on its screen based on how close the user's eyes are 0546 * expected to be from it on average, and its target DPI value needs to be 0547 * changed accordingly. 0548 */ 0549 int outputTargetDpi; 0550 0551 if (output->type() != KScreen::Output::Panel) { 0552 outputTargetDpi = targetDpiDesktop; 0553 } else { 0554 if (isLaptop()) { 0555 outputTargetDpi = targetDpiLaptop; 0556 } else { 0557 outputTargetDpi = targetDpiMobile; 0558 } 0559 } 0560 0561 const qreal outputPixelHeight = output->currentMode()->size().height(); 0562 const qreal outputPhysicalHeight = output->sizeMm().height() / 25.4; // convert mm to inches 0563 const qreal outputDpi = outputPixelHeight / outputPhysicalHeight; 0564 0565 const qreal scale = round(outputDpi / outputTargetDpi * scaleRoundingness) / scaleRoundingness; 0566 0567 // Sanity check for outputs with such a low pixel density that the calculated 0568 // scale would be less than 1; this isn't well supported, so just use 1 0569 if (scale < 1) { 0570 return 1.0; 0571 } 0572 // The KCM doesn't support manually setting the scale higher than 3x, so limit 0573 // the auto-calculated value to that 0574 else if (scale > 3) { 0575 return 3.0; 0576 } 0577 0578 return scale; 0579 } 0580 0581 KScreen::ModePtr Generator::bestModeForOutput(const KScreen::OutputPtr &output) 0582 { 0583 if (KScreen::ModePtr outputMode = output->preferredMode()) { 0584 return outputMode; 0585 } 0586 0587 return biggestMode(output->modes()); 0588 } 0589 0590 KScreen::OutputPtr Generator::biggestOutput(const KScreen::OutputList &outputs) 0591 { 0592 ASSERT_OUTPUTS(outputs) 0593 0594 int area, total = 0; 0595 KScreen::OutputPtr biggest; 0596 for (const KScreen::OutputPtr &output : outputs) { 0597 const KScreen::ModePtr mode = bestModeForOutput(output); 0598 if (!mode) { 0599 continue; 0600 } 0601 area = mode->size().width() * mode->size().height(); 0602 if (area <= total) { 0603 continue; 0604 } 0605 0606 total = area; 0607 biggest = output; 0608 } 0609 0610 return biggest; 0611 } 0612 0613 void Generator::disableAllDisconnectedOutputs(const KScreen::OutputList &outputs) 0614 { 0615 for (const KScreen::OutputPtr &output : outputs) { 0616 if (!output->isConnected()) { 0617 qCDebug(KSCREEN_KDED) << output->name() << " Disabled"; 0618 output->setEnabled(false); 0619 } 0620 } 0621 } 0622 0623 KScreen::OutputPtr Generator::embeddedOutput(const KScreen::OutputList &outputs) 0624 { 0625 for (const KScreen::OutputPtr &output : outputs) { 0626 if (output->type() == KScreen::Output::Panel) { 0627 return output; 0628 } 0629 } 0630 0631 return KScreen::OutputPtr(); 0632 } 0633 0634 bool Generator::isLaptop() const 0635 { 0636 if (m_forceLaptop) { 0637 return true; 0638 } 0639 if (m_forceNotLaptop) { 0640 return false; 0641 } 0642 0643 return Device::self()->isLaptop(); 0644 } 0645 0646 bool Generator::isLidClosed() const 0647 { 0648 if (m_forceLidClosed) { 0649 return true; 0650 } 0651 if (m_forceNotLaptop) { 0652 return false; 0653 } 0654 0655 return Device::self()->isLidClosed(); 0656 } 0657 0658 bool Generator::isDocked() const 0659 { 0660 return m_forceDocked; 0661 } 0662 0663 void Generator::setForceLaptop(bool force) 0664 { 0665 m_forceLaptop = force; 0666 } 0667 0668 void Generator::setForceLidClosed(bool force) 0669 { 0670 m_forceLidClosed = force; 0671 } 0672 0673 void Generator::setForceDocked(bool force) 0674 { 0675 m_forceDocked = force; 0676 } 0677 0678 void Generator::setForceNotLaptop(bool force) 0679 { 0680 m_forceNotLaptop = force; 0681 }