Plasma-workspace

outputorderwatcher.cpp
1/*
2 SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>
3 SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "outputorderwatcher.h"
9
10#include <ranges>
11
12#include <QScreen>
13#include <QTimer>
14
15#include <KWindowSystem>
16
17#include "qwayland-kde-output-order-v1.h"
18#include <QtWaylandClient/QWaylandClientExtension>
19#include <QtWaylandClient/QtWaylandClientVersion>
20
21#if HAVE_X11
22#include <X11/Xlib.h>
23#include <xcb/randr.h>
24#include <xcb/xcb_event.h>
25#endif // HAVE_X11
26
27template<typename T>
29
30class WaylandOutputOrder : public QWaylandClientExtensionTemplate<WaylandOutputOrder, &QtWayland::kde_output_order_v1::destroy>,
31 public QtWayland::kde_output_order_v1
32{
33 Q_OBJECT
34public:
35 WaylandOutputOrder(QObject *parent)
36 : QWaylandClientExtensionTemplate(1)
37 {
38 setParent(parent);
39 initialize();
40 }
41
42protected:
43 void kde_output_order_v1_output(const QString &outputName) override
44 {
45 if (m_done) {
46 m_outputOrder.clear();
47 m_done = false;
48 }
49 m_outputOrder.append(outputName);
50 }
51
52 void kde_output_order_v1_done() override
53 {
54 // If no output arrived it means we don't have *any* usable output
55 if (m_done) {
56 m_outputOrder.clear();
57 }
58 m_done = true;
59 Q_EMIT outputOrderChanged(m_outputOrder);
60 }
61
62Q_SIGNALS:
63 void outputOrderChanged(const QStringList &outputName);
64
65private:
66 QStringList m_outputOrder;
67 bool m_done = true;
68};
69
70OutputOrderWatcher::OutputOrderWatcher(QObject *parent)
71 : QObject(parent)
72{
75}
76
78{
79 m_orderProtocolPresent = !fallback;
80 if (fallback) {
82 refresh();
83 }
84}
85
87{
88#if HAVE_X11
90 return new X11OutputOrderWatcher(parent);
91 } else
92#endif
94 return new WaylandOutputOrderWatcher(parent);
95 }
96 // return default impl that does something at least
97 return new OutputOrderWatcher(parent);
98}
99
101{
102 Q_ASSERT(!m_orderProtocolPresent);
103
104 QStringList pendingOutputOrder;
105
106 pendingOutputOrder.clear();
107 for (auto *s : qApp->screens()) {
108 pendingOutputOrder.append(s->name());
109 }
110
111 auto outputLess = [](const QString &c1, const QString &c2) {
112 if (c1 == qApp->primaryScreen()->name()) {
113 return true;
114 } else if (c2 == qApp->primaryScreen()->name()) {
115 return false;
116 } else {
117 return c1 < c2;
118 }
119 };
120 std::sort(pendingOutputOrder.begin(), pendingOutputOrder.end(), outputLess);
121
122 if (m_outputOrder != pendingOutputOrder) {
123 m_outputOrder = pendingOutputOrder;
124 Q_EMIT outputOrderChanged(m_outputOrder);
125 }
126 return;
127}
128
130{
131 return m_outputOrder;
132}
133
134X11OutputOrderWatcher::X11OutputOrderWatcher(QObject *parent)
135 : OutputOrderWatcher(parent)
136 , m_x11Interface(qGuiApp->nativeInterface<QNativeInterface::QX11Application>())
137{
138 if (!m_x11Interface) [[unlikely]] {
139 Q_ASSERT(false);
140 return;
141 }
142 // This timer is used to signal only when a qscreen for every output is already created, perhaps by monitoring
143 // screenadded/screenremoved and tracking the outputs still missing
144 m_delayTimer = new QTimer(this);
145 m_delayTimer->setSingleShot(true);
146 m_delayTimer->setInterval(0);
147 connect(m_delayTimer, &QTimer::timeout, this, [this]() {
148 refresh();
149 });
150
151 // By default try to use the protocol on x11
152 m_orderProtocolPresent = true;
153
154 qGuiApp->installNativeEventFilter(this);
155 const xcb_query_extension_reply_t *reply = xcb_get_extension_data(m_x11Interface->connection(), &xcb_randr_id);
156 m_xrandrExtensionOffset = reply->first_event;
157
158 constexpr const char *effectName = "_KDE_SCREEN_INDEX";
159 xcb_intern_atom_cookie_t atomCookie =
160 xcb_intern_atom_unchecked(m_x11Interface->connection(), false, std::char_traits<char>::length(effectName), effectName);
161 xcb_intern_atom_reply_t *atom(xcb_intern_atom_reply(m_x11Interface->connection(), atomCookie, nullptr));
162 if (!atom) {
163 useFallback(true);
164 return;
165 }
166
167 m_kdeScreenAtom = atom->atom;
168 m_delayTimer->start();
169}
170
171void X11OutputOrderWatcher::refresh()
172{
173 if (!m_orderProtocolPresent) {
175 return;
176 }
178
179 ScopedPointer<xcb_randr_get_screen_resources_current_reply_t> reply(xcb_randr_get_screen_resources_current_reply(
180 m_x11Interface->connection(),
181 xcb_randr_get_screen_resources_current(m_x11Interface->connection(), DefaultRootWindow(m_x11Interface->display())),
182 NULL));
183
184 xcb_timestamp_t timestamp = reply->config_timestamp;
185 int len = xcb_randr_get_screen_resources_current_outputs_length(reply.data());
186 xcb_randr_output_t *randr_outputs = xcb_randr_get_screen_resources_current_outputs(reply.data());
187
188 for (int i = 0; i < len; i++) {
190 xcb_randr_get_output_info_reply(m_x11Interface->connection(),
191 xcb_randr_get_output_info(m_x11Interface->connection(), randr_outputs[i], timestamp),
192 NULL));
193
194 if (output == NULL || output->connection == XCB_RANDR_CONNECTION_DISCONNECTED || output->crtc == 0) {
195 continue;
196 }
197
198 auto orderCookie = xcb_randr_get_output_property(m_x11Interface->connection(), randr_outputs[i], m_kdeScreenAtom, XCB_ATOM_ANY, 0, 100, false, false);
200 xcb_randr_get_output_property_reply(m_x11Interface->connection(), orderCookie, nullptr));
201 // If there is even a single screen without _KDE_SCREEN_INDEX info, fall back to alphabetical ordering
202 if (!orderReply) {
203 useFallback(true);
204 return;
205 }
206
207 if (!(orderReply->type == XCB_ATOM_INTEGER && orderReply->format == 32 && orderReply->num_items == 1)) {
208 useFallback(true);
209 return;
210 }
211
212 const uint32_t order = *xcb_randr_get_output_property_data(orderReply.data());
213
214 if (order > 0) { // 0 is the special case for disabled, so we ignore it
215 orderMap.emplace_back(order,
216 QString::fromUtf8(reinterpret_cast<const char *>(xcb_randr_get_output_info_name(output.get())),
217 xcb_randr_get_output_info_name_length(output.get())));
218 }
219 }
220
221 const auto screens = qGuiApp->screens();
222 std::vector<QString> screenNames;
223 screenNames.reserve(screens.size());
224 std::transform(screens.begin(), screens.end(), std::back_inserter(screenNames), [](const QScreen *screen) {
225 return screen->name();
226 });
227 const bool isScreenPresent = std::all_of(orderMap.cbegin(), orderMap.cend(), [&screenNames](const auto &pr) {
228 return std::ranges::find(screenNames, std::get<QString>(pr)) != screenNames.end();
229 });
230 if (!isScreenPresent) [[unlikely]] {
231 // if the pending output order refers to screens
232 // we don't know of yet, try again next time a screen is added
233 // this seems unlikely given we have the server lock and the timing thing
234 m_delayTimer->start();
235 return;
236 }
237
238 std::sort(orderMap.begin(), orderMap.end());
239
240 // Rather verbose ifdef due to clang support of ranges API
241#if defined(__clang__) && __clang_major__ < 16
242 const auto getAllValues = [](const QList<std::pair<uint, QString>> &orderMap) -> QList<QString> {
243 QList<QString> values;
244 values.reserve(orderMap.size());
245 std::transform(orderMap.begin(), orderMap.end(), std::back_inserter(values), [](const auto &pair) {
246 return pair.second;
247 });
248 return values;
249 };
250 if (const auto pendingOutputs = getAllValues(orderMap); pendingOutputs != m_outputOrder) {
251 m_outputOrder = pendingOutputs;
252#else
253 if (const auto pendingOutputs = std::views::values(std::as_const(orderMap)); !std::ranges::equal(pendingOutputs, std::as_const(m_outputOrder))) {
254 m_outputOrder = QStringList{pendingOutputs.begin(), pendingOutputs.end()};
255#endif
256 Q_EMIT outputOrderChanged(m_outputOrder);
257 }
258}
259
260bool X11OutputOrderWatcher::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)
261{
262 Q_UNUSED(result);
263 // a particular edge case: when we switch the only enabled screen
264 // we don't have any signal about it, the primary screen changes but we have the same old QScreen* getting recycled
265 // see https://bugs.kde.org/show_bug.cgi?id=373880
266 // 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
267 if (eventType[0] != 'x') {
268 return false;
269 }
270
271 xcb_generic_event_t *ev = static_cast<xcb_generic_event_t *>(message);
272
273 const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev);
274
275 if (responseType == m_xrandrExtensionOffset + XCB_RANDR_NOTIFY) {
276 auto *randrEvent = reinterpret_cast<xcb_randr_notify_event_t *>(ev);
277 if (randrEvent->subCode == XCB_RANDR_NOTIFY_OUTPUT_PROPERTY) {
278 xcb_randr_output_property_t property = randrEvent->u.op;
279
280 if (property.atom == m_kdeScreenAtom) {
281 // Force an X11 roundtrip to make sure we have all other
282 // screen events in the buffer when we process the deferred refresh
283 useFallback(false);
284 roundtrip();
285 m_delayTimer->start();
286 }
287 } else if (randrEvent->subCode == XCB_RANDR_NOTIFY_OUTPUT_CHANGE) {
288 // When the ast screen is removed, its qscreen becomes name ":0.0" as the fake screen, but nothing happens really,
289 // screenpool doesn't notice (and looking at the assert_x there are, that was expected"
290 // then the screen gets connected again, a new screen gets conencted, the old 0.0 one
291 // gets disconnected, but the screen order stuff doesn't say anything as it's still
292 // the same connector name as before
293 // so screenpool finds itself with an empty screenorder
294 if (randrEvent->u.oc.connection == XCB_RANDR_CONNECTION_DISCONNECTED) {
295 xcb_randr_output_t output = randrEvent->u.oc.output;
296 xcb_connection_t *connection = m_x11Interface->connection();
297 xcb_randr_get_output_info_cookie_t cookie = xcb_randr_get_output_info(connection, output, XCB_CURRENT_TIME);
298 xcb_randr_get_output_info_reply_t *reply = xcb_randr_get_output_info_reply(connection, cookie, nullptr);
299
300 if (reply) {
301 int name_len = xcb_randr_get_output_info_name_length(reply);
302 char *name = reinterpret_cast<char *>(xcb_randr_get_output_info_name(reply));
303 QString connectorName = QString::fromLatin1(name, name_len);
304
305 free(reply);
306
307 m_outputOrder.removeAll(connectorName);
308 // Cause ScreenPool to reevaluate screenorder again, so the screen :0.0 will
309 // be correctly moved to fakeScreens
310 m_delayTimer->start();
311 }
312 }
313 }
314 }
315 return false;
316}
317
318void X11OutputOrderWatcher::roundtrip() const
319{
320 const auto cookie = xcb_get_input_focus(m_x11Interface->connection());
321 xcb_generic_error_t *error = nullptr;
322 ScopedPointer<xcb_get_input_focus_reply_t> sync(xcb_get_input_focus_reply(m_x11Interface->connection(), cookie, &error));
323 if (error) {
324 free(error);
325 }
326}
327
328WaylandOutputOrderWatcher::WaylandOutputOrderWatcher(QObject *parent)
329 : OutputOrderWatcher(parent)
330{
331 // Asking for primaryOutputName() before this happened, will return qGuiApp->primaryScreen()->name() anyways, so set it so the outputOrderChanged will
332 // have parameters that are coherent
334
335 auto outputListManagement = new WaylandOutputOrder(this);
336 m_orderProtocolPresent = outputListManagement->isActive();
337 if (!m_orderProtocolPresent) {
338 useFallback(true);
339 return;
340 }
341 connect(outputListManagement, &WaylandOutputOrder::outputOrderChanged, this, [this](const QStringList &order) {
342 m_pendingOutputOrder = order;
343
344 if (hasAllScreens()) {
345 if (m_pendingOutputOrder != m_outputOrder) {
346 m_outputOrder = m_pendingOutputOrder;
347 Q_EMIT outputOrderChanged(m_outputOrder);
348 }
349 }
350 // otherwise wait for next QGuiApp screenAdded/removal
351 // to keep things in sync
352 });
353}
354
355bool WaylandOutputOrderWatcher::hasAllScreens() const
356{
357 // for each name in our ordered list, find a screen with that name
358 for (const auto &name : std::as_const(m_pendingOutputOrder)) {
359 bool present = false;
360 for (auto *s : qApp->screens()) {
361 if (s->name() == name) {
362 present = true;
363 break;
364 }
365 }
366 if (!present) {
367 return false;
368 }
369 }
370 return true;
371}
372
373void WaylandOutputOrderWatcher::refresh()
374{
375 if (!m_orderProtocolPresent) {
377 return;
378 }
379
380 if (!hasAllScreens()) {
381 return;
382 }
383
384 if (m_outputOrder != m_pendingOutputOrder) {
385 m_outputOrder = m_pendingOutputOrder;
386 Q_EMIT outputOrderChanged(m_outputOrder);
387 }
388}
389
390#include "outputorderwatcher.moc"
static bool isPlatformX11()
static bool isPlatformWayland()
This class watches for output ordering changes from the relevant backend.
void useFallback(bool fallback)
Backend failed, use QScreen based implementaion.
static OutputOrderWatcher * instance(QObject *parent)
Create the correct OutputOrderWatcher.
QStringList outputOrder() const
Returns the list of outputs in order.
KCRASH_EXPORT void initialize()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString name(StandardAction id)
void primaryScreenChanged(QScreen *screen)
void screenAdded(QScreen *screen)
void screenRemoved(QScreen *screen)
void append(QList< T > &&value)
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
void clear()
reference emplace_back(Args &&... args)
iterator end()
void reserve(qsizetype size)
qsizetype size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
UniqueConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:19:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.