File indexing completed on 2024-04-28 16:55: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(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         Q_ASSERT(!m_redundantScreens.contains(screen));
0234         Q_ASSERT(!m_availableScreens.contains(screen));
0235         m_fakeScreens.remove(screen);
0236     } else if (isOutputFake(screen)) {
0237         // This happens when an output is recycled because it was the last one and became fake
0238         Q_ASSERT(m_availableScreens.contains(screen));
0239         Q_ASSERT(!m_redundantScreens.contains(screen));
0240         Q_ASSERT(!m_fakeScreens.contains(screen));
0241         Q_ASSERT(m_sizeSortedScreens.isEmpty());
0242         m_sizeSortedScreens.append(screen);
0243         m_availableScreens.removeAll(screen);
0244         m_fakeScreens.insert(screen);
0245     } else if (m_availableScreens.contains(screen)) {
0246         // It's possible that the screen was already "removed" by handleOutputOrderChanged
0247         Q_ASSERT(m_availableScreens.contains(screen));
0248         Q_ASSERT(!m_redundantScreens.contains(screen));
0249         Q_ASSERT(!m_fakeScreens.contains(screen));
0250         m_orderChangedPendingSignal = true;
0251         Q_EMIT screenRemoved(screen);
0252         m_availableScreens.removeAll(screen);
0253     }
0254 
0255     // We can't call CHECK_SCREEN_INVARIANTS here, but on handleOutputOrderChanged which is about to arrive
0256 }
0257 
0258 void ScreenPool::handleOutputOrderChanged(const QStringList &newOrder)
0259 {
0260     QHash<QString, QScreen *> connMap;
0261     for (auto s : qApp->screens()) {
0262         connMap[s->name()] = s;
0263     }
0264 
0265     QList<QScreen *> newAvailableScreens;
0266     m_redundantScreens.clear();
0267 
0268     for (const auto &c : newOrder) {
0269         // When a screen is removed we reevaluate the order, but the order changed signal may not have happened yet,
0270         // so filter out outputs which now are invalid
0271         if (!connMap.contains(c)) {
0272             continue;
0273         }
0274         auto *s = connMap[c];
0275         if (!m_sizeSortedScreens.contains(s)) {
0276             handleScreenAdded(s);
0277         }
0278         if (isOutputFake(s)) {
0279             m_fakeScreens.insert(s);
0280             continue;
0281         }
0282 
0283         auto *toScreen = outputRedundantTo(s);
0284         if (!toScreen) {
0285             newAvailableScreens.append(s);
0286         } else {
0287             m_redundantScreens.insert(s, toScreen);
0288         }
0289     }
0290 
0291     // always emit removals before updating the sorted list
0292     for (auto screen : std::as_const(m_availableScreens)) {
0293         if (!newAvailableScreens.contains(screen)) {
0294             Q_EMIT screenRemoved(screen);
0295         }
0296     }
0297 
0298     if (m_orderChangedPendingSignal || newAvailableScreens != m_availableScreens) {
0299         m_availableScreens = newAvailableScreens;
0300         Q_EMIT screenOrderChanged(m_availableScreens);
0301     }
0302 
0303     m_orderChangedPendingSignal = false;
0304 
0305     CHECK_SCREEN_INVARIANTS
0306     // FIXME Async as the status can be incoherent before a fake screen goes away?
0307 }
0308 
0309 void ScreenPool::reconsiderOutputOrder()
0310 {
0311     // prevent race condition between us and the watcher. depending on who receives signals first,
0312     // the current screens may not be the ones the watcher knows about -> force an update.
0313     m_outputOrderWatcher->refresh();
0314     handleOutputOrderChanged(m_outputOrderWatcher->outputOrder());
0315 }
0316 
0317 void ScreenPool::screenInvariants()
0318 {
0319     if (m_outputOrderWatcher->outputOrder().isEmpty()) {
0320         return;
0321     }
0322     // 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
0323     // real outputs scenario
0324     Q_ASSERT(noRealOutputsConnected() || !m_availableScreens.isEmpty());
0325     // Is the primary screen available? TODO: it can be redundant
0326     // Q_ASSERT(m_availableScreens.contains(primaryScreen()));
0327 
0328     // QScreen bookeeping integrity
0329     auto allScreens = qGuiApp->screens();
0330     // Do we actually track every screen?
0331     qWarning() << "Checking screens: available:" << m_availableScreens << "redundant:" << m_redundantScreens << "fake:" << m_fakeScreens
0332                << "all:" << allScreens;
0333     Q_ASSERT((m_availableScreens.count() + m_redundantScreens.count() + m_fakeScreens.count()) == m_outputOrderWatcher->outputOrder().count());
0334     Q_ASSERT(allScreens.count() == m_sizeSortedScreens.count());
0335 
0336     // At most one fake output
0337     Q_ASSERT(m_fakeScreens.count() <= 1);
0338     for (QScreen *screen : allScreens) {
0339         // ignore screens filtered out by the output order
0340         if (!m_outputOrderWatcher->outputOrder().contains(screen->name())) {
0341             continue;
0342         }
0343 
0344         if (m_availableScreens.contains(screen)) {
0345             // If available can't be redundant
0346             Q_ASSERT(!m_redundantScreens.contains(screen));
0347         } else if (m_redundantScreens.contains(screen)) {
0348             // If redundant can't be available
0349             Q_ASSERT(!m_availableScreens.contains(screen));
0350         } else if (m_fakeScreens.contains(screen)) {
0351             // A fake screen can't be anywhere else
0352             // 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
0353             Q_ASSERT(!m_availableScreens.contains(screen));
0354             Q_ASSERT(!m_redundantScreens.contains(screen));
0355         } else {
0356             // We can't have a screen unaccounted for
0357             Q_ASSERT(false);
0358         }
0359     }
0360 
0361     for (QScreen *screen : m_redundantScreens.keys()) {
0362         Q_ASSERT(outputRedundantTo(screen) != nullptr);
0363     }
0364 }
0365 
0366 QDebug operator<<(QDebug debug, const ScreenPool *pool)
0367 {
0368     debug << pool->metaObject()->className() << '(' << static_cast<const void *>(pool) << ") Internal state:\n";
0369     debug << "Screen Order:\t" << pool->m_availableScreens << '\n';
0370     debug << "\"Fake\" screens:\t" << pool->m_fakeScreens << '\n';
0371     debug << "Redundant screens covered by other ones:\t" << pool->m_redundantScreens << '\n';
0372     debug << "All screens, ordered by size:\t" << pool->m_sizeSortedScreens << '\n';
0373     debug << "All screen that QGuiApplication knows:\t" << qGuiApp->screens() << '\n';
0374     return debug;
0375 }
0376 
0377 #include "moc_screenpool.cpp"