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"