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