1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2018 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtCore module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | #include "qxcbconnection.h" |
40 | #include "qxcbscreen.h" |
41 | #include "qxcbintegration.h" |
42 | |
43 | #include <QtGui/private/qhighdpiscaling_p.h> |
44 | #include <QtCore/QString> |
45 | #include <QtCore/QList> |
46 | |
47 | #include <qpa/qwindowsysteminterface.h> |
48 | |
49 | #include <xcb/xinerama.h> |
50 | |
51 | void QXcbConnection::xrandrSelectEvents() |
52 | { |
53 | xcb_screen_iterator_t rootIter = xcb_setup_roots_iterator(R: setup()); |
54 | for (; rootIter.rem; xcb_screen_next(i: &rootIter)) { |
55 | xcb_randr_select_input(c: xcb_connection(), |
56 | window: rootIter.data->root, |
57 | enable: XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE | |
58 | XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE | |
59 | XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE | |
60 | XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY |
61 | ); |
62 | } |
63 | } |
64 | |
65 | QXcbScreen* QXcbConnection::findScreenForCrtc(xcb_window_t rootWindow, xcb_randr_crtc_t crtc) const |
66 | { |
67 | for (QXcbScreen *screen : m_screens) { |
68 | if (screen->root() == rootWindow && screen->crtc() == crtc) |
69 | return screen; |
70 | } |
71 | |
72 | return nullptr; |
73 | } |
74 | |
75 | QXcbScreen* QXcbConnection::findScreenForOutput(xcb_window_t rootWindow, xcb_randr_output_t output) const |
76 | { |
77 | for (QXcbScreen *screen : m_screens) { |
78 | if (screen->root() == rootWindow && screen->output() == output) |
79 | return screen; |
80 | } |
81 | |
82 | return nullptr; |
83 | } |
84 | |
85 | QXcbVirtualDesktop* QXcbConnection::virtualDesktopForRootWindow(xcb_window_t rootWindow) const |
86 | { |
87 | for (QXcbVirtualDesktop *virtualDesktop : m_virtualDesktops) { |
88 | if (virtualDesktop->screen()->root == rootWindow) |
89 | return virtualDesktop; |
90 | } |
91 | |
92 | return nullptr; |
93 | } |
94 | |
95 | /*! |
96 | \brief Synchronizes the screen list, adds new screens, removes deleted ones |
97 | */ |
98 | void QXcbConnection::updateScreens(const xcb_randr_notify_event_t *event) |
99 | { |
100 | if (event->subCode == XCB_RANDR_NOTIFY_CRTC_CHANGE) { |
101 | xcb_randr_crtc_change_t crtc = event->u.cc; |
102 | QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(rootWindow: crtc.window); |
103 | if (!virtualDesktop) |
104 | // Not for us |
105 | return; |
106 | |
107 | QXcbScreen *screen = findScreenForCrtc(rootWindow: crtc.window, crtc: crtc.crtc); |
108 | qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_CRTC_CHANGE:" << crtc.crtc |
109 | << "mode" << crtc.mode << "relevant screen" << screen; |
110 | // Only update geometry when there's a valid mode on the CRTC |
111 | // CRTC with node mode could mean that output has been disabled, and we'll |
112 | // get RRNotifyOutputChange notification for that. |
113 | if (screen && crtc.mode) { |
114 | if (crtc.rotation == XCB_RANDR_ROTATION_ROTATE_90 || |
115 | crtc.rotation == XCB_RANDR_ROTATION_ROTATE_270) |
116 | std::swap(a&: crtc.width, b&: crtc.height); |
117 | screen->updateGeometry(geometry: QRect(crtc.x, crtc.y, crtc.width, crtc.height), rotation: crtc.rotation); |
118 | if (screen->mode() != crtc.mode) |
119 | screen->updateRefreshRate(mode: crtc.mode); |
120 | } |
121 | |
122 | } else if (event->subCode == XCB_RANDR_NOTIFY_OUTPUT_CHANGE) { |
123 | xcb_randr_output_change_t output = event->u.oc; |
124 | QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(rootWindow: output.window); |
125 | if (!virtualDesktop) |
126 | // Not for us |
127 | return; |
128 | |
129 | QXcbScreen *screen = findScreenForOutput(rootWindow: output.window, output: output.output); |
130 | qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_OUTPUT_CHANGE:" << output.output; |
131 | |
132 | if (screen && output.connection == XCB_RANDR_CONNECTION_DISCONNECTED) { |
133 | qCDebug(lcQpaScreen) << "screen" << screen->name() << "has been disconnected" ; |
134 | destroyScreen(screen); |
135 | } else if (!screen && output.connection == XCB_RANDR_CONNECTION_CONNECTED) { |
136 | // New XRandR output is available and it's enabled |
137 | if (output.crtc != XCB_NONE && output.mode != XCB_NONE) { |
138 | auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(), |
139 | output.output, output.config_timestamp); |
140 | // Find a fake screen |
141 | const auto scrs = virtualDesktop->screens(); |
142 | for (QPlatformScreen *scr : scrs) { |
143 | QXcbScreen *xcbScreen = static_cast<QXcbScreen *>(scr); |
144 | if (xcbScreen->output() == XCB_NONE) { |
145 | screen = xcbScreen; |
146 | break; |
147 | } |
148 | } |
149 | |
150 | if (screen) { |
151 | QString nameWas = screen->name(); |
152 | // Transform the fake screen into a physical screen |
153 | screen->setOutput(outputId: output.output, outputInfo: outputInfo.get()); |
154 | updateScreen(screen, outputChange: output); |
155 | qCDebug(lcQpaScreen) << "output" << screen->name() |
156 | << "is connected and enabled; was fake:" << nameWas; |
157 | } else { |
158 | screen = createScreen(virtualDesktop, outputChange: output, outputInfo: outputInfo.get()); |
159 | qCDebug(lcQpaScreen) << "output" << screen->name() << "is connected and enabled" ; |
160 | } |
161 | QHighDpiScaling::updateHighDpiScaling(); |
162 | } |
163 | } else if (screen) { |
164 | if (output.crtc == XCB_NONE && output.mode == XCB_NONE) { |
165 | // Screen has been disabled |
166 | auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(), |
167 | output.output, output.config_timestamp); |
168 | if (outputInfo->crtc == XCB_NONE) { |
169 | qCDebug(lcQpaScreen) << "output" << screen->name() << "has been disabled" ; |
170 | destroyScreen(screen); |
171 | } else { |
172 | qCDebug(lcQpaScreen) << "output" << screen->name() << "has been temporarily disabled for the mode switch" ; |
173 | // Reset crtc to skip RRCrtcChangeNotify events, |
174 | // because they may be invalid in the middle of the mode switch |
175 | screen->setCrtc(XCB_NONE); |
176 | } |
177 | } else { |
178 | updateScreen(screen, outputChange: output); |
179 | qCDebug(lcQpaScreen) << "output has changed" << screen; |
180 | } |
181 | } |
182 | |
183 | qCDebug(lcQpaScreen) << "primary output is" << qAsConst(t&: m_screens).first()->name(); |
184 | } |
185 | } |
186 | |
187 | bool QXcbConnection::checkOutputIsPrimary(xcb_window_t rootWindow, xcb_randr_output_t output) |
188 | { |
189 | auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), rootWindow); |
190 | if (!primary) |
191 | qWarning(msg: "failed to get the primary output of the screen" ); |
192 | |
193 | const bool isPrimary = primary ? (primary->output == output) : false; |
194 | |
195 | return isPrimary; |
196 | } |
197 | |
198 | void QXcbConnection::updateScreen(QXcbScreen *screen, const xcb_randr_output_change_t &outputChange) |
199 | { |
200 | screen->setCrtc(outputChange.crtc); // Set the new crtc, because it can be invalid |
201 | screen->updateGeometry(timestamp: outputChange.config_timestamp); |
202 | if (screen->mode() != outputChange.mode) |
203 | screen->updateRefreshRate(mode: outputChange.mode); |
204 | // Only screen which belongs to the primary virtual desktop can be a primary screen |
205 | if (screen->screenNumber() == primaryScreenNumber()) { |
206 | if (!screen->isPrimary() && checkOutputIsPrimary(rootWindow: outputChange.window, output: outputChange.output)) { |
207 | screen->setPrimary(true); |
208 | |
209 | // If the screen became primary, reshuffle the order in QGuiApplicationPrivate |
210 | const int idx = m_screens.indexOf(t: screen); |
211 | if (idx > 0) { |
212 | qAsConst(t&: m_screens).first()->setPrimary(false); |
213 | m_screens.swapItemsAt(i: 0, j: idx); |
214 | } |
215 | screen->virtualDesktop()->setPrimaryScreen(screen); |
216 | QWindowSystemInterface::handlePrimaryScreenChanged(newPrimary: screen); |
217 | } |
218 | } |
219 | } |
220 | |
221 | QXcbScreen *QXcbConnection::createScreen(QXcbVirtualDesktop *virtualDesktop, |
222 | const xcb_randr_output_change_t &outputChange, |
223 | xcb_randr_get_output_info_reply_t *outputInfo) |
224 | { |
225 | QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputChange.output, outputInfo); |
226 | // Only screen which belongs to the primary virtual desktop can be a primary screen |
227 | if (screen->screenNumber() == primaryScreenNumber()) |
228 | screen->setPrimary(checkOutputIsPrimary(rootWindow: outputChange.window, output: outputChange.output)); |
229 | |
230 | if (screen->isPrimary()) { |
231 | if (!m_screens.isEmpty()) |
232 | qAsConst(t&: m_screens).first()->setPrimary(false); |
233 | |
234 | m_screens.prepend(t: screen); |
235 | } else { |
236 | m_screens.append(t: screen); |
237 | } |
238 | virtualDesktop->addScreen(s: screen); |
239 | QWindowSystemInterface::handleScreenAdded(screen, isPrimary: screen->isPrimary()); |
240 | |
241 | return screen; |
242 | } |
243 | |
244 | void QXcbConnection::destroyScreen(QXcbScreen *screen) |
245 | { |
246 | QXcbVirtualDesktop *virtualDesktop = screen->virtualDesktop(); |
247 | if (virtualDesktop->screens().count() == 1) { |
248 | // If there are no other screens on the same virtual desktop, |
249 | // then transform the physical screen into a fake screen. |
250 | const QString nameWas = screen->name(); |
251 | screen->setOutput(XCB_NONE, outputInfo: nullptr); |
252 | qCDebug(lcQpaScreen) << "transformed" << nameWas << "to fake" << screen; |
253 | } else { |
254 | // There is more than one screen on the same virtual desktop, remove the screen |
255 | m_screens.removeOne(t: screen); |
256 | virtualDesktop->removeScreen(s: screen); |
257 | |
258 | // When primary screen is removed, set the new primary screen |
259 | // which belongs to the primary virtual desktop. |
260 | if (screen->isPrimary()) { |
261 | QXcbScreen *newPrimary = static_cast<QXcbScreen *>(virtualDesktop->screens().at(i: 0)); |
262 | newPrimary->setPrimary(true); |
263 | const int idx = m_screens.indexOf(t: newPrimary); |
264 | if (idx > 0) |
265 | m_screens.swapItemsAt(i: 0, j: idx); |
266 | QWindowSystemInterface::handlePrimaryScreenChanged(newPrimary); |
267 | } |
268 | |
269 | QWindowSystemInterface::handleScreenRemoved(screen); |
270 | } |
271 | } |
272 | |
273 | void QXcbConnection::initializeScreens() |
274 | { |
275 | xcb_screen_iterator_t it = xcb_setup_roots_iterator(R: setup()); |
276 | int xcbScreenNumber = 0; // screen number in the xcb sense |
277 | QXcbScreen *primaryScreen = nullptr; |
278 | while (it.rem) { |
279 | // Each "screen" in xcb terminology is a virtual desktop, |
280 | // potentially a collection of separate juxtaposed monitors. |
281 | // But we want a separate QScreen for each output (e.g. DVI-I-1, VGA-1, etc.) |
282 | // which will become virtual siblings. |
283 | xcb_screen_t *xcbScreen = it.data; |
284 | QXcbVirtualDesktop *virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber); |
285 | m_virtualDesktops.append(t: virtualDesktop); |
286 | QList<QPlatformScreen *> siblings; |
287 | if (hasXRandr()) { |
288 | // RRGetScreenResourcesCurrent is fast but it may return nothing if the |
289 | // configuration is not initialized wrt to the hardware. We should call |
290 | // RRGetScreenResources in this case. |
291 | auto resources_current = Q_XCB_REPLY(xcb_randr_get_screen_resources_current, |
292 | xcb_connection(), xcbScreen->root); |
293 | decltype(Q_XCB_REPLY(xcb_randr_get_screen_resources, |
294 | xcb_connection(), xcbScreen->root)) resources; |
295 | if (!resources_current) { |
296 | qWarning(msg: "failed to get the current screen resources" ); |
297 | } else { |
298 | xcb_timestamp_t timestamp = 0; |
299 | xcb_randr_output_t *outputs = nullptr; |
300 | int outputCount = xcb_randr_get_screen_resources_current_outputs_length(R: resources_current.get()); |
301 | if (outputCount) { |
302 | timestamp = resources_current->config_timestamp; |
303 | outputs = xcb_randr_get_screen_resources_current_outputs(R: resources_current.get()); |
304 | } else { |
305 | resources = Q_XCB_REPLY(xcb_randr_get_screen_resources, |
306 | xcb_connection(), xcbScreen->root); |
307 | if (!resources) { |
308 | qWarning(msg: "failed to get the screen resources" ); |
309 | } else { |
310 | timestamp = resources->config_timestamp; |
311 | outputCount = xcb_randr_get_screen_resources_outputs_length(R: resources.get()); |
312 | outputs = xcb_randr_get_screen_resources_outputs(R: resources.get()); |
313 | } |
314 | } |
315 | |
316 | if (outputCount) { |
317 | auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), xcbScreen->root); |
318 | if (!primary) { |
319 | qWarning(msg: "failed to get the primary output of the screen" ); |
320 | } else { |
321 | for (int i = 0; i < outputCount; i++) { |
322 | auto output = Q_XCB_REPLY_UNCHECKED(xcb_randr_get_output_info, |
323 | xcb_connection(), outputs[i], timestamp); |
324 | // Invalid, disconnected or disabled output |
325 | if (!output) |
326 | continue; |
327 | |
328 | if (output->connection != XCB_RANDR_CONNECTION_CONNECTED) { |
329 | qCDebug(lcQpaScreen, "Output %s is not connected" , qPrintable( |
330 | QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()), |
331 | xcb_randr_get_output_info_name_length(output.get())))); |
332 | continue; |
333 | } |
334 | |
335 | if (output->crtc == XCB_NONE) { |
336 | qCDebug(lcQpaScreen, "Output %s is not enabled" , qPrintable( |
337 | QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()), |
338 | xcb_randr_get_output_info_name_length(output.get())))); |
339 | continue; |
340 | } |
341 | |
342 | QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputs[i], output.get()); |
343 | siblings << screen; |
344 | m_screens << screen; |
345 | |
346 | // There can be multiple outputs per screen, use either |
347 | // the first or an exact match. An exact match isn't |
348 | // always available if primary->output is XCB_NONE |
349 | // or currently disconnected output. |
350 | if (primaryScreenNumber() == xcbScreenNumber) { |
351 | if (!primaryScreen || (primary && outputs[i] == primary->output)) { |
352 | if (primaryScreen) |
353 | primaryScreen->setPrimary(false); |
354 | primaryScreen = screen; |
355 | primaryScreen->setPrimary(true); |
356 | siblings.prepend(t: siblings.takeLast()); |
357 | } |
358 | } |
359 | } |
360 | } |
361 | } |
362 | } |
363 | } else if (hasXinerama()) { |
364 | // Xinerama is available |
365 | auto screens = Q_XCB_REPLY(xcb_xinerama_query_screens, xcb_connection()); |
366 | if (screens) { |
367 | xcb_xinerama_screen_info_iterator_t it = xcb_xinerama_query_screens_screen_info_iterator(R: screens.get()); |
368 | while (it.rem) { |
369 | xcb_xinerama_screen_info_t *screen_info = it.data; |
370 | QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, |
371 | XCB_NONE, nullptr, |
372 | screen_info, it.index); |
373 | siblings << screen; |
374 | m_screens << screen; |
375 | xcb_xinerama_screen_info_next(i: &it); |
376 | } |
377 | } |
378 | } |
379 | if (siblings.isEmpty()) { |
380 | // If there are no XRandR outputs or XRandR extension is missing, |
381 | // then create a fake/legacy screen. |
382 | QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, XCB_NONE, nullptr); |
383 | qCDebug(lcQpaScreen) << "created fake screen" << screen; |
384 | m_screens << screen; |
385 | if (primaryScreenNumber() == xcbScreenNumber) { |
386 | primaryScreen = screen; |
387 | primaryScreen->setPrimary(true); |
388 | } |
389 | siblings << screen; |
390 | } |
391 | virtualDesktop->setScreens(std::move(siblings)); |
392 | xcb_screen_next(i: &it); |
393 | ++xcbScreenNumber; |
394 | } // for each xcb screen |
395 | |
396 | for (QXcbVirtualDesktop *virtualDesktop : qAsConst(t&: m_virtualDesktops)) |
397 | virtualDesktop->subscribeToXFixesSelectionNotify(); |
398 | |
399 | if (m_virtualDesktops.isEmpty()) { |
400 | qFatal(msg: "QXcbConnection: no screens available" ); |
401 | } else { |
402 | // Ensure the primary screen is first on the list |
403 | if (primaryScreen) { |
404 | if (qAsConst(t&: m_screens).first() != primaryScreen) { |
405 | m_screens.removeOne(t: primaryScreen); |
406 | m_screens.prepend(t: primaryScreen); |
407 | } |
408 | } |
409 | |
410 | // Push the screens to QGuiApplication |
411 | for (QXcbScreen *screen : qAsConst(t&: m_screens)) { |
412 | qCDebug(lcQpaScreen) << "adding" << screen << "(Primary:" << screen->isPrimary() << ")" ; |
413 | QWindowSystemInterface::handleScreenAdded(screen, isPrimary: screen->isPrimary()); |
414 | } |
415 | |
416 | qCDebug(lcQpaScreen) << "primary output is" << qAsConst(t&: m_screens).first()->name(); |
417 | } |
418 | } |
419 | |