1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qoffscreenintegration.h" |
5 | #include "qoffscreenwindow.h" |
6 | #include "qoffscreencommon.h" |
7 | |
8 | #if defined(Q_OS_UNIX) |
9 | #include <QtGui/private/qgenericunixeventdispatcher_p.h> |
10 | #if defined(Q_OS_MAC) |
11 | #include <qpa/qplatformfontdatabase.h> |
12 | #include <QtGui/private/qcoretextfontdatabase_p.h> |
13 | #else |
14 | #include <QtGui/private/qgenericunixfontdatabase_p.h> |
15 | #endif |
16 | #elif defined(Q_OS_WIN) |
17 | #include <QtGui/private/qfreetypefontdatabase_p.h> |
18 | #include <QtCore/private/qeventdispatcher_win_p.h> |
19 | #endif |
20 | |
21 | #include <QtCore/qfile.h> |
22 | #include <QtCore/qjsonarray.h> |
23 | #include <QtCore/qjsondocument.h> |
24 | #include <QtCore/qjsonobject.h> |
25 | #include <QtCore/qjsonvalue.h> |
26 | #include <QtGui/private/qpixmap_raster_p.h> |
27 | #include <QtGui/private/qguiapplication_p.h> |
28 | #include <qpa/qplatforminputcontextfactory_p.h> |
29 | #include <qpa/qplatforminputcontext.h> |
30 | #include <qpa/qplatformtheme.h> |
31 | #include <qpa/qwindowsysteminterface.h> |
32 | |
33 | #include <qpa/qplatformservices.h> |
34 | |
35 | #if QT_CONFIG(xlib) && QT_CONFIG(opengl) && !QT_CONFIG(opengles2) |
36 | #include "qoffscreenintegration_x11.h" |
37 | #endif |
38 | |
39 | QT_BEGIN_NAMESPACE |
40 | |
41 | using namespace Qt::StringLiterals; |
42 | |
43 | class QCoreTextFontEngine; |
44 | |
45 | template <typename BaseEventDispatcher> |
46 | class QOffscreenEventDispatcher : public BaseEventDispatcher |
47 | { |
48 | public: |
49 | explicit QOffscreenEventDispatcher(QObject *parent = nullptr) |
50 | : BaseEventDispatcher(parent) |
51 | { |
52 | } |
53 | |
54 | bool processEvents(QEventLoop::ProcessEventsFlags flags) override |
55 | { |
56 | bool didSendEvents = BaseEventDispatcher::processEvents(flags); |
57 | |
58 | return QWindowSystemInterface::sendWindowSystemEvents(flags) || didSendEvents; |
59 | } |
60 | }; |
61 | |
62 | QOffscreenIntegration::QOffscreenIntegration(const QStringList& paramList) |
63 | { |
64 | #if defined(Q_OS_UNIX) |
65 | #if defined(Q_OS_MAC) |
66 | m_fontDatabase.reset(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>); |
67 | #else |
68 | m_fontDatabase.reset(other: new QGenericUnixFontDatabase()); |
69 | #endif |
70 | #elif defined(Q_OS_WIN) |
71 | m_fontDatabase.reset(new QFreeTypeFontDatabase()); |
72 | #endif |
73 | |
74 | #if QT_CONFIG(draganddrop) |
75 | m_drag.reset(other: new QOffscreenDrag); |
76 | #endif |
77 | m_services.reset(other: new QPlatformServices); |
78 | |
79 | QJsonObject config = resolveConfigFileConfiguration(paramList).value_or(u: defaultConfiguration()); |
80 | setConfiguration(config); |
81 | } |
82 | |
83 | QOffscreenIntegration::~QOffscreenIntegration() |
84 | { |
85 | while (!m_screens.isEmpty()) |
86 | QWindowSystemInterface::handleScreenRemoved(screen: m_screens.takeLast()); |
87 | } |
88 | |
89 | /* |
90 | The offscren platform plugin is configurable with a JSON configuration. |
91 | The confiuration can be provided either from a file on disk on startup, |
92 | or at by calling setConfiguration(). |
93 | |
94 | To provide a configuration on startuip, write the config to disk and pass |
95 | the file path as a platform argument: |
96 | |
97 | ./myapp -platform offscreen:configfile=/path/to/config.json |
98 | |
99 | The supported top-level config keys are: |
100 | { |
101 | "synchronousWindowSystemEvents": <bool> |
102 | "windowFrameMargins": <bool>, |
103 | "screens": [<screens>], |
104 | } |
105 | |
106 | "screens" is an array of: |
107 | { |
108 | "name": string, |
109 | "x": int, |
110 | "y": int, |
111 | "width": int, |
112 | "height": int, |
113 | "logicalDpi": int, |
114 | "logicalBaseDpi": int, |
115 | "dpr": double, |
116 | } |
117 | */ |
118 | |
119 | QJsonObject QOffscreenIntegration::defaultConfiguration() const |
120 | { |
121 | const auto defaultScreen = QJsonObject { |
122 | {"name" , "" }, |
123 | {"x" , 0}, |
124 | {"y" , 0}, |
125 | {"width" , 800}, |
126 | {"height" , 800}, |
127 | {"logicalDpi" , 96}, |
128 | {"logicalBaseDpi" , 96}, |
129 | {"dpr" , 1.0}, |
130 | }; |
131 | const auto defaultConfiguration = QJsonObject { |
132 | {"synchronousWindowSystemEvents" , false}, |
133 | {"windowFrameMargins" , true}, |
134 | {"screens" , QJsonArray { defaultScreen } }, |
135 | }; |
136 | return defaultConfiguration; |
137 | } |
138 | |
139 | std::optional<QJsonObject> QOffscreenIntegration::resolveConfigFileConfiguration(const QStringList& paramList) const |
140 | { |
141 | bool hasConfigFile = false; |
142 | QString configFilePath; |
143 | for (const QString ¶m : paramList) { |
144 | // Look for "configfile=/path/to/file/" |
145 | QString configPrefix("configfile="_L1 ); |
146 | if (param.startsWith(s: configPrefix)) { |
147 | hasConfigFile = true; |
148 | configFilePath = param.mid(position: configPrefix.size()); |
149 | } |
150 | } |
151 | if (!hasConfigFile) |
152 | return std::nullopt; |
153 | |
154 | // Read config file |
155 | if (configFilePath.isEmpty()) |
156 | qFatal(msg: "Missing file path for -configfile platform option" ); |
157 | QFile configFile(configFilePath); |
158 | if (!configFile.exists()) |
159 | qFatal(msg: "Could not find platform config file %s" , qPrintable(configFilePath)); |
160 | if (!configFile.open(flags: QIODevice::ReadOnly)) |
161 | qFatal(msg: "Could not open platform config file for reading %s, %s" , qPrintable(configFilePath), qPrintable(configFile.errorString())); |
162 | |
163 | QByteArray json = configFile.readAll(); |
164 | QJsonParseError error; |
165 | QJsonDocument config = QJsonDocument::fromJson(json, error: &error); |
166 | if (config.isNull()) |
167 | qFatal(msg: "Platform config file parse error: %s" , qPrintable(error.errorString())); |
168 | |
169 | return config.object(); |
170 | } |
171 | |
172 | |
173 | void QOffscreenIntegration::setConfiguration(const QJsonObject &configuration) |
174 | { |
175 | // Apply the new configuration, diffing against the current m_configuration |
176 | |
177 | const bool synchronousWindowSystemEvents = configuration["synchronousWindowSystemEvents" ].toBool( |
178 | defaultValue: m_configuration["synchronousWindowSystemEvents" ].toBool(defaultValue: false)); |
179 | QWindowSystemInterface::setSynchronousWindowSystemEvents(synchronousWindowSystemEvents); |
180 | |
181 | m_windowFrameMarginsEnabled = configuration["windowFrameMargins" ].toBool( |
182 | defaultValue: m_configuration["windowFrameMargins" ].toBool(defaultValue: true)); |
183 | |
184 | // Diff screens array, using the screen name as the screen identity. |
185 | QJsonArray currentScreens = m_configuration["screens" ].toArray(); |
186 | QJsonArray newScreens = configuration["screens" ].toArray(); |
187 | |
188 | auto getScreenNames = [](const QJsonArray &screens) -> QList<QString> { |
189 | QList<QString> names; |
190 | for (QJsonValue screen : screens) { |
191 | names.append(t: screen["name" ].toString()); |
192 | }; |
193 | std::sort(first: names.begin(), last: names.end()); |
194 | return names; |
195 | }; |
196 | |
197 | auto currentNames = getScreenNames(currentScreens); |
198 | auto newNames = getScreenNames(newScreens); |
199 | |
200 | QList<QString> present; |
201 | std::set_intersection(first1: currentNames.begin(), last1: currentNames.end(), first2: newNames.begin(), last2: newNames.end(), |
202 | result: std::inserter(x&: present, i: present.begin())); |
203 | QList<QString> added; |
204 | std::set_difference(first1: newNames.begin(), last1: newNames.end(), first2: currentNames.begin(), last2: currentNames.end(), |
205 | result: std::inserter(x&: added, i: added.begin())); |
206 | QList<QString> removed; |
207 | std::set_difference(first1: currentNames.begin(), last1: currentNames.end(), first2: newNames.begin(), last2: newNames.end(), |
208 | result: std::inserter(x&: removed, i: removed.begin())); |
209 | |
210 | auto platformScreenByName = [](const QString &name, QList<QOffscreenScreen *> screens) -> QOffscreenScreen * { |
211 | for (QOffscreenScreen *screen : screens) { |
212 | if (screen->m_name == name) |
213 | return screen; |
214 | } |
215 | Q_UNREACHABLE(); |
216 | }; |
217 | |
218 | auto screenConfigByName = [](const QString &name, QJsonArray screenConfigs) -> QJsonValue { |
219 | for (QJsonValue screenConfig : screenConfigs) { |
220 | if (screenConfig["name" ].toString() == name) |
221 | return screenConfig; |
222 | } |
223 | Q_UNREACHABLE(); |
224 | }; |
225 | |
226 | auto geometryFromConfig = [](const QJsonObject &config) -> QRect { |
227 | return QRect(config["x" ].toInt(defaultValue: 0), config["y" ].toInt(defaultValue: 0), config["width" ].toInt(defaultValue: 640), config["height" ].toInt(defaultValue: 480)); |
228 | }; |
229 | |
230 | // Remove removed screens |
231 | for (const QString &remove : removed) { |
232 | QOffscreenScreen *screen = platformScreenByName(remove, m_screens); |
233 | m_screens.removeAll(t: screen); |
234 | QWindowSystemInterface::handleScreenRemoved(screen); |
235 | } |
236 | |
237 | // Add new screens |
238 | for (const QString &add : added) { |
239 | QJsonValue configValue = screenConfigByName(add, newScreens); |
240 | QJsonObject config = configValue.toObject(); |
241 | if (config.isEmpty()) { |
242 | qWarning(msg: "empty screen object" ); |
243 | continue; |
244 | } |
245 | QOffscreenScreen *offscreenScreen = new QOffscreenScreen(this); |
246 | offscreenScreen->m_name = config["name" ].toString(); |
247 | offscreenScreen->m_geometry = geometryFromConfig(config); |
248 | offscreenScreen->m_logicalDpi = config["logicalDpi" ].toInt(defaultValue: 96); |
249 | offscreenScreen->m_logicalBaseDpi = config["logicalBaseDpi" ].toInt(defaultValue: 96); |
250 | offscreenScreen->m_dpr = config["dpr" ].toDouble(defaultValue: 1.0); |
251 | m_screens.append(t: offscreenScreen); |
252 | QWindowSystemInterface::handleScreenAdded(screen: offscreenScreen); |
253 | } |
254 | |
255 | // Update present screens |
256 | for (const QString &pres : present) { |
257 | QOffscreenScreen *screen = platformScreenByName(pres, m_screens); |
258 | Q_ASSERT(screen); |
259 | QJsonObject currentConfig = screenConfigByName(pres, currentScreens).toObject(); |
260 | QJsonObject newConfig = screenConfigByName(pres, newScreens).toObject(); |
261 | |
262 | // Name can't change, because it'd be a different screen |
263 | Q_ASSERT(currentConfig["name" ] == newConfig["name" ]); |
264 | |
265 | // Geometry |
266 | QRect currentGeomtry = geometryFromConfig(currentConfig); |
267 | QRect newGeomtry = geometryFromConfig(newConfig); |
268 | if (currentGeomtry != newGeomtry) { |
269 | screen->m_geometry = newGeomtry; |
270 | QWindowSystemInterface::handleScreenGeometryChange(screen: screen->screen(), newGeometry: newGeomtry, newAvailableGeometry: newGeomtry); |
271 | } |
272 | |
273 | // logical DPI |
274 | int currentLogicalDpi = currentConfig["logicalDpi" ].toInt(defaultValue: 96); |
275 | int newLogicalDpi = newConfig["logicalDpi" ].toInt(defaultValue: 96); |
276 | if (currentLogicalDpi != newLogicalDpi) { |
277 | screen->m_logicalDpi = newLogicalDpi; |
278 | QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(screen: screen->screen(), newDpiX: newLogicalDpi, newDpiY: newLogicalDpi); |
279 | } |
280 | |
281 | // The base DPI is more of a platform constant, and should not change, and |
282 | // there is no handleChange function for it. Print a warning. |
283 | int currentLogicalBaseDpi = currentConfig["logicalBaseDpi" ].toInt(defaultValue: 96); |
284 | int newLogicalBaseDpi = newConfig["logicalBaseDpi" ].toInt(defaultValue: 96); |
285 | if (currentLogicalBaseDpi != newLogicalBaseDpi) { |
286 | screen->m_logicalBaseDpi = newLogicalBaseDpi; |
287 | qWarning(msg: "You ain't supposed to change logicalBaseDpi - its a platform constant. Qt may not react to the change" ); |
288 | } |
289 | |
290 | // DPR. There is also no handleChange function in Qt at this point, instead |
291 | // the new DPR value will be used during the next repaint. We could repaint |
292 | // all windows here, but don't. Print a warning. |
293 | double currentDpr = currentConfig["dpr" ].toDouble(defaultValue: 1); |
294 | double newDpr = newConfig["dpr" ].toDouble(defaultValue: 1); |
295 | if (currentDpr != newDpr) { |
296 | screen->m_dpr = newDpr; |
297 | qWarning(msg: "DPR change notifications is not implemented - Qt may not react to the change" ); |
298 | } |
299 | } |
300 | |
301 | // Now the new configuration is the current configuration |
302 | m_configuration = configuration; |
303 | } |
304 | |
305 | QJsonObject QOffscreenIntegration::configuration() const |
306 | { |
307 | return m_configuration; |
308 | } |
309 | |
310 | void QOffscreenIntegration::initialize() |
311 | { |
312 | m_inputContext.reset(other: QPlatformInputContextFactory::create()); |
313 | } |
314 | |
315 | QPlatformInputContext *QOffscreenIntegration::inputContext() const |
316 | { |
317 | return m_inputContext.data(); |
318 | } |
319 | |
320 | bool QOffscreenIntegration::hasCapability(QPlatformIntegration::Capability cap) const |
321 | { |
322 | switch (cap) { |
323 | case ThreadedPixmaps: return true; |
324 | case MultipleWindows: return true; |
325 | case RhiBasedRendering: return false; |
326 | default: return QPlatformIntegration::hasCapability(cap); |
327 | } |
328 | } |
329 | |
330 | QPlatformWindow *QOffscreenIntegration::createPlatformWindow(QWindow *window) const |
331 | { |
332 | Q_UNUSED(window); |
333 | QPlatformWindow *w = new QOffscreenWindow(window, m_windowFrameMarginsEnabled); |
334 | w->requestActivateWindow(); |
335 | return w; |
336 | } |
337 | |
338 | QPlatformBackingStore *QOffscreenIntegration::createPlatformBackingStore(QWindow *window) const |
339 | { |
340 | return new QOffscreenBackingStore(window); |
341 | } |
342 | |
343 | QAbstractEventDispatcher *QOffscreenIntegration::createEventDispatcher() const |
344 | { |
345 | #if defined(Q_OS_UNIX) |
346 | return createUnixEventDispatcher(); |
347 | #elif defined(Q_OS_WIN) |
348 | return new QOffscreenEventDispatcher<QEventDispatcherWin32>(); |
349 | #else |
350 | return 0; |
351 | #endif |
352 | } |
353 | |
354 | QPlatformNativeInterface *QOffscreenIntegration::nativeInterface() const |
355 | { |
356 | if (!m_nativeInterface) |
357 | m_nativeInterface.reset(other: new QOffscreenPlatformNativeInterface(const_cast<QOffscreenIntegration*>(this))); |
358 | return m_nativeInterface.get(); |
359 | } |
360 | |
361 | static QString themeName() { return QStringLiteral("offscreen" ); } |
362 | |
363 | QStringList QOffscreenIntegration::themeNames() const |
364 | { |
365 | return QStringList(themeName()); |
366 | } |
367 | |
368 | // Restrict the styles to "fusion" to prevent native styles requiring native |
369 | // window handles (eg Windows Vista style) from being used. |
370 | class OffscreenTheme : public QPlatformTheme |
371 | { |
372 | public: |
373 | OffscreenTheme() {} |
374 | |
375 | QVariant themeHint(ThemeHint h) const override |
376 | { |
377 | switch (h) { |
378 | case StyleNames: |
379 | return QVariant(QStringList(QStringLiteral("Fusion" ))); |
380 | default: |
381 | break; |
382 | } |
383 | return QPlatformTheme::themeHint(hint: h); |
384 | } |
385 | |
386 | virtual const QFont *font(Font type = SystemFont) const override |
387 | { |
388 | static QFont systemFont("Sans Serif"_L1 , 9); |
389 | static QFont fixedFont("monospace"_L1 , 9); |
390 | switch (type) { |
391 | case QPlatformTheme::SystemFont: |
392 | return &systemFont; |
393 | case QPlatformTheme::FixedFont: |
394 | return &fixedFont; |
395 | default: |
396 | return nullptr; |
397 | } |
398 | } |
399 | }; |
400 | |
401 | QPlatformTheme *QOffscreenIntegration::createPlatformTheme(const QString &name) const |
402 | { |
403 | return name == themeName() ? new OffscreenTheme() : nullptr; |
404 | } |
405 | |
406 | QPlatformFontDatabase *QOffscreenIntegration::fontDatabase() const |
407 | { |
408 | return m_fontDatabase.data(); |
409 | } |
410 | |
411 | #if QT_CONFIG(draganddrop) |
412 | QPlatformDrag *QOffscreenIntegration::drag() const |
413 | { |
414 | return m_drag.data(); |
415 | } |
416 | #endif |
417 | |
418 | QPlatformServices *QOffscreenIntegration::services() const |
419 | { |
420 | return m_services.data(); |
421 | } |
422 | |
423 | QOffscreenIntegration *QOffscreenIntegration::createOffscreenIntegration(const QStringList& paramList) |
424 | { |
425 | QOffscreenIntegration *offscreenIntegration = nullptr; |
426 | |
427 | #if QT_CONFIG(xlib) && QT_CONFIG(opengl) && !QT_CONFIG(opengles2) |
428 | QByteArray glx = qgetenv(varName: "QT_QPA_OFFSCREEN_NO_GLX" ); |
429 | if (glx.isEmpty()) |
430 | offscreenIntegration = new QOffscreenX11Integration(paramList); |
431 | #endif |
432 | |
433 | if (!offscreenIntegration) |
434 | offscreenIntegration = new QOffscreenIntegration(paramList); |
435 | return offscreenIntegration; |
436 | } |
437 | |
438 | QList<QOffscreenScreen *> QOffscreenIntegration::screens() const |
439 | { |
440 | return m_screens; |
441 | } |
442 | |
443 | QT_END_NAMESPACE |
444 | |