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 <QScreen>
11#include <QTimer>
12
13#include <KWindowSystem>
14
15#include "qwayland-kde-output-order-v1.h"
16#include <QtWaylandClient/QWaylandClientExtension>
17#include <QtWaylandClient/QtWaylandClientVersion>
18
19#if HAVE_X11
20#include <X11/Xlib.h>
21#include <xcb/randr.h>
22#include <xcb/xcb_event.h>
23#endif // HAVE_X11
24
25template<typename T>
27
28class WaylandOutputOrder : public QWaylandClientExtensionTemplate<WaylandOutputOrder, &QtWayland::kde_output_order_v1::destroy>,
29 public QtWayland::kde_output_order_v1
30{
31 Q_OBJECT
32public:
33 WaylandOutputOrder(QObject *parent)
34 : QWaylandClientExtensionTemplate(1)
35 {
36 setParent(parent);
37 initialize();
38 }
39
40protected:
41 void kde_output_order_v1_output(const QString &outputName) override
42 {
43 if (m_done) {
44 m_outputOrder.clear();
45 m_done = false;
46 }
47 m_outputOrder.append(outputName);
48 }
49
50 void kde_output_order_v1_done() override
51 {
52 // If no output arrived it means we don't have *any* usable output
53 if (m_done) {
54 m_outputOrder.clear();
55 }
56 m_done = true;
57 Q_EMIT outputOrderChanged(m_outputOrder);
58 }
59
60Q_SIGNALS:
61 void outputOrderChanged(const QStringList &outputName);
62
63private:
64 QStringList m_outputOrder;
65 bool m_done = true;
66};
67
68OutputOrderWatcher::OutputOrderWatcher(QObject *parent)
69 : QObject(parent)
70{
73}
74
76{
77 m_orderProtocolPresent = !fallback;
78 if (fallback) {
80 refresh();
81 }
82}
83
85{
86#if HAVE_X11
88 return new X11OutputOrderWatcher(parent);
89 } else
90#endif
92 return new WaylandOutputOrderWatcher(parent);
93 }
94 // return default impl that does something at least
95 return new OutputOrderWatcher(parent);
96}
97
99{
100 Q_ASSERT(!m_orderProtocolPresent);
101
102 QStringList pendingOutputOrder;
103
104 pendingOutputOrder.clear();
105 for (auto *s : qApp->screens()) {
106 pendingOutputOrder.append(s->name());
107 }
108
109 auto outputLess = [](const QString &c1, const QString &c2) {
110 if (c1 == qApp->primaryScreen()->name()) {
111 return true;
112 } else if (c2 == qApp->primaryScreen()->name()) {
113 return false;
114 } else {
115 return c1 < c2;
116 }
117 };
118 std::sort(pendingOutputOrder.begin(), pendingOutputOrder.end(), outputLess);
119
120 if (m_outputOrder != pendingOutputOrder) {
121 m_outputOrder = pendingOutputOrder;
122 Q_EMIT outputOrderChanged(m_outputOrder);
123 }
124 return;
125}
126
128{
129 return m_outputOrder;
130}
131
132X11OutputOrderWatcher::X11OutputOrderWatcher(QObject *parent)
133 : OutputOrderWatcher(parent)
134 , m_x11Interface(qGuiApp->nativeInterface<QNativeInterface::QX11Application>())
135{
136 if (!m_x11Interface) [[unlikely]] {
137 Q_ASSERT(false);
138 return;
139 }
140 // This timer is used to signal only when a qscreen for every output is already created, perhaps by monitoring
141 // screenadded/screenremoved and tracking the outputs still missing
142 m_delayTimer = new QTimer(this);
143 m_delayTimer->setSingleShot(true);
144 m_delayTimer->setInterval(0);
145 connect(m_delayTimer, &QTimer::timeout, this, [this]() {
146 refresh();
147 });
148
149 // By default try to use the protocol on x11
150 m_orderProtocolPresent = true;
151
152 qGuiApp->installNativeEventFilter(this);
153 const xcb_query_extension_reply_t *reply = xcb_get_extension_data(m_x11Interface->connection(), &xcb_randr_id);
154 m_xrandrExtensionOffset = reply->first_event;
155
156 const QByteArray effectName = QByteArrayLiteral("_KDE_SCREEN_INDEX");
157 xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(m_x11Interface->connection(), false, effectName.length(), effectName);
158 xcb_intern_atom_reply_t *atom(xcb_intern_atom_reply(m_x11Interface->connection(), atomCookie, nullptr));
159 if (!atom) {
160 useFallback(true);
161 return;
162 }
163
164 m_kdeScreenAtom = atom->atom;
165 m_delayTimer->start();
166}
167
168void X11OutputOrderWatcher::refresh()
169{
170 if (!m_orderProtocolPresent) {
172 return;
173 }
174 QMap<int, QString> orderMap;
175
176 ScopedPointer<xcb_randr_get_screen_resources_current_reply_t> reply(xcb_randr_get_screen_resources_current_reply(
177 m_x11Interface->connection(),
178 xcb_randr_get_screen_resources_current(m_x11Interface->connection(), DefaultRootWindow(m_x11Interface->display())),
179 NULL));
180
181 xcb_timestamp_t timestamp = reply->config_timestamp;
182 int len = xcb_randr_get_screen_resources_current_outputs_length(reply.data());
183 xcb_randr_output_t *randr_outputs = xcb_randr_get_screen_resources_current_outputs(reply.data());
184
185 for (int i = 0; i < len; i++) {
187 xcb_randr_get_output_info_reply(m_x11Interface->connection(),
188 xcb_randr_get_output_info(m_x11Interface->connection(), randr_outputs[i], timestamp),
189 NULL));
190
191 if (output == NULL || output->connection == XCB_RANDR_CONNECTION_DISCONNECTED || output->crtc == 0) {
192 continue;
193 }
194
195 const auto screenName =
196 QString::fromUtf8((const char *)xcb_randr_get_output_info_name(output.get()), xcb_randr_get_output_info_name_length(output.get()));
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[order] = screenName;
216 }
217 }
218
219 QStringList pendingOutputOrder;
220
221 for (const auto &screenName : orderMap) {
222 pendingOutputOrder.append(screenName);
223 }
224
225 for (const auto &name : std::as_const(pendingOutputOrder)) {
226 bool present = false;
227 for (auto *s : qApp->screens()) {
228 if (s->name() == name) {
229 present = true;
230 break;
231 }
232 }
233 // if the pending output order refers to screens
234 // we don't know of yet, try again next time a screen is added
235
236 // this seems unlikely given we have the server lock and the timing thing
237 if (!present) {
238 m_delayTimer->start();
239 return;
240 }
241 }
242
243 if (pendingOutputOrder != m_outputOrder) {
244 m_outputOrder = pendingOutputOrder;
245 Q_EMIT outputOrderChanged(m_outputOrder);
246 }
247}
248
249bool X11OutputOrderWatcher::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)
250{
251 Q_UNUSED(result);
252 // a particular edge case: when we switch the only enabled screen
253 // we don't have any signal about it, the primary screen changes but we have the same old QScreen* getting recycled
254 // see https://bugs.kde.org/show_bug.cgi?id=373880
255 // 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
256 if (eventType[0] != 'x') {
257 return false;
258 }
259
260 xcb_generic_event_t *ev = static_cast<xcb_generic_event_t *>(message);
261
262 const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev);
263
264 if (responseType == m_xrandrExtensionOffset + XCB_RANDR_NOTIFY) {
265 auto *randrEvent = reinterpret_cast<xcb_randr_notify_event_t *>(ev);
266 if (randrEvent->subCode == XCB_RANDR_NOTIFY_OUTPUT_PROPERTY) {
267 xcb_randr_output_property_t property = randrEvent->u.op;
268
269 if (property.atom == m_kdeScreenAtom) {
270 // Force an X11 roundtrip to make sure we have all other
271 // screen events in the buffer when we process the deferred refresh
272 useFallback(false);
273 roundtrip();
274 m_delayTimer->start();
275 }
276 }
277 }
278 return false;
279}
280
281void X11OutputOrderWatcher::roundtrip() const
282{
283 const auto cookie = xcb_get_input_focus(m_x11Interface->connection());
284 xcb_generic_error_t *error = nullptr;
285 ScopedPointer<xcb_get_input_focus_reply_t> sync(xcb_get_input_focus_reply(m_x11Interface->connection(), cookie, &error));
286 if (error) {
287 free(error);
288 }
289}
290
291WaylandOutputOrderWatcher::WaylandOutputOrderWatcher(QObject *parent)
292 : OutputOrderWatcher(parent)
293{
294 // Asking for primaryOutputName() before this happened, will return qGuiApp->primaryScreen()->name() anyways, so set it so the outputOrderChanged will
295 // have parameters that are coherent
297
298 auto outputListManagement = new WaylandOutputOrder(this);
299 m_orderProtocolPresent = outputListManagement->isActive();
300 if (!m_orderProtocolPresent) {
301 useFallback(true);
302 return;
303 }
304 connect(outputListManagement, &WaylandOutputOrder::outputOrderChanged, this, [this](const QStringList &order) {
305 m_pendingOutputOrder = order;
306
307 if (hasAllScreens()) {
308 if (m_pendingOutputOrder != m_outputOrder) {
309 m_outputOrder = m_pendingOutputOrder;
310 Q_EMIT outputOrderChanged(m_outputOrder);
311 }
312 }
313 // otherwise wait for next QGuiApp screenAdded/removal
314 // to keep things in sync
315 });
316}
317
318bool WaylandOutputOrderWatcher::hasAllScreens() const
319{
320 // for each name in our ordered list, find a screen with that name
321 for (const auto &name : std::as_const(m_pendingOutputOrder)) {
322 bool present = false;
323 for (auto *s : qApp->screens()) {
324 if (s->name() == name) {
325 present = true;
326 break;
327 }
328 }
329 if (!present) {
330 return false;
331 }
332 }
333 return true;
334}
335
336void WaylandOutputOrderWatcher::refresh()
337{
338 if (!m_orderProtocolPresent) {
340 return;
341 }
342
343 if (!hasAllScreens()) {
344 return;
345 }
346
347 if (m_outputOrder != m_pendingOutputOrder) {
348 m_outputOrder = m_pendingOutputOrder;
349 Q_EMIT outputOrderChanged(m_outputOrder);
350 }
351}
352
353#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.
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
void initialize(StandardShortcut id)
qsizetype length() const const
void primaryScreenChanged(QScreen *screen)
void screenAdded(QScreen *screen)
void screenRemoved(QScreen *screen)
void append(QList< T > &&value)
iterator begin()
void clear()
iterator end()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
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 Sat Apr 27 2024 22:12:02 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.