File indexing completed on 2024-11-10 04:55:34
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 : std::as_const(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 void Generator::cloneScreens(const KScreen::ConfigPtr &config) 0277 { 0278 KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0279 0280 ASSERT_OUTPUTS(connectedOutputs); 0281 if (connectedOutputs.isEmpty()) { 0282 return; 0283 } 0284 0285 QSet<QSize> commonSizes; 0286 const QSize maxScreenSize = config->screen()->maxSize(); 0287 0288 Q_FOREACH (const KScreen::OutputPtr &output, connectedOutputs) { 0289 QSet<QSize> modeSizes; 0290 Q_FOREACH (const KScreen::ModePtr &mode, output->modes()) { 0291 const QSize size = mode->size(); 0292 if (size.width() > maxScreenSize.width() || size.height() > maxScreenSize.height()) { 0293 continue; 0294 } 0295 modeSizes.insert(mode->size()); 0296 } 0297 0298 // If we have nothing to compare against 0299 if (commonSizes.isEmpty()) { 0300 commonSizes = modeSizes; 0301 } else { 0302 commonSizes.intersect(modeSizes); 0303 } 0304 0305 // If there's already nothing in common, bail 0306 if (commonSizes.isEmpty()) { 0307 break; 0308 } 0309 } 0310 0311 qCDebug(KSCREEN_KDED) << "Common sizes: " << commonSizes; 0312 // fallback to biggestMode if no commonSizes have been found 0313 if (commonSizes.isEmpty()) { 0314 for (KScreen::OutputPtr &output : connectedOutputs) { 0315 if (output->modes().isEmpty()) { 0316 continue; 0317 } 0318 output->setEnabled(true); 0319 output->setPos(QPoint(0, 0)); 0320 const KScreen::ModePtr mode = biggestMode(output->modes()); 0321 Q_ASSERT(mode); 0322 output->setCurrentModeId(mode->id()); 0323 } 0324 return; 0325 } 0326 0327 // At this point, we know we have common sizes, let's get the biggest on 0328 QList<QSize> commonSizeList = commonSizes.values(); 0329 std::sort(commonSizeList.begin(), commonSizeList.end()); 0330 const QSize biggestSize = commonSizeList.last(); 0331 0332 // Finally, look for the mode with biggestSize and biggest refreshRate and set it 0333 qCDebug(KSCREEN_KDED) << "Biggest Size: " << biggestSize; 0334 KScreen::ModePtr bestMode; 0335 for (KScreen::OutputPtr &output : connectedOutputs) { 0336 if (output->modes().isEmpty()) { 0337 continue; 0338 } 0339 bestMode = bestModeForSize(output->modes(), biggestSize); 0340 Q_ASSERT(bestMode); // we resolved this mode previously, so it better works 0341 output->setEnabled(true); 0342 output->setPos(QPoint(0, 0)); 0343 output->setCurrentModeId(bestMode->id()); 0344 } 0345 } 0346 0347 void Generator::singleOutput(KScreen::ConfigPtr &config) 0348 { 0349 const KScreen::OutputList connectedOutputs = config->connectedOutputs(); 0350 0351 ASSERT_OUTPUTS(connectedOutputs); 0352 if (connectedOutputs.isEmpty()) { 0353 return; 0354 } 0355 0356 KScreen::OutputPtr output = connectedOutputs.first(); 0357 if (output->modes().isEmpty()) { 0358 return; 0359 } 0360 0361 config->setPrimaryOutput(output); 0362 output->setPos(QPoint(0, 0)); 0363 } 0364 0365 void Generator::laptop(KScreen::ConfigPtr &config) 0366 { 0367 KScreen::OutputList usableOutputs = config->connectedOutputs(); 0368 0369 ASSERT_OUTPUTS(usableOutputs) 0370 if (usableOutputs.isEmpty()) { 0371 return; 0372 } 0373 0374 KScreen::OutputPtr embedded = embeddedOutput(usableOutputs); 0375 /* Apparently older laptops use "VGA-*" as embedded output ID, so embeddedOutput() 0376 * will fail, because it looks only for modern "LVDS", "EDP", etc. If we 0377 * fail to detect which output is embedded, just use the one with the lowest 0378 * ID. It's a wild guess, but I think it's highly probable that it will work. 0379 * See bug #318907 for further reference. -- dvratil 0380 */ 0381 if (!embedded) { 0382 QList<int> keys = usableOutputs.keys(); 0383 std::sort(keys.begin(), keys.end()); 0384 embedded = usableOutputs.value(keys.first()); 0385 } 0386 usableOutputs.remove(embedded->id()); 0387 0388 if (usableOutputs.isEmpty() || embedded->modes().isEmpty()) { 0389 qCWarning(KSCREEN_KDED) << "No external outputs found, going for singleOutput()"; 0390 return singleOutput(config); 0391 } 0392 0393 if (isLidClosed() && usableOutputs.count() == 1) { 0394 qCDebug(KSCREEN_KDED) << "With lid closed"; 0395 embedded->setEnabled(false); 0396 0397 KScreen::OutputPtr external = usableOutputs.first(); 0398 if (external->modes().isEmpty()) { 0399 return; 0400 } 0401 config->setPrimaryOutput(external); 0402 external->setPos(QPoint(0, 0)); 0403 return; 0404 } 0405 0406 if (isLidClosed() && usableOutputs.count() > 1) { 0407 qCDebug(KSCREEN_KDED) << "Lid is closed, and more than one output"; 0408 embedded->setEnabled(false); 0409 0410 extendToRight(config, usableOutputs); 0411 return; 0412 } 0413 0414 qCDebug(KSCREEN_KDED) << "Lid is open"; 0415 0416 // If lid is open, laptop screen should be primary 0417 embedded->setPos(QPoint(0, 0)); 0418 embedded->setEnabled(true); 0419 int globalWidth = embedded->geometry().width(); 0420 0421 KScreen::OutputPtr biggest = biggestOutput(usableOutputs); 0422 Q_ASSERT(biggest); 0423 usableOutputs.remove(biggest->id()); 0424 0425 biggest->setPos(QPoint(globalWidth, 0)); 0426 biggest->setEnabled(true); 0427 globalWidth += biggest->geometry().width(); 0428 0429 for (KScreen::OutputPtr output : std::as_const(usableOutputs)) { 0430 output->setEnabled(true); 0431 output->setPos(QPoint(globalWidth, 0)); 0432 globalWidth += output->geometry().width(); 0433 } 0434 0435 if (isDocked()) { 0436 qCDebug(KSCREEN_KDED) << "Docked"; 0437 config->setPrimaryOutput(biggest); 0438 } else { 0439 config->setPrimaryOutput(embedded); 0440 } 0441 } 0442 0443 void Generator::extendToRight(KScreen::ConfigPtr &config, KScreen::OutputList usableOutputs) 0444 { 0445 ASSERT_OUTPUTS(usableOutputs); 0446 if (usableOutputs.isEmpty()) { 0447 return; 0448 } 0449 0450 qCDebug(KSCREEN_KDED) << "Extending to the right"; 0451 0452 KScreen::OutputPtr biggest = biggestOutput(usableOutputs); 0453 Q_ASSERT(biggest); 0454 usableOutputs.remove(biggest->id()); 0455 0456 biggest->setEnabled(true); 0457 biggest->setPos(QPoint(0, 0)); 0458 int globalWidth = biggest->geometry().width(); 0459 0460 for (KScreen::OutputPtr output : std::as_const(usableOutputs)) { 0461 output->setEnabled(true); 0462 output->setPos(QPoint(globalWidth, 0)); 0463 globalWidth += output->geometry().width(); 0464 } 0465 0466 config->setPrimaryOutput(biggest); 0467 } 0468 0469 void Generator::initializeOutput(const KScreen::OutputPtr &output, KScreen::Config::Features features) 0470 { 0471 if (output->modes().empty()) { 0472 output->setEnabled(false); 0473 return; 0474 } 0475 Output::GlobalConfig config = Output::readGlobal(output); 0476 output->setCurrentModeId(config.modeId.value_or(bestModeForOutput(output)->id())); 0477 output->setRotation(config.rotation.value_or(output->rotation())); 0478 if (features & KScreen::Config::Feature::PerOutputScaling) { 0479 output->setScale(config.scale.value_or(bestScaleForOutput(output))); 0480 } 0481 } 0482 0483 KScreen::ModePtr Generator::biggestMode(const KScreen::ModeList &modes) 0484 { 0485 Q_ASSERT(!modes.isEmpty()); 0486 0487 int modeArea, biggestArea = 0; 0488 KScreen::ModePtr biggestMode; 0489 for (const KScreen::ModePtr &mode : modes) { 0490 modeArea = mode->size().width() * mode->size().height(); 0491 if (modeArea < biggestArea) { 0492 continue; 0493 } 0494 if (modeArea == biggestArea && mode->refreshRate() < biggestMode->refreshRate()) { 0495 continue; 0496 } 0497 if (modeArea == biggestArea && mode->refreshRate() > biggestMode->refreshRate()) { 0498 biggestMode = mode; 0499 continue; 0500 } 0501 0502 biggestArea = modeArea; 0503 biggestMode = mode; 0504 } 0505 0506 return biggestMode; 0507 } 0508 0509 KScreen::ModePtr Generator::bestModeForSize(const KScreen::ModeList &modes, const QSize &size) 0510 { 0511 KScreen::ModePtr bestMode; 0512 for (const KScreen::ModePtr &mode : modes) { 0513 if (mode->size() != size) { 0514 continue; 0515 } 0516 0517 if (!bestMode) { 0518 bestMode = mode; 0519 continue; 0520 } 0521 0522 if (mode->refreshRate() > bestMode->refreshRate()) { 0523 bestMode = mode; 0524 } 0525 } 0526 0527 return bestMode; 0528 } 0529 0530 qreal Generator::bestScaleForOutput(const KScreen::OutputPtr &output) 0531 { 0532 // Sanity check outputs that tell us they have no physical size 0533 if (output->sizeMm().height() <= 0) { 0534 return 1.0; 0535 } 0536 0537 /* The eye's ability to perceive detail diminishes with distance, so objects 0538 * that are closer can be smaller and their details remain equally 0539 * distinguishable. As a result, each device type has its own ideal physical 0540 * size of items on its screen based on how close the user's eyes are 0541 * expected to be from it on average, and its target DPI value needs to be 0542 * changed accordingly. 0543 */ 0544 int outputTargetDpi; 0545 0546 if (output->type() != KScreen::Output::Panel) { 0547 outputTargetDpi = targetDpiDesktop; 0548 } else { 0549 if (isLaptop()) { 0550 outputTargetDpi = targetDpiLaptop; 0551 } else { 0552 outputTargetDpi = targetDpiMobile; 0553 } 0554 } 0555 0556 const qreal outputPixelHeight = output->currentMode()->size().height(); 0557 const qreal outputPhysicalHeight = output->sizeMm().height() / 25.4; // convert mm to inches 0558 const qreal outputDpi = outputPixelHeight / outputPhysicalHeight; 0559 0560 const qreal scale = round(outputDpi / outputTargetDpi * scaleRoundingness) / scaleRoundingness; 0561 0562 // Sanity check for outputs with such a low pixel density that the calculated 0563 // scale would be less than 1; this isn't well supported, so just use 1 0564 if (scale < 1) { 0565 return 1.0; 0566 } 0567 // The KCM doesn't support manually setting the scale higher than 3x, so limit 0568 // the auto-calculated value to that 0569 else if (scale > 3) { 0570 return 3.0; 0571 } 0572 0573 return scale; 0574 } 0575 0576 KScreen::ModePtr Generator::bestModeForOutput(const KScreen::OutputPtr &output) 0577 { 0578 if (KScreen::ModePtr outputMode = output->preferredMode()) { 0579 return outputMode; 0580 } 0581 0582 return biggestMode(output->modes()); 0583 } 0584 0585 KScreen::OutputPtr Generator::biggestOutput(const KScreen::OutputList &outputs) 0586 { 0587 ASSERT_OUTPUTS(outputs) 0588 0589 int area, total = 0; 0590 KScreen::OutputPtr biggest; 0591 for (const KScreen::OutputPtr &output : outputs) { 0592 const KScreen::ModePtr mode = bestModeForOutput(output); 0593 if (!mode) { 0594 continue; 0595 } 0596 area = mode->size().width() * mode->size().height(); 0597 if (area <= total) { 0598 continue; 0599 } 0600 0601 total = area; 0602 biggest = output; 0603 } 0604 0605 return biggest; 0606 } 0607 0608 void Generator::disableAllDisconnectedOutputs(const KScreen::OutputList &outputs) 0609 { 0610 for (const KScreen::OutputPtr &output : outputs) { 0611 if (!output->isConnected()) { 0612 qCDebug(KSCREEN_KDED) << output->name() << " Disabled"; 0613 output->setEnabled(false); 0614 } 0615 } 0616 } 0617 0618 KScreen::OutputPtr Generator::embeddedOutput(const KScreen::OutputList &outputs) 0619 { 0620 for (const KScreen::OutputPtr &output : outputs) { 0621 if (output->type() == KScreen::Output::Panel) { 0622 return output; 0623 } 0624 } 0625 0626 return KScreen::OutputPtr(); 0627 } 0628 0629 bool Generator::isLaptop() const 0630 { 0631 if (m_forceLaptop) { 0632 return true; 0633 } 0634 if (m_forceNotLaptop) { 0635 return false; 0636 } 0637 0638 return Device::self()->isLaptop(); 0639 } 0640 0641 bool Generator::isLidClosed() const 0642 { 0643 if (m_forceLidClosed) { 0644 return true; 0645 } 0646 if (m_forceNotLaptop) { 0647 return false; 0648 } 0649 0650 return Device::self()->isLidClosed(); 0651 } 0652 0653 bool Generator::isDocked() const 0654 { 0655 return m_forceDocked; 0656 } 0657 0658 void Generator::setForceLaptop(bool force) 0659 { 0660 m_forceLaptop = force; 0661 } 0662 0663 void Generator::setForceLidClosed(bool force) 0664 { 0665 m_forceLidClosed = force; 0666 } 0667 0668 void Generator::setForceDocked(bool force) 0669 { 0670 m_forceDocked = force; 0671 } 0672 0673 void Generator::setForceNotLaptop(bool force) 0674 { 0675 m_forceNotLaptop = force; 0676 } 0677 0678 #include "moc_generator.cpp"