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
39QT_BEGIN_NAMESPACE
40
41using namespace Qt::StringLiterals;
42
43class QCoreTextFontEngine;
44
45template <typename BaseEventDispatcher>
46class QOffscreenEventDispatcher : public BaseEventDispatcher
47{
48public:
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
62QOffscreenIntegration::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
83QOffscreenIntegration::~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
119QJsonObject 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
139std::optional<QJsonObject> QOffscreenIntegration::resolveConfigFileConfiguration(const QStringList& paramList) const
140{
141 bool hasConfigFile = false;
142 QString configFilePath;
143 for (const QString &param : 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
173void 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
305QJsonObject QOffscreenIntegration::configuration() const
306{
307 return m_configuration;
308}
309
310void QOffscreenIntegration::initialize()
311{
312 m_inputContext.reset(other: QPlatformInputContextFactory::create());
313}
314
315QPlatformInputContext *QOffscreenIntegration::inputContext() const
316{
317 return m_inputContext.data();
318}
319
320bool 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
330QPlatformWindow *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
338QPlatformBackingStore *QOffscreenIntegration::createPlatformBackingStore(QWindow *window) const
339{
340 return new QOffscreenBackingStore(window);
341}
342
343QAbstractEventDispatcher *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
354QPlatformNativeInterface *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
361static QString themeName() { return QStringLiteral("offscreen"); }
362
363QStringList 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.
370class OffscreenTheme : public QPlatformTheme
371{
372public:
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
401QPlatformTheme *QOffscreenIntegration::createPlatformTheme(const QString &name) const
402{
403 return name == themeName() ? new OffscreenTheme() : nullptr;
404}
405
406QPlatformFontDatabase *QOffscreenIntegration::fontDatabase() const
407{
408 return m_fontDatabase.data();
409}
410
411#if QT_CONFIG(draganddrop)
412QPlatformDrag *QOffscreenIntegration::drag() const
413{
414 return m_drag.data();
415}
416#endif
417
418QPlatformServices *QOffscreenIntegration::services() const
419{
420 return m_services.data();
421}
422
423QOffscreenIntegration *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
438QList<QOffscreenScreen *> QOffscreenIntegration::screens() const
439{
440 return m_screens;
441}
442
443QT_END_NAMESPACE
444

source code of qtbase/src/plugins/platforms/offscreen/qoffscreenintegration.cpp