File indexing completed on 2024-04-28 05:36:00

0001 /*
0002     SPDX-FileCopyrightText: 2016 Marco Martin <mart@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "screenpool.h"
0008 #include "outputorderwatcher.h"
0009 #include "screenpool-debug.h"
0010 
0011 #include <KWindowSystem>
0012 #include <QDebug>
0013 #include <QGuiApplication>
0014 #include <QScreen>
0015 
0016 #ifndef NDEBUG
0017 #define CHECK_SCREEN_INVARIANTS screenInvariants();
0018 #else
0019 #define CHECK_SCREEN_INVARIANTS
0020 #endif
0021 
0022 #include <chrono>
0023 
0024 using namespace std::chrono_literals;
0025 
0026 ScreenPool::ScreenPool(QObject *parent)
0027     : QObject(parent)
0028 {
0029     qRegisterMetaType<QList<QScreen *>>("QList<QScreen *>");
0030     connect(qGuiApp, &QGuiApplication::screenAdded, this, [this](QScreen *screen) {
0031         connect(screen, &QScreen::geometryChanged, this, [this, screen]() {
0032             handleScreenGeometryChanged(screen);
0033         });
0034         handleScreenAdded(screen);
0035     });
0036 
0037     for (auto screen : qApp->screens()) {
0038         connect(screen, &QScreen::geometryChanged, this, [this, screen]() {
0039             handleScreenGeometryChanged(screen);
0040         });
0041     }
0042 
0043     connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ScreenPool::handleScreenRemoved);
0044 
0045     // Note that the ScreenPool must process the QGuiApplication::screenAdded signal
0046     // before the primary output watcher.
0047     m_outputOrderWatcher = OutputOrderWatcher::instance(this);
0048     connect(m_outputOrderWatcher, &OutputOrderWatcher::outputOrderChanged, this, &ScreenPool::handleOutputOrderChanged);
0049 
0050     reconsiderOutputOrder();
0051 }
0052 
0053 ScreenPool::~ScreenPool()
0054 {
0055 }
0056 
0057 int ScreenPool::idForName(const QString &connector) const
0058 {
0059     int i = 0;
0060     for (auto *s : m_availableScreens) {
0061         if (s->name() == connector) {
0062             return i;
0063         }
0064         ++i;
0065     }
0066     return -1;
0067 }
0068 
0069 QList<QScreen *> ScreenPool::screenOrder() const
0070 {
0071     return m_availableScreens;
0072 }
0073 
0074 QScreen *ScreenPool::primaryScreen() const
0075 {
0076     if (m_availableScreens.isEmpty()) {
0077         return nullptr;
0078     }
0079     return m_availableScreens.first();
0080 }
0081 
0082 QScreen *ScreenPool::screenForId(int id) const
0083 {
0084     if (id < 0 || m_availableScreens.size() <= id) {
0085         return nullptr;
0086     }
0087 
0088     return m_availableScreens[id];
0089 }
0090 
0091 int ScreenPool::idForScreen(const QScreen *screen) const
0092 {
0093     return m_availableScreens.indexOf(screen);
0094 }
0095 
0096 bool ScreenPool::noRealOutputsConnected() const
0097 {
0098     if (qApp->screens().count() > 1) {
0099         return false;
0100     } else if (m_availableScreens.isEmpty()) {
0101         return true;
0102     }
0103 
0104     return isOutputFake(m_availableScreens.first());
0105 }
0106 
0107 bool ScreenPool::isOutputFake(QScreen *screen) const
0108 {
0109     Q_ASSERT(screen);
0110     // On X11 the output named :0.0 is fake (the geometry is usually valid and whatever the geometry
0111     // of the last connected screen was), on wayland the fake output has no name and no geometry
0112     const bool fake = screen->name() == QStringLiteral(":0.0") || screen->geometry().isEmpty() || screen->name().isEmpty();
0113     // If there is a fake output we can only have one screen left (the fake one)
0114     //    Q_ASSERT(!fake || fake == (qGuiApp->screens().count() == 1));
0115     return fake;
0116 }
0117 
0118 QScreen *ScreenPool::outputRedundantTo(QScreen *screen) const
0119 {
0120     Q_ASSERT(screen);
0121     // Manage fake screens separately
0122     if (isOutputFake(screen)) {
0123         return nullptr;
0124     }
0125     const QRect thisGeometry = screen->geometry();
0126 
0127     // Don't use it as we don't want to consider redundants
0128     const int thisId = m_outputOrderWatcher->outputOrder().indexOf(screen->name());
0129 
0130     // FIXME: QScreen doesn't have any idea of "this qscreen is clone of this other one
0131     // so this ultra inefficient heuristic has to stay until we have a slightly better api
0132     // logic is:
0133     // a screen is redundant if:
0134     //* its geometry is contained in another one
0135     //* if their resolutions are different, the "biggest" one wins
0136     //* if they have the same geometry, the one with the lowest id wins (arbitrary, but gives reproducible behavior and makes the primary screen win)
0137     for (QScreen *s : m_sizeSortedScreens) {
0138         // don't compare with itself
0139         if (screen == s) {
0140             continue;
0141         }
0142 
0143         const QRect otherGeometry = s->geometry();
0144 
0145         if (otherGeometry.isNull()) {
0146             continue;
0147         }
0148 
0149         const int otherId = m_outputOrderWatcher->outputOrder().indexOf(s->name());
0150 
0151         if (otherGeometry.contains(thisGeometry, false)
0152             && ( // since at this point contains is true, if either
0153                  // measure of othergeometry is bigger, has a bigger area
0154                 otherGeometry.width() > thisGeometry.width() || otherGeometry.height() > thisGeometry.height() ||
0155                 // ids not -1 are considered in descending order of importance
0156                 //-1 means that is a screen not known yet, just arrived and
0157                 // not yet in screenpool: this happens for screens that
0158                 // are hotplugged and weren't known. it does NOT happen
0159                 // at first startup, as screenpool populates on load with all screens connected at the moment before the rest of the shell starts up
0160                 (thisId == -1 && otherId != -1) || (thisId > otherId && otherId != -1))) {
0161             return s;
0162         }
0163     }
0164 
0165     return nullptr;
0166 }
0167 
0168 void ScreenPool::insertSortedScreen(QScreen *screen)
0169 {
0170     if (m_sizeSortedScreens.contains(screen)) {
0171         // This should happen only when a fake screen isn't anymore
0172         return;
0173     }
0174     auto before = std::find_if(m_sizeSortedScreens.begin(), m_sizeSortedScreens.end(), [this, screen](QScreen *otherScreen) {
0175         return (screen->geometry().width() > otherScreen->geometry().width() && screen->geometry().height() > otherScreen->geometry().height())
0176             || idForName(screen->name()) < idForName(otherScreen->name());
0177     });
0178     m_sizeSortedScreens.insert(before, screen);
0179 }
0180 
0181 void ScreenPool::handleScreenGeometryChanged(QScreen *screen)
0182 {
0183     m_sizeSortedScreens.removeAll(screen);
0184     insertSortedScreen(screen);
0185     reconsiderOutputOrder();
0186 }
0187 
0188 void ScreenPool::handleScreenAdded(QScreen *screen)
0189 {
0190     qCDebug(SCREENPOOL) << "handleScreenAdded" << screen << screen->geometry();
0191 
0192     insertSortedScreen(screen);
0193 
0194     if (isOutputFake(screen)) {
0195         m_fakeScreens.insert(screen);
0196         return;
0197     } else {
0198         m_fakeScreens.remove(screen);
0199     }
0200 
0201     // TODO: remove?
0202     if (QScreen *toScreen = outputRedundantTo(screen)) {
0203         m_redundantScreens.insert(screen, toScreen);
0204         return;
0205     }
0206 
0207     if (m_fakeScreens.contains(screen)) {
0208         qCDebug(SCREENPOOL) << "not fake anymore" << screen;
0209         m_fakeScreens.remove(screen);
0210     }
0211 
0212     Q_ASSERT(!m_availableScreens.contains(screen));
0213 }
0214 
0215 void ScreenPool::handleScreenRemoved(QScreen *screen)
0216 {
0217     qCDebug(SCREENPOOL) << "handleScreenRemoved" << screen;
0218 
0219     auto it = std::find_if(m_redundantScreens.begin(), m_redundantScreens.end(), [screen](QScreen *value) {
0220         return (value == screen);
0221     });
0222 
0223     if (it != m_redundantScreens.end()) {
0224         m_redundantScreens.erase(it);
0225     }
0226 
0227     m_sizeSortedScreens.removeAll(screen);
0228     if (m_redundantScreens.contains(screen)) {
0229         Q_ASSERT(!m_fakeScreens.contains(screen));
0230         Q_ASSERT(!m_availableScreens.contains(screen));
0231         m_redundantScreens.remove(screen);
0232     } else if (m_fakeScreens.contains(screen)) {
0233         // If is in fake screens, then must be fake, this will only happen on Wayland
0234         Q_ASSERT(isOutputFake(screen));
0235         Q_ASSERT(!m_redundantScreens.contains(screen));
0236         Q_ASSERT(!m_availableScreens.contains(screen));
0237         m_fakeScreens.remove(screen);
0238     } else if (isOutputFake(screen)) {
0239         // Fake but not in m_fakeScreens can only happen on X11, where the last output quietly renames itself to ":0.0" without signals
0240 #if HAVE_X11
0241         Q_ASSERT(KWindowSystem::isPlatformX11());
0242 #endif
0243         Q_ASSERT(m_availableScreens.contains(screen));
0244         Q_ASSERT(!m_redundantScreens.contains(screen));
0245         Q_ASSERT(!m_fakeScreens.contains(screen));
0246         m_availableScreens.removeAll(screen);
0247     } else if (m_availableScreens.contains(screen)) {
0248         // It's possible that the screen was already "removed" by handleOutputOrderChanged
0249         Q_ASSERT(m_availableScreens.contains(screen));
0250         Q_ASSERT(!m_redundantScreens.contains(screen));
0251         Q_ASSERT(!m_fakeScreens.contains(screen));
0252         m_orderChangedPendingSignal = true;
0253         Q_EMIT screenRemoved(screen);
0254         m_availableScreens.removeAll(screen);
0255     }
0256 
0257     // We can't call CHECK_SCREEN_INVARIANTS here, but on handleOutputOrderChanged which is about to arrive
0258 }
0259 
0260 void ScreenPool::handleOutputOrderChanged(const QStringList &newOrder)
0261 {
0262     QHash<QString, QScreen *> connMap;
0263     for (auto s : qApp->screens()) {
0264         connMap[s->name()] = s;
0265     }
0266 
0267     QList<QScreen *> newAvailableScreens;
0268     m_redundantScreens.clear();
0269 
0270     for (const auto &c : newOrder) {
0271         // When a screen is removed we reevaluate the order, but the order changed signal may not have happened yet,
0272         // so filter out outputs which now are invalid
0273         if (!connMap.contains(c)) {
0274             continue;
0275         }
0276         auto *s = connMap[c];
0277         if (!m_sizeSortedScreens.contains(s)) {
0278             handleScreenAdded(s);
0279         }
0280         if (isOutputFake(s)) {
0281             m_fakeScreens.insert(s);
0282             continue;
0283         }
0284 
0285         auto *toScreen = outputRedundantTo(s);
0286         if (!toScreen) {
0287             newAvailableScreens.append(s);
0288         } else {
0289             m_redundantScreens.insert(s, toScreen);
0290         }
0291     }
0292 
0293     // always emit removals before updating the sorted list
0294     for (auto screen : std::as_const(m_availableScreens)) {
0295         if (!newAvailableScreens.contains(screen)) {
0296             Q_EMIT screenRemoved(screen);
0297         }
0298     }
0299 
0300     if (m_orderChangedPendingSignal || newAvailableScreens != m_availableScreens) {
0301         qCDebug(SCREENPOOL) << "screenOrderChanged, old order:" << m_availableScreens << "new order:" << newAvailableScreens;
0302         m_availableScreens = newAvailableScreens;
0303         Q_EMIT screenOrderChanged(m_availableScreens);
0304     }
0305 
0306     m_orderChangedPendingSignal = false;
0307 
0308     CHECK_SCREEN_INVARIANTS
0309     // FIXME Async as the status can be incoherent before a fake screen goes away?
0310 }
0311 
0312 void ScreenPool::reconsiderOutputOrder()
0313 {
0314     // prevent race condition between us and the watcher. depending on who receives signals first,
0315     // the current screens may not be the ones the watcher knows about -> force an update.
0316     m_outputOrderWatcher->refresh();
0317     handleOutputOrderChanged(m_outputOrderWatcher->outputOrder());
0318 }
0319 
0320 void ScreenPool::screenInvariants()
0321 {
0322     if (m_outputOrderWatcher->outputOrder().isEmpty()) {
0323         return;
0324     }
0325     // Is the primary connector in sync with the actual primaryScreen? The only way it can get out of sync with primaryConnector() is the single fake screen/no
0326     // real outputs scenario
0327     Q_ASSERT(noRealOutputsConnected() || !m_availableScreens.isEmpty());
0328     // Is the primary screen available? TODO: it can be redundant
0329     // Q_ASSERT(m_availableScreens.contains(primaryScreen()));
0330 
0331     // QScreen bookeeping integrity
0332     auto allScreens = qGuiApp->screens();
0333     // Do we actually track every screen?
0334     qCDebug(SCREENPOOL) << "Checking screens: available:" << m_availableScreens << "redundant:" << m_redundantScreens << "fake:" << m_fakeScreens
0335                         << "all:" << allScreens;
0336     Q_ASSERT((m_availableScreens.count() + m_redundantScreens.count()) == m_outputOrderWatcher->outputOrder().count());
0337     Q_ASSERT(allScreens.count() == m_sizeSortedScreens.count());
0338 
0339     // At most one fake output
0340     Q_ASSERT(m_fakeScreens.count() <= 1);
0341     for (QScreen *screen : allScreens) {
0342         // ignore screens filtered out by the output order
0343         if (!m_outputOrderWatcher->outputOrder().contains(screen->name())) {
0344             continue;
0345         }
0346 
0347         if (m_availableScreens.contains(screen)) {
0348             // If available can't be redundant
0349             Q_ASSERT(!m_redundantScreens.contains(screen));
0350         } else if (m_redundantScreens.contains(screen)) {
0351             // If redundant can't be available
0352             Q_ASSERT(!m_availableScreens.contains(screen));
0353         } else if (m_fakeScreens.contains(screen)) {
0354             // A fake screen can't be anywhere else
0355             // This branch is quite rare, happens only during the transition from a fake screen to a real one, for a brief moment both screens can be there
0356             Q_ASSERT(!m_availableScreens.contains(screen));
0357             Q_ASSERT(!m_redundantScreens.contains(screen));
0358         } else {
0359             // We can't have a screen unaccounted for
0360             Q_ASSERT(false);
0361         }
0362     }
0363 
0364     for (QScreen *screen : m_redundantScreens.keys()) {
0365         Q_ASSERT(outputRedundantTo(screen) != nullptr);
0366     }
0367 }
0368 
0369 QDebug operator<<(QDebug debug, const ScreenPool *pool)
0370 {
0371     debug << pool->metaObject()->className() << '(' << static_cast<const void *>(pool) << ") Internal state:\n";
0372     debug << "Screen Order:\t" << pool->m_availableScreens << '\n';
0373     debug << "\"Fake\" screens:\t" << pool->m_fakeScreens << '\n';
0374     debug << "Redundant screens covered by other ones:\t" << pool->m_redundantScreens << '\n';
0375     debug << "All screens, ordered by size:\t" << pool->m_sizeSortedScreens << '\n';
0376     debug << "All screen that QGuiApplication knows:\t" << qGuiApp->screens() << '\n';
0377     return debug;
0378 }
0379 
0380 #include "moc_screenpool.cpp"