File indexing completed on 2024-04-28 16:54:58

0001 /*
0002     SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>
0003     SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "outputorderwatcher.h"
0009 #include "debug.h"
0010 
0011 #include <QGuiApplication>
0012 #include <QScreen>
0013 #include <QTimer>
0014 
0015 #include <KWindowSystem>
0016 
0017 #include "qwayland-kde-output-order-v1.h"
0018 #include <QtWaylandClient/QWaylandClientExtension>
0019 #include <QtWaylandClient/QtWaylandClientVersion>
0020 
0021 #if HAVE_X11
0022 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0023 #include <private/qtx11extras_p.h>
0024 #else
0025 #include <QX11Info>
0026 #endif
0027 
0028 #include <xcb/randr.h>
0029 #include <xcb/xcb_event.h>
0030 #endif // HAVE_X11
0031 
0032 template<typename T>
0033 using ScopedPointer = QScopedPointer<T, QScopedPointerPodDeleter>;
0034 
0035 class WaylandOutputOrder : public QWaylandClientExtensionTemplate<WaylandOutputOrder>, public QtWayland::kde_output_order_v1
0036 {
0037     Q_OBJECT
0038 public:
0039     WaylandOutputOrder(QObject *parent)
0040         : QWaylandClientExtensionTemplate(1)
0041     {
0042         setParent(parent);
0043 #if QTWAYLANDCLIENT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
0044         initialize();
0045 #else
0046         QMetaObject::invokeMethod(this, "addRegistryListener");
0047 #endif
0048     }
0049 
0050 protected:
0051     void kde_output_order_v1_output(const QString &outputName) override
0052     {
0053         if (m_done) {
0054             m_outputOrder.clear();
0055             m_done = false;
0056         }
0057         m_outputOrder.append(outputName);
0058     }
0059 
0060     void kde_output_order_v1_done() override
0061     {
0062         m_done = true;
0063         Q_EMIT outputOrderChanged(m_outputOrder);
0064     }
0065 
0066 Q_SIGNALS:
0067     void outputOrderChanged(const QStringList &outputName);
0068 
0069 private:
0070     QStringList m_outputOrder;
0071     bool m_done = true;
0072 };
0073 
0074 OutputOrderWatcher::OutputOrderWatcher(QObject *parent)
0075     : QObject(parent)
0076 {
0077     connect(qGuiApp, &QGuiApplication::screenAdded, this, &OutputOrderWatcher::refresh);
0078     connect(qGuiApp, &QGuiApplication::screenRemoved, this, [this](QScreen *screen) {
0079         // Are we in the special fake single screen situation?
0080         if (m_outputOrder.size() == 1 && m_outputOrder.contains(screen->name())) {
0081             m_outputOrder.clear();
0082         }
0083         refresh();
0084     });
0085 }
0086 
0087 void OutputOrderWatcher::useFallback(bool fallback)
0088 {
0089     m_orderProtocolPresent = !fallback;
0090     if (fallback) {
0091         connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &OutputOrderWatcher::refresh, Qt::UniqueConnection);
0092         refresh();
0093     }
0094 }
0095 
0096 OutputOrderWatcher *OutputOrderWatcher::instance(QObject *parent)
0097 {
0098 #if HAVE_X11
0099     if (KWindowSystem::isPlatformX11()) {
0100         return new X11OutputOrderWatcher(parent);
0101     } else
0102 #endif
0103         if (KWindowSystem::isPlatformWayland()) {
0104         return new WaylandOutputOrderWatcher(parent);
0105     }
0106     // return default impl that does something at least
0107     return new OutputOrderWatcher(parent);
0108 }
0109 
0110 void OutputOrderWatcher::refresh()
0111 {
0112     Q_ASSERT(!m_orderProtocolPresent);
0113 
0114     QStringList pendingOutputOrder;
0115 
0116     pendingOutputOrder.clear();
0117     for (auto *s : qApp->screens()) {
0118         pendingOutputOrder.append(s->name());
0119     }
0120 
0121     auto outputLess = [](const QString &c1, const QString &c2) {
0122         if (c1 == qApp->primaryScreen()->name()) {
0123             return true;
0124         } else if (c2 == qApp->primaryScreen()->name()) {
0125             return false;
0126         } else {
0127             return c1 < c2;
0128         }
0129     };
0130     std::sort(pendingOutputOrder.begin(), pendingOutputOrder.end(), outputLess);
0131 
0132     if (m_outputOrder != pendingOutputOrder) {
0133         m_outputOrder = pendingOutputOrder;
0134         Q_EMIT outputOrderChanged(m_outputOrder);
0135     }
0136     return;
0137 }
0138 
0139 QStringList OutputOrderWatcher::outputOrder() const
0140 {
0141     return m_outputOrder;
0142 }
0143 
0144 X11OutputOrderWatcher::X11OutputOrderWatcher(QObject *parent)
0145     : OutputOrderWatcher(parent)
0146 {
0147     // This timer is used to signal only when a qscreen for every output is already created, perhaps by monitoring
0148     // screenadded/screenremoved and tracking the outputs still missing
0149     m_delayTimer = new QTimer(this);
0150     m_delayTimer->setSingleShot(true);
0151     m_delayTimer->setInterval(0);
0152     connect(m_delayTimer, &QTimer::timeout, this, [this]() {
0153         refresh();
0154     });
0155 
0156     // By default try to use the protocol on x11
0157     m_orderProtocolPresent = true;
0158 
0159     qGuiApp->installNativeEventFilter(this);
0160     const xcb_query_extension_reply_t *reply = xcb_get_extension_data(QX11Info::connection(), &xcb_randr_id);
0161     m_xrandrExtensionOffset = reply->first_event;
0162 
0163     const QByteArray effectName = QByteArrayLiteral("_KDE_SCREEN_INDEX");
0164     xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(QX11Info::connection(), false, effectName.length(), effectName);
0165     xcb_intern_atom_reply_t *atom(xcb_intern_atom_reply(QX11Info::connection(), atomCookie, nullptr));
0166     if (!atom) {
0167         useFallback(true);
0168         return;
0169     }
0170 
0171     m_kdeScreenAtom = atom->atom;
0172     m_delayTimer->start();
0173 }
0174 
0175 void X11OutputOrderWatcher::refresh()
0176 {
0177     if (!m_orderProtocolPresent) {
0178         OutputOrderWatcher::refresh();
0179         return;
0180     }
0181     QMap<int, QString> orderMap;
0182 
0183     ScopedPointer<xcb_randr_get_screen_resources_current_reply_t> reply(
0184         xcb_randr_get_screen_resources_current_reply(QX11Info::connection(),
0185                                                      xcb_randr_get_screen_resources_current(QX11Info::connection(), QX11Info::appRootWindow()),
0186                                                      NULL));
0187 
0188     xcb_timestamp_t timestamp = reply->config_timestamp;
0189     int len = xcb_randr_get_screen_resources_current_outputs_length(reply.data());
0190     xcb_randr_output_t *randr_outputs = xcb_randr_get_screen_resources_current_outputs(reply.data());
0191 
0192     for (int i = 0; i < len; i++) {
0193         ScopedPointer<xcb_randr_get_output_info_reply_t> output(
0194             xcb_randr_get_output_info_reply(QX11Info::connection(), xcb_randr_get_output_info(QX11Info::connection(), randr_outputs[i], timestamp), NULL));
0195 
0196         if (output == NULL || output->connection == XCB_RANDR_CONNECTION_DISCONNECTED || output->crtc == 0) {
0197             continue;
0198         }
0199 
0200         const auto screenName = QString::fromUtf8((const char *)xcb_randr_get_output_info_name(output.get()), xcb_randr_get_output_info_name_length(output.get()));
0201 
0202         auto orderCookie = xcb_randr_get_output_property(QX11Info::connection(), randr_outputs[i], m_kdeScreenAtom, XCB_ATOM_ANY, 0, 100, false, false);
0203         ScopedPointer<xcb_randr_get_output_property_reply_t> orderReply(xcb_randr_get_output_property_reply(QX11Info::connection(), orderCookie, nullptr));
0204         // If there is even a single screen without _KDE_SCREEN_INDEX info, fall back to alphabetical ordering
0205         if (!orderReply) {
0206             useFallback(true);
0207             return;
0208         }
0209 
0210         if (!(orderReply->type == XCB_ATOM_INTEGER && orderReply->format == 32 && orderReply->num_items == 1)) {
0211             useFallback(true);
0212             return;
0213         }
0214 
0215         const uint32_t order = *xcb_randr_get_output_property_data(orderReply.data());
0216 
0217         if (order > 0) { // 0 is the special case for disabled, so we ignore it
0218             orderMap[order] = screenName;
0219         }
0220     }
0221 
0222     QStringList pendingOutputOrder;
0223 
0224     for (const auto &screenName : orderMap) {
0225         pendingOutputOrder.append(screenName);
0226     }
0227 
0228     for (const auto &name : std::as_const(pendingOutputOrder)) {
0229         bool present = false;
0230         for (auto *s : qApp->screens()) {
0231             if (s->name() == name) {
0232                 present = true;
0233                 break;
0234             }
0235         }
0236         // if the pending output order refers to screens
0237         // we don't know of yet, try again next time a screen is added
0238 
0239         // this seems unlikely given we have the server lock and the timing thing
0240         if (!present) {
0241             m_delayTimer->start();
0242             return;
0243         }
0244     }
0245 
0246     if (pendingOutputOrder != m_outputOrder) {
0247         m_outputOrder = pendingOutputOrder;
0248         Q_EMIT outputOrderChanged(m_outputOrder);
0249     }
0250 }
0251 
0252 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0253 bool X11OutputOrderWatcher::nativeEventFilter(const QByteArray &eventType, void *message, long int *result)
0254 #else
0255 bool X11OutputOrderWatcher::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)
0256 #endif
0257 {
0258     Q_UNUSED(result);
0259     // a particular edge case: when we switch the only enabled screen
0260     // we don't have any signal about it, the primary screen changes but we have the same old QScreen* getting recycled
0261     // see https://bugs.kde.org/show_bug.cgi?id=373880
0262     // if this slot will be invoked many times, their//second time on will do nothing as name and primaryOutputName will be the same by then
0263     if (eventType[0] != 'x') {
0264         return false;
0265     }
0266 
0267     xcb_generic_event_t *ev = static_cast<xcb_generic_event_t *>(message);
0268 
0269     const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev);
0270 
0271     if (responseType == m_xrandrExtensionOffset + XCB_RANDR_NOTIFY) {
0272         auto *randrEvent = reinterpret_cast<xcb_randr_notify_event_t *>(ev);
0273         if (randrEvent->subCode == XCB_RANDR_NOTIFY_OUTPUT_PROPERTY) {
0274             xcb_randr_output_property_t property = randrEvent->u.op;
0275 
0276             if (property.atom == m_kdeScreenAtom) {
0277                 // Force an X11 roundtrip to make sure we have all other
0278                 // screen events in the buffer when we process the deferred refresh
0279                 useFallback(false);
0280                 QX11Info::getTimestamp();
0281                 m_delayTimer->start();
0282             }
0283         }
0284     }
0285     return false;
0286 }
0287 
0288 WaylandOutputOrderWatcher::WaylandOutputOrderWatcher(QObject *parent)
0289     : OutputOrderWatcher(parent)
0290 {
0291     // Asking for primaryOutputName() before this happened, will return qGuiApp->primaryScreen()->name() anyways, so set it so the outputOrderChanged will
0292     // have parameters that are coherent
0293     OutputOrderWatcher::refresh();
0294 
0295     auto outputListManagement = new WaylandOutputOrder(this);
0296     m_orderProtocolPresent = outputListManagement->isActive();
0297     if (!m_orderProtocolPresent) {
0298         useFallback(true);
0299         return;
0300     }
0301     connect(outputListManagement, &WaylandOutputOrder::outputOrderChanged, this, [this](const QStringList &order) {
0302         m_pendingOutputOrder = order;
0303 
0304         if (hasAllScreens()) {
0305             if (m_pendingOutputOrder != m_outputOrder) {
0306                 m_outputOrder = m_pendingOutputOrder;
0307                 Q_EMIT outputOrderChanged(m_outputOrder);
0308             }
0309         }
0310         // otherwse wait for next QGuiApp screenAdded/removal
0311         // to keep things in sync
0312     });
0313 }
0314 
0315 bool WaylandOutputOrderWatcher::hasAllScreens() const
0316 {
0317     // for each name in our ordered list, find a screen with that name
0318     for (const auto &name : std::as_const(m_pendingOutputOrder)) {
0319         bool present = false;
0320         for (auto *s : qApp->screens()) {
0321             if (s->name() == name) {
0322                 present = true;
0323                 break;
0324             }
0325         }
0326         if (!present) {
0327             return false;
0328         }
0329     }
0330     return true;
0331 }
0332 
0333 void WaylandOutputOrderWatcher::refresh()
0334 {
0335     if (!m_orderProtocolPresent) {
0336         OutputOrderWatcher::refresh();
0337         return;
0338     }
0339 
0340     if (!hasAllScreens()) {
0341         return;
0342     }
0343 
0344     if (m_outputOrder != m_pendingOutputOrder) {
0345         m_outputOrder = m_pendingOutputOrder;
0346         Q_EMIT outputOrderChanged(m_outputOrder);
0347     }
0348 }
0349 
0350 #include "outputorderwatcher.moc"