File indexing completed on 2024-04-28 16:45:09

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 &currentConfig)
0081 {
0082     m_currentConfig = currentConfig;
0083 }
0084 
0085 KScreen::ConfigPtr Generator::idealConfig(const KScreen::ConfigPtr &currentConfig)
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 }