File indexing completed on 2024-04-28 05:35:35

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